diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 09:05:35 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 0d04bc9214cccce42d74862b31376d49bdda3956 (patch) | |
| tree | b45286fe0847b08c76e6abd562f6e2f985322e17 /src/pardes.zig | |
| parent | 66c0106691e4de925011f314200e85ee265b7861 (diff) | |
| download | pardes-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.zig | 77 |
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; |
