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