diff options
| -rw-r--r-- | src/pardes.zig | 108 | ||||
| -rw-r--r-- | test/snapshots/fsearch.golden | 310 | ||||
| -rw-r--r-- | test/snapshots/fsearch.snap | 63 |
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 |
