Merge pull request #149 from travisstaloch/lsp-rename

implement lsp rename
This commit is contained in:
CJ van den Berg 2025-01-18 23:33:25 +01:00 committed by GitHub
commit 9f29853cd6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 328 additions and 4 deletions

View file

@ -782,6 +782,150 @@ fn send_completion_item(_: *Self, to: tp.pid_ref, file_path: []const u8, row: us
}) catch error.ClientFailed;
}
const Rename = struct {
uri: []const u8,
new_text: []const u8,
range: Range,
};
pub fn rename_symbol(self: *Self, from: tp.pid_ref, file_path: []const u8, row: usize, col: usize) (LspOrClientError || GetLineOfFileError || InvalidMessageError || cbor.Error)!void {
const lsp = try self.get_language_server(file_path);
const uri = try self.make_URI(file_path);
defer self.allocator.free(uri);
const response = lsp.send_request(self.allocator, "textDocument/rename", .{
.textDocument = .{ .uri = uri },
.position = .{ .line = row, .character = col },
.newName = "PLACEHOLDER",
}) catch return error.LspFailed;
defer self.allocator.free(response.buf);
var result: []const u8 = undefined;
// buffer the renames in order to send as a single, atomic message
var renames = std.ArrayList(Rename).init(self.allocator);
defer renames.deinit();
if (try cbor.match(response.buf, .{ "child", tp.string, "result", tp.map })) {
if (try cbor.match(response.buf, .{ tp.any, tp.any, tp.any, tp.extract_cbor(&result) })) {
try self.decode_rename_symbol_map(result, &renames);
// write the renames message manually since there doesn't appear to be an array helper
var msg_buf = std.ArrayList(u8).init(self.allocator);
defer msg_buf.deinit();
const w = msg_buf.writer();
try cbor.writeArrayHeader(w, 3);
try cbor.writeValue(w, "cmd");
try cbor.writeValue(w, "rename_symbol_item");
try cbor.writeArrayHeader(w, renames.items.len);
for (renames.items) |rename| {
if (!std.mem.eql(u8, rename.uri[0..7], "file://")) return error.InvalidTargetURI;
var file_path_buf: [std.fs.max_path_bytes]u8 = undefined;
const file_path_ = std.Uri.percentDecodeBackwards(&file_path_buf, rename.uri[7..]);
if (builtin.os.tag == .windows) {
if (file_path[0] == '/') file_path = file_path[1..];
for (file_path, 0..) |c, i| if (c == '/') {
file_path[i] = '\\';
};
}
const line = try self.get_line_of_file(self.allocator, file_path, rename.range.start.line);
try cbor.writeValue(w, .{
file_path_,
rename.range.start.line,
rename.range.start.character,
rename.range.end.line,
rename.range.end.character,
rename.new_text,
line,
});
}
from.send_raw(.{ .buf = msg_buf.items }) catch return error.ClientFailed;
}
}
}
// decode a WorkspaceEdit record which may have shape {"changes": {}} or {"documentChanges": []}
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspaceEdit
fn decode_rename_symbol_map(self: *Self, result: []const u8, renames: *std.ArrayList(Rename)) (ClientError || InvalidMessageError || cbor.Error)!void {
var iter = result;
var len = cbor.decodeMapHeader(&iter) catch return error.InvalidMessage;
var changes: []const u8 = "";
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, "changes")) {
if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&changes)))) return error.InvalidMessageField;
try self.decode_rename_symbol_changes(changes, renames);
return;
} else if (std.mem.eql(u8, field_name, "documentChanges")) {
if (!(try cbor.matchValue(&iter, cbor.extract_cbor(&changes)))) return error.InvalidMessageField;
try self.decode_rename_symbol_doc_changes(changes, renames);
return;
} else {
try cbor.skipValue(&iter);
}
}
return error.ClientFailed;
}
fn decode_rename_symbol_changes(self: *Self, changes: []const u8, renames: *std.ArrayList(Rename)) (ClientError || InvalidMessageError || cbor.Error)!void {
var iter = changes;
var files_len = cbor.decodeMapHeader(&iter) catch return error.InvalidMessage;
while (files_len > 0) : (files_len -= 1) {
var file_uri: []const u8 = undefined;
if (!(try cbor.matchString(&iter, &file_uri))) return error.InvalidMessage;
try decode_rename_symbol_item(self, file_uri, &iter, renames);
}
}
fn decode_rename_symbol_doc_changes(self: *Self, changes: []const u8, renames: *std.ArrayList(Rename)) (ClientError || InvalidMessageError || cbor.Error)!void {
var iter = changes;
var changes_len = cbor.decodeArrayHeader(&iter) catch return error.InvalidMessage;
while (changes_len > 0) : (changes_len -= 1) {
var dc_fields_len = cbor.decodeMapHeader(&iter) catch return error.InvalidMessage;
var file_uri: []const u8 = "";
while (dc_fields_len > 0) : (dc_fields_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, "textDocument")) {
var td_fields_len = cbor.decodeMapHeader(&iter) catch return error.InvalidMessage;
while (td_fields_len > 0) : (td_fields_len -= 1) {
var td_field_name: []const u8 = undefined;
if (!(try cbor.matchString(&iter, &td_field_name))) return error.InvalidMessage;
if (std.mem.eql(u8, td_field_name, "uri")) {
if (!(try cbor.matchString(&iter, &file_uri))) return error.InvalidMessage;
} else try cbor.skipValue(&iter); // skip "version": 1
}
} else if (std.mem.eql(u8, field_name, "edits")) {
if (file_uri.len == 0) return error.InvalidMessage;
try decode_rename_symbol_item(self, file_uri, &iter, renames);
}
}
}
}
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEdit
fn decode_rename_symbol_item(_: *Self, file_uri: []const u8, iter: *[]const u8, renames: *std.ArrayList(Rename)) (ClientError || InvalidMessageError || cbor.Error)!void {
var text_edits_len = cbor.decodeArrayHeader(iter) catch return error.InvalidMessage;
while (text_edits_len > 0) : (text_edits_len -= 1) {
var m_range: ?Range = null;
var new_text: []const u8 = "";
var edits_len = cbor.decodeMapHeader(iter) catch return error.InvalidMessage;
while (edits_len > 0) : (edits_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, "range")) {
var range: []const u8 = undefined;
if (!(try cbor.matchValue(iter, cbor.extract_cbor(&range)))) return error.InvalidMessageField;
m_range = try read_range(range);
} else if (std.mem.eql(u8, field_name, "newText")) {
if (!(try cbor.matchString(iter, &new_text))) return error.InvalidMessageField;
} else {
try cbor.skipValue(iter);
}
}
const range = m_range orelse return error.InvalidMessageField;
try renames.append(.{ .uri = file_uri, .range = range, .new_text = new_text });
}
}
pub fn hover(self: *Self, from: tp.pid_ref, file_path: []const u8, row: usize, col: usize) (LspOrClientError || InvalidMessageError || cbor.Error)!void {
const lsp = try self.get_language_server(file_path);
const uri = try self.make_URI(file_path);
@ -1396,7 +1540,7 @@ fn format_lsp_name_func(
const eol = '\n';
const GetLineOfFileError = (OutOfMemoryError || std.fs.File.OpenError || std.fs.File.Reader.Error);
pub const GetLineOfFileError = (OutOfMemoryError || std.fs.File.OpenError || std.fs.File.Reader.Error);
fn get_line_of_file(self: *Self, allocator: std.mem.Allocator, file_path: []const u8, line_: usize) GetLineOfFileError![]const u8 {
const line = line_ + 1;

View file

@ -35,6 +35,7 @@ pub const ArgumentType = enum {
integer,
float,
object,
array,
};
pub fn Closure(comptime T: type) type {

View file

@ -37,7 +37,25 @@
["ctrl+x ctrl+c", "quit"],
["ctrl+x b", "open_recent"],
["alt+x", "open_command_palette"],
["ctrl+space", "enter_mode", "select"]
["ctrl+space", "enter_mode", "select"],
["ctrl+c l = =", "format"],
["ctrl+c l = r", "format"],
["ctrl+c l g g", "goto_definition"],
["ctrl+c l g i", "goto_implementation"],
["ctrl+c l g d", "goto_declaration"],
["ctrl+c l g r", "references"],
["ctrl+c l h h", "hover"],
["ctrl+c l r r", "rename_symbol"],
["super+l = =", "format"],
["super+l = r", "format"],
["super+l g g", "goto_definition"],
["super+l g i", "goto_implementation"],
["super+l g d", "goto_declaration"],
["super+l g r", "references"],
["super+l h h", "hover"],
["super+l r r", "rename_symbol"]
]
},
"select": {

View file

@ -127,7 +127,8 @@
["shift+enter", "smart_insert_line_before"],
["shift+backspace", "delete_backward"],
["shift+tab", "unindent"],
["f2", "toggle_input_mode"],
["f2", "rename_symbol"],
["f4", "toggle_input_mode"],
["ctrl+f2", "insert_command_name"],
["f3", "goto_next_match"],
["f15", "goto_prev_match"],
@ -229,7 +230,7 @@
["alt+i", "toggle_inputview"],
["alt+x", "open_command_palette"],
["f1", "open_help"],
["f2", "toggle_input_mode"],
["f4", "toggle_input_mode"],
["ctrl+f2", "insert_command_name"],
["f6", "open_config"],
["f9", "theme_prev"],

View file

@ -44,6 +44,7 @@
["gi", "goto_implementation"],
["gy", "goto_type_definition"],
["gg", "move_buffer_begin"],
["grn", "rename_symbol"],
["g<S-d>", "goto_declaration"],
["<S-g>", "move_buffer_end"],

View file

@ -176,6 +176,13 @@ pub fn completion(file_path: []const u8, row: usize, col: usize) (ProjectManager
return send(.{ "completion", project, file_path, row, col });
}
pub fn rename_symbol(file_path: []const u8, row: usize, col: usize) (ProjectManagerError || ProjectError)!void {
const project = tp.env.get().str("project");
if (project.len == 0)
return error.NoProject;
return send(.{ "rename_symbol", project, file_path, row, col });
}
pub fn hover(file_path: []const u8, row: usize, col: usize) (ProjectManagerError || ProjectError)!void {
const project = tp.env.get().str("project");
if (project.len == 0)
@ -339,6 +346,8 @@ const Process = struct {
self.references(from, project_directory, path, row, col) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
} else if (try cbor.match(m.buf, .{ "completion", tp.extract(&project_directory), tp.extract(&path), tp.extract(&row), tp.extract(&col) })) {
self.completion(from, project_directory, path, row, col) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
} else if (try cbor.match(m.buf, .{ "rename_symbol", tp.extract(&project_directory), tp.extract(&path), tp.extract(&row), tp.extract(&col) })) {
self.rename_symbol(from, project_directory, path, row, col) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
} else if (try cbor.match(m.buf, .{ "hover", tp.extract(&project_directory), tp.extract(&path), tp.extract(&row), tp.extract(&col) })) {
self.hover(from, project_directory, path, row, col) catch |e| return from.forward_error(e, @errorReturnTrace()) catch error.ClientFailed;
} else if (try cbor.match(m.buf, .{ "get_mru_position", tp.extract(&project_directory), tp.extract(&path) })) {
@ -492,6 +501,13 @@ const Process = struct {
return project.completion(from, file_path, row, col);
}
fn rename_symbol(self: *Process, from: tp.pid_ref, project_directory: []const u8, file_path: []const u8, row: usize, col: usize) (ProjectError || Project.InvalidMessageError || Project.LspOrClientError || Project.GetLineOfFileError || cbor.Error)!void {
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".rename_symbol" });
defer frame.deinit();
const project = self.projects.get(project_directory) orelse return error.NoProject;
return project.rename_symbol(from, file_path, row, col);
}
fn hover(self: *Process, from: tp.pid_ref, project_directory: []const u8, file_path: []const u8, row: usize, col: usize) (ProjectError || Project.InvalidMessageError || Project.LspOrClientError || cbor.Error)!void {
const frame = tracy.initZone(@src(), .{ .name = module_name ++ ".hover" });
defer frame.deinit();

View file

@ -4247,6 +4247,104 @@ pub const Editor = struct {
}
pub const completion_meta = .{ .description = "Language: Show completions at cursor" };
pub fn rename_symbol(self: *Self, _: Context) Result {
const file_path = self.file_path orelse return;
const root = self.buf_root() catch return;
const primary = self.get_primary();
const col = try root.get_line_width_to_pos(primary.cursor.row, primary.cursor.col, self.metrics);
return project_manager.rename_symbol(file_path, primary.cursor.row, col);
}
pub const rename_symbol_meta = .{ .description = "Language: Rename symbol at cursor" };
pub fn add_cursor_from_selection(self: *Self, sel_: Selection, op: enum { cancel, push }) !void {
switch (op) {
.cancel => self.cancel_all_selections(),
.push => try self.push_cursor(),
}
const root = self.buf_root() catch return;
const sel: Selection = .{
.begin = .{
.row = sel_.begin.row,
.col = try root.pos_to_width(sel_.begin.row, sel_.begin.col, self.metrics),
},
.end = .{
.row = sel_.end.row,
.col = try root.pos_to_width(sel_.end.row, sel_.end.col, self.metrics),
},
};
const primary = self.get_primary();
primary.selection = sel;
primary.cursor = sel.end;
self.need_render();
}
pub fn add_cursors_from_content_diff(self: *Self, new_content: []const u8) !void {
const frame = tracy.initZone(@src(), .{ .name = "editor diff syntax" });
defer frame.deinit();
var content_ = std.ArrayList(u8).init(self.allocator);
defer content_.deinit();
const root = self.buf_root() catch return;
const eol_mode = self.buf_eol_mode() catch return;
try root.store(content_.writer(), eol_mode);
const content = content_.items;
var last_begin_row: usize = 0;
var last_begin_col_pos: usize = 0;
var last_end_row: usize = 0;
var last_end_col_pos: usize = 0;
const diffs = try @import("diff").diff(self.allocator, new_content, content);
defer self.allocator.free(diffs);
var first = true;
for (diffs) |diff| {
switch (diff.kind) {
.delete => {
var begin_row, var begin_col_pos = count_lines(content[0..diff.start]);
const end_row, const end_col_pos = count_lines(content[0 .. diff.start + diff.bytes.len]);
if (begin_row == last_end_row and begin_col_pos == last_end_col_pos) {
begin_row = last_begin_row;
begin_col_pos = last_begin_col_pos;
} else {
if (first) {
self.cancel_all_selections();
} else {
try self.push_cursor();
}
first = false;
}
const begin_col = try root.pos_to_width(begin_row, begin_col_pos, self.metrics);
const end_col = try root.pos_to_width(end_row, end_col_pos, self.metrics);
last_begin_row = begin_row;
last_begin_col_pos = begin_col_pos;
last_end_row = end_row;
last_end_col_pos = end_col_pos;
const sel: Selection = .{
.begin = .{ .row = begin_row, .col = begin_col },
.end = .{ .row = end_row, .col = end_col },
};
const primary = self.get_primary();
primary.selection = sel;
primary.cursor = sel.end;
self.need_render();
},
else => {},
}
}
}
fn count_lines(content: []const u8) struct { usize, usize } {
var pos = content;
var offset = content.len;
var lines: usize = 0;
while (pos.len > 0) : (pos = pos[1..]) if (pos[0] == '\n') {
offset = pos.len - 1;
lines += 1;
};
return .{ lines, offset };
}
pub fn hover(self: *Self, _: Context) Result {
const primary = self.get_primary();
return self.hover_at(primary.cursor.row, primary.cursor.col);

View file

@ -554,6 +554,51 @@ const cmds = struct {
}
pub const add_diagnostic_meta = .{ .arguments = &.{ .string, .string, .string, .string, .integer, .integer, .integer, .integer, .integer } };
pub fn rename_symbol_item(self: *Self, ctx: Ctx) Result {
const editor = self.get_active_editor() orelse return;
// because the incoming message is an array of Renames, we manuallly
// parse instead of using ctx.args.match() which doesn't seem to return
// the parsed length needed to correctly advance iter.
var iter = ctx.args.buf;
var len = try cbor.decodeArrayHeader(&iter);
var first = true;
while (len != 0) : (len -= 1) {
if (try cbor.decodeArrayHeader(&iter) != 7) return error.InvalidRenameSymbolItemArgument;
var file_path: []const u8 = undefined;
if (!try cbor.matchString(&iter, &file_path)) return error.MissingArgument;
var sel: ed.Selection = .{};
if (!try cbor.matchInt(usize, &iter, &sel.begin.row)) return error.MissingArgument;
if (!try cbor.matchInt(usize, &iter, &sel.begin.col)) return error.MissingArgument;
if (!try cbor.matchInt(usize, &iter, &sel.end.row)) return error.MissingArgument;
if (!try cbor.matchInt(usize, &iter, &sel.end.col)) return error.MissingArgument;
var new_text: []const u8 = undefined;
if (!try cbor.matchString(&iter, &new_text)) return error.MissingArgument;
var line_text: []const u8 = undefined;
if (!try cbor.matchString(&iter, &line_text)) return error.MissingArgument;
file_path = project_manager.normalize_file_path(file_path);
if (std.mem.eql(u8, file_path, editor.file_path orelse "")) {
if (len == 1 and sel.begin.row == 0 and sel.begin.col == 0 and sel.end.row > 0) //probably a full file edit
return editor.add_cursors_from_content_diff(new_text);
try editor.add_cursor_from_selection(sel, if (first) .cancel else .push);
first = false;
} else {
try self.add_find_in_files_result(
.references,
file_path,
sel.begin.row + 1,
sel.begin.col,
sel.end.row + 1,
sel.end.col,
line_text,
.Information,
);
}
}
}
pub const rename_symbol_item_meta = .{ .arguments = &.{.array} };
pub const rename_symbol_item_elem_meta = .{ .arguments = &.{ .string, .integer, .integer, .integer, .integer, .string } };
pub fn clear_diagnostics(self: *Self, ctx: Ctx) Result {
var file_path: []const u8 = undefined;
if (!try ctx.args.match(.{tp.extract(&file_path)})) return error.InvalidClearDiagnosticsArgument;