feat: first (mostly) working version of goto_definition command

Tested only with zls and clangd so far.
This commit is contained in:
CJ van den Berg 2024-04-05 22:52:47 +02:00
parent 7e17f713e7
commit fc15c8bda6
6 changed files with 186 additions and 56 deletions

View file

@ -9,8 +9,8 @@ a: std.mem.Allocator,
name: []const u8,
files: std.ArrayList(File),
open_time: i64,
lsp: ?LSP = null,
lsp_name: [:0]const u8,
language_servers: std.StringHashMap(LSP),
file_language_server: std.StringHashMap(LSP),
const Self = @This();
@ -25,27 +25,44 @@ pub fn init(a: std.mem.Allocator, name: []const u8) error{OutOfMemory}!Self {
.name = try a.dupe(u8, name),
.files = std.ArrayList(File).init(a),
.open_time = std.time.milliTimestamp(),
.lsp_name = "zls",
.language_servers = std.StringHashMap(LSP).init(a),
.file_language_server = std.StringHashMap(LSP).init(a),
};
}
pub fn deinit(self: *Self) void {
var i_ = self.file_language_server.iterator();
while (i_.next()) |p| {
self.a.free(p.key_ptr.*);
}
var i = self.language_servers.iterator();
while (i.next()) |p| {
self.a.free(p.key_ptr.*);
p.value_ptr.*.deinit();
}
for (self.files.items) |file| self.a.free(file.path);
self.files.deinit();
if (self.lsp) |*lsp| lsp.deinit();
self.a.free(self.name);
}
fn get_lsp(self: *Self) !LSP {
if (self.lsp) |lsp| return lsp;
self.lsp = try LSP.open(self.a, tp.message.fmt(.{self.lsp_name}), self.lsp_name);
fn get_lsp(self: *Self, language_server: []const u8) !LSP {
if (self.language_servers.get(language_server)) |lsp| return lsp;
const lsp = try LSP.open(self.a, .{ .buf = language_server });
try self.language_servers.put(try self.a.dupe(u8, language_server), lsp);
const uri = try self.make_URI(null);
defer self.a.free(uri);
const basename_begin = std.mem.lastIndexOfScalar(u8, self.name, std.fs.path.sep);
const basename = if (basename_begin) |begin| self.name[begin + 1 ..] else self.name;
const response = try self.send_lsp_init_request(self.name, basename, uri);
const response = try self.send_lsp_init_request(lsp, self.name, basename, uri);
defer self.a.free(response.buf);
return self.lsp.?;
try lsp.send_notification("initialized", .{});
return lsp;
}
fn get_file_lsp(self: *Self, file_path: []const u8) !LSP {
const lsp = self.file_language_server.get(file_path) orelse return tp.exit("no language server");
if (lsp.pid.expired()) return tp.exit("no language server");
return lsp;
}
fn make_URI(self: *Self, file_path: ?[]const u8) ![]const u8 {
@ -92,9 +109,12 @@ pub fn query_recent_files(self: *Self, from: tp.pid_ref, max: usize, query: []co
return i;
}
pub fn did_open(self: *Self, from: tp.pid_ref, file_path: []const u8, file_type: []const u8, version: usize, text: []const u8) tp.result {
_ = from; // autofix
const lsp = self.get_lsp() catch |e| return tp.exit_error(e);
pub fn did_open(self: *Self, file_path: []const u8, file_type: []const u8, language_server: []const u8, version: usize, text: []const u8) tp.result {
const lsp = self.get_lsp(language_server) catch |e| return tp.exit_error(e);
if (!self.file_language_server.contains(file_path)) {
const key = self.a.dupe(u8, file_path) catch |e| return tp.exit_error(e);
self.file_language_server.put(key, lsp) catch |e| return tp.exit_error(e);
}
const uri = self.make_URI(file_path) catch |e| return tp.exit_error(e);
defer self.a.free(uri);
try lsp.send_notification("textDocument/didOpen", .{
@ -102,8 +122,8 @@ pub fn did_open(self: *Self, from: tp.pid_ref, file_path: []const u8, file_type:
});
}
pub fn goto_definition(self: *Self, from: tp.pid_ref, file_path: []const u8, row: usize, col: usize) tp.result {
const lsp = self.get_lsp() catch |e| return tp.exit_error(e);
pub fn goto_definition(self: *Self, from: tp.pid_ref, file_path: []const u8, row: usize, col: usize) !void {
const lsp = try self.get_file_lsp(file_path);
const uri = self.make_URI(file_path) catch |e| return tp.exit_error(e);
defer self.a.free(uri);
const response = try lsp.send_request(self.a, "textDocument/definition", .{
@ -111,11 +131,100 @@ pub fn goto_definition(self: *Self, from: tp.pid_ref, file_path: []const u8, row
.position = .{ .line = row, .character = col },
});
defer self.a.free(response.buf);
try from.send_raw(response);
var link: []const u8 = undefined;
if (try response.match(.{ "child", tp.string, "result", tp.array })) {
if (try response.match(.{ tp.any, tp.any, tp.any, .{ tp.extract_cbor(&link), tp.more } })) {
try self.navigate_to_location_link(from, link);
} else if (try response.match(.{ tp.any, tp.any, tp.any, .{tp.extract_cbor(&link)} })) {
try self.navigate_to_location_link(from, link);
}
} else if (try response.match(.{ "child", tp.string, "result", tp.null_ })) {
return;
} else if (try response.match(.{ "child", tp.string, "result", tp.extract_cbor(&link) })) {
try self.navigate_to_location_link(from, link);
}
}
fn send_lsp_init_request(self: *Self, project_path: []const u8, project_basename: []const u8, project_uri: []const u8) error{Exit}!tp.message {
return self.lsp.?.send_request(self.a, "initialize", .{
fn navigate_to_location_link(self: *Self, from: tp.pid_ref, location_link: []const u8) !void {
var iter = location_link;
var targetUri: ?[]const u8 = null;
var targetRange: ?Range = null;
var targetSelectionRange: ?Range = null;
var len = try cbor.decodeMapHeader(&iter);
while (len > 0) : (len -= 1) {
var field_name: []const u8 = undefined;
if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidMessage;
if (std.mem.eql(u8, field_name, "targetUri") or std.mem.eql(u8, field_name, "uri")) {
var value: []const u8 = undefined;
if (!(try cbor.matchValue(&iter, cbor.extract(&value)))) return error.InvalidMessageField;
targetUri = value;
} else if (std.mem.eql(u8, field_name, "targetRange") or std.mem.eql(u8, field_name, "range")) {
var range: []const u8 = undefined;
if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&range)))) return error.InvalidMessageField;
targetRange = try read_range(range);
} else if (std.mem.eql(u8, field_name, "targetSelectionRange")) {
var range: []const u8 = undefined;
if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&range)))) return error.InvalidMessageField;
targetSelectionRange = try read_range(range);
} else {
try cbor.skipValue(&iter);
}
}
if (targetUri == null or targetRange == null) return error.InvalidMessageField;
if (!std.mem.eql(u8, targetUri.?[0..7], "file://")) return error.InvalidTargetURI;
const file_path = try std.Uri.unescapeString(self.a, targetUri.?[7..]);
defer self.a.free(file_path);
try from.send(.{ "cmd", "navigate", .{ .file = file_path, .line = targetRange.?.start.line + 1, .column = targetRange.?.start.character + 1 } });
}
const Range = struct { start: Position, end: Position };
fn read_range(range: []const u8) !Range {
var iter = range;
var start: ?Position = null;
var end: ?Position = null;
var len = try cbor.decodeMapHeader(&iter);
while (len > 0) : (len -= 1) {
var field_name: []const u8 = undefined;
if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidMessage;
if (std.mem.eql(u8, field_name, "start")) {
var position: []const u8 = undefined;
if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&position)))) return error.InvalidMessageField;
start = try read_position(position);
} else if (std.mem.eql(u8, field_name, "end")) {
var position: []const u8 = undefined;
if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&position)))) return error.InvalidMessageField;
end = try read_position(position);
} else {
try cbor.skipValue(&iter);
}
}
if (start == null or end == null) return error.InvalidMessageField;
return .{ .start = start.?, .end = end.? };
}
const Position = struct { line: usize, character: usize };
fn read_position(position: []const u8) !Position {
var iter = position;
var line: ?usize = 0;
var character: ?usize = 0;
var len = try cbor.decodeMapHeader(&iter);
while (len > 0) : (len -= 1) {
var field_name: []const u8 = undefined;
if (!(try cbor.matchString(&iter, &field_name))) return error.InvalidMessage;
if (std.mem.eql(u8, field_name, "line")) {
if (!(try cbor.matchValue(&iter, cbor.extract(&line)))) return error.InvalidMessageField;
} else if (std.mem.eql(u8, field_name, "character")) {
if (!(try cbor.matchValue(&iter, cbor.extract(&character)))) return error.InvalidMessageField;
} else {
try cbor.skipValue(&iter);
}
}
if (line == null or character == null) return error.InvalidMessageField;
return .{ .line = line.?, .character = character.? };
}
fn send_lsp_init_request(self: *Self, lsp: LSP, project_path: []const u8, project_basename: []const u8, project_uri: []const u8) error{Exit}!tp.message {
return lsp.send_request(self.a, "initialize", .{
.processId = std.os.linux.getpid(),
.rootPath = project_path,
.rootUri = project_uri,
@ -145,7 +254,7 @@ fn send_lsp_init_request(self: *Self, project_path: []const u8, project_basename
.normalizesLineEndings = true,
.changeAnnotationSupport = .{ .groupsOnLabel = true },
},
.configuration = true,
// .configuration = true,
.didChangeWatchedFiles = .{
.dynamicRegistration = true,
.relativePatternSupport = true,
@ -160,7 +269,7 @@ fn send_lsp_init_request(self: *Self, project_path: []const u8, project_basename
},
.codeLens = .{ .refreshSupport = true },
.executeCommand = .{ .dynamicRegistration = true },
.didChangeConfiguration = .{ .dynamicRegistration = true },
// .didChangeConfiguration = .{ .dynamicRegistration = true },
.workspaceFolders = true,
.semanticTokens = .{ .refreshSupport = true },
.fileOperations = .{
@ -402,7 +511,7 @@ fn send_lsp_init_request(self: *Self, project_path: []const u8, project_basename
.parser = "marked",
.version = "1.1.0",
},
.positionEncodings = .{"utf-16"},
.positionEncodings = .{"utf-8"},
},
.notebookDocument = .{
.synchronization = .{