From ec4f300060ec0c93cc920dd5562eae1e3153d8b7 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 16:46:46 -0300 Subject: gw labels the words in view and jumps to the one whose label is typed helix goto_word: the words of two or more word characters between the view's first and last lines get two-letter labels, nearest the cursor first, one forward and one back in turn (modal.jumpWords). The next two keys choose one and select it, stretching the selection to it in select mode; any other key takes the labels away. The words live on Pane.jump, and renderBody paints their labels over the body's cells in the selection colours, the one change on the render side. Co-Authored-By: Claude Opus 5.5 --- src/modal.zig | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) (limited to 'src/modal.zig') diff --git a/src/modal.zig b/src/modal.zig index 16196b4f..6371475c 100644 --- a/src/modal.zig +++ b/src/modal.zig @@ -30,6 +30,7 @@ pub const Normal = struct { goto_view_center, goto_view_bottom, goto_last_modification, + goto_word, goto_definition, goto_declaration, goto_type_definition, @@ -346,6 +347,7 @@ pub const Normal = struct { pipe_selection: PipeBehavior, search, search_step: Direction, + goto_word, macro_record, macro_replay: u32, node: NodeWalk, @@ -364,6 +366,7 @@ pub const Normal = struct { .macro_record, .macro_replay, .node, + .goto_word, .repeat_insert, => .once, .edit => |edit| switch (edit.kind) { @@ -447,6 +450,7 @@ pub const Normal = struct { if (key.has(.goto_view_top)) return resultAction(.{ .goto = .{ .target = .view_top, .count = count, .explicit_count = counted.explicit } }); if (key.has(.goto_view_center)) return resultAction(.{ .goto = .{ .target = .view_center, .count = count, .explicit_count = counted.explicit } }); if (key.has(.goto_view_bottom)) return resultAction(.{ .goto = .{ .target = .view_bottom, .count = count, .explicit_count = counted.explicit } }); + if (key.has(.goto_word)) return resultAction(.goto_word); if (key.has(.goto_last_modification)) return resultAction(.{ .goto = .{ .target = .last_modification, .count = count, .explicit_count = counted.explicit } }); if (key.has(.goto_definition)) return resultAction(.{ .lsp = .definition }); if (key.has(.goto_declaration)) return resultAction(.{ .lsp = .declaration }); @@ -1382,6 +1386,53 @@ fn wmIsPrev(t: WordTarget) bool { } /// w/b/e/W/B/E: helix word_move — each step selects the traversed span. +/// helix jump_to_word's candidates: the words of two or more word +/// characters between `start` and `end`, nearest the cursor first, taking one +/// forward and one back in turn, each over its word pointing the way it was +/// found, as many as `out` holds. +pub fn jumpWords(text: []const u8, cursor: usize, start: usize, end: usize, out: []Selection) usize { + var fwd: Selection = .{ .anchor = cursor, .head = cursor }; + var rev = fwd; + if (cursor < text.len and !isWordWhitespace(wordClassAt(text, cursor))) { + const word_end = moveWord(text, fwd, 1, .next_word_end); + if (word_end.anchor == cursor) fwd = word_end; // a word of one grapheme + const word_start = moveWord(text, rev, 1, .prev_word_start); + if (word_start.anchor == nextGrapheme(text, cursor)) rev = word_start; + } + var n: usize = 0; + while (n < out.len) { + var changed = false; + while (fwd.head < end) { + fwd = moveWord(text, fwd, 1, .next_word_end); + // the two graphemes before the head are word characters + const one = prevGrapheme(text, fwd.head); + if (fwd.head == 0 or one == 0 or wordClassAt(text, one) != .word or wordClassAt(text, prevGrapheme(text, one)) != .word) continue; + changed = true; + var from = fwd.anchor; + while (from < fwd.head and wordClassAt(text, from) != .word) from = nextGrapheme(text, from); + fwd.anchor = from; + out[n] = fwd; + n += 1; + break; + } + if (n == out.len) break; + while (rev.head > start) { + rev = moveWord(text, rev, 1, .prev_word_start); + // the two graphemes from the head are word characters + if (wordClassAt(text, rev.head) != .word or wordClassAt(text, nextGrapheme(text, rev.head)) != .word) continue; + changed = true; + var to = rev.anchor; + while (to > rev.head and wordClassAt(text, prevGrapheme(text, to)) != .word) to = prevGrapheme(text, to); + rev.anchor = to; + out[n] = rev; + n += 1; + break; + } + if (!changed) break; + } + return n; +} + pub fn moveWord(text: []const u8, r0: Selection, count: usize, target: WordTarget) Selection { const is_prev = wmIsPrev(target); if ((is_prev and r0.head == 0) or (!is_prev and r0.head == text.len)) return r0; -- cgit v1.3