summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 09:05:35 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit0d04bc9214cccce42d74862b31376d49bdda3956 (patch)
treeb45286fe0847b08c76e6abd562f6e2f985322e17 /src/pardes.zig
parent66c0106691e4de925011f314200e85ee265b7861 (diff)
downloadpardes-0d04bc9214cccce42d74862b31376d49bdda3956.tar.gz
pardes-0d04bc9214cccce42d74862b31376d49bdda3956.zip
Let s, S and | pressed in a tag answer for the tag's own text
A tag is a text like the body, so selecting on a regex and piping act on the text they are pressed in: a prompt records the text it answers for, the tag keeps the keyboard while it is typed, and a header's prompt goes on the active pane's band. `/` still searches the body from anywhere, as acme's Look from a tag searches the body. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig77
1 files changed, 63 insertions, 14 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index 3194b059..65f3aebb 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -2911,6 +2911,25 @@ test "a header's execute key runs the word under its cursor and lets go of the k
try std.testing.expectEqual(@as(usize, 1), p.col_n[0]);
}
+test "s in a header selects in the header's own text" {
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 12, .tty_only = true });
+ defer p.deinit();
+ const pane = try p.setTestFile("body col\n");
+ tagline.enterHeader(p, null);
+ p.update(.{ .key = .{ .cp = 'x' } });
+ p.update(.{ .key = .{ .cp = 's' } });
+ try std.testing.expect(pane.prompt == .search and pane.prompt_for == .workspace);
+ // the prompt takes the keys while the header keeps the keyboard
+ p.update(.{ .key = .{ .cp = 'c', .text = "col" } });
+ p.update(.{ .key = .{ .cp = Key.enter } });
+ try std.testing.expect(p.header_focus and pane.prompt == .none);
+ var ranges: [Text.max_selections]modal.Selection = undefined;
+ const text = try edit.flatSurface(p, &p.global_tag);
+ const got = p.global_tag.ranges(text, 0, &ranges);
+ try std.testing.expectEqual(@as(usize, 3), got.n); // Newcol, Joincol, NextColor
+ try std.testing.expectEqual(@as(u8, 0), pane.body.nsel);
+}
+
test "Enter in a header runs the word under its cursor, as the execute key does" {
const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 12, .tty_only = true });
defer p.deinit();
@@ -4832,7 +4851,8 @@ pub const Pardes = struct {
return;
}
const pane = p.panes[p.active] orelse return;
- const header = tagline.focusedHeader(p);
+ // An open prompt takes the keys even when it answers for a header.
+ const header = if (pane.prompt != .none) null else tagline.focusedHeader(p);
if (header == null and pane.isTerminal() and pane.body.mode == .tty and pane.focus == .body and pane.prompt == .none) {
p.leader_on = false;
p.ctrl_w_pending = false;
@@ -4927,22 +4947,25 @@ pub const Pardes = struct {
if (header == null and pane.prompt != .none) {
if (hit(key, config.search_submit) or hit(key, config.escape)) {
const submit = hit(key, config.search_submit);
+ const target = p.promptTarget(pane);
switch (pane.prompt) {
.pipe => if (submit) Pipe.submitPipe(p, p.active),
.save => if (submit) exec.submitSave(p, p.active),
.search => if (submit)
look.submitSearch(p, p.active)
- else if (normal.selRegexArmed(pane)) |_|
- normal.applySelRegex(p, pane, "", false),
+ else if (normal.selRegexArmed(pane)) |_| if (target) |answered|
+ normal.applySelRegex(p, pane, answered, "", false),
.none, .del_side, .name => unreachable,
}
pane.closePrompt();
- pane.body.mode = .normal;
- pane.body.normal.clear();
+ if (target) |answered| {
+ answered.mode = .normal;
+ answered.normal.clear();
+ }
return;
}
edit.handleInsert(p, &pane.input, key);
- if (normal.selRegexArmed(pane)) |a| normal.applySelRegex(p, pane, a.pat, a.split);
+ if (normal.selRegexArmed(pane)) |a| if (p.promptTarget(pane)) |answered| normal.applySelRegex(p, pane, answered, a.pat, a.split);
return;
}
if (t.mode == .normal and (hit(key, config.look_key) or hit(key, config.exec_key))) {
@@ -5045,7 +5068,9 @@ pub const Pardes = struct {
return pane.cwdSlice();
}
- pub fn startPrompt(p: *Pardes, pane: *Pane, request: union(enum) {
+ /// Open a prompt on `pane`'s notice band answering for `t`: the body,
+ /// the pane's tag, or the header that has the keyboard.
+ pub fn startPrompt(p: *Pardes, pane: *Pane, t: *Text, request: union(enum) {
search: []const u8,
pipe: modal.Normal.PipeBehavior,
save,
@@ -5054,8 +5079,12 @@ pub const Pardes = struct {
.search => |marker| marker,
.save => config.save_marker,
.pipe => |how| pipe: {
- const file = pane.file orelse return;
- if (!panes.Output.fileTraits(file.output).saves) return;
+ // A body is piped only when it is a file that saves; a tag's
+ // or a header's text is its own.
+ if (t.what == .body) {
+ const file = pane.file orelse return;
+ if (!panes.Output.fileTraits(file.output).saves) return;
+ }
break :pipe Pipe.pipeMarker(how);
},
};
@@ -5064,7 +5093,16 @@ pub const Pardes = struct {
const dir = if (request == .save) paneDir(pane) else "";
const slash: []const u8 = if (request == .save and (dir.len == 0 or dir[dir.len - 1] != '/')) "/" else "";
const answer = std.mem.concat(p.gpa, u8, &.{ marker, dir, slash }) catch return;
- pane.focus = .body;
+ // The keyboard comes back to the text answered for; a prompt for
+ // the body takes it out of a tag or header.
+ switch (t.what) {
+ .body => {
+ pane.focus = .body;
+ if (p.panes[p.active] == pane) tagline.exitHeader(p);
+ },
+ .tag => pane.focus = .tag,
+ .column, .workspace, .input => {},
+ }
// Not closePrompt: an s/S snapshot was taken for this very prompt.
if (pane.input.own) |old| p.gpa.free(old);
pane.prompt = switch (request) {
@@ -5072,11 +5110,22 @@ pub const Pardes = struct {
.pipe => |how| .{ .pipe = how },
.save => .save,
};
+ pane.prompt_for = t.what;
pane.input = .{ .what = .input, .own = answer, .mode = .insert, .cur_pinned = true, .cur_col = @intCast(answer.len) };
- // Whatever mode the body was in, it is in normal mode once the
+ // Whatever mode the text was in, it is in normal mode once the
// prompt is answered or dropped.
- pane.body.mode = .normal;
- pane.body.normal.clear();
+ t.mode = .normal;
+ t.normal.clear();
+ }
+
+ /// The text `pane`'s open prompt answers for, if it is still there.
+ pub fn promptTarget(p: *Pardes, pane: *Pane) ?*Text {
+ return switch (pane.prompt_for) {
+ .body => &pane.body,
+ .tag => &pane.tag,
+ .column, .workspace => if (tagline.focusedHeader(p)) |h| (if (h.what == pane.prompt_for) h else null) else null,
+ .input => null,
+ };
}
pub const SavePath = Effect.Buf(effect_path_cap);
@@ -6308,7 +6357,7 @@ test "a prompt chip is a band with its caret, still on the grid, and only the ch
// the chip ends at the pane's right edge -- so this is the frame that
// used to put it one column past the pane, off the grid and past what the
// detached wire will encode.
- p.startPrompt(pane, .{ .search = "/" });
+ p.startPrompt(pane, &pane.body, .{ .search = "/" });
_ = try p.render(frame.allocator());
const r = p.rects[id];
const cursor = p.surface.cursor orelse return error.MissingPromptCursor;