summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig198
1 files changed, 192 insertions, 6 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index e29c3662..32de2f1b 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -17,6 +17,7 @@ const syntax = @import("syntax.zig");
const tracy = @import("tracy.zig");
pub const image = @import("image.zig");
pub const dump = @import("dump.zig");
+pub const lsp = @import("lsp.zig");
const SYNTAX_CONTEXT_BEFORE_ROWS: usize = 0;
const SYNTAX_CONTEXT_AFTER_ROWS: usize = 2;
@@ -99,6 +100,21 @@ const Builtin = enum {
Up,
Right,
Toggleterm,
+ // The language group. Every one of these is a helix `<space>` command and
+ // keeps helix's key; the pardes builtins that used to sit on k/d/s moved
+ // (see leader_path). They are builtins rather than bare keys for the same
+ // reason Save is one: the word is executable wherever it appears, so a
+ // middle-click on `Hover` in a tag does what SPC k does. The five GOTOS
+ // are not here — helix binds them under `g` as motions, and a motion has
+ // no business being a word you can click.
+ Hover,
+ Rename,
+ CodeAction,
+ SelectRefs,
+ Symbols,
+ WsSymbols,
+ Diagnostics,
+ WsDiagnostics,
};
/// SPC leader: ONE key path per builtin, the whole remapping surface. An
@@ -108,14 +124,29 @@ const Builtin = enum {
/// lists what the prefix typed so far can still reach.
const leader_path = std.EnumArray(Builtin, []const u8).init(.{
.Help = "?",
- .Del = "d",
- .Kill = "k",
+ // HELIX OWNS THESE EIGHT LETTERS. The LSP commands keep the exact keys
+ // helix gives them, which is the whole point of pardes's modal layer being
+ // helix's — a muscle-memory map with three exceptions is not a map. What
+ // moved is ours: Kill k->q, Del d->wc (it closes a window, and helix's
+ // window group already spells close `c`), Dump/Restore s?->f? (writing and
+ // reading a session file is a FILE operation), Tutor ht->T (freeing `h`).
+ .Hover = "k",
+ .Rename = "r",
+ .CodeAction = "a",
+ .SelectRefs = "h",
+ .Symbols = "s",
+ .WsSymbols = "S",
+ .Diagnostics = "d",
+ .WsDiagnostics = "D",
+ .Kill = "q",
+ .Tutor = "T",
// the `f` file group (spacemacs): Save left vim's `w` to join Find here,
// which frees `w` for the window group (SPC w h/j/k/l) to move into.
.Save = "fs",
.Find = "ff",
.Grep = "fg",
- .Tutor = "ht",
+ .Dump = "fd",
+ .Restore = "fr",
.Newcol = "cn",
.Delcol = "cd",
.Debug = "td",
@@ -127,16 +158,16 @@ const leader_path = std.EnumArray(Builtin, []const u8).init(.{
.Petscii = "tp",
.Palette = "tl",
.Ascii = "ta",
- .Dump = "sd",
- .Restore = "sr",
// the `w` window group `Save` vacated: the four directional focus moves
// the Ctrl-w prefix does, spelled h/j/k/l because focus IS a motion, plus
- // `t` for the file<->terminal hop.
+ // `t` for the file<->terminal hop and `c` for close (helix's own spelling
+ // in its window mode, and where Del landed when `d` went to diagnostics).
.Left = "wh",
.Down = "wj",
.Up = "wk",
.Right = "wl",
.Toggleterm = "wt",
+ .Del = "wc",
});
/// The leader tree, flattened: one row per builtin SORTED BY PATH, so every
@@ -147,6 +178,9 @@ const leader_path = std.EnumArray(Builtin, []const u8).init(.{
/// rendering, padded here so Help is a filter over an array of lines.
const Leader = struct { path: []const u8, cmd: Builtin, line: []const u8 };
const leader_rows: [@typeInfo(Builtin).@"enum".fields.len]Leader = blk: {
+ // the insertion sort below is O(n²) comptime branches, and n grew with the
+ // language group
+ @setEvalBranchQuota(8000);
var rows: [@typeInfo(Builtin).@"enum".fields.len]Leader = undefined;
// the widest spaced-out path ("h t" = 3), so the names line up
var keyw: usize = 0;
@@ -529,6 +563,12 @@ pub const Event = union(enum) {
resize: struct { cols: u16, rows: u16 },
output: struct { pane: u8, bytes: []const u8 },
eof: struct { pane: u8 },
+ /// a language query the shell ran on a worker has finished. `rows` is
+ /// `+Search`-format text (see lsp.zig) and is borrowed for this call only,
+ /// exactly like `output` bytes. An id the core no longer recognises is a
+ /// stale answer (the pane was closed, or a newer query superseded it) and
+ /// is dropped.
+ lsp_resp: struct { id: u32, rows: []const u8 },
paste: []const u8,
pinch: f32,
touch_scroll: f32,
@@ -551,6 +591,11 @@ pub const Effect = union(enum) {
write_dump,
/// the yank register changed; the shell reads it off the core (OSC 52 out)
set_clipboard,
+ /// answer a language query OFF the event loop and post the rows back as an
+ /// `lsp_resp` Event. The shell reads the file's path and content off the
+ /// core (like save_file) and must SNAPSHOT them before the worker starts —
+ /// the core keeps editing while this is in flight.
+ lsp: struct { id: u32, kind: lsp.Kind, pane: u8, offset: u32, arg: Buf(128) },
quit,
fn Buf(comptime n: usize) type {
@@ -726,6 +771,10 @@ pub const Pane = struct {
search_at: u16 = 0,
search_pane: ?usize = null,
search_row: ?usize = null,
+ /// which language query filled search_pane, or null when a plain `/`,
+ /// Find or Grep did. Read only by `]d`/`[d`, which step the diagnostics
+ /// list when it is already up and ask for one when it is not.
+ search_kind: ?lsp.Kind = null,
/// the editable tag tail: seeded with the default builtins on first touch;
/// edited in place by a modal one-line editor sharing the pane's mode
tag_tail: std.ArrayList(u8) = .empty,
@@ -962,6 +1011,14 @@ pub const Pardes = struct {
/// press during an active middle select-drag; consumed on middle release
chord_arg: ?[]u8 = null,
+ /// The one language query in flight. ONE, deliberately: every one of these
+ /// is a keystroke the user is waiting on, so a second press means "I meant
+ /// this one" — the id bump makes the older answer stale and lspResponse
+ /// drops it. A queue would only buy the right to render an answer nobody
+ /// is waiting for any more.
+ lsp_seq: u32 = 0,
+ lsp_wait: ?struct { id: u32, kind: lsp.Kind, pane: usize } = null,
+
/// pending effects, drained by the shell after each update. Growable: one
/// update can emit unbounded writes (a big selection executed is chunked
/// into 64-byte write effects) — a fixed ring overflows and panics.
@@ -1206,6 +1263,7 @@ pub const Pardes = struct {
pane.vt.screens.active.scroll(.active);
},
.eof => |e| p.removePane(e.pane),
+ .lsp_resp => |r| p.lspResponse(r.id, r.rows),
.key => |key| p.handleKey(key),
.mouse => |m| p.handleMouse(m),
.paste => |bytes| {
@@ -2368,6 +2426,14 @@ pub const Pardes = struct {
return pointMove(pane, pl, text, range, @min(ls + (cnt - 1), modal.hxLineEndIdx(text, line)));
}
if (is(key, 't') or is(key, 'c') or is(key, 'b')) return gotoWindow(pane, pl, text, range, key.cp, cnt);
+ // helix's five LSP gotos, all under `g` and nowhere else. They ask
+ // and return; the answer arrives later as an lsp_resp Event and
+ // either jumps or opens a list (lspResponse).
+ if (is(key, 'd')) return p.lspRequest(p.active, .definition, "");
+ if (key.cp == 'D' and !key.ctrl and !key.alt) return p.lspRequest(p.active, .declaration, "");
+ if (is(key, 'y')) return p.lspRequest(p.active, .type_definition, "");
+ if (is(key, 'i')) return p.lspRequest(p.active, .implementation, "");
+ if (is(key, 'r')) return p.lspRequest(p.active, .references, "");
return;
}
if (pane.pending == 'z') {
@@ -2453,6 +2519,27 @@ pub const Pardes = struct {
return setPaneRange(pane, pl, text, r2, false);
}
if (is(key, ' ')) return p.addNewline(pane, fwd, cnt);
+ // ]d / [d: step the diagnostics list, asking for one if it is not
+ // up yet. The list is an ordinary results buffer, so "step" is the
+ // n/N stepper — `d` differs from `n` only in what it guarantees is
+ // showing. ]D / [D jump to the last/first, helix's pair.
+ if (is(key, 'd')) {
+ if (pane.search_kind == .diagnostics and p.searchStep(p.active, if (fwd) 1 else -1)) return;
+ return p.lspRequest(p.active, .diagnostics, "");
+ }
+ if (key.cp == 'D' and !key.ctrl and !key.alt) {
+ if (pane.search_kind != .diagnostics) return p.lspRequest(p.active, .diagnostics, "");
+ if (!fwd) { // [D = first
+ pane.search_row = null;
+ _ = p.searchStep(p.active, 1);
+ return;
+ }
+ const rp = p.panes[pane.search_pane orelse return] orelse return;
+ const rf = rp.file orelse return;
+ pane.search_row = std.mem.count(u8, rf.content, "\n"); // one past the last
+ _ = p.searchStep(p.active, -1);
+ return;
+ }
return;
}
// prefix setters (the count survives into the continuation)
@@ -2578,6 +2665,9 @@ pub const Pardes = struct {
if (is(key, 'J')) return p.normalJoin(pane);
if (is(key, '>')) return p.normalIndent(pane, cnt, true);
if (is(key, '<')) return p.normalIndent(pane, cnt, false);
+ // helix's `=` (format_selections) — its neighbour on the keyboard and
+ // in the keymap. Unbound in pardes until now.
+ if (is(key, '=')) return p.lspRequest(p.active, .format, "");
if (isC(key, 'a')) return p.normalAdjustNumber(pane, @intCast(cnt));
if (isC(key, 'x')) return p.normalAdjustNumber(pane, -@as(i64, @intCast(cnt)));
if (is(key, 'u')) return p.doUndo(pane);
@@ -2624,6 +2714,10 @@ pub const Pardes = struct {
const search_marker = " /";
const find_marker = " Find /";
const grep_marker = " Grep /";
+ // the two language queries that need a word typed: same tag input, same
+ // Enter, and the marker is still what says which one is armed
+ const rename_marker = " Rename /";
+ const symbol_marker = " WsSymbols /";
const Search = enum { text, find, grep };
/// `/` (and the Find builtin) on any pane: type the pattern into the tag
@@ -2656,6 +2750,8 @@ pub const Pardes = struct {
const tail = pane.tag_tail.items;
const armed = tail[@min(pane.search_at, tail.len)..];
const slash = std.mem.indexOfScalar(u8, armed, '/') orelse return;
+ if (std.mem.startsWith(u8, armed, rename_marker)) return p.lspRequest(id, .rename, armed[slash + 1 ..]);
+ if (std.mem.startsWith(u8, armed, symbol_marker)) return p.lspRequest(id, .workspace_symbols, armed[slash + 1 ..]);
const kind: Search = if (std.mem.startsWith(u8, armed, find_marker))
.find
else if (std.mem.startsWith(u8, armed, grep_marker))
@@ -2755,6 +2851,7 @@ pub const Pardes = struct {
p.active = id;
pane.search_pane = free;
pane.search_row = null;
+ pane.search_kind = null;
}
/// n/N: step to the next/previous row of this pane's results buffer and
@@ -2794,6 +2891,76 @@ pub const Pardes = struct {
return true;
}
+ /// Ask the backend something about the symbol under the cursor. Only a
+ /// real file can be asked: a terminal's rows are a program's output and an
+ /// output buffer is our own text, neither of which has a language behind
+ /// it. Unsupported kinds never get here (the keymap drops them), so a
+ /// backend that answers nothing simply never opens a buffer.
+ fn lspRequest(p: *Pardes, id: usize, kind: lsp.Kind, arg: []const u8) void {
+ if (!lsp.supports.contains(kind)) return;
+ const pane = p.panes[id] orelse return;
+ const f = pane.file orelse return;
+ if (f.output) return;
+ if (arg.len > 128) return; // the effect's arg is a Buf(128)
+ const off = modal.hxOff(f.content, .{
+ .row = @intCast(@max(0, pane.cur_row)),
+ .col = @intCast(@max(0, pane.cur_col)),
+ });
+ p.lsp_seq +%= 1;
+ p.lsp_wait = .{ .id = p.lsp_seq, .kind = kind, .pane = id };
+ p.emit(.{ .lsp = .{
+ .id = p.lsp_seq,
+ .kind = kind,
+ .pane = @intCast(id),
+ .offset = @intCast(off),
+ .arg = .from(arg),
+ } });
+ }
+
+ /// A worker answered. Rows are `+Search` format with ABSOLUTE paths, so
+ /// both dispositions below are the ordinary look path:
+ /// one row, a goto -> jump straight there (helix jumps on a single
+ /// location and shows a picker on several)
+ /// anything else -> an output buffer, which n/N already steps. That
+ /// buffer IS the picker; there was never one to write.
+ pub fn lspResponse(p: *Pardes, id: u32, rows: []const u8) void {
+ const w = p.lsp_wait orelse return;
+ if (w.id != id) return; // superseded by a newer press, or the pane died
+ p.lsp_wait = null;
+ const pane = p.panes[w.pane] orelse return;
+ if (rows.len == 0) return;
+
+ const nrows = std.mem.count(u8, rows, "\n");
+ if (w.kind.jumpsWhenSingle() and nrows == 1) {
+ const ln = std.mem.trimEnd(u8, rows, "\n");
+ var hi: usize = 0;
+ while (hi < ln.len and look.isFileChar(ln[hi])) hi += 1;
+ // push the jumplist the way helix does before navigating away
+ pane.pinCursor();
+ return p.actOnSelection(.right, w.pane, ln[0..hi], null);
+ }
+
+ const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice();
+ const content = p.gpa.dupe(u8, rows) catch return;
+ const free = p.freeSlot() orelse {
+ p.gpa.free(content);
+ return;
+ };
+ const np = p.openOutputView(free, dir, w.kind.bufferName(), content) catch {
+ p.gpa.free(content);
+ return;
+ };
+ p.placeDoc(w.pane, free, np);
+ p.active = w.pane;
+ // prose is not a list of locations: n/N over a hover blurb would step
+ // to nowhere, so only location buffers arm the stepper.
+ if (std.mem.eql(u8, w.kind.bufferName(), "+Search")) {
+ pane.search_pane = free;
+ pane.search_row = null;
+ pane.search_kind = w.kind;
+ }
+ }
+
/// n/N on a terminal pane: a MOTION over lookable tokens. Select the
/// next/prev whitespace-separated token that look.resolve can turn into a
/// file/dir (several per line: an ls row hops big.txt -> plain.txt), park
@@ -5090,6 +5257,25 @@ pub const Pardes = struct {
p.quit = true;
p.emit(.quit);
},
+ // The language builtins are all one call: ask, and let the answer
+ // land in lspResponse. Nothing here blocks or knows what a backend
+ // is — swapping backends changes lsp.query and not one line below.
+ .Hover => p.lspRequest(id, .hover, ""),
+ .CodeAction => p.lspRequest(id, .code_action, ""),
+ .SelectRefs => p.lspRequest(id, .select_refs, ""),
+ .Symbols => p.lspRequest(id, .document_symbols, ""),
+ .Diagnostics => p.lspRequest(id, .diagnostics, ""),
+ .WsDiagnostics => p.lspRequest(id, .workspace_diagnostics, ""),
+ // The two that need a word from the user, handled exactly the way
+ // Find and Grep are: an argument means run it now (a selection
+ // chorded onto the name), no argument arms the tag input and Enter
+ // submits (submitSearch).
+ .Rename, .WsSymbols => {
+ const kind: lsp.Kind = if (b == .Rename) .rename else .workspace_symbols;
+ const a = std.mem.trim(u8, arg orelse "", " \t\r\n");
+ if (a.len > 0) return p.lspRequest(id, kind, a);
+ p.startSearch(pane, if (b == .Rename) rename_marker else symbol_marker);
+ },
.Save => {
// an output buffer has no file behind it — nothing to write
if (pane.file) |f| if (!f.output) p.emit(.{ .save_file = .{ .pane = @intCast(id) } });