summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Pipe.zig63
-rw-r--r--src/config.zig4
-rw-r--r--src/modal.zig7
-rw-r--r--src/tutor.txt2
4 files changed, 71 insertions, 5 deletions
diff --git a/src/Pipe.zig b/src/Pipe.zig
index 1ad7b826..37145edf 100644
--- a/src/Pipe.zig
+++ b/src/Pipe.zig
@@ -35,6 +35,7 @@ pub fn pipeMarker(how: modal.Normal.PipeBehavior) []const u8 {
.ignore => config.pipe_marker_to,
.insert => config.pipe_marker_insert,
.append => config.pipe_marker_append,
+ .keep => config.pipe_marker_keep,
};
}
@@ -117,7 +118,13 @@ pub fn submitPipe(p: *Pardes, id: usize) void {
p.gpa.free(inputs);
return;
}
- const command_copy = p.gpa.dupe(u8, command) catch {
+ // `$` wants a verdict per selection where the runner's is all or
+ // nothing: the command runs in a subshell whose exit status becomes the
+ // output, so the runner always succeeds and the answer says which kept
+ const command_copy = (if (how == .keep)
+ std.fmt.allocPrint(p.gpa, "(\n{s}\n) >/dev/null; echo $?", .{command})
+ else
+ p.gpa.dupe(u8, command)) catch {
for (inputs) |input| p.gpa.free(@constCast(input.bytes));
p.gpa.free(inputs);
return;
@@ -202,7 +209,7 @@ fn pipeCut(wait: *const PendingPipe, content: []const u8, i: usize) ?struct { lo
const hi = @max(range.anchor, range.head);
if (hi > content.len) return null;
return switch (wait.how) {
- .replace, .ignore => .{ .lo = lo, .hi = hi },
+ .replace, .ignore, .keep => .{ .lo = lo, .hi = hi },
.insert => .{ .lo = lo, .hi = lo },
.append => .{ .lo = hi, .hi = hi },
};
@@ -260,6 +267,21 @@ pub fn pipeResponse(
if (changed) return p.reportError(wait.pane, "pipe", error.FileChangedWhileFiltering);
if (wait.how == .ignore) return;
+ if (wait.how == .keep) {
+ // helix shell_keep_pipe: the primary stays if kept, else the last
+ // kept range takes over; keeping none changes nothing
+ var kept: [panes.Text.max_selections]modal.Selection = undefined;
+ var m: usize = 0;
+ var pri: ?usize = null;
+ for (0..wait.nranges) |i| {
+ if (!std.mem.eql(u8, std.mem.trim(u8, outputs[i], " \n"), "0")) continue;
+ if (i == wait.primary) pri = m;
+ kept[m] = wait.ranges[i];
+ m += 1;
+ }
+ if (m == 0) return;
+ return t.setRanges(content, kept[0..m], &.{}, pri orelse m - 1, wait.explicit);
+ }
const n = wait.nranges;
var total_output: usize = 0;
@@ -366,6 +388,43 @@ test "| in a tag pipes the tag's own text, and undo there brings it back" {
try std.testing.expect(std.mem.startsWith(u8, pardes.tagline.curTail(pane), " Save "));
}
+test "$ keeps the selections the command exits 0 on, and a real shell gives that verdict" {
+ const gpa = std.testing.allocator;
+ const p = try Pardes.init(gpa, .{ .tty_only = true });
+ defer p.deinit();
+ while (p.nextEffect()) |_| {}
+ const pane = try p.setTestFile("aa bb cc\n");
+ const ranges = [_]modal.Selection{
+ .{ .anchor = 0, .head = 2 },
+ .{ .anchor = 3, .head = 5 },
+ .{ .anchor = 6, .head = 8 },
+ };
+ pane.body.setRanges(pane.file.?.content, &ranges, &.{}, 1, true);
+ p.update(.{ .key = .{ .cp = '$' } });
+ try std.testing.expectEqualStrings(config.pipe_marker_keep, pane.promptText().?);
+ p.update(.{ .key = .{ .cp = 'g', .text = "grep -v b" } });
+ p.update(.{ .key = .{ .cp = Key.enter } });
+ const id = nextPipeEffect(p) orelse return error.MissingPipeEffect;
+ const request = p.pipe.pipeRequest(id) orelse return error.MissingPipeRequest;
+ try std.testing.expectEqual(@as(usize, 3), request.inputs.len);
+ // the wrapper turns each run's status into its output, as the runner sees it
+ const job = try selection_pipe.Job.copy(gpa, request);
+ defer job.deinit(gpa);
+ var response = selection_pipe.runJob(gpa, std.testing.io, job);
+ defer response.deinit(gpa);
+ try std.testing.expect(response.success);
+ try std.testing.expectEqualStrings("0\n", response.outputs[0]);
+ try std.testing.expectEqualStrings("1\n", response.outputs[1]);
+ p.update(.{ .pipe_resp = .{ .id = id, .success = true, .outputs = response.outputs } });
+ try std.testing.expectEqualStrings("aa bb cc\n", pane.file.?.content);
+ var out: [panes.Text.max_selections]modal.Selection = undefined;
+ const got = pane.body.ranges(pane.file.?.content, 0, &out);
+ try std.testing.expectEqual(@as(usize, 2), got.n);
+ try std.testing.expectEqual(@as(usize, 6), out[1].anchor);
+ // the primary was dropped: the last kept range takes over
+ try std.testing.expectEqual(@as(usize, 1), got.pri);
+}
+
test "selection pipe replaces all ranges atomically and undo restores them" {
const gpa = std.testing.allocator;
const p = try Pardes.init(gpa, .{ .tty_only = true });
diff --git a/src/config.zig b/src/config.zig
index 4412ef0d..91665b14 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -266,10 +266,12 @@ pub const pipe_selection: []const Chord = &.{.{ .cp = '|' }};
pub const pipe_selection_to: []const Chord = &.{.{ .cp = '|', .alt = true }};
pub const insert_output: []const Chord = &.{.{ .cp = '!' }};
pub const append_output: []const Chord = &.{.{ .cp = '!', .alt = true }};
+pub const keep_pipe: []const Chord = &.{.{ .cp = '$' }};
// Armed inputs are parsed from their distinct, nonempty tag markers.
pub const pipe_marker_to = " |-";
pub const pipe_marker_insert = " !";
pub const pipe_marker_append = " !+";
+pub const pipe_marker_keep = " $";
pub const search_submit: []const Chord = &.{.{ .cp = Key.enter }};
// Del from the keyboard on a pane with open panes both above and below it
@@ -454,7 +456,7 @@ pub const next_long_word_start: []const Chord = &.{.{ .cp = 'W' }};
pub const prev_long_word_start: []const Chord = &.{.{ .cp = 'B' }};
pub const next_long_word_end: []const Chord = &.{.{ .cp = 'E' }};
pub const line_start: []const Chord = &.{ .{ .cp = '0' }, .{ .cp = Key.home } };
-pub const line_end: []const Chord = &.{ .{ .cp = '$' }, .{ .cp = Key.end } };
+pub const line_end: []const Chord = &.{.{ .cp = Key.end }};
pub const line_first_nonws: []const Chord = &.{.{ .cp = '^' }};
// Bare G does nothing; ge reaches the last line.
pub const goto_line: []const Chord = &.{.{ .cp = 'G' }};
diff --git a/src/modal.zig b/src/modal.zig
index aec1c579..bce941d5 100644
--- a/src/modal.zig
+++ b/src/modal.zig
@@ -128,6 +128,7 @@ pub const Normal = struct {
pipe_selection_to,
insert_output,
append_output,
+ keep_pipe,
search,
search_next,
search_prev,
@@ -196,10 +197,13 @@ pub const Normal = struct {
insert,
/// `A-!` — no stdin, and the output is appended AFTER each selection.
append,
+ /// `$` — stdin is the selection; the output is discarded, and only the
+ /// selections the command exits 0 on are kept.
+ keep,
/// Do the selections become stdin? helix's `pipe` flag.
pub fn pipes(b: PipeBehavior) bool {
- return b == .replace or b == .ignore;
+ return b == .replace or b == .ignore or b == .keep;
}
};
@@ -597,6 +601,7 @@ pub const Normal = struct {
if (key.has(.pipe_selection_to)) return resultAction(.{ .pipe_selection = .ignore });
if (key.has(.insert_output)) return resultAction(.{ .pipe_selection = .insert });
if (key.has(.append_output)) return resultAction(.{ .pipe_selection = .append });
+ if (key.has(.keep_pipe)) return resultAction(.{ .pipe_selection = .keep });
if (key.has(.search)) return resultAction(.search);
if (key.has(.search_next)) return resultAction(.{ .search_step = .forward });
if (key.has(.search_prev)) return resultAction(.{ .search_step = .backward });
diff --git a/src/tutor.txt b/src/tutor.txt
index 507ce40e..0893415e 100644
--- a/src/tutor.txt
+++ b/src/tutor.txt
@@ -461,7 +461,7 @@ typed
N frontends share ONE screen at the smallest common grid
the default 9P socket works in a daemon too
- KEYS h j k l w b e 0 $ ^ gg ge f F t T v x d c y p
+ KEYS h j k l w b e 0 gl ^ gg ge f F t T v x d c y p
i a I A o O u undo U redo
motions SELECT, so `i` types at the selection's start and
`;` collapses first