diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-09 04:20:20 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-10 09:17:07 -0300 |
| commit | 628aa40f13e9bbd313b51ab625f193110aad8dd0 (patch) | |
| tree | e60902495e308505959361d8719c382401b88295 /src/pardes.zig | |
| parent | 0a15af8d98771180e32402e68ea844f9f377cefb (diff) | |
| download | pardes-628aa40f13e9bbd313b51ab625f193110aad8dd0.tar.gz pardes-628aa40f13e9bbd313b51ab625f193110aad8dd0.zip | |
Tab after a dot in insert mode lists what could go there
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 122 |
1 files changed, 110 insertions, 12 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index 2ea52e15..0dba0abf 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -4195,8 +4195,13 @@ pub const Pardes = struct { /// `arg` rides along only so the buffer the answer opens can record what /// was asked (a rename's new name, a symbol query) — the query itself has /// it in the effect already. + /// `row`/`col` are where the cursor was when the question was asked. Only + /// `completion` reads them, and only to undo itself: Tab diverted instead + /// of indenting, so an empty answer has to put the indent back — but only + /// if the cursor has not moved since, or four spaces appear under someone + /// who kept typing. lsp_seq: u32 = 0, - lsp_wait: ?struct { id: u32, kind: lsp.Kind, pane: usize, arg: Effect.Buf(128) } = null, + lsp_wait: ?struct { id: u32, kind: lsp.Kind, pane: usize, arg: Effect.Buf(128), row: i32 = 0, col: i32 = 0 } = null, /// One current shell-filter request. A newer submit frees and supersedes /// it; old worker answers then fail the id check. The request itself owns @@ -7445,7 +7450,14 @@ pub const Pardes = struct { .col = @intCast(@max(0, pane.cur_col)), }) else 0; p.lsp_seq +%= 1; - p.lsp_wait = .{ .id = p.lsp_seq, .kind = kind, .pane = id, .arg = .from(arg) }; + p.lsp_wait = .{ + .id = p.lsp_seq, + .kind = kind, + .pane = id, + .arg = .from(arg), + .row = pane.cur_row, + .col = pane.cur_col, + }; p.emit(.{ .lsp = .{ .id = p.lsp_seq, .kind = kind, @@ -7466,7 +7478,20 @@ pub const Pardes = struct { 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; + if (rows.len == 0) { + // No rows is a legal answer everywhere except here. Tab DIVERTED + // instead of indenting, so an empty answer would eat the keystroke + // — a dot in a comment, a dot in a string, a half-typed line + // nothing can be made of — and a Tab that silently does nothing is + // worse than not having the feature. So the indent happens now, + // late, on the condition that nothing has moved: same pane, still + // in insert, one cursor, and the cursor still on the cell the Tab + // was pressed at. Anyone who kept typing during the query gets + // nothing rather than four spaces landing 300ms behind their hands. + if (w.kind == .completion and pane.mode == .insert and pane.nsel == 0 and + pane.cur_row == w.row and pane.cur_col == w.col) p.insertTab(pane); + return; + } const from: output_pane.Origin = .{ .query = w.kind }; const nrows = std.mem.count(u8, rows, "\n"); @@ -7484,6 +7509,44 @@ pub const Pardes = struct { 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; + // The same KIND asked again REFILLS the buffer it already opened, the + // rule runSearch has always had. It was missing here and that was + // survivable while every language query was a deliberate press: `gr` + // twice left two identical lists and you closed one. Tab after a dot + // is an ordinary typing keystroke, which turns the same bug fatal — + // measured, twenty Tabs stacked fifteen byte-identical `+Search` panes + // under the file, crushed it to one visible line, and from the + // sixteenth on freeSlot returned null and the key was eaten for the + // rest of the session with nothing said. One code path, so every kind + // that lands in a results buffer gets the fix. + // + // Unlike runSearch this does NOT key on the ARGUMENT. A search is + // identified by its pattern; a language query is asked about a + // different symbol every time with the same (usually empty) arg, so + // the arg cannot tell two lists apart and the KIND is the natural + // unit: a second `gr` replaces the first list rather than growing a + // stack of them. + for (p.panes, 0..) |slot, i| { + if (i == w.pane) continue; + const rp = slot orelse continue; + const rf = if (rp.file) |*f| f else continue; + const o = rf.output orelse continue; + if (!std.meta.eql(o.from, from)) continue; + if (!std.mem.eql(u8, std.fs.path.dirname(rf.path) orelse "", dir)) continue; + output_pane.setArg(&rf.output.?, w.arg.slice()); + // a refill that changes nothing keeps its place (runSearch's rule, + // and the same reason): re-asking about a symbol you are already + // stepping must not throw the list back to the top + const same = std.mem.eql(u8, rf.content, content); + file_pane.setContent(p, rf, content); + if (!same) rf.scroll = 0; + p.active = w.pane; + if (output_pane.traits(from).steps) { + pane.search_pane = i; + pane.search_row = null; + } + return; + } const free = p.freeSlot() orelse { p.gpa.free(content); return; @@ -7941,20 +8004,55 @@ pub const Pardes = struct { pane.cur_pinned = true; }, Key.tab => { - // helix insert_tab with a Spaces indent style: spaces to - // the next tab stop (smart-tab machinery skipped) - const pad = modal.INDENT_W - (c.col % modal.INDENT_W); - const spaces = " "; - const new = modal.insertAt(p.gpa, text, c, spaces[0..pad]) catch return; - p.setEditText(pane, new); - pane.cur_col += @intCast(pad); - pane.cur_pinned = true; - pane.ensureCursorVisible(); + // Tab straight after a `.` asks the language backend what + // could go there — an output buffer of DEFINITIONS, one row + // per candidate, not an autocomplete popup and not an + // insertion. Only where an answer is possible: a terminal, an + // output buffer or a file the backend does not speak still + // indents, because a Tab that silently does nothing is worse + // than not having the feature. The extension list stays the + // backend's (lsp.speaks); this only asks. (An answer that + // comes back EMPTY indents too, late — see lspResponse.) + // + // Never with several cursors. A language query is a + // per-KEYSTROKE action inside a per-SELECTION replay, so + // multiOnce would stop the replay dead: the other cursors + // would neither ask nor indent and the whole multicursor would + // collapse on a Tab. Every other insert key applies to all of + // them, and so does this one — by indenting. + const ln = modal.lineSlice(text, c.row); + if (!p.multi_on and c.col > 0 and c.col <= ln.len and ln[c.col - 1] == '.') dot: { + const f = pane.file orelse break :dot; + if (f.output != null or !lsp.speaks(f.path)) break :dot; + return p.lspRequest(p.active, .completion, ""); + } + p.insertTab(pane); }, else => {}, } } + /// helix insert_tab with a Spaces indent style: spaces to the next tab + /// stop (smart-tab machinery skipped). It is a function rather than the + /// five lines it used to be inside the Tab prong because Tab after a `.` + /// asks the language backend FIRST and indents only if the answer comes + /// back empty — which happens on another turn of the loop entirely, so + /// lspResponse needs to be able to press the same key. + fn insertTab(p: *Pardes, pane: *Pane) void { + const eb = p.editText(pane, pane.cur_row, pane.cur_row, pane.cur_col) orelse return; + const c: modal.Cursor = .{ + .row = @intCast(@max(0, pane.cur_row - eb.row0)), + .col = @intCast(@max(0, pane.cur_col)), + }; + const pad = modal.INDENT_W - (c.col % modal.INDENT_W); + const spaces = " "; + const new = modal.insertAt(p.gpa, eb.text, c, spaces[0..pad]) catch return; + p.setEditText(pane, new); + pane.cur_col += @intCast(pad); + pane.cur_pinned = true; + pane.ensureCursorVisible(); + } + const Bounds = struct { lo_row: i32, lo_col: i32, hi_row: i32, hi_col: i32 }; /// a range's two cells, normalized to document order |
