diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 124 |
1 files changed, 86 insertions, 38 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index 682b0e3e..07120954 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -46,7 +46,7 @@ pub const swap_enter_tab = false; // each other: they are one pair — Find matches file NAMES, Grep their CONTENTS // — and every word's start column is what a topbar click dispatches on, so // appending is the edit that leaves the bar people already know where it was. -pub const topbar_str = "Kill Newcol Tutor Debug NextColor Dump Find Grep"; +pub const topbar_str = "Kill Newcol Tutor Debug NextColor Dump Find Grep Help"; const pane_builtins_str = "Del"; const file_pane_builtins_str = "Save Del"; const pane_tail = " " ++ pane_builtins_str; @@ -100,13 +100,12 @@ 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. + // The language group, reached as `SPC l <helix's letter>` — see + // leader_path for why the prefix exists. 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 l 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, @@ -115,38 +114,54 @@ const Builtin = enum { WsSymbols, Diagnostics, WsDiagnostics, + // Introspection. A language backend that answers nothing looks exactly + // like one that is broken — from the outside, `gd` doing nothing is both + // "there is no definition" and "the analyser threw and we swallowed it". + // These two are how you tell: Lspinfo says what the backend IS, Lspwhy + // says what it just DID and where it stopped. + Lspinfo, + Lspwhy, }; /// SPC leader: ONE key path per builtin, the whole remapping surface. An /// EnumArray, so a new enum field is a compile error until it has a path. /// Groups are just shared first letters (f files, h docs, c columns, t -/// toggles, s session); `?` is Help's path and is honored at ANY depth — it -/// lists what the prefix typed so far can still reach. +/// toggles, s session, l language); `?` is Help's path and is honored at ANY +/// depth — it lists what the prefix typed so far can still reach. const leader_path = std.EnumArray(Builtin, []const u8).init(.{ .Help = "?", - // 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 whole LANGUAGE group lives under `l`, and pardes's own builtins keep + // the letters they always had — `SPC d` is Del, `SPC k` is Kill. + // + // Helix puts these on bare `<space>` letters, and an earlier pass followed + // it there, which cost `d`, `k`, `s`, `h` and the session group. That is + // the wrong trade: those five are pardes's most-pressed keys and predate + // the language work, whereas an LSP command is something you reach for + // deliberately and can afford one more keystroke. Each one still keeps + // HELIX'S OWN LETTER inside the group, so the mapping is `<space>X` -> + // `SPC l X` with nothing to re-learn but the prefix. + // + // The five GOTOS are untouched and remain exactly helix's — `gd` `gD` `gy` + // `gi` `gr`, plus `]d`/`[d` and `=`. Those never collided with anything, so + // there was never a reason to move them. + .Hover = "lk", + .Rename = "lr", + .CodeAction = "la", + .SelectRefs = "lh", + .Symbols = "ls", + .WsSymbols = "lS", + .Diagnostics = "ld", + .WsDiagnostics = "lD", + .Lspinfo = "li", + .Lspwhy = "lw", + .Del = "d", + .Kill = "k", // 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", - .Dump = "fd", - .Restore = "fr", + .Tutor = "ht", .Newcol = "cn", .Delcol = "cd", .Debug = "td", @@ -158,16 +173,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 and `c` for close (helix's own spelling - // in its window mode, and where Del landed when `d` went to diagnostics). + // `t` for the file<->terminal hop. .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 @@ -218,6 +233,15 @@ const leader_rows: [@typeInfo(Builtin).@"enum".fields.len]Leader = blk: { // forget, and a bar you read every frame should not spend width on them now // that `SPC t c` / `SPC t r` press them. NextColor stays — it is the one you // cycle repeatedly, so a click beats a three-key path. +// +// Help is LAST and is the one word that has to be here. A bare `pardes` boots +// straight into tty mode (main.zig: `args.len == 1`), where every printable +// key belongs to the shell — so SPC never reaches the leader and `SPC ?`, the +// thing that would tell you the leader exists, is exactly what you cannot +// press. Row 0 is not a pane, so a middle-click on it is dispatched before any +// pane's mode is consulted: this word works in tty mode, which is the only +// reason it earns the width. Appended rather than inserted so every existing +// word keeps its column and no golden's click coordinates move. comptime { @setEvalBranchQuota(8000); // the tokenizer walks the string one branch per byte, stringToEnum builds a map per name var it = std.mem.tokenizeScalar(u8, topbar_str, ' '); @@ -517,6 +541,11 @@ pub const Mouse = struct { kind: Kind, col: u16, row: u16, + /// Ctrl held during the click. Only the left press reads it (ctrl-click = + /// goto-definition, the one chord every editor with an LSP has); every + /// other button ignores it, because acme's button semantics are already + /// the vocabulary here and modifiers are not part of it. + ctrl: bool = false, }; pub const Key = struct { @@ -821,7 +850,7 @@ pub const Pane = struct { reply: [256]u8 = undefined, reply_len: u16 = 0, - fn cwdSlice(p: *const Pane) []const u8 { + pub fn cwdSlice(p: *const Pane) []const u8 { return p.cwd[0..p.cwd_len]; } @@ -930,7 +959,7 @@ const Drag = union(enum) { tag: struct { id: usize }, /// chorded: a 1-2/1-3 cut/paste chord fired during this left drag — /// the drag's own release is then inert - select: struct { id: usize, button: Mouse.Button, chorded: bool = false }, + select: struct { id: usize, button: Mouse.Button, chorded: bool = false, ctrl: bool = false }, }; const Rect = struct { x: u16, y: u16, w: u16, h: u16 }; @@ -2899,13 +2928,20 @@ pub const Pardes = struct { 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; + // `status` is about the BACKEND, not about a document, so it answers + // from ANY pane — a terminal, a +Search, anywhere. That matters + // precisely when the pane you are sitting in is the thing going wrong. + // Every other kind needs a real file: a terminal's rows are a + // program's output and an output buffer is our own text. + if (kind != .status) { + 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, .{ + const off = if (pane.file) |f| modal.hxOff(f.content, .{ .row = @intCast(@max(0, pane.cur_row)), .col = @intCast(@max(0, pane.cur_col)), - }); + }) else 0; p.lsp_seq +%= 1; p.lsp_wait = .{ .id = p.lsp_seq, .kind = kind, .pane = id }; p.emit(.{ .lsp = .{ @@ -4383,7 +4419,11 @@ pub const Pardes = struct { const sc: i32 = @as(i32, mcol) - @as(i32, r.x + GUTTER); const v: i32 = @as(i32, mrow) - @as(i32, r.y); pane.sel[0] = .{ .state = .dragging, .c0 = sc, .c1 = sc, .r0 = v, .r1 = v }; - p.drag = .{ .select = .{ .id = id, .button = .left } }; + // Ctrl rides on the drag rather than firing here: the + // click does not place the modal cursor until RELEASE + // (dragRelease), and a goto asked at press time would + // answer about wherever the cursor happened to be. + p.drag = .{ .select = .{ .id = id, .button = .left, .ctrl = m.ctrl } }; } }, .drag => p.dragUpdate(mcol, mrow), @@ -4564,6 +4604,12 @@ pub const Pardes = struct { // dismisses it rather than dragging the anchored span // to the click (explicit v/x keeps vim's click-extend) if (!pane.vsel.explicit) pane.vsel.active = false; + // Ctrl-click IS `gd`, asked now that the cursor has + // landed — the mouse spelling of the keyboard motion, + // through the identical request. A ctrl-DRAG still + // selects and still asks, about where it started, + // which is the same thing `gd` would answer. + if (s.ctrl) p.lspRequest(s.id, .definition, ""); } } else { // acme execute (middle) / look (right): a no-drag click @@ -5269,6 +5315,8 @@ pub const Pardes = struct { .Symbols => p.lspRequest(id, .document_symbols, ""), .Diagnostics => p.lspRequest(id, .diagnostics, ""), .WsDiagnostics => p.lspRequest(id, .workspace_diagnostics, ""), + .Lspinfo => p.lspRequest(id, .status, ""), + .Lspwhy => p.lspRequest(id, .explain, ""), // 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 |
