summaryrefslogtreecommitdiff
path: root/src/Pipe.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 16:15:15 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit06fe2290db7d0eb654b9a908358942cfde298e29 (patch)
tree3951919743798ec978e7ae925529c4fbd4ea130a /src/Pipe.zig
parent07a3f53993a27c886ccb3ed665acb9c8b34a2873 (diff)
downloadpardes-06fe2290db7d0eb654b9a908358942cfde298e29.tar.gz
pardes-06fe2290db7d0eb654b9a908358942cfde298e29.zip
$ keeps the selections a shell command exits 0 on, and line end is gl
$ was a second key for line end, which helix never meant: in helix it is shell_keep_pipe. It now arms the pipe prompt with a $ marker, runs the command once per range with that range's text on stdin, and keeps the ranges it exits 0 on; the primary stays if kept, else the last kept range takes over, and keeping none changes nothing. The runner's answer is all or nothing, so the command runs in a subshell whose status is echoed as its output. Line end stays on helix's own gl and End; the snapshots, tutor and docs that pressed $ for it press gl. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/Pipe.zig')
-rw-r--r--src/Pipe.zig63
1 files changed, 61 insertions, 2 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 });