summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig124
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