Merge pull request #149 from travisstaloch/lsp-rename
implement lsp rename
This commit is contained in:
commit
9f29853cd6
8 changed files with 328 additions and 4 deletions
146
src/Project.zig
146
src/Project.zig
|
@ -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;
|
||||
|
|
|
@ -35,6 +35,7 @@ pub const ArgumentType = enum {
|
|||
integer,
|
||||
float,
|
||||
object,
|
||||
array,
|
||||
};
|
||||
|
||||
pub fn Closure(comptime T: type) type {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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"],
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Reference in a new issue