summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/host_io.zig35
-rw-r--r--src/lsp/lsp.zig15
-rw-r--r--src/lsp/lsp_client.zig8
-rw-r--r--test/lspmock.zig31
-rw-r--r--test/snapshots/lsp-rename-files.golden27
-rw-r--r--test/snapshots/lsp-rename-files.snap29
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