diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 198 |
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) } }); |
