diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-31 05:04:26 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:08 -0300 |
| commit | 5bf8d6dd077517270377e5d8551108ecf252374f (patch) | |
| tree | c7783fa9fc133980d3d6fff0129e8de5a1b43601 /src/config.zig | |
| parent | eefac04995ffad847a4098f16d2e82ccab16438b (diff) | |
| download | pardes-5bf8d6dd077517270377e5d8551108ecf252374f.tar.gz pardes-5bf8d6dd077517270377e5d8551108ecf252374f.zip | |
multiple cursors, regex selection, and Ctrl-c comments
The primary cursor stays exactly where it was — cur_row/cur_col plus vsel — and
sels[] holds helix's OTHER ranges. That split is why nothing moved at one
cursor: with nsel == 0 not one line of the existing motion, operator, render or
mouse code takes a different branch, which is what protects 800 differential
cases and 67 goldens.
paneRanges/setPaneRanges are the whole list; setPaneRanges IS helix's
Selection::new (min width 1, sorted, overlaps merged, primary follows its range
through a merge). An ordinary key runs the single-selection handler once per
range, visited last-first so an edit never disturbs a range still waiting, and
each finished pass is remembered as a distance from the END of the text, which
an earlier edit cannot move — helix's change mapping without a change map.
pushUndo fires once per keystroke, yanks accumulate, and a builtin acts from
the primary and stops the replay, which also closes the use-after-free window
if it frees the pane.
s and S reuse the / prompt wholesale rather than growing a second one: the
pattern is typed into the tag tail, and every keystroke re-runs the match from
the selection the prompt opened on, so the preview is live and Esc is just the
empty pattern. mvzr does runtime patterns — a bytecode VM in a fixed-size
struct with no allocator — with 64 ops and 8 char classes per pattern, no
case-insensitive flag (helix's smart case is done by folding a scratch copy),
no captures, no multi-line anchors. The last two are the two waivers.
Ctrl-c is a whole-list key and not a per-cursor replay, because helix decides
comment-vs-uncomment ONCE for the whole selection; replaying it would take that
decision n times. Comment tokens are a table in config.zig keyed on the same
extension syntax.zig picks grammars by.
Found and fixed a pre-existing single-cursor bug on the way: la<bs><esc> left
the cursor one cell before where the append began. helix's restore_cursor can
never walk past the origin; ours backed up unconditionally. hxdiff was green
before AND after — the old one-selection contract could not see it.
hxdiff 360 -> 481 cases, hxparity 440 -> 561, all goldens from real helix; the
harness contract now reports every range and its primary, omitted when there is
one, so 359 of the 360 old goldens are byte-identical. The one that moved is
o-count: helix's 2o really does leave two cursors and could not say so before.
Diffstat (limited to 'src/config.zig')
| -rw-r--r-- | src/config.zig | 62 |
1 files changed, 62 insertions, 0 deletions
diff --git a/src/config.zig b/src/config.zig index 7f83155a..8f2175f6 100644 --- a/src/config.zig +++ b/src/config.zig @@ -443,6 +443,30 @@ pub const url_schemes = [_][]const u8{ "http://", "https://" }; /// a path ending in one of these opens an image pane instead of a file pane pub const image_exts = [_][]const u8{ ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ppm", ".pgm", ".tga" }; +/// What `Ctrl-c` (comment_toggle) puts at the front of a line, by file +/// EXTENSION — which is how src/syntax.zig already tells one language from +/// another, so this is that same notion and not a second one. A pane whose +/// path matches nothing here (and every terminal, which has no path at all) +/// gets `comment_token_default`. +/// +/// `#` as the default is not a guess: it is helix's own DEFAULT_COMMENT_TOKEN, +/// which is what a helix buffer with no language configured comments with — +/// and therefore what the differential oracle answers, since the harness runs +/// with zero language configs. +/// Languages with no LINE comment at all (css, html, json, ocaml) are absent +/// on purpose: helix leaves those buffers on its default too, and inventing a +/// token for them would be a divergence nothing asked for. +pub const comment_token_default = "#"; +pub const comment_tokens: []const struct { exts: []const []const u8, token: []const u8 } = &.{ + .{ .token = "//", .exts = &.{ ".zig", ".zon", ".c", ".h", ".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx", ".rs", ".go", ".java", ".scala", ".sc", ".kt", ".kts", ".cs", ".csx", ".php", ".pas", ".pp", ".p", ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".swift", ".dart" } }, + .{ .token = "#", .exts = &.{ ".py", ".pyw", ".sh", ".bash", ".zsh", ".rb", ".rake", ".ex", ".exs", ".ps1", ".psm1", ".psd1", ".pl", ".pm", ".r", ".jl", ".nix", ".toml", ".yaml", ".yml", ".cmake", ".mk", ".tf" } }, + .{ .token = "--", .exts = &.{ ".lua", ".hs", ".lhs", ".elm", ".sql", ".adb", ".ads", ".ada" } }, + .{ .token = ";", .exts = &.{ ".clj", ".cljs", ".cljc", ".edn", ".el", ".lisp", ".scm", ".asm", ".s" } }, + .{ .token = "%", .exts = &.{ ".erl", ".hrl", ".tex", ".cls", ".sty" } }, + .{ .token = "!", .exts = &.{ ".f", ".for", ".ftn", ".f90", ".f95", ".f03", ".f08" } }, + .{ .token = "\"", .exts = &.{ ".vim", ".vimrc" } }, +}; + /// What an armed search writes into the tag tail — and the ONLY record of /// which search it is: Enter reads the marker back (submitSearch) instead of /// pardes carrying a second piece of pane state per command. The pattern is @@ -454,6 +478,12 @@ pub const find_marker = " Find /"; pub const grep_marker = " Grep /"; pub const rename_marker = " Rename /"; pub const symbol_marker = " WsSymbols /"; +/// helix `s` / `S`. The only two markers whose word is NOT a builtin — there +/// is no Select/Split command to run from a tag, they name the key that armed +/// the input so the tag still reads as what it is about to do. They also mark +/// the one input that previews as you type (pardes.zig, previewSelRegex). +pub const select_marker = " Select /"; +pub const split_marker = " Split /"; /// Output-buffer names (acme's +Errors). Cosmetic now, and deliberately so: a /// buffer is DERIVED from the command that opened it (output_pane.traits), and @@ -605,6 +635,33 @@ pub const collapse_selection: []const Chord = &.{.{ .cp = ';' }}; pub const flip_selection: []const Chord = &.{.{ .cp = ';', .alt = true }}; pub const select_all: []const Chord = &.{.{ .cp = '%' }}; +// ---- multiple cursors ---- +// +// helix's Selection is a LIST of ranges with a primary index, and these ten +// keys are the ones that act on the list rather than on the text: every other +// key is replayed once per range instead (pardes.zig, replaySels). Alt-C is +// Alt-SHIFT-c and so does not collide with pane_to_new_column's Alt-c — `hit` +// compares the codepoint, and `C` is `C`. +pub const copy_sel_below: []const Chord = &.{.{ .cp = 'C' }}; +pub const copy_sel_above: []const Chord = &.{.{ .cp = 'C', .alt = true }}; +pub const keep_primary_sel: []const Chord = &.{.{ .cp = ',' }}; +pub const remove_primary_sel: []const Chord = &.{.{ .cp = ',', .alt = true }}; +pub const rotate_sel_fwd: []const Chord = &.{.{ .cp = ')' }}; +pub const rotate_sel_back: []const Chord = &.{.{ .cp = '(' }}; +pub const split_sel_newline: []const Chord = &.{.{ .cp = 's', .alt = true }}; +pub const merge_sels: []const Chord = &.{.{ .cp = '-', .alt = true }}; +pub const merge_consecutive_sels: []const Chord = &.{.{ .cp = '_', .alt = true }}; +pub const trim_sels: []const Chord = &.{.{ .cp = '_' }}; + +// The other two list-making keys: a REGEX turns each range into many. Both +// arm the tag input above (select_marker / split_marker) instead of doing +// anything immediately, so `s` and `S` are the only normal-mode keys whose +// effect lands a keystroke later, on Enter — or live, as you type. +// `s` is free here despite `gs` (goto_first_nonws) also being `s`: a pending +// prefix is matched by the stored codepoint, never by these chords. +pub const select_regex: []const Chord = &.{.{ .cp = 's' }}; +pub const split_regex: []const Chord = &.{.{ .cp = 'S' }}; + // ---- edits ---- pub const delete: []const Chord = &.{.{ .cp = 'd' }}; @@ -628,6 +685,11 @@ pub const increment: []const Chord = &.{.{ .cp = 'a', .ctrl = true }}; pub const decrement: []const Chord = &.{.{ .cp = 'x', .ctrl = true }}; pub const undo: []const Chord = &.{.{ .cp = 'u' }}; pub const redo: []const Chord = &.{.{ .cp = 'U' }}; +/// helix `toggle_comments`: comment or uncomment every line the selection +/// touches, with `comment_tokens` above choosing the token. Ctrl-c reaches a +/// terminal pane only in NORMAL mode — raw tty forwards it to the program, +/// where it is still SIGINT. +pub const comment_toggle: []const Chord = &.{.{ .cp = 'c', .ctrl = true }}; /// Exit select mode / drop pending prefixes and the count; leave insert mode; /// abandon a tag, an armed search or the topbar. On an OUTPUT buffer there is |
