summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/body_layer.zig23
-rw-r--r--src/config.zig3
-rw-r--r--src/modal.zig51
-rw-r--r--src/normal.zig39
-rw-r--r--src/panes.zig4
-rw-r--r--src/pardes.zig2
6 files changed, 122 insertions, 0 deletions
diff --git a/src/body_layer.zig b/src/body_layer.zig
index c8e229c4..a354512f 100644
--- a/src/body_layer.zig
+++ b/src/body_layer.zig
@@ -477,6 +477,29 @@ pub fn renderBody(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, id
}
}
+ // `gw`'s labels, two letters over the start of each word
+ if (pane.jump.items.len > 0) labels: {
+ const flat = edit.flatSurface(p, &pane.body) catch break :labels;
+ const letters = config.jump_label_alphabet;
+ const style: CellStyle = .{ .fg = .{ .rgb = th.sel_fg }, .bg = .{ .rgb = th.sel_bg }, .bold = true };
+ for (pane.jump.items, 0..) |word, i| {
+ if (pane.jump_first) |first| if (i / letters.len != first) continue;
+ var at = @min(word.anchor, word.head);
+ for ([_]u8{ letters[i / letters.len], letters[i % letters.len] }) |letter| {
+ const pos = modal.positionAt(flat, at);
+ const row: i32 = @intCast(pos.row);
+ const wp = pane.wrapRow(row, @intCast(pos.col));
+ const cx = if (pane.file != null)
+ @as(i32, panes.File.gutterWidth(pane)) + panes.File.displayOffset(pane, row, wp.at, @intCast(pos.col))
+ else
+ panes.File.lineDisplayOffset(modal.lineSlice(flat, pos.row), @intCast(@max(0, wp.at)), pos.col);
+ if (wp.row >= 0 and wp.row < body_h and cx >= 0 and cx < tw)
+ s.set(tx + @as(u16, @intCast(cx)), body_y + @as(u16, @intCast(wp.row)), &.{letter}, style);
+ at = modal.nextGrapheme(flat, at);
+ }
+ }
+ }
+
// cursor: tracks the shell cursor until pinned by a click or a key
// (the tag's or a prompt's cursor wins while that is being typed into)
if (active and pane.focus == .body and (pane.prompt == .none or pane.prompt == .del_side or pane.prompt == .repl_choice)) {
diff --git a/src/config.zig b/src/config.zig
index daa40028..3739d3f4 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -490,6 +490,9 @@ pub const goto_view_top: []const Chord = &.{.{ .cp = 't' }};
pub const goto_view_center: []const Chord = &.{.{ .cp = 'c' }};
pub const goto_view_bottom: []const Chord = &.{.{ .cp = 'b' }};
pub const goto_last_modification: []const Chord = &.{.{ .cp = '.' }};
+pub const goto_word: []const Chord = &.{.{ .cp = 'w' }};
+/// the jump labels' letters, helix's jump_label_alphabet
+pub const jump_label_alphabet = "abcdefghijklmnopqrstuvwxyz";
pub const goto_definition: []const Chord = &.{.{ .cp = 'd' }};
pub const goto_declaration: []const Chord = &.{.{ .cp = 'D' }};
pub const goto_type_definition: []const Chord = &.{.{ .cp = 'y' }};
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;
diff --git a/src/normal.zig b/src/normal.zig
index f7ba3810..4459ab53 100644
--- a/src/normal.zig
+++ b/src/normal.zig
@@ -245,6 +245,44 @@ pub fn nodeSelect(p: *Pardes, pane: *Pane, t: *Text, walk: modal.Normal.NodeWalk
t.setRanges(text, out[0..m], &.{}, @min(got.pri, m - 1), expl);
}
+/// `gw` (helix goto_word): label the words in view, nearest the cursor first,
+/// with two letters each; the next two keys choose one (`jumpKey`).
+fn startJump(p: *Pardes, pane: *Pane, t: *Text) void {
+ if (t.what != .body) return;
+ const text = edit.flatSurface(p, t) catch return;
+ const cursor = modal.selectionCursor(text, t.primaryRange(text, 0));
+ const top: usize = @intCast(@max(0, pane.scroll()));
+ const start = @min(modal.lineStartOffset(text, top), text.len);
+ const end = @min(modal.lineStartOffset(text, top + pane.rows), text.len);
+ const letters = config.jump_label_alphabet.len;
+ pane.jump.resize(p.gpa, letters * letters) catch return;
+ pane.jump.shrinkRetainingCapacity(modal.jumpWords(text, cursor, start, end, pane.jump.items));
+ pane.jump_first = null;
+}
+
+/// A key while `gw`'s labels are up: a label's first letter, then its
+/// second, which selects that word; anything else takes the labels away.
+pub fn jumpKey(p: *Pardes, pane: *Pane, t: *Text, key: Key) void {
+ const letters = config.jump_label_alphabet;
+ const at = if (key.ctrl or key.alt or key.cp > 0x7f) null else std.mem.indexOfScalar(u8, letters, @intCast(key.cp));
+ const i = at orelse return pane.jump.clearRetainingCapacity();
+ const first = pane.jump_first orelse {
+ if (i * letters.len > pane.jump.items.len) return pane.jump.clearRetainingCapacity();
+ pane.jump_first = @intCast(i);
+ return;
+ };
+ const chosen = first * letters.len + i;
+ defer pane.jump.clearRetainingCapacity();
+ if (chosen >= pane.jump.items.len) return;
+ const r = pane.jump.items[chosen];
+ const text = edit.flatSurface(p, t) catch return;
+ if (!t.select) return t.setRange(text, 0, .{ .anchor = @min(r.anchor, r.head), .head = @max(r.anchor, r.head) }, true);
+ // select mode stretches the primary to the word (helix Movement::Extend)
+ const pr = t.primaryRange(text, 0);
+ const anchor = if (r.anchor < r.head) @min(r.anchor, @min(pr.anchor, pr.head)) else @max(r.anchor, @max(pr.anchor, pr.head));
+ t.setRange(text, 0, .{ .anchor = anchor, .head = r.head }, true);
+}
+
const KeyState = struct {
mode: Text.Mode,
select: bool,
@@ -706,6 +744,7 @@ fn executeNormalAction(p: *Pardes, t: *Text, semantic: modal.Normal.Action) void
if (direction == .forward) @as(i32, 1) else -1,
),
.node => |walk| return nodeSelect(p, pane, t, walk),
+ .goto_word => return startJump(p, pane, t),
.macro_record => {
const m = &p.macro;
if (m.recording == 0) {
diff --git a/src/panes.zig b/src/panes.zig
index 67954abd..7114b68e 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -154,6 +154,10 @@ pub const Pane = struct {
search_pane: ?usize = null,
search_row: ?usize = null,
look_at: ?LookSpot = null,
+ /// `gw`'s words, labelled in order while it waits for a label to be
+ /// typed (gpa-owned); `jump_first` is the label's first letter once typed.
+ jump: std.ArrayList(modal.Selection) = .empty,
+ jump_first: ?u8 = null,
sel_snap: [Text.max_selections]modal.Selection = undefined,
nsel_snap: u16 = 0,
sel_snap_pri: u16 = 0,
diff --git a/src/pardes.zig b/src/pardes.zig
index d43b38b1..6b397017 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -4395,6 +4395,7 @@ pub const Pardes = struct {
pane.body.deinit(p.gpa);
pane.tag.deinit(p.gpa);
pane.input.deinit(p.gpa);
+ pane.jump.deinit(p.gpa);
panes.Terminal.deinitEmulator(pane, p.gpa);
pane.clearCwd();
pane.fs.deinit(p.gpa);
@@ -5284,6 +5285,7 @@ pub const Pardes = struct {
if (normal.selRegexArmed(pane)) |a| if (p.promptTarget(pane)) |answered| normal.applySelRegex(p, pane, answered, a.pat, a.mode);
return;
}
+ if (t == &pane.body and pane.jump.items.len > 0) return normal.jumpKey(p, pane, t, key);
if (t.mode == .normal and p.macro.replaying == 0 and (hit(key, config.look_key) or hit(key, config.exec_key))) {
// A header's words are the workspace's and column's commands, so
// Enter runs them there, as the execute key does.