summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lsp/lsp.zig26
-rw-r--r--src/lsp/lsp_client.zig14
-rw-r--r--src/pardes.zig52
3 files changed, 83 insertions, 9 deletions
diff --git a/src/lsp/lsp.zig b/src/lsp/lsp.zig
index 43f94f95..33e95570 100644
--- a/src/lsp/lsp.zig
+++ b/src/lsp/lsp.zig
@@ -31,8 +31,34 @@ pub const Req = struct {
offset: u32,
arg: []const u8 = "",
root: []const u8 = "",
+ /// For `explain`: the query it narrates, read from `arg` (`<kind>
+ /// <arg>`) by `explaining`; a definition when none was asked before.
+ explains: Kind = .definition,
};
+/// 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 {
+ if (req.kind != .explain) return req;
+ var r = req;
+ const sp = std.mem.indexOfScalar(u8, req.arg, ' ') orelse req.arg.len;
+ const named = std.meta.stringToEnum(Kind, req.arg[0..sp]) orelse .definition;
+ r.explains = if (named == .explain or named == .status) .definition else named;
+ r.arg = if (sp < req.arg.len) req.arg[sp + 1 ..] else "";
+ return r;
+}
+
+test "an explain request narrates the query named in its arg, a definition by default" {
+ const base: Req = .{ .kind = .explain, .path = "/x.zig", .source = "", .offset = 0 };
+ var r = base;
+ r.arg = "hover ";
+ try std.testing.expectEqual(Kind.hover, explaining(r).explains);
+ r.arg = "rename newName";
+ try std.testing.expectEqual(Kind.rename, explaining(r).explains);
+ try std.testing.expectEqualStrings("newName", explaining(r).arg);
+ try std.testing.expectEqual(Kind.definition, explaining(base).explains);
+}
+
/// Every language, Zig's (zls) included, through the protocol client, which
/// runs its server as a child process. None on the web or the board.
const backends = if (@import("pardes_config").platform == .web or @import("pardes_config").platform == .esp32p4)
diff --git a/src/lsp/lsp_client.zig b/src/lsp/lsp_client.zig
index 7704d0ad..0c5e3b7e 100644
--- a/src/lsp/lsp_client.zig
+++ b/src/lsp/lsp_client.zig
@@ -405,8 +405,9 @@ const Trace = struct {
// ------------------------------------------------------------------- query
-pub fn query(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: lsp.Req, out: *std.Io.Writer) !void {
+pub fn query(gpa: std.mem.Allocator, arena: std.mem.Allocator, asked: lsp.Req, out: *std.Io.Writer) !void {
_ = gpa;
+ const req = lsp.explaining(asked);
if (req.kind == .status) return status(req, out);
var tr: Trace = .{ .on = req.kind == .explain };
@@ -443,8 +444,8 @@ pub fn query(gpa: std.mem.Allocator, arena: std.mem.Allocator, req: lsp.Req, out
fn traceOut(tr: *const Trace, req: lsp.Req, out: *std.Io.Writer, rows: usize, us: u64) std.Io.Writer.Error!void {
if (req.kind != .explain) return;
- try out.print("lsp explain — the definition query at byte {d} of {s}\n\n", .{
- req.offset, if (req.path.len == 0) "(no file)" else std.fs.path.basename(req.path),
+ try out.print("lsp explain — the {s} query at byte {d} of {s}\n\n", .{
+ @tagName(req.explains), req.offset, if (req.path.len == 0) "(no file)" else std.fs.path.basename(req.path),
});
try out.writeAll(tr.buf[0..tr.len]);
if (hideTime())
@@ -488,9 +489,10 @@ fn answer(arena: std.mem.Allocator, si: usize, req: lsp.Req, out: *std.Io.Writer
}
fn run(c: *Conn, si: usize, arena: std.mem.Allocator, req: lsp.Req, out: *std.Io.Writer, tr: *Trace) Err!void {
- // `explain` narrates the definition query — dispatch below on the
- // effective kind so the trace follows the code `gd` really runs.
- const kind: lsp.Kind = if (req.kind == .explain) .definition else req.kind;
+ // `explain` narrates the last query asked (a definition, `gd`, when
+ // none was) — dispatch below on the effective kind so the trace
+ // follows the code that query really runs.
+ const kind: lsp.Kind = if (req.kind == .explain) req.explains else req.kind;
// Both arg-taking kinds are useless without one, and an empty
// workspace/symbol query means "every symbol in the project".
diff --git a/src/pardes.zig b/src/pardes.zig
index eb9aa11f..f0d22d57 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -1112,6 +1112,36 @@ test "first document fallback splits like Tty from the originating pane" {
};
}
+test "Lspwhy after a Hover asks to explain that hover, at its place" {
+ if (!lsp.supports.contains(.hover)) return error.SkipZigTest;
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ const pane = try p.setTestFile("const a = 1;\nconst b = a;\n");
+ const f = &pane.file.?;
+ p.gpa.free(f.path);
+ f.path = try p.gpa.dupe(u8, "/tmp/x.zig");
+ while (p.nextEffect()) |_| {}
+ pane.body.cur_row = 1;
+ pane.body.cur_col = 10;
+ p.lspRequest(0, .hover, "");
+ var hover_at: u32 = 0;
+ while (p.nextEffect()) |e| if (e == .lsp) {
+ hover_at = e.lsp.offset;
+ };
+ pane.body.cur_row = 0;
+ pane.body.cur_col = 0;
+ p.lsp_wait = null;
+ p.lspRequest(0, .explain, "");
+ var seen = false;
+ while (p.nextEffect()) |e| if (e == .lsp) {
+ try std.testing.expectEqual(lsp.Kind.explain, e.lsp.kind);
+ try std.testing.expectEqualStrings("hover ", e.lsp.arg.slice());
+ try std.testing.expectEqual(hover_at, e.lsp.offset);
+ seen = true;
+ };
+ try std.testing.expect(seen);
+}
+
test "the Tutor pane is named +Tutor in the session's directory, never a bare path at the root" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
@@ -3553,7 +3583,9 @@ pub const Effect = union(enum) {
write_dump,
set_clipboard,
read_clipboard,
- lsp: struct { id: u32, kind: lsp.Kind, pane: u8, offset: u32, arg: Buf(128) },
+ /// `arg` up to 128 bytes, or for `explain` the query it explains,
+ /// `<kind> <arg>`.
+ lsp: struct { id: u32, kind: lsp.Kind, pane: u8, offset: u32, arg: Buf(160) },
/// Snapshot the matching request with pipeRequest(id), then run it away
/// from the UI/event loop and answer with pipe_resp.
pipe: struct { id: u32 },
@@ -4351,6 +4383,9 @@ pub const Pardes = struct {
can_ask: bool = false,
lsp_seq: u32 = 0,
+ /// The last query asked (Lspwhy explains it): its kind, argument, and
+ /// the pane and place it was asked at.
+ lsp_last: ?struct { kind: lsp.Kind, arg: Effect.Buf(128), serial: u32, offset: u32 } = null,
lsp_wait: ?struct {
id: u32,
kind: lsp.Kind,
@@ -6295,6 +6330,17 @@ pub const Pardes = struct {
.row = @intCast(@max(0, pane.body.cur_row)),
.col = @intCast(@max(0, pane.body.cur_col)),
}) else 0;
+ // Lspwhy asks the last query again, narrated: its kind and argument,
+ // at its place when asked from its pane. None yet: a definition.
+ var explained: [160]u8 = undefined;
+ var sent = arg;
+ var at = off;
+ if (kind == .explain) {
+ if (p.lsp_last) |last| {
+ sent = std.fmt.bufPrint(&explained, "{s} {s}", .{ @tagName(last.kind), last.arg.slice() }) catch "";
+ if (last.serial == pane.serial) at = last.offset;
+ }
+ } else if (kind != .status) p.lsp_last = .{ .kind = kind, .arg = .from(arg), .serial = pane.serial, .offset = @intCast(off) };
p.lsp_seq +%= 1;
// A 9P write that asked it is answered once the answer is in.
if (p.fs.serving) p.fs.lsp_answer_at = turn.lsp_answers;
@@ -6312,8 +6358,8 @@ pub const Pardes = struct {
.id = p.lsp_seq,
.kind = kind,
.pane = @intCast(id),
- .offset = @intCast(off),
- .arg = .from(arg),
+ .offset = @intCast(at),
+ .arg = .from(sent),
} });
}