diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 16:41:37 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 78daa25e5c99cc0d71da36b6e0dcdd362b0cb284 (patch) | |
| tree | fd98816559ccd4f4a54b1993a7defa39a4f7fda2 | |
| parent | 686673bfb67ac80ed464351528d9ab1bceaa1d02 (diff) | |
| download | pardes-78daa25e5c99cc0d71da36b6e0dcdd362b0cb284.tar.gz pardes-78daa25e5c99cc0d71da36b6e0dcdd362b0cb284.zip | |
Tab after text leaves the syntax node in insert mode; Shift-Tab indents
helix smart_tab: in insert mode, Tab where text precedes the cursor on its
line moves past the end of the node the cursor is in, the parent-node end of
Alt-e; in leading blanks it indents. Only where the file has a grammar: the
reference pins smart tab off, which a file without a grammar agrees with, so
there Tab still indents. Shift-Tab is insert_tab, and the differential
harness now reads the S- modifier.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | docs/helix-keys.md | 2 | ||||
| -rw-r--r-- | src/edit.zig | 12 | ||||
| -rw-r--r-- | src/normal.zig | 6 | ||||
| -rw-r--r-- | test/hxcases/cases.jsonl | 2 | ||||
| -rw-r--r-- | test/hxcases/goldens.jsonl | 2 | ||||
| -rw-r--r-- | test/hxdiff.zig | 3 |
6 files changed, 25 insertions, 2 deletions
diff --git a/docs/helix-keys.md b/docs/helix-keys.md index bb260802..c0ffe0d7 100644 --- a/docs/helix-keys.md +++ b/docs/helix-keys.md @@ -341,6 +341,7 @@ text. | `Home` / `End` | line start / line end past-the-last-char (`goto_line_end_newline`) | file panes (terminal insert cursor rides its run, as before) | helix-verified (phase 5) | | `PageUp` / `PageDown` | cursor page up / down, col kept (clamped to line) | file panes | helix-verified (phase 5) | | `Tab` | indent to the next 4-column stop with SPACES (`insertTab`, `edit.zig`: `pad = INDENT_W - col % INDENT_W`) — no `\t` byte ever reaches the file | helix's Spaces indent style; helix smart-tab skipped. After a `.` in a file the backend speaks, Tab instead asks for completion and only indents if the answer is empty (section A) | helix-verified (phase 5) | +| insert `Tab` after text / `Shift-Tab` | in a file with a grammar, Tab after text on its line leaves the syntax node the cursor is in (to the enclosing named node's end); Shift-Tab always indents | helix `smart_tab` / `insert_tab`. The reference pins smart tab off, so a file without a grammar indents as the harness's helix does, where stock helix would do nothing there. Smart tab is checked against the installed hx on JSON (`normal.zig` test) | helix-verified (plain text), hx-checked (JSON) | | insert `Ctrl-s` | commit an undo checkpoint: `u` after the session goes back to the text as it was here, a second `u` to before the session | helix `commit_undo_checkpoint`; a snapshot pushed mid-session | helix-verified | ## C. Skipped @@ -361,7 +362,6 @@ text. | `:` | command mode | side-effects/file-ops — pardes builtins live in the tag, and `:` is bound to focusing it (section A) | | `gf` | goto file under selection | covered by pardes Enter-look | | `Ctrl-z` | suspend | pardes IS the terminal multiplexer | -| `Shift-Tab` (insert), smart-tab semantics | insert tab / smart tab | smart-tab machinery; plain Tab-inserts-tab lands in B | | `Z` (sticky view mode) | persistent view mode | marginal; `z` one-shots suffice | | `zm` (view) | align middle horizontally | marginal even with hscroll | | Select/extend mode section (prose) | `v` turns all motions into extenders, `n`/`N` keep selections | implemented for motions (phase 5: `pane.select` + the fixed anchor, differential-verified — see the `v` row in A); helix's search-`n`/`N` extension doesn't apply (pardes' `n`/`N` is the look-ring motion, which REPLACES the selection with the span it lands on) | diff --git a/src/edit.zig b/src/edit.zig index 39050b9d..01b34237 100644 --- a/src/edit.zig +++ b/src/edit.zig @@ -13,6 +13,7 @@ const body_layer = @import("body_layer.zig"); const config = @import("config.zig"); const pdf_enabled = panes.Pdf.enabled; const lsp = @import("lsp/lsp.zig"); +const syntax = @import("syntax.zig"); const platform = pardes.platform; const Pane = panes.Pane; const Text = panes.Text; @@ -929,6 +930,17 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void { p.lspRequest(p.active, .completion, ""); if (p.lsp_seq != seq) return; } + // helix smart_tab: after text, Tab leaves the syntax node the + // cursor is in; only where the file has a grammar, since the + // reference pins smart tab off, which a file without one agrees + // with. Shift-Tab always types the tab. + const after_text = std.mem.indexOfNone(u8, ln[0..@min(c.col, ln.len)], " \t") != null; + if (!key.shift and after_text and t.what == .body) smart: { + const pane = t.pane() orelse break :smart; + const f = pane.file orelse break :smart; + if (!syntax.supportsPath(f.path)) break :smart; + return normal.nodeSelect(p, pane, t, .parent_end); + } insertTab(p, t); }, else => {}, diff --git a/src/normal.zig b/src/normal.zig index 228c9c39..f9d7cf25 100644 --- a/src/normal.zig +++ b/src/normal.zig @@ -171,7 +171,7 @@ pub fn applySelRegex(p: *Pardes, pane: *Pane, t: *Text, pat: []const u8, mode: m /// which is made on the first of these keys after an edit and kept, never /// made again for a key that changed nothing. A range the tree has nothing /// to say about stays as it was; a text without a grammar, all of them. -fn nodeSelect(p: *Pardes, pane: *Pane, t: *Text, walk: modal.Normal.NodeWalk) void { +pub fn nodeSelect(p: *Pardes, pane: *Pane, t: *Text, walk: modal.Normal.NodeWalk) void { if (t.what != .body) return; const f = if (pane.file) |*file| file else return; if (f.node_tree == null or f.node_tree_revision != f.revision) { @@ -945,6 +945,8 @@ test "tree-sitter selection keys do in a JSON file what hx 25.07.1 does" { .{ .keys = "6lx\x1bid", .want = "\n" }, .{ .keys = "10l\x1beiX\x1b", .want = "{\"a\": [1, 2X, 3], \"b\": true}\n" }, .{ .keys = "10l\x1bb\x1bbiX\x1b", .want = "{X\"a\": [1, 2, 3], \"b\": true}\n" }, + // insert Tab after text leaves the node (smart tab) + .{ .keys = "8li\tX\x1b", .want = "{\"a\": [1, 2, 3]X, \"b\": true}\n" }, }) |c| { const pane = try p.setTestFile(text); p.gpa.free(pane.file.?.path); @@ -959,6 +961,8 @@ test "tree-sitter selection keys do in a JSON file what hx 25.07.1 does" { p.update(.{ .key = if (k == '>') .{ .cp = Key.right, .alt = true } else .{ .cp = k, .alt = true } }); } else if (ch == 0x1b) { p.update(.{ .key = .{ .cp = Key.escape } }); + } else if (ch == '\t') { + p.update(.{ .key = .{ .cp = Key.tab } }); } else p.update(.{ .key = .{ .cp = ch, .text = c.keys[i..][0..1] } }); while (p.nextEffect()) |_| {} } diff --git a/test/hxcases/cases.jsonl b/test/hxcases/cases.jsonl index 15e53590..f589755a 100644 --- a/test/hxcases/cases.jsonl +++ b/test/hxcases/cases.jsonl @@ -452,6 +452,8 @@ {"name": "ins-checkpoint-undo-twice", "pane": "file", "text": "ab\n", "keys": "iX<C-s>Y<esc>uu"} {"name": "ins-checkpoint-redo", "pane": "file", "text": "ab\n", "keys": "iX<C-s>Y<esc>uuU"} {"name": "msel-ins-checkpoint", "pane": "file", "text": "ab\nab\n", "keys": "CiX<C-s>Y<esc>u"} +{"name": "ins-tab-after-text-plain", "pane": "file", "text": "ab cd\n", "keys": "2li<tab>X<esc>"} +{"name": "ins-shift-tab", "pane": "file", "text": "ab cd\n", "keys": "2li<S-tab>X<esc>"} {"name": "msel-align-basic", "pane": "file", "text": "a=1\nbbb=2\n", "keys": "%s=<ret>&"} {"name": "msel-align-columns", "pane": "file", "text": "a,b,c\nccc,d,e\n", "keys": "%s,<ret>&"} {"name": "msel-align-uneven", "pane": "file", "text": "a,b\nccc,d,eee,f\n", "keys": "%s,<ret>&"} diff --git a/test/hxcases/goldens.jsonl b/test/hxcases/goldens.jsonl index 7a195980..ce1b461c 100644 --- a/test/hxcases/goldens.jsonl +++ b/test/hxcases/goldens.jsonl @@ -452,6 +452,8 @@ {"name":"ins-checkpoint-undo-twice","text":"ab\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}} {"name":"ins-checkpoint-redo","text":"Xab\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}} {"name":"msel-ins-checkpoint","text":"Xab\nXab\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1},"sels":[{"cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}},{"cursor":{"row":1,"col":1},"anchor":{"row":1,"col":1}}],"primary":1} +{"name":"ins-tab-after-text-plain","text":"ab X cd\n","mode":"normal","cursor":{"row":0,"col":5},"anchor":{"row":0,"col":5}} +{"name":"ins-shift-tab","text":"ab X cd\n","mode":"normal","cursor":{"row":0,"col":5},"anchor":{"row":0,"col":5}} {"name":"msel-align-basic","text":"a =1\nbbb=2\n","mode":"normal","cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3},"sels":[{"cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3}},{"cursor":{"row":1,"col":3},"anchor":{"row":1,"col":3}}],"primary":0} {"name":"msel-align-columns","text":"a ,b,c\nccc,d,e\n","mode":"normal","cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3},"sels":[{"cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3}},{"cursor":{"row":0,"col":5},"anchor":{"row":0,"col":5}},{"cursor":{"row":1,"col":3},"anchor":{"row":1,"col":3}},{"cursor":{"row":1,"col":5},"anchor":{"row":1,"col":5}}],"primary":0} {"name":"msel-align-uneven","text":"a ,b\nccc,d,eee,f\n","mode":"normal","cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3},"sels":[{"cursor":{"row":0,"col":3},"anchor":{"row":0,"col":3}},{"cursor":{"row":1,"col":3},"anchor":{"row":1,"col":3}},{"cursor":{"row":1,"col":5},"anchor":{"row":1,"col":5}},{"cursor":{"row":1,"col":9},"anchor":{"row":1,"col":9}}],"primary":0} diff --git a/test/hxdiff.zig b/test/hxdiff.zig index 627bbac3..48c7ab93 100644 --- a/test/hxdiff.zig +++ b/test/hxdiff.zig @@ -419,6 +419,9 @@ fn runCase(arena: std.mem.Allocator, case: Case, pane_kind: []const u8) !Result } else if (std.mem.startsWith(u8, name, "A-")) { key.alt = true; name = name[2..]; + } else if (std.mem.startsWith(u8, name, "S-")) { + key.shift = true; + name = name[2..]; } else break; } if (name.len == 1 or (name.len > 1 and name.len <= 4 and name[0] >= 0x80)) { |
