summaryrefslogtreecommitdiff
path: root/src/normal.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 16:29:36 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit79cdf76fd2dd0ba03622bfe742b525b770d51684 (patch)
tree8f0d73a0dfdb3f39839d0b5be9dff9af0473638a /src/normal.zig
parent7ce5d94b5d48b68c86d015060ebcabe63e299e1a (diff)
downloadpardes-79cdf76fd2dd0ba03622bfe742b525b770d51684.tar.gz
pardes-79cdf76fd2dd0ba03622bfe742b525b770d51684.zip
K and Alt-K keep or drop the ranges a regex matches in; Alt-: turns every range forward
K and Alt-K share the s/S prompt, which now carries a mode rather than a split flag: a range stays when a match starts inside it (K) or when none does (Alt-K), the primary becomes the first, and keeping none leaves the selection as it was (helix keep_or_remove_matches). Alt-: is ensure_selections_forward. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/normal.zig')
-rw-r--r--src/normal.zig58
1 files changed, 40 insertions, 18 deletions
diff --git a/src/normal.zig b/src/normal.zig
index 98ba80d6..d6c646ef 100644
--- a/src/normal.zig
+++ b/src/normal.zig
@@ -69,27 +69,38 @@ pub fn multiOnce(p: *Pardes) bool {
return p.multi_first;
}
-fn startSelRegex(p: *Pardes, pane: *Pane, t: *Text, split: bool) void {
+fn startSelRegex(p: *Pardes, pane: *Pane, t: *Text, mode: modal.Normal.RegexMode) void {
const text = edit.flatSurface(p, t) catch return;
const got = t.ranges(text, 0, &pane.sel_snap);
pane.nsel_snap = @intCast(got.n);
pane.sel_snap_pri = @intCast(got.pri);
pane.sel_snap_expl = (t.vsel.active and t.vsel.explicit) or t.msel.active;
- p.startPrompt(pane, t, .{ .search = if (split) config.split_marker else config.select_marker });
+ p.startPrompt(pane, t, .{ .search = regexMarker(mode) });
}
-pub fn selRegexArmed(pane: *Pane) ?struct { pat: []const u8, split: bool } {
+fn regexMarker(mode: modal.Normal.RegexMode) []const u8 {
+ return switch (mode) {
+ .select => config.select_marker,
+ .split => config.split_marker,
+ .keep => config.keep_marker,
+ .remove => config.remove_marker,
+ };
+}
+
+pub fn selRegexArmed(pane: *Pane) ?struct { pat: []const u8, mode: modal.Normal.RegexMode } {
if (pane.prompt != .search or pane.nsel_snap == 0) return null;
const armed = pane.input.own orelse "";
- const split = std.mem.startsWith(u8, armed, config.split_marker);
- if (!split and !std.mem.startsWith(u8, armed, config.select_marker)) return null;
+ const mode: modal.Normal.RegexMode = for (std.enums.values(modal.Normal.RegexMode)) |m| {
+ if (std.mem.startsWith(u8, armed, regexMarker(m))) break m;
+ } else return null;
const slash = std.mem.indexOfScalar(u8, armed, '/') orelse return null;
- return .{ .pat = armed[slash + 1 ..], .split = split };
+ return .{ .pat = armed[slash + 1 ..], .mode = mode };
}
-/// Select (or split on) `pat` inside the selections `t` had when s/S was
-/// pressed; the prompt's snapshot of them lives on `pane`.
-pub fn applySelRegex(p: *Pardes, pane: *Pane, t: *Text, pat: []const u8, split: bool) void {
+/// Select (or split on, keep or remove by) `pat` inside the selections `t`
+/// had when s/S/K/Alt-K was pressed; the prompt's snapshot of them lives on
+/// `pane`.
+pub fn applySelRegex(p: *Pardes, pane: *Pane, t: *Text, pat: []const u8, mode: modal.Normal.RegexMode) void {
const text = edit.flatSurface(p, t) catch return;
const snap = pane.sel_snap[0..pane.nsel_snap];
var out: [Text.max_selections]modal.Selection = undefined;
@@ -108,6 +119,17 @@ pub fn applySelRegex(p: *Pardes, pane: *Pane, t: *Text, pat: []const u8, split:
const from = @min(r.anchor, r.head);
const to = @min(@max(r.anchor, r.head), text.len);
if (from >= to) continue;
+ if (mode == .keep or mode == .remove) {
+ // helix keep_or_remove_matches: the range stays when a match
+ // starts inside it (K), or when none does (Alt-K)
+ const hit_at = re.find(hay_all, from, to, to) catch null;
+ const found = if (hit_at) |h| h.start < to else false;
+ if (found == (mode == .keep) and m < Text.max_selections) {
+ out[m] = r;
+ m += 1;
+ }
+ continue;
+ }
// Searched as sam searches, the way addr does (regexp.zig): the
// text around the selection still says where its lines begin.
var at = from;
@@ -115,7 +137,7 @@ pub fn applySelRegex(p: *Pardes, pane: *Pane, t: *Text, pat: []const u8, split:
while (at < to and m < Text.max_selections) {
// A search too slow to finish keeps what it found so far.
const hit_at = (re.find(hay_all, at, to, to) catch break) orelse break;
- if (split) {
+ if (mode == .split) {
out[m] = .{ .anchor = piece, .head = hit_at.start };
m += 1;
piece = hit_at.end;
@@ -126,7 +148,7 @@ pub fn applySelRegex(p: *Pardes, pane: *Pane, t: *Text, pat: []const u8, split:
// an empty match would otherwise never advance
at = if (hit_at.end > hit_at.start) hit_at.end else hit_at.end + 1;
}
- if (split and piece < to and m < Text.max_selections) {
+ if (mode == .split and piece < to and m < Text.max_selections) {
out[m] = .{ .anchor = piece, .head = to };
m += 1;
}
@@ -542,7 +564,7 @@ fn executeNormalAction(p: *Pardes, t: *Text, semantic: modal.Normal.Action) void
},
// s, S and | answer for the text they are pressed in; `/` searches
// the body wherever it is pressed, as acme's Look from a tag does.
- .select_regex => |split| return startSelRegex(p, pane, t, split),
+ .select_regex => |mode| return startSelRegex(p, pane, t, mode),
.edit => |op| switch (op.kind) {
.delete => return edit.normalDelete(p, t, true),
.delete_noyank => return edit.normalDelete(p, t, false),
@@ -795,20 +817,20 @@ test "s and S search a multi-line selection as addr does: ^ and $ per line, . wi
defer p.deinit();
const text = "alpha x\nbeta alpha\nalphabet\n";
const pane = try p.setTestFile(text);
- const Case = struct { pat: []const u8, split: bool, want: []const [2]usize };
+ const Case = struct { pat: []const u8, mode: modal.Normal.RegexMode, want: []const [2]usize };
for ([_]Case{
// ^ at each line's start, not only the selection's
- .{ .pat = "^alpha", .split = false, .want = &.{ .{ 0, 5 }, .{ 19, 24 } } },
+ .{ .pat = "^alpha", .mode = .select, .want = &.{ .{ 0, 5 }, .{ 19, 24 } } },
// $ at each line's end
- .{ .pat = "alpha$", .split = false, .want = &.{.{ 13, 18 }} },
+ .{ .pat = "alpha$", .mode = .select, .want = &.{.{ 13, 18 }} },
// . stops at the newline: one selection per line, not one for all
- .{ .pat = "a.*", .split = false, .want = &.{ .{ 0, 7 }, .{ 11, 18 }, .{ 19, 27 } } },
+ .{ .pat = "a.*", .mode = .select, .want = &.{ .{ 0, 7 }, .{ 11, 18 }, .{ 19, 27 } } },
// S splits on a line's end
- .{ .pat = "$", .split = true, .want = &.{ .{ 0, 7 }, .{ 7, 18 }, .{ 18, 27 }, .{ 27, 28 } } },
+ .{ .pat = "$", .mode = .split, .want = &.{ .{ 0, 7 }, .{ 7, 18 }, .{ 18, 27 }, .{ 27, 28 } } },
}) |c| {
pane.sel_snap[0] = .{ .anchor = 0, .head = text.len };
pane.nsel_snap = 1;
- applySelRegex(p, pane, &pane.body, c.pat, c.split);
+ applySelRegex(p, pane, &pane.body, c.pat, c.mode);
var out: [Text.max_selections]modal.Selection = undefined;
const got = pane.body.ranges(text, 0, &out);
try std.testing.expectEqual(c.want.len, got.n);