From d9da3ce9e87cf4c43bf34664031f090e6dde2410 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 26 Jul 2026 23:25:53 -0300 Subject: 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. --- src/look.zig | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) (limited to 'src/look.zig') diff --git a/src/look.zig b/src/look.zig index f34fa367..61970de1 100644 --- a/src/look.zig +++ b/src/look.zig @@ -170,6 +170,68 @@ const platform_has_fs = switch (pardes.platform) { .web => false, }; +// Find's safety rails. The core is SYNCHRONOUS — a Find at `/` runs inside the +// keystroke that asked for it — so the walk must end whatever it is pointed at. +// Three caps, because each alone leaks: hits bound the results buffer, depth +// bounds a deep tree, and steps bound a wide shallow one (a pattern that never +// matches would otherwise walk the whole disk without ever filling `hits`). +const find_max_hits = 512; +const find_max_depth = 16; +const find_max_steps = 100_000; + +/// `fd`, in-core: every path under `dir` whose NAME contains `pat` (plain +/// case-insensitive substring — fd's default is a regex and pardes has no +/// regex engine to spend on one), one ABSOLUTE path per line into `out`. +/// Absolute because each line is an ordinary look target that outlives the +/// walk: a terminal's cwd can move out from under its own results buffer. +/// Only real directories are entered, so a symlink can never close a cycle, +/// and `.git` is skipped whole (fd's default too). +pub fn find(arena: std.mem.Allocator, dir: []const u8, pat: []const u8, out: *std.ArrayList(u8)) void { + var hits: std.ArrayList([]const u8) = .empty; + if (platform_has_fs) { + // Zig 0.16 moved the filesystem behind std.Io; the blocking + // single-threaded implementation (the one std.debug itself holds) IS + // the synchronous walk a sans-IO core wants — no pool, no cancelation. + const io = std.Io.Threaded.global_single_threaded.io(); + var root = std.Io.Dir.cwd().openDir(io, dir, .{ .iterate = true }) catch return; + defer root.close(io); + // walkSelectively, not walk: descending is opt-in, which is the only + // way to express the depth cap and the .git skip at all. + var w = root.walkSelectively(arena) catch return; + defer w.deinit(); + var steps: usize = 0; + while (steps < find_max_steps and hits.items.len < find_max_hits) { + steps += 1; // an unreadable dir burns a step too, so it cannot spin + const e = (w.next(io) catch continue) orelse break; + if (std.ascii.indexOfIgnoreCase(e.basename, pat) != null) + hits.append(arena, std.fmt.allocPrint(arena, "{s}/{s}", .{ + std.mem.trimEnd(u8, dir, "/"), e.path, + }) catch break) catch break; + if (e.kind == .directory and e.depth() < find_max_depth and !std.mem.eql(u8, e.basename, ".git")) + w.enter(io, e) catch {}; + } + } else { + // web: the build-generated source archive IS the filesystem, and it is + // already a flat list of paths — the whole walk is the match. + for (embedded_sources.all) |s| { + if (hits.items.len >= find_max_hits) break; + if (std.ascii.indexOfIgnoreCase(std.fs.path.basename(s.path), pat) != null) + hits.append(arena, s.path) catch break; + } + } + // readdir order is undefined; sort so the same tree gives the same buffer + // twice running and n/N walks it in a sane order. + std.mem.sort([]const u8, hits.items, {}, struct { + fn lt(_: void, a: []const u8, b: []const u8) bool { + return std.mem.lessThan(u8, a, b); + } + }.lt); + for (hits.items) |h| { + out.appendSlice(arena, h) catch return; + out.append(arena, '\n') catch return; + } +} + /// true if `path` exists and is a directory (open(O_DIRECTORY), no stat needed) fn isDir(path: [*:0]const u8) bool { const fd = libc.open(path, .{ .ACCMODE = .RDONLY, .DIRECTORY = true, .CLOEXEC = true }); -- cgit v1.3