summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Text.zig9
-rw-r--r--src/config.zig5
-rw-r--r--src/edit.zig5
-rw-r--r--src/look.zig2
-rw-r--r--src/modal.zig15
-rw-r--r--src/normal.zig58
-rw-r--r--src/pardes.zig4
7 files changed, 72 insertions, 26 deletions
diff --git a/src/Text.zig b/src/Text.zig
index 56ee199b..411c2ec8 100644
--- a/src/Text.zig
+++ b/src/Text.zig
@@ -328,6 +328,15 @@ pub fn multiSelAction(t: *Text, text: []const u8, kind: modal.Normal.Multi, cnt:
}
return setRanges(t, text, out[0..m], &.{}, pri, expl);
}
+ if (kind == .ensure_forward) {
+ // helix ensure_selections_forward: every range points forward
+ for (rs[0..n]) |*r| {
+ const from = @min(r.anchor, r.head);
+ r.head = @max(r.anchor, r.head);
+ r.anchor = from;
+ }
+ return setRanges(t, text, rs[0..n], &.{}, got.pri, expl);
+ }
if (kind == .split_newline) {
// helix selection::split_on_newline — one range per line the
// selection covers, the newlines themselves left out
diff --git a/src/config.zig b/src/config.zig
index bc053176..8c5c9771 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -414,6 +414,8 @@ pub const symbol_marker = " WsSymbols /";
pub const save_marker = " Save ";
pub const select_marker = " Select /";
pub const split_marker = " Split /";
+pub const keep_marker = " Keep /";
+pub const remove_marker = " Remove /";
pub const pipe_marker = " |";
pub const search_buffer = "+Search";
@@ -532,6 +534,9 @@ pub const trim_sels: []const Chord = &.{.{ .cp = '_' }};
pub const select_regex: []const Chord = &.{.{ .cp = 's' }};
pub const split_regex: []const Chord = &.{.{ .cp = 'S' }};
+pub const keep_regex: []const Chord = &.{.{ .cp = 'K' }};
+pub const remove_regex: []const Chord = &.{.{ .cp = 'K', .alt = true }};
+pub const ensure_forward: []const Chord = &.{.{ .cp = ':', .alt = true }};
pub const delete: []const Chord = &.{.{ .cp = 'd' }};
pub const delete_noyank: []const Chord = &.{.{ .cp = 'd', .alt = true }};
diff --git a/src/edit.zig b/src/edit.zig
index a373741c..5e0d20a4 100644
--- a/src/edit.zig
+++ b/src/edit.zig
@@ -148,7 +148,7 @@ pub fn applyPaste(p: *Pardes, bytes: []const u8) void {
n += 1;
};
if (n > 0) insertKey(p, &pane.input, .{ .cp = line[0], .text = line[0..n] });
- if (normal.selRegexArmed(pane)) |a| if (p.promptTarget(pane)) |t| normal.applySelRegex(p, pane, t, a.pat, a.split);
+ if (normal.selRegexArmed(pane)) |a| if (p.promptTarget(pane)) |t| normal.applySelRegex(p, pane, t, a.pat, a.mode);
return;
}
if (pane.focus == .tag) return pasteInto(p, &pane.tag, bytes);
@@ -355,7 +355,8 @@ pub fn exitInsert(p: *Pardes, t: *Text) void {
for (rs[0..got.n]) |*r| {
const from = @min(r.anchor, r.head);
const to = @max(r.anchor, r.head);
- r.* = .{ .anchor = from, .head = if (r.head > r.anchor) @max(from, modal.prevGrapheme(text, to)) else to };
+ const head = if (r.head > r.anchor) @max(from, modal.prevGrapheme(text, to)) else to;
+ r.* = .{ .anchor = from, .head = head };
}
t.setRanges(text, rs[0..got.n], &.{}, got.pri, expl);
}
diff --git a/src/look.zig b/src/look.zig
index 30fb0dab..3da2e044 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -505,7 +505,7 @@ pub fn submitSearch(p: *Pardes, id: usize) void {
if (normal.selRegexArmed(pane)) |a| {
// helix keeps the pattern in `/`
if (a.pat.len > 0) p.registers.put(p.gpa, '/', a.pat, 0, 1, true);
- return if (p.promptTarget(pane)) |t| normal.applySelRegex(p, pane, t, a.pat, a.split);
+ return if (p.promptTarget(pane)) |t| normal.applySelRegex(p, pane, t, a.pat, a.mode);
}
const armed = pane.promptText() orelse return;
const slash = std.mem.indexOfScalar(u8, armed, '/') orelse return;
diff --git a/src/modal.zig b/src/modal.zig
index 34c4dac7..f5f3081e 100644
--- a/src/modal.zig
+++ b/src/modal.zig
@@ -97,6 +97,9 @@ pub const Normal = struct {
trim_sels,
select_regex,
split_regex,
+ keep_regex,
+ remove_regex,
+ ensure_forward,
delete,
delete_noyank,
@@ -210,6 +213,8 @@ pub const Normal = struct {
}
};
+ /// what the s/S/K/Alt-K prompt does with its pattern's matches
+ pub const RegexMode = enum { select, split, keep, remove };
pub const Scope = enum { once, per_selection };
pub const Direction = enum { backward, forward };
pub const Motion = enum {
@@ -262,6 +267,7 @@ pub const Normal = struct {
merge,
merge_consecutive,
trim,
+ ensure_forward,
};
pub const Edit = enum {
delete,
@@ -314,7 +320,7 @@ pub const Normal = struct {
insert: struct { kind: Insert, count: u32 },
select: struct { kind: Select, count: u32 },
multi: struct { kind: Multi, count: u32 },
- select_regex: bool, // false = select, true = split
+ select_regex: RegexMode,
edit: struct { kind: Edit, count: u32 },
lsp: Lsp,
adjust_number: i64,
@@ -577,8 +583,11 @@ pub const Normal = struct {
if (key.has(.merge_sels)) return resultAction(.{ .multi = .{ .kind = .merge, .count = count } });
if (key.has(.merge_consecutive_sels)) return resultAction(.{ .multi = .{ .kind = .merge_consecutive, .count = count } });
if (key.has(.trim_sels)) return resultAction(.{ .multi = .{ .kind = .trim, .count = count } });
- if (key.has(.select_regex)) return resultAction(.{ .select_regex = false });
- if (key.has(.split_regex)) return resultAction(.{ .select_regex = true });
+ if (key.has(.select_regex)) return resultAction(.{ .select_regex = .select });
+ if (key.has(.split_regex)) return resultAction(.{ .select_regex = .split });
+ if (key.has(.keep_regex)) return resultAction(.{ .select_regex = .keep });
+ if (key.has(.remove_regex)) return resultAction(.{ .select_regex = .remove });
+ if (key.has(.ensure_forward)) return resultAction(.{ .multi = .{ .kind = .ensure_forward, .count = count } });
if (key.has(.delete)) return resultAction(.{ .edit = .{ .kind = .delete, .count = count } });
if (key.has(.delete_noyank)) return resultAction(.{ .edit = .{ .kind = .delete_noyank, .count = count } });
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);
diff --git a/src/pardes.zig b/src/pardes.zig
index a7bcb195..d43b38b1 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -5270,7 +5270,7 @@ pub const Pardes = struct {
.search => if (submit)
look.submitSearch(p, p.active)
else if (normal.selRegexArmed(pane)) |_| if (target) |answered|
- normal.applySelRegex(p, pane, answered, "", false),
+ normal.applySelRegex(p, pane, answered, "", .select),
.none, .del_side, .repl_choice, .name => unreachable,
}
pane.closePrompt();
@@ -5281,7 +5281,7 @@ pub const Pardes = struct {
return;
}
edit.handleInsert(p, &pane.input, key);
- if (normal.selRegexArmed(pane)) |a| if (p.promptTarget(pane)) |answered| normal.applySelRegex(p, pane, answered, a.pat, a.split);
+ if (normal.selRegexArmed(pane)) |a| if (p.promptTarget(pane)) |answered| normal.applySelRegex(p, pane, answered, a.pat, a.mode);
return;
}
if (t.mode == .normal and p.macro.replaying == 0 and (hit(key, config.look_key) or hit(key, config.exec_key))) {