summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-06 13:54:34 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commit65bffe2c7b7fcdbd82cc112315e64e1bca8dd150 (patch)
treebf7a05fa31931d463ac0e8e8f659727016f275ee
parentde4def3548a6729b0dfd2120495a61beef8c8c2c (diff)
downloadpardes-65bffe2c7b7fcdbd82cc112315e64e1bca8dd150.tar.gz
pardes-65bffe2c7b7fcdbd82cc112315e64e1bca8dd150.zip
file search: / on a file pane types a pattern into the tag tail (the existing modal one-line editor — persistent, no popup); Enter runs grep -nH '<pat>' '<path>' in a shell in the file's dir via the existing exec path (ttyForDir + write effects), so results are ordinary persistent terminal text. n/N walk the result rows of that pane (paneCursorLines already blanks prompt/echo rows via the OSC 133 semantic marks), select the row with the modal line selection, and hand path:NNN: to the existing look (actOnSelection right) which scrolls the file pane there. Esc cancels and restores the tag. State: four Pane fields (search_edit/search_at/search_pane/search_row) — no results mirror, no highlight engine. New snap script fsearch.snap + golden; all 18 frozen goldens stay green (19/19).
-rw-r--r--src/pardes.zig108
-rw-r--r--test/snapshots/fsearch.golden310
-rw-r--r--test/snapshots/fsearch.snap63
3 files changed, 481 insertions, 0 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index c4893b25..88b10374 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -462,6 +462,14 @@ pub const Pane = struct {
vsel: CharSel = .{},
/// one pending prefix codepoint (0 = none): `g` and `z`
pending: u21 = 0,
+ /// `/` file search: the pattern is typed into the tag tail (no popup);
+ /// search_at is the tail length before " /" was appended (submit/cancel
+ /// restore it). n/N walk the grep results in pane search_pane, starting
+ /// just below search_row (the results shell's prompt row at submit).
+ search_edit: bool = false,
+ search_at: u16 = 0,
+ search_pane: ?usize = null,
+ search_row: i32 = 0,
/// the editable tag tail: seeded with the default builtins on first touch;
/// edited in place by a modal one-line editor sharing the pane's mode
tag_tail: std.ArrayList(u8) = .empty,
@@ -878,6 +886,7 @@ pub const Pardes = struct {
fn exitTagEdit(pane: *Pane) void {
pane.tag_edit = false;
pane.tag_sel = false;
+ pane.search_edit = false; // an abandoned `/` input stays as tag text
}
fn tagSelBounds(pane: *Pane) struct { lo: usize, hi: usize } {
@@ -1165,6 +1174,16 @@ pub const Pardes = struct {
} else p.enterTty(p.active);
return;
}
+ // `/` search input in flight: Enter greps, Esc abandons; both restore
+ // the tag tail and hand focus back to the body.
+ if (pane.search_edit and (is(key, Key.enter) or is(key, Key.escape))) {
+ if (is(key, Key.enter)) p.submitSearch(pane);
+ pane.tag_tail.shrinkRetainingCapacity(@min(pane.search_at, pane.tag_tail.items.len));
+ exitTagEdit(pane);
+ pane.mode = .normal;
+ pane.pending = 0;
+ return;
+ }
// normal-mode Enter/Tab: the acme chords — look at (Enter) or execute
// (Tab) the modal selection, else the file-ish word under the cursor
// (on the tag tail when it's focused for editing, else the body).
@@ -1522,6 +1541,95 @@ pub const Pardes = struct {
if (is(key, 'y')) return p.normalYank(pane);
if (is(key, 'u')) return p.doUndo(pane);
if (is(key, 'U')) return p.doRedo(pane);
+ // file search: `/` types a pattern into the tag, n/N walk the results
+ if (pane.file != null and is(key, '/')) return p.startSearch(pane);
+ if (pane.file != null and is(key, 'n')) return p.searchStep(pane, 1);
+ if (pane.file != null and is(key, 'N')) return p.searchStep(pane, -1);
+ }
+
+ // ---- file search (`/` + n/N): grep into a shell pane, look the results ----
+
+ /// `/` on a file pane: type the pattern into the tag tail after a " /"
+ /// marker — the existing modal one-line editor, visible while typing,
+ /// nothing that disappears. Enter/Esc are intercepted in handleKey.
+ fn startSearch(p: *Pardes, pane: *Pane) void {
+ if (!pane.tag_init) {
+ pane.tag_tail.appendSlice(p.gpa, file_pane_tail) catch return;
+ pane.tag_init = true;
+ }
+ pane.search_at = @intCast(pane.tag_tail.items.len);
+ pane.tag_tail.appendSlice(p.gpa, " /") catch return;
+ pane.search_edit = true;
+ pane.tag_edit = true;
+ pane.tag_sel = false;
+ pane.mode = .insert;
+ pane.pending = 0;
+ pane.tag_col = @intCast(pane.tag_tail.items.len);
+ }
+
+ /// Enter on a `/` input: run `grep -nH '<pat>' <path>` in a shell in the
+ /// file's directory (the same path middle-click send takes), so the
+ /// results are ordinary persistent terminal text. Remember the results
+ /// pane and start n/N below its current prompt row.
+ fn submitSearch(p: *Pardes, pane: *Pane) void {
+ const f = pane.file orelse return;
+ const tail = pane.tag_tail.items;
+ const pat = std.mem.trim(u8, tail[@min(@as(usize, pane.search_at) + 2, tail.len)..], " \t\r\n");
+ if (pat.len == 0) return;
+ const arena = p.scratch.allocator();
+ var cmd: std.ArrayList(u8) = .empty;
+ cmd.appendSlice(arena, "grep -nH '") catch return;
+ for (pat) |ch| {
+ if (ch == '\'') cmd.appendSlice(arena, "'\\''") catch return else cmd.append(arena, ch) catch return;
+ }
+ cmd.appendSlice(arena, "' '") catch return;
+ cmd.appendSlice(arena, f.path) catch return;
+ cmd.append(arena, '\'') catch return;
+ const dst = p.ttyForDir(std.fs.path.dirname(f.path) orelse "/") orelse return;
+ const rp = p.panes[dst] orelse return;
+ p.padOutputBelowEdits(dst);
+ p.emitWrite(dst, cmd.items);
+ p.emitWrite(dst, "\r");
+ pane.search_pane = dst;
+ // absolute row of the shell's cursor (the prompt echoing the grep);
+ // results land below it, so n scans from there — older output and
+ // stale results of a previous search stay behind
+ const sb = rp.vt.screens.active.pages.scrollbar();
+ pane.search_row = @intCast(sb.total -| rp.rows + rp.vt.screens.active.cursor.y);
+ }
+
+ /// n/N: select the next/previous grep result row in the results pane and
+ /// look it. Result rows read `path:NNN:...` and are found by their path
+ /// prefix; prompt/echo rows are already blanked by paneCursorLines (the
+ /// OSC 133 semantic row marks), so only real output is walked.
+ fn searchStep(p: *Pardes, pane: *Pane, delta: i32) void {
+ const f = pane.file orelse return;
+ const rid = pane.search_pane orelse return;
+ const rp = p.panes[rid] orelse return;
+ if (!rp.isTerminal()) return;
+ const pl = p.paneCursorLines(rp) catch return;
+ const nrows: i32 = @intCast(pl.lines.len);
+ var r = pane.search_row + delta;
+ while (r >= 0 and r < nrows) : (r += delta) {
+ const ln = pl.lines[@intCast(r)];
+ if (!std.mem.startsWith(u8, ln, f.path) or ln.len <= f.path.len or ln[f.path.len] != ':') continue;
+ pane.search_row = r;
+ // select the result line in the results pane and keep it in view
+ rp.msel = .{ .active = true, .r0 = r, .r1 = r };
+ rp.vsel.active = false;
+ rp.cur_row = r;
+ rp.cur_col = 0;
+ rp.cur_off = 0;
+ rp.cur_pinned = true;
+ rp.ensureCursorVisible();
+ // look the leading file-ish word (path:NNN:) — the ordinary look
+ // path scrolls this file pane to the line
+ var hi: usize = 0;
+ while (hi < ln.len and look.isFileChar(ln[hi])) hi += 1;
+ p.actOnSelection(.right, rid, ln[0..hi], null);
+ return;
+ }
+ // no result that way (or none at all): nowhere to go, stay put
}
const InsertAt = enum { at, append, line_start, line_end, open_below, open_above };
diff --git a/test/snapshots/fsearch.golden b/test/snapshots/fsearch.golden
new file mode 100644
index 00000000..0df3dbcf
--- /dev/null
+++ b/test/snapshots/fsearch.golden
@@ -0,0 +1,310 @@
+== snap opened grid=120x30 cursor=7,2
+|Kill Newcol Tutor Debug Colors NextColor Dump
+| nm /tmp/pardes-snap/fsearch/cwd/g.txt Save Del nm /tmp/pardes-snap/fsearch/cwd Del
+| 1 l1
+| 2 MARK a g.txt
+| 3 l3
+| 4 l4
+| 5 l5
+| 6 MARK b
+| 7 l7
+| 8 l8
+| 9 l9
+| nm /tmp/pardes-snap/fsearch/cwd Del
+|
+| g.txt
+|
+|
+|
+| g.txt
+|
+|
+|
+| nm /tmp/pardes-snap/fsearch/cwd Del
+|
+| g.txt
+|
+|
+|
+|
+|
+|
+== snap typing grid=120x30 cursor=54,1
+|Kill Newcol Tutor Debug Colors NextColor Dump
+| in /tmp/pardes-snap/fsearch/cwd/g.txt Save Del /MARK nm /tmp/pardes-snap/fsearch/cwd Del
+| 1 l1
+| 2 MARK a g.txt
+| 3 l3
+| 4 l4
+| 5 l5
+| 6 MARK b
+| 7 l7
+| 8 l8
+| 9 l9
+| nm /tmp/pardes-snap/fsearch/cwd Del
+|
+| g.txt
+|
+|
+|
+| g.txt
+|
+|
+|
+| nm /tmp/pardes-snap/fsearch/cwd Del
+|
+| g.txt
+|
+|
+|
+|
+|
+|
+== snap results grid=120x30 cursor=7,2
+|Kill Newcol Tutor Debug Colors NextColor Dump
+| nm /tmp/pardes-snap/fsearch/cwd/g.txt Save Del nm /tmp/pardes-snap/fsearch/cwd Del
+| 1 l1
+| 2 MARK a g.txt
+| 3 l3
+| 4 l4
+| 5 l5
+| 6 MARK b
+| 7 l7
+| 8 l8
+| 9 l9
+| nm /tmp/pardes-snap/fsearch/cwd Del
+| g.txt
+|
+|
+|
+| g.txt
+| /tmp/pardes-snap/fsearch/cwd/g.txt:2:MARK a
+| /tmp/pardes-snap/fsearch/cwd/g.txt:6:MARK b
+| /tmp/pardes-snap/fsearch/cwd/g.txt:10:MARK c
+|
+| nm /tmp/pardes-snap/fsearch/cwd Del
+|
+| g.txt
+|
+|
+|
+|
+|
+|
+== snap first grid=120x30 cursor=7,1
+|Kill Newcol Tutor Debug Colors NextColor Dump
+| nm /tmp/pardes-snap/fsearch/cwd/g.txt Save Del nm /tmp/pardes-snap/fsearch/cwd Del
+| 2 MARK a
+| 3 l3 g.txt
+| 4 l4
+| 5 l5
+| 6 MARK b
+| 7 l7
+| 8 l8
+| 9 l9
+| 10 MARK c
+| nm /tmp/pardes-snap/fsearch/cwd Del
+| g.txt
+|
+|
+|
+| g.txt
+| /tmp/pardes-snap/fsearch/cwd/g.txt:2:MARK a
+| /tmp/pardes-snap/fsearch/cwd/g.txt:6:MARK b
+| /tmp/pardes-snap/fsearch/cwd/g.txt:10:MARK c
+|
+| nm /tmp/pardes-snap/fsearch/cwd Del
+|
+| g.txt
+|
+|
+|
+|
+|
+|
+== style first
+|0: 0-44 #969696,#2c2a3e, 45-119 d,#2c2a3e,
+|1: 0-1 d,#6e4fd0, 2-47 #969696,#2c2a3e, 48-59 d,#2c2a3e, 60-61 d,#383c54, 62-96 #969696,#2c2a3e, 97-119 d,#2c2a3e,
+|2: 0-1 d,#525252, 2-6 #5a5a5a,d, 7-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|3: 0-1 d,#525252, 2-6 #5a5a5a,d, 7-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|4: 0-1 d,#525252, 2-6 #5a5a5a,d, 7-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|5: 0-1 d,#525252, 2-6 #5a5a5a,d, 7-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|6: 0-1 d,#525252, 2-6 #5a5a5a,d, 7-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|7: 0-1 d,#525252, 2-6 #5a5a5a,d, 7-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|8: 0-1 d,#2a2a2a, 2-6 #5a5a5a,d, 7-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|9: 0-1 d,#2a2a2a, 2-6 #5a5a5a,d, 7-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|10: 0-1 d,#2a2a2a, 2-6 #5a5a5a,d, 7-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|11: 0-1 d,#383c54, 2-36 #969696,#2c2a3e, 37-59 d,#2c2a3e, 60-61 d,#525252, 62-119 d,d,
+|12: 0-1 d,#525252, 2-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|13: 0-1 d,#525252, 2-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|14: 0-1 d,#525252, 2-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|15: 0-1 d,#525252, 2-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|16: 0-1 d,#525252, 2-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|17: 0-1 d,#525252, 2-59 #d8c8a8,#4a3a1e, 60-61 d,#525252, 62-119 d,d,
+|18: 0-1 d,#525252, 2-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|19: 0-1 d,#525252, 2-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|20: 0-1 d,#2a2a2a, 2-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|21: 0-1 d,#383c54, 2-36 #969696,#2c2a3e, 37-59 d,#2c2a3e, 60-61 d,#525252, 62-119 d,d,
+|22: 0-1 d,#525252, 2-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|23: 0-1 d,#525252, 2-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|24: 0-1 d,#525252, 2-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|25: 0-1 d,#525252, 2-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|26: 0-1 d,#525252, 2-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|27: 0-1 d,#525252, 2-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|28: 0-1 d,#525252, 2-59 d,d, 60-61 d,#525252, 62-119 d,d,
+|29: 0-1 d,#525252, 2-59 d,d, 60-61 d,#525252, 62-119 d,d,
+== snap second grid=120x30 cursor=7,1
+|Kill Newcol Tutor Debug Colors NextColor Dump
+| nm /tmp/pardes-snap/fsearch/cwd/g.txt Save Del nm /tmp/pardes-snap/fsearch/cwd Del
+| 6 MARK b
+| 7 l7 g.txt
+| 8 l8
+| 9 l9
+| 10 MARK c
+| 11 l11
+| 12 l12
+|
+|
+| nm /tmp/pardes-snap/fsearch/cwd Del
+| g.txt
+|
+|
+|
+| g.txt
+| /tmp/pardes-snap/fsearch/cwd/g.txt:2:MARK a
+| /tmp/pardes-snap/fsearch/cwd/g.txt:6:MARK b
+| /tmp/pardes-snap/fsearch/cwd/g.txt:10:MARK c
+|
+| nm /tmp/pardes-snap/fsearch/cwd Del
+|
+| g.txt
+|
+|
+|
+|
+|
+|
+== snap third grid=120x30 cursor=7,1
+|Kill Newcol Tutor Debug Colors NextColor Dump
+| nm /tmp/pardes-snap/fsearch/cwd/g.txt Save Del nm /tmp/pardes-snap/fsearch/cwd Del
+| 10 MARK c
+| 11 l11 g.txt
+| 12 l12
+|
+|
+|
+|
+|
+|
+| nm /tmp/pardes-snap/fsearch/cwd Del
+| g.txt
+|
+|
+|
+| g.txt
+| /tmp/pardes-snap/fsearch/cwd/g.txt:2:MARK a
+| /tmp/pardes-snap/fsearch/cwd/g.txt:6:MARK b
+| /tmp/pardes-snap/fsearch/cwd/g.txt:10:MARK c
+|
+| nm /tmp/pardes-snap/fsearch/cwd Del
+|
+| g.txt
+|
+|
+|
+|
+|
+|
+== snap back grid=120x30 cursor=7,1
+|Kill Newcol Tutor Debug Colors NextColor Dump
+| nm /tmp/pardes-snap/fsearch/cwd/g.txt Save Del nm /tmp/pardes-snap/fsearch/cwd Del
+| 6 MARK b
+| 7 l7 g.txt
+| 8 l8
+| 9 l9
+| 10 MARK c
+| 11 l11
+| 12 l12
+|
+|
+| nm /tmp/pardes-snap/fsearch/cwd Del
+| g.txt
+|
+|
+|
+| g.txt
+| /tmp/pardes-snap/fsearch/cwd/g.txt:2:MARK a
+| /tmp/pardes-snap/fsearch/cwd/g.txt:6:MARK b
+| /tmp/pardes-snap/fsearch/cwd/g.txt:10:MARK c
+|
+| nm /tmp/pardes-snap/fsearch/cwd Del
+|
+| g.txt
+|
+|
+|
+|
+|
+|
+== snap cancel grid=120x30 cursor=52,1
+|Kill Newcol Tutor Debug Colors NextColor Dump
+| nm /tmp/pardes-snap/fsearch/cwd/g.txt Save Del nm /tmp/pardes-snap/fsearch/cwd Del
+| 6 MARK b
+| 7 l7 g.txt
+| 8 l8
+| 9 l9
+| 10 MARK c
+| 11 l11
+| 12 l12
+|
+|
+| nm /tmp/pardes-snap/fsearch/cwd Del
+| g.txt
+|
+|
+|
+| g.txt
+| /tmp/pardes-snap/fsearch/cwd/g.txt:2:MARK a
+| /tmp/pardes-snap/fsearch/cwd/g.txt:6:MARK b
+| /tmp/pardes-snap/fsearch/cwd/g.txt:10:MARK c
+|
+| nm /tmp/pardes-snap/fsearch/cwd Del
+|
+| g.txt
+|
+|
+|
+|
+|
+|
+== snap nomatch grid=120x30 cursor=54,1
+|Kill Newcol Tutor Debug Colors NextColor Dump
+| nm /tmp/pardes-snap/fsearch/cwd/g.txt Save Del nm /tmp/pardes-snap/fsearch/cwd Del
+| 6 MARK b
+| 7 l7 g.txt
+| 8 l8
+| 9 l9
+| 10 MARK c
+| 11 l11
+| 12 l12
+|
+|
+| nm /tmp/pardes-snap/fsearch/cwd Del
+|
+|
+|
+| g.txt
+| /tmp/pardes-snap/fsearch/cwd/g.txt:2:MARK a
+| /tmp/pardes-snap/fsearch/cwd/g.txt:6:MARK b
+| /tmp/pardes-snap/fsearch/cwd/g.txt:10:MARK c
+|
+|
+| nm /tmp/pardes-snap/fsearch/cwd Del
+|
+| g.txt
+|
+|
+|
+|
+|
+|
diff --git a/test/snapshots/fsearch.snap b/test/snapshots/fsearch.snap
new file mode 100644
index 00000000..eab702cb
--- /dev/null
+++ b/test/snapshots/fsearch.snap
@@ -0,0 +1,63 @@
+# `/` file search: the pattern types into the tag (persistent, no popup);
+# Enter runs grep -nH into a shell pane so the results are ordinary terminal
+# text; n/N select the next/prev result row there (prompt rows skipped via the
+# OSC 133 semantic marks) and look it — the file pane scrolls to path:NNN.
+# Esc abandons the input; a no-match search leaves n with nowhere to go.
+file g.txt l1\nMARK a\nl3\nl4\nl5\nMARK b\nl7\nl8\nl9\nMARK c\nl11\nl12
+start 30 120
+wait 8000 g.txt
+stable 700 20000
+# open g.txt: type the name in the shell, look it (as in look-file.snap)
+press left 5 8
+release left 5 8
+stable 400 5000
+key i
+text g.txt
+key esc
+settle 700
+stable 400 5000
+press right 6 8
+release right 6 8
+wait 10000 MARK a
+stable 700 15000
+snap opened
+# / starts the search input in the tag; the pattern is visible while typing
+key /
+stable 400 5000
+text MARK
+stable 400 5000
+snap typing
+key enter
+wait 10000 g.txt:2:MARK
+stable 700 10000
+snap results
+# n selects result 1 in the results pane + scrolls the file to line 2
+key n
+stable 700 15000
+snap first
+snapstyle first
+key n
+stable 700 15000
+snap second
+key n
+stable 700 15000
+snap third
+# N walks back up to result 2 (line 6)
+key N
+stable 700 15000
+snap back
+# Esc abandons the input: the tag tail is restored
+key /
+text zz
+key esc
+settle 700
+stable 400 5000
+snap cancel
+# a no-match search: n has nowhere to go (nothing changes, no crash)
+key /
+text zzqq
+key enter
+stable 700 10000
+key n
+stable 400 5000
+snap nomatch