diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-26 23:25:53 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | d9da3ce9e87cf4c43bf34664031f090e6dde2410 (patch) | |
| tree | 1ab9627da3c8d8066a282f20fe4796e7001c3f94 /src/pardes.zig | |
| parent | fc3b5633cb00f2c0495b940c3f1fb07000502819 (diff) | |
| download | pardes-d9da3ce9e87cf4c43bf34664031f090e6dde2410.tar.gz pardes-d9da3ce9e87cf4c43bf34664031f090e6dde2410.zip | |
Find builtin (SPC f f): fd in-core, results into +Search, n/N opens each hit
Save moves w -> f s to free the w group for window navigation.
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 134 |
1 files changed, 84 insertions, 50 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index 89f295cc..fadaf3cc 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -67,6 +67,7 @@ const Builtin = enum { Dump, Restore, Save, + Find, Del, Delcol, Help, @@ -74,14 +75,17 @@ const Builtin = enum { /// SPC leader: ONE key path per builtin, the whole remapping surface. An /// EnumArray, so a new enum field is a compile error until it has a path. -/// Groups are just shared first letters (h docs, c columns, t toggles, s -/// session); `?` is Help's path and is honored at ANY depth — it lists what -/// the prefix typed so far can still reach. +/// Groups are just shared first letters (f files, h docs, c columns, t +/// toggles, s session); `?` is Help's path and is honored at ANY depth — it +/// lists what the prefix typed so far can still reach. const leader_path = std.EnumArray(Builtin, []const u8).init(.{ .Help = "?", .Del = "d", .Kill = "k", - .Save = "w", // vim's :w — `s` is the session group + // the `f` file group (spacemacs): Save left vim's `w` to join Find here, + // which frees `w` for the window group (SPC w h/j/k/l) to move into. + .Save = "fs", + .Find = "ff", .Tutor = "ht", .Newcol = "cn", .Delcol = "cd", @@ -594,11 +598,12 @@ pub const Pane = struct { /// one pending prefix codepoint (0 = none): `g` `z` `m` `[` `]`, or an /// op waiting for its char argument (`f` `F` `t` `T` `r`) pending: u21 = 0, - /// `/` search: the pattern is typed into the tag tail (no popup); - /// search_at is the tail length before " /" was appended (submit/cancel - /// restore it). Enter fills the output buffer in pane search_pane with one - /// result per row; n/N walk it, search_row being the row they last landed - /// on (null = fresh results, nothing stepped to yet). + /// `/` search and the Find builtin: the pattern is typed into the tag tail + /// (no popup); search_at is the tail length before the marker was appended + /// (submit/cancel restore it, and the marker itself says WHICH search this + /// is). Enter fills the output buffer in pane search_pane with one result + /// per row; n/N walk it, search_row being the row they last landed on + /// (null = fresh results, nothing stepped to yet). search_edit: bool = false, search_at: u16 = 0, search_pane: ?usize = null, @@ -1148,7 +1153,7 @@ pub const Pardes = struct { } else pane.mode = .normal; pane.tag_edit = false; pane.tag_sel = false; - pane.search_edit = false; // an abandoned `/` input stays as tag text + pane.search_edit = false; // an abandoned search input stays as tag text } fn tagSelBounds(pane: *Pane) struct { lo: usize, hi: usize } { @@ -1421,8 +1426,8 @@ pub const Pardes = struct { } else p.enterTty(p.active); return; } - // `/` search input in flight: Enter searches, Esc abandons; both - // restore the tag tail and hand focus back to the body. + // a search input in flight (`/` or Find): Enter searches, 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(p.active); pane.tag_tail.shrinkRetainingCapacity(@min(pane.search_at, pane.tag_tail.items.len)); @@ -2305,7 +2310,7 @@ pub const Pardes = struct { // search: `/` types a pattern into the tag, n/N walk the results. Same // keys on every kind of pane; a terminal with no search armed falls // back to n/N as a motion over the lookable tokens in its output. - if (is(key, '/')) return p.startSearch(pane); + if (is(key, '/')) return p.startSearch(pane, search_marker); if (is(key, 'n') or is(key, 'N')) { const delta: i32 = if (is(key, 'n')) 1 else -1; if (p.searchStep(p.active, delta)) return; @@ -2314,18 +2319,25 @@ pub const Pardes = struct { } } - // ---- search (`/` + n/N): results into an output buffer, look the rows ---- + // ---- search (`/`, Find, n/N): results into an output buffer, look the rows ---- - /// `/` on any 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 { + /// What an armed search writes into the tag tail — and the ONLY record of + /// which search it is: Enter reads the marker back (submitSearch) instead + /// of pardes carrying a second piece of pane state for two commands that + /// differ in nothing but where the rows come from. + const search_marker = " /"; + const find_marker = " Find /"; + + /// `/` (and the Find builtin) on any pane: type the pattern into the tag + /// tail after `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, marker: []const u8) void { if (!pane.tag_init) { pane.tag_tail.appendSlice(p.gpa, curTail(pane)) catch return; pane.tag_init = true; } pane.search_at = @intCast(pane.tag_tail.items.len); - pane.tag_tail.appendSlice(p.gpa, " /") catch return; + pane.tag_tail.appendSlice(p.gpa, marker) catch return; pane.search_edit = true; pane.tag_edit = true; pane.tag_sel = false; @@ -2334,47 +2346,61 @@ pub const Pardes = struct { pane.tag_col = @intCast(pane.tag_tail.items.len); } - /// Enter on a `/` input: peel the typed pattern off the tag tail (past the - /// " /" marker) and run it through the shared search. + /// Enter on an armed input: the pattern is everything past the marker's + /// `/` (so a pattern may itself contain slashes), and the marker names the + /// search — " Find /" walks the filesystem, " /" reads the pane's text. + /// No `/` left means the editor ate the marker: nothing to run. fn submitSearch(p: *Pardes, id: usize) void { const pane = p.panes[id] orelse return; const tail = pane.tag_tail.items; - p.runSearch(id, tail[@min(@as(usize, pane.search_at) + 2, tail.len)..]); + const armed = tail[@min(pane.search_at, tail.len)..]; + const slash = std.mem.indexOfScalar(u8, armed, '/') orelse return; + p.runSearch(id, armed[slash + 1 ..], std.mem.startsWith(u8, armed, find_marker)); } - /// Search a pane's own text for `pat_raw` (plain substring, case-insensitive - /// — the same flat view Look and Execute read, so a terminal searches its - /// scrollback exactly as a file searches its content) and write the hits - /// into this pane's output buffer, one `location text` row per hit, the - /// location a bare `LINE:COL` look target ended by a SPACE (a trailing `:` - /// would read as part of it). It is the pane's path when it has a real - /// file, else `@pN` — either way an ordinary look target, so n/N is just - /// "step a row and look it". No matches = an empty buffer. - fn runSearch(p: *Pardes, id: usize, pat_raw: []const u8) void { + /// Fill this pane's results buffer with everything matching `pat_raw` + /// (plain substring, case-insensitive) — ONE function for both searches, + /// because they differ only in where the rows come from and agree on every + /// row being a LOOK TARGET, which is what makes n/N work: + /// text (`/`) — the pane's own flat text, the same view Look and Execute + /// read, so a terminal searches its scrollback exactly as a file + /// searches its content. Rows are `location text`, the location ended + /// by a SPACE (a trailing `:` would read as part of it): the pane's + /// path when it has a real file, else `@pN`. + /// find — the pane's DIRECTORY, walked like fd. Rows are bare paths. + /// No matches = an empty buffer. + fn runSearch(p: *Pardes, id: usize, pat_raw: []const u8, find: bool) void { const pane = p.panes[id] orelse return; const pat = std.mem.trim(u8, pat_raw, " \t\r\n"); if (pat.len == 0) return; const arena = p.scratch.allocator(); - const pl = p.paneCursorLines(pane) catch return; - // a real file names itself; a terminal or an output buffer has no path - const has_path = if (pane.file) |f| !f.output else false; - var idbuf: [16]u8 = undefined; - const loc: []const u8 = if (has_path) - pane.file.?.path - else - std.fmt.bufPrint(&idbuf, "@p{d}", .{id}) catch return; + // where the pane lives: a file's directory, a shell's cwd — the walk + // root, and the directory the results buffer is named in. + const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice(); var out: std.ArrayList(u8) = .empty; - for (pl.lines, 0..) |ln, i| { - const at = std.ascii.indexOfIgnoreCase(ln, pat) orelse continue; - const row = std.fmt.allocPrint(arena, "{s}:{d}:{d} {s}\n", .{ - loc, i + 1, at + 1, std.mem.trimEnd(u8, ln, " \t"), - }) catch return; - out.appendSlice(arena, row) catch return; + if (find) look.find(arena, dir, pat, &out) else { + const pl = p.paneCursorLines(pane) catch return; + // a real file names itself; a terminal or an output buffer has no path + const has_path = if (pane.file) |f| !f.output else false; + var idbuf: [16]u8 = undefined; + const loc: []const u8 = if (has_path) + pane.file.?.path + else + std.fmt.bufPrint(&idbuf, "@p{d}", .{id}) catch return; + for (pl.lines, 0..) |ln, i| { + const at = std.ascii.indexOfIgnoreCase(ln, pat) orelse continue; + const row = std.fmt.allocPrint(arena, "{s}:{d}:{d} {s}\n", .{ + loc, i + 1, at + 1, std.mem.trimEnd(u8, ln, " \t"), + }) catch return; + out.appendSlice(arena, row) catch return; + } } const content = p.gpa.dupe(u8, out.items) catch return; // reuse this pane's own results buffer while it lives, else open one - // next to the docs; focus stays here so n/N keeps stepping. - if (pane.search_pane) |sid| if (p.panes[sid]) |sp| if (sp.file) |*sf| if (sf.output) { + // next to the docs; focus stays here so n/N keeps stepping. search_pane + // is a raw slot index and a deleted buffer frees its slot, so the NAME + // is the proof the pane found there is still the one we made. + if (pane.search_pane) |sid| if (p.panes[sid]) |sp| if (sp.file) |*sf| if (sf.output and std.mem.endsWith(u8, sf.path, "/+Search")) { p.setFileContent(sf, content); sf.scroll = 0; sp.cur_row = 0; @@ -2382,7 +2408,6 @@ pub const Pardes = struct { pane.search_row = null; return; }; - const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice(); const free = p.freeSlot() orelse { p.gpa.free(content); return; @@ -2407,7 +2432,8 @@ pub const Pardes = struct { const rid = pane.search_pane orelse return false; const rp = p.panes[rid] orelse return false; const rf = if (rp.file) |*f| f else return false; - if (!rf.output) return false; + // a freed slot can be reused by an unrelated pane: the name says ours + if (!rf.output or !std.mem.endsWith(u8, rf.path, "/+Search")) return false; // fresh results: n starts at the first row, N has nothing behind it const nrows: i64 = @intCast(std.mem.count(u8, rf.content, "\n")); const step: i64 = if (pane.search_row) |c| @as(i64, @intCast(c)) + delta else if (delta > 0) 0 else -1; @@ -2425,6 +2451,11 @@ pub const Pardes = struct { var hi: usize = 0; while (hi < ln.len and look.isFileChar(ln[hi])) hi += 1; p.actOnSelection(.right, rid, ln[0..hi], null); + // the look may focus what it opened — a Find row opens a whole new + // file pane every time — so focus comes back to the pane that owns the + // search and the next n keeps stepping. A `/` row looks at the + // searching pane itself, so this is what already happened there. + p.active = id; return true; } @@ -4566,7 +4597,7 @@ pub const Pardes = struct { // end; an image pane has no text to search either way. .none => { const bmode = if (pane.tag_edit) pane.tag_mode else pane.mode; - if (pane.image == null and bmode != .tty) p.runSearch(id, txt); + if (pane.image == null and bmode != .tty) p.runSearch(id, txt, false); }, // `@p7:10:5`: pane 7, line 10, column 5 — how a search result // points at a terminal or an output buffer, neither of which @@ -4676,6 +4707,9 @@ pub const Pardes = struct { // an output buffer has no file behind it — nothing to write if (pane.file) |f| if (!f.output) p.emit(.{ .save_file = .{ .pane = @intCast(id) } }); }, + // the one builtin that ASKS for something: it arms the same tag + // input `/` does, and Enter there runs the walk (submitSearch). + .Find => p.startSearch(pane, find_marker), .Newcol => { const free = p.freeSlot() orelse return; if (p.ncol >= MAX_COLS) return; |
