summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/helix-keys.md6
-rw-r--r--src/edit.zig15
-rw-r--r--test/hxcases/cases.jsonl5
-rw-r--r--test/hxcases/goldens.jsonl5
-rw-r--r--test/hxcases/parity-waivers.jsonl1
5 files changed, 27 insertions, 5 deletions
diff --git a/docs/helix-keys.md b/docs/helix-keys.md
index 23d63454..8023ba6e 100644
--- a/docs/helix-keys.md
+++ b/docs/helix-keys.md
@@ -129,7 +129,7 @@ language-backend queries, and the shell pipe.
| `n` / `N` (any pane) | MOTION, not a jump: move the SELECTION to the next / previous look-able text and open NOTHING. Enter — the look chord — on what it leaves selected is what opens it | KEEP, do not touch; not in the corpus (helix's `n`/`N` walk regex search hits). What a step selects is the pane's GRAIN (`output_pane.Grain`, read in `Pardes.lookSpanIn`): in FREE TEXT — a terminal, a file, a PDF, a prose answer buffer — the largest whitespace-delimited run `look.resolve` can act on (`look.lookableSpan`, wrapper punctuation peeled off both ends), several to a line; in a RESULTS BUFFER one stop per ROW, the largest run its head resolves as (`look.lookableLineSpan`), because a row there IS one location and the words after it are the match rather than a second place to go; in a COMMAND list the whole line. The walk is a RING across PANES: every pane that has performed a Look, most recent first (`Pardes.look_src`), then the output buffers that have not, newest first, and only when both are empty the active pane. Exhausting a pane enters the next at its first (forward) / last (backward) span and the end wraps to the start, so `N` is the exact inverse of `n`. What it lands on becomes an EXPLICIT `vsel` with the cursor on its FIRST column, in the pane the walk focuses. ONE motion in every pane kind and every buffer kind — a PDF steps the `+Search` buffer its own search filled, `n` to select the row and Enter to jump. The single thing a buffer may change is that grain, and it changes it by BEING a kind of buffer rather than by a branch: `output_pane.Traits.steps` (a list of locations) makes a step take one row at a time, and `Traits.commands` makes it take the WHOLE LINE, because a command list (`ThemeSel`/`FontSel`) holds words to run and there is no path inside `Theme gruvbox` to pick out. Tab on what `n` selected wears the theme, which is the same middle click on the row is. `]d`/`[d` are helix's diagnostic motions, a different binding, and they do still jump to each diagnostic (`docs/lsp.md`) | pardes-specific |
| insert: printable text | file: real edit; terminal: typed run splice | | helix-verified (file) |
| insert: `Enter` | newline; keeps the current full indent levels and adds one 4-space logical tab when the text before the cursor ends in `(`, `[`, `{`, or `)` (including `})`) | plain lines match `insert_newline`; delimiter heuristic is pardes-specific | helix-verified (plain) / pardes-specific (delimiter) |
-| insert: `Backspace` (+ `Shift-Backspace`) | delete prev char, joins lines at col 0 | matches `delete_char_backward`; `Ctrl-h` alias in B | helix-verified |
+| insert: `Backspace` (+ `Shift-Backspace`) | delete prev char, joins lines at col 0; in a line's leading blanks, back to the previous 4-column indent stop (a whole unit when on one; a tab still goes alone) | matches `delete_char_backward` and its dedent; `Ctrl-h` alias in B | helix-verified |
| insert: `Up` `Down` `Left` `Right` | move cursor | matches helix's "not recommended" insert arrows | helix-verified |
| `gd` `gD` `gy` `gi` `gr` | LSP definition / declaration / type-definition / implementation / references. ONE answer jumps straight there; several fill `+Search`, where n/N walk and Enter opens | in-process ZLS (`src/lsp/lsp_zls.zig`), `.zig` only — on a file the backend does not speak these do nothing at all, with no error row. Ctrl+left-click is the mouse spelling of `gd` | out of corpus |
| `]d` `[d` / `]D` `[D` | step the diagnostics list / go to its last or first; if no list is up, asking the backend for one is part of the press | | out of corpus |
@@ -397,11 +397,11 @@ Files (all in `test/hxcases/`):
`sel-regex-dot-newline` (mvzr is not the Rust regex crate: no
multi-line `^`/`$`, and `.` matches a newline).
- `parity.jsonl` — 80 further cases, used only by the parity gate below.
-- `parity-waivers.jsonl` — 13 named exemptions for the parity gate, in three
+- `parity-waivers.jsonl` — 14 named exemptions for the parity gate, in three
classes: one deliberate pardes binding (`ctrl-b-page`, since `Ctrl-b` IS the
tty toggle), eight pty VIEWPORT divergences (a terminal's view cannot scroll
below the vt's live grid bottom, so the cursor snaps into a different
- scrolloff band — no text differs), and four case texts a pty cannot hold
+ scrolloff band — no text differs), and five case texts a pty cannot hold
verbatim (a literal TAB the emulator expands, a file with no trailing
newline, an all-whitespace last row the dump trims).
- `golf.jsonl` + `golf-goldens.jsonl` — every example on helix-golf
diff --git a/src/edit.zig b/src/edit.zig
index 2dc79ab5..5ba46ca7 100644
--- a/src/edit.zig
+++ b/src/edit.zig
@@ -694,8 +694,19 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void {
Key.backspace => {
if (t.cur_col > 0) {
const line = modal.lineSlice(text, c.row);
- const prev = modal.prevGrapheme(line, c.col);
- const new = modal.deleteChar(p.gpa, text, .{ .row = c.row, .col = prev }) catch return;
+ var prev = modal.prevGrapheme(line, c.col);
+ // in leading blanks, back to the previous indent stop (helix
+ // dedent): a whole unit when on one, else the part past it
+ const lead = line[0..@min(c.col, line.len)];
+ if (std.mem.indexOfNone(u8, lead, " \t") == null and prev < line.len and line[prev] != '\t') {
+ var width: usize = 0;
+ for (lead) |ch| width += if (ch == '\t') modal.INDENT_W else 1;
+ var drop = width % modal.INDENT_W;
+ if (drop == 0) drop = modal.INDENT_W;
+ prev = c.col;
+ while (drop > 0 and prev > 0 and line[prev - 1] == ' ') : (drop -= 1) prev -= 1;
+ }
+ const new = modal.deleteSpan(p.gpa, text, .{ .row = c.row, .col = prev }, c) catch return;
setEditText(p, t, new);
t.cur_col = @intCast(prev);
t.cur_pinned = true;
diff --git a/test/hxcases/cases.jsonl b/test/hxcases/cases.jsonl
index 471d3a6c..cfd82cce 100644
--- a/test/hxcases/cases.jsonl
+++ b/test/hxcases/cases.jsonl
@@ -355,6 +355,11 @@
{"name": "ins-enter-indent", "pane": "file", "text": " foo\n", "keys": "A<ret>x<esc>"}
{"name": "ins-backspace", "pane": "file", "text": "abc\n", "keys": "lli<backspace><esc>"}
{"name": "ins-backspace-join", "pane": "file", "text": "ab\ncd\n", "keys": "ji<backspace><esc>"}
+{"name": "ins-backspace-indent-unit", "pane": "file", "text": " x\n", "keys": "8li<backspace>"}
+{"name": "ins-backspace-indent-partial", "pane": "file", "text": " x\n", "keys": "6li<backspace>"}
+{"name": "ins-backspace-indent-tab", "pane": "file", "text": "\t\tx\n", "keys": "2li<backspace>"}
+{"name": "ins-backspace-open-indent", "pane": "file", "text": " ab\n", "keys": "O<backspace>"}
+{"name": "ins-backspace-after-text", "pane": "file", "text": " a x\n", "keys": "7li<backspace>"}
{"name": "ins-ctrl-w", "pane": "file", "text": "one two\n", "keys": "A<C-w><esc>"}
{"name": "ins-ctrl-w-col0-join", "pane": "file", "text": "ab\ncd\n", "keys": "ji<C-w><esc>"}
{"name": "ins-alt-bs", "pane": "file", "text": "one two\n", "keys": "A<A-backspace><esc>"}
diff --git a/test/hxcases/goldens.jsonl b/test/hxcases/goldens.jsonl
index 516b2417..e258b485 100644
--- a/test/hxcases/goldens.jsonl
+++ b/test/hxcases/goldens.jsonl
@@ -355,6 +355,11 @@
{"name":"ins-enter-indent","text":" foo\n x\n","mode":"normal","cursor":{"row":1,"col":5},"anchor":{"row":1,"col":5}}
{"name":"ins-backspace","text":"ac\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}}
{"name":"ins-backspace-join","text":"abcd\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}}
+{"name":"ins-backspace-indent-unit","text":" x\n","mode":"insert","cursor":{"row":0,"col":4},"anchor":{"row":0,"col":4}}
+{"name":"ins-backspace-indent-partial","text":" x\n","mode":"insert","cursor":{"row":0,"col":4},"anchor":{"row":0,"col":4}}
+{"name":"ins-backspace-indent-tab","text":"\tx\n","mode":"insert","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}}
+{"name":"ins-backspace-open-indent","text":"\n ab\n","mode":"insert","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}}
+{"name":"ins-backspace-after-text","text":" a x\n","mode":"insert","cursor":{"row":0,"col":6},"anchor":{"row":0,"col":6}}
{"name":"ins-ctrl-w","text":"one \n","mode":"normal","cursor":{"row":0,"col":4},"anchor":{"row":0,"col":4}}
{"name":"ins-ctrl-w-col0-join","text":"cd\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}}
{"name":"ins-alt-bs","text":"one \n","mode":"normal","cursor":{"row":0,"col":4},"anchor":{"row":0,"col":4}}
diff --git a/test/hxcases/parity-waivers.jsonl b/test/hxcases/parity-waivers.jsonl
index 39a6a7c9..ad24eaa9 100644
--- a/test/hxcases/parity-waivers.jsonl
+++ b/test/hxcases/parity-waivers.jsonl
@@ -11,3 +11,4 @@
{"name":"comment-tab-indent","reason":"The terminal expands literal tabs to spaces before editing.","reference":{"name":"comment-tab-indent","text":"\t# alpha\n\t# beta\n","mode":"normal","cursor":{"row":1,"col":7},"anchor":{"row":0,"col":0}},"expected":{"name":"comment-tab-indent","text":" # alpha\n # beta\n","mode":"normal","cursor":{"row":1,"col":14},"anchor":{"row":0,"col":0}}}
{"name":"comment-noeol","reason":"Terminal capture restores a trailing newline that the source omits.","reference":{"name":"comment-noeol","text":"# alpha","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}},"expected":{"name":"comment-noeol","text":"# alpha\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":2}}}
{"name":"comment-only-blank","reason":"Terminal capture expands tabs and trims trailing blank rows.","reference":{"name":"comment-only-blank","text":" \n\t\n","mode":"normal","cursor":{"row":1,"col":1},"anchor":{"row":0,"col":0}},"expected":{"name":"comment-only-blank","text":" \n","mode":"normal","cursor":{"row":0,"col":3},"anchor":{"row":0,"col":0}}}
+{"name":"ins-backspace-indent-tab","reason":"The terminal expands literal tabs to spaces before editing.","reference":{"name":"ins-backspace-indent-tab","text":"\tx\n","mode":"insert","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}},"expected":{"name":"ins-backspace-indent-tab","text":" x\n","mode":"insert","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}}}