diff options
| -rw-r--r-- | src/host_io.zig | 35 | ||||
| -rw-r--r-- | src/lsp/lsp.zig | 15 | ||||
| -rw-r--r-- | src/lsp/lsp_client.zig | 8 | ||||
| -rw-r--r-- | test/lspmock.zig | 31 | ||||
| -rw-r--r-- | test/snapshots/lsp-rename-files.golden | 27 | ||||
| -rw-r--r-- | test/snapshots/lsp-rename-files.snap | 29 |
6 files changed, 142 insertions, 3 deletions
diff --git a/src/host_io.zig b/src/host_io.zig index f944e1cb..9e9a37ed 100644 --- a/src/host_io.zig +++ b/src/host_io.zig @@ -143,12 +143,19 @@ pub const Lsp = struct { source: [:0]u8, arg: []u8, root: []u8, + /// The other open files, for a query that can reach them. + others: []pardes.lsp.Doc = &.{}, pub fn free(job: *Job, gpa: std.mem.Allocator) void { gpa.free(job.path); gpa.free(job.source); gpa.free(job.arg); gpa.free(job.root); + for (job.others) |doc| { + gpa.free(doc.path); + gpa.free(doc.source); + } + gpa.free(job.others); gpa.destroy(job); } }; @@ -169,6 +176,7 @@ pub const Lsp = struct { errdefer gpa.free(arg); const declared_root = if (file) |f| std.fs.path.dirname(f.path) orelse "/" else pane.cwdSlice(); const root = try gpa.dupe(u8, filesystem.localPath(declared_root) orelse declared_root); + errdefer gpa.free(root); job.* = .{ .id = req.id, .kind = req.kind, @@ -178,6 +186,32 @@ pub const Lsp = struct { .arg = arg, .root = root, }; + // ponytail: a copy of every open file per such query; a server + // already told of one is sent nothing more (syncDoc). + if (pardes.lsp.reachesOtherFiles(req.kind)) { + var others: std.ArrayList(pardes.lsp.Doc) = .empty; + errdefer { + for (others.items) |doc| { + gpa.free(doc.path); + gpa.free(doc.source); + } + others.deinit(gpa); + } + for (core.panes, 0..) |slot, i| { + const other = slot orelse continue; + if (i == req.pane) continue; + const f = other.file orelse continue; + if (f.output != null) continue; + const local = filesystem.localPath(f.path) orelse continue; + const other_path = try gpa.dupe(u8, local); + const other_source = gpa.dupeZ(u8, f.content) catch |err| { + gpa.free(other_path); + return err; + }; + try others.append(gpa, .{ .path = other_path, .source = other_source }); + } + job.others = try others.toOwnedSlice(gpa); + } return job; } @@ -197,6 +231,7 @@ pub const Lsp = struct { .offset = job.offset, .arg = job.arg, .root = job.root, + .others = job.others, }, &out.writer) catch { deliver(ctx, job.id, null); return; diff --git a/src/lsp/lsp.zig b/src/lsp/lsp.zig index 33e95570..8dc4da4f 100644 --- a/src/lsp/lsp.zig +++ b/src/lsp/lsp.zig @@ -31,11 +31,26 @@ pub const Req = struct { offset: u32, arg: []const u8 = "", root: []const u8 = "", + /// The other files open in the editor, for a query whose answer can + /// reach them (a rename, references): the server is told of each first, + /// or it answers from what it has seen and misses the rest. + others: []const Doc = &.{}, /// For `explain`: the query it narrates, read from `arg` (`<kind> /// <arg>`) by `explaining`; a definition when none was asked before. explains: Kind = .definition, }; +pub const Doc = struct { path: []const u8, source: [:0]const u8 }; + +/// Whether a query's answer can name places in other files, so every open +/// file of the server's language is synced before it is asked. +pub fn reachesOtherFiles(kind: Kind) bool { + return switch (kind) { + .rename, .references, .implementation, .workspace_symbols, .incoming_calls, .outgoing_calls, .supertypes, .subtypes => true, + else => false, + }; +} + /// An `explain` request made the one it narrates: `explains` and `arg` set /// from its `<kind> <arg>`; any other request as it is. pub fn explaining(req: Req) Req { diff --git a/src/lsp/lsp_client.zig b/src/lsp/lsp_client.zig index 0c5e3b7e..b57b348e 100644 --- a/src/lsp/lsp_client.zig +++ b/src/lsp/lsp_client.zig @@ -501,6 +501,14 @@ fn run(c: *Conn, si: usize, arena: std.mem.Allocator, req: lsp.Req, out: *std.Io var uri: std.ArrayList(u8) = .empty; try uriOf(&uri, arena, req.path); try syncDoc(c, si, arena, uri.items, req.source); + // Every other open file this server answers for, before a query whose + // answer reaches them: zls renames only in the documents it has. + for (req.others) |doc| if (specFor(doc.path) == si and !std.mem.eql(u8, doc.path, req.path)) { + var other: std.ArrayList(u8) = .empty; + try uriOf(&other, arena, doc.path); + try syncDoc(c, si, arena, other.items, doc.source); + tr.note("synced {s} first", .{std.fs.path.basename(doc.path)}); + }; const pos = posOf(req.source, req.offset, c.caps.enc); var cx: Cx = .{ .arena = arena, .base = req.root, .cur_path = req.path, .cur_src = req.source, .out = out }; diff --git a/test/lspmock.zig b/test/lspmock.zig index 7b379b49..001c3488 100644 --- a/test/lspmock.zig +++ b/test/lspmock.zig @@ -97,6 +97,20 @@ fn range(a: std.mem.Allocator, line: u32, from: u32, to: u32) []const u8 { var root_uri: [512]u8 = undefined; var root_uri_len: usize = 0; +/// The documents the editor has opened here (didOpen), as zls keeps its +/// store: a rename answers in each of them, and in no file it never saw. +var opened: [8][512]u8 = undefined; +var opened_len: [8]usize = @splat(0); +var opened_n: usize = 0; + +fn noteOpened(uri: []const u8) void { + for (opened[0..opened_n], opened_len[0..opened_n]) |*o, n| if (std.mem.eql(u8, o[0..n], uri)) return; + if (opened_n == opened.len or uri.len > opened[0].len) return; + @memcpy(opened[opened_n][0..uri.len], uri); + opened_len[opened_n] = uri.len; + opened_n += 1; +} + pub fn main(init: std.process.Init) !void { _ = init; if (getenv("LSPMOCK_MODE")) |m| { @@ -187,6 +201,7 @@ fn handle(a: std.mem.Allocator, raw: []const u8) void { notify(a, "$/progress", "{{\"token\":\"t1\",\"value\":{{\"kind\":\"end\"}}}}", .{}); return; } + if (std.mem.eql(u8, method, "textDocument/didOpen")) noteOpened(reqUri(params)); if (id == null) return; // didOpen/didChange/$/cancelRequest: nothing to say if (mode == .mute) return; // swallow every request after the handshake @@ -227,10 +242,20 @@ fn handle(a: std.mem.Allocator, raw: []const u8) void { q, root_uri[0..root_uri_len], range(a, 1, 7, 10), }); } else if (std.mem.eql(u8, method, "textDocument/rename")) { + // In every document it has seen: lib.rs's definition, main.rs's two + // calls (any other file reads as main.rs's shape). const nn = str(get(params, "newName")) orelse "renamed"; - reply(a, rid, "{{\"changes\":{{\"{s}\":[{{\"range\":{s},\"newText\":\"{s}\"}},{{\"range\":{s},\"newText\":\"{s}\"}}]}}}}", .{ - uri, range(a, 3, 12, 15), nn, range(a, 4, 12, 15), nn, - }); + var changes: std.ArrayList(u8) = .empty; + for (opened[0..opened_n], opened_len[0..opened_n], 0..) |*o, n, k| { + const doc = o[0..n]; + const edits = if (std.mem.endsWith(u8, doc, "/lib.rs")) + std.fmt.allocPrint(a, "[{{\"range\":{s},\"newText\":\"{s}\"}}]", .{ range(a, 1, 7, 10), nn }) catch return + else + std.fmt.allocPrint(a, "[{{\"range\":{s},\"newText\":\"{s}\"}},{{\"range\":{s},\"newText\":\"{s}\"}}]", .{ range(a, 3, 12, 15), nn, range(a, 4, 12, 15), nn }) catch return; + changes.print(a, "{s}\"{s}\":{s}", .{ if (k > 0) "," else "", doc, edits }) catch return; + } + if (opened_n == 0) changes.print(a, "\"{s}\":[{{\"range\":{s},\"newText\":\"{s}\"}},{{\"range\":{s},\"newText\":\"{s}\"}}]", .{ uri, range(a, 3, 12, 15), nn, range(a, 4, 12, 15), nn }) catch return; + reply(a, rid, "{{\"changes\":{{{s}}}}}", .{changes.items}); } else if (std.mem.eql(u8, method, "textDocument/formatting")) { reply(a, rid, "[{{\"range\":{s},\"newText\":\"// formatted by mock\\n\"}}]", .{range(a, 0, 0, 0)}); } else if (std.mem.eql(u8, method, "textDocument/codeAction")) { diff --git a/test/snapshots/lsp-rename-files.golden b/test/snapshots/lsp-rename-files.golden new file mode 100644 index 00000000..230246f6 --- /dev/null +++ b/test/snapshots/lsp-rename-files.golden @@ -0,0 +1,27 @@ +== snap opened grid=100x25 cursor=7,8 +|Newcol Joincol Find Grep Help Changelog Tutor Dump Themes Config Debug Exit +| New Tty Find Grep Joincol Delcol +| /tmp/pardes-snap/lsp-rename-files/cwd/lib.rs Save Tty Collapse Del TreeContext +| 1 // main.rs +| 2 pub fn add(a: i32, b: i32) -> i32 { +| 3 a + b +| 4 } +| /tmp/pardes-snap/lsp-rename-files/cwd/main.rs Save Tty Collapse Del TreeContext +| 1 mod lib; +| 2 +| 3 fn main() { +| 4 let x = add(1, 2); +| 5 let y = add(3, 4); +| 6 let _ = x + y; +| 7 } +| 8 +== snap previewed grid=100x25 cursor=14,4 +|3: 1 // main.rs Rename: 3 edit(s) across files, previewed, not applied +|15: /tmp/pardes-snap/lsp-rename-files/cwd/+Search Save Tty Collapse Del LocationsConfig +|16: 1 lib.rs:2:8 +|17: 2 sum +|18: 3 main.rs:4:13-15 +|19: 4 sum +|20: 5 main.rs:5:13-15 +|21: 6 sum +|22: 7 diff --git a/test/snapshots/lsp-rename-files.snap b/test/snapshots/lsp-rename-files.snap new file mode 100644 index 00000000..0114c795 --- /dev/null +++ b/test/snapshots/lsp-rename-files.snap @@ -0,0 +1,29 @@ +# A Rename reaches the other files open in the editor. A server renames in +# the documents it has seen (zls's store), so before a query whose answer can +# reach other files the client tells it of every open file of its language. +# Here main.rs is opened by a look, asking the server nothing, and the rename +# is asked from lib.rs: the mock answers in each document it was told of, so +# the preview lists main.rs's two calls and lib.rs's definition. +file lib.rs // main.rs\npub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n +file main.rs mod lib;\n\nfn main() {\n let x = add(1, 2);\n let y = add(3, 4);\n let _ = x + y;\n}\n +start 25 100 lib.rs +wait 8000 pub fn add +stable 700 20000 +press right @main.rs 4 +release right @main.rs 4 +wait 8000 let x = add +stable 700 15000 +snap opened +# into lib.rs's `add`, and rename it from there +press left @add 5 +release left @add 5 +stable 400 5000 +key space +key l +key r +stable 400 5000 +text sum +key enter +wait 10000 sum +stable 700 15000 +snap previewed |
