summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 06:22:44 -0300
committerGabriel Schneider <[email protected]>2026-10-01 07:16:43 -0300
commit85b60dedb6bdb192abd11b2d58ca7c0e8c944b18 (patch)
tree13bc6a9f1f1551088c992a29cb365f5d0a63b239
parentfb330ce04c4eec4de317637d26310b74e2ee5923 (diff)
downloadpardes-85b60dedb6bdb192abd11b2d58ca7c0e8c944b18.tar.gz
pardes-85b60dedb6bdb192abd11b2d58ca7c0e8c944b18.zip
A Rename, references and the like tell the server of every open file of its language first, so a rename reaches files it had not seen and the preview lists them
zls renames only in the documents in its store, and syncDoc told it of a file only when a query was asked from it, so renaming in progress.zig left main.zig unchanged and broken, with no preview. For a query whose answer can reach other files (rename, references, implementation, workspace symbols, calls and type hierarchy), the host snapshots every other open file and the client syncs those its server answers for first. The mock server answers a rename in each document it was told of; lsp-rename-files opens main.rs by a look, renames from lib.rs, and its preview lists both. Co-Authored-By: Claude Opus 5.5 <[email protected]>
-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