summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig108
1 files changed, 108 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 };