summaryrefslogtreecommitdiff
path: root/src/look.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-28 16:49:51 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:07 -0300
commit65b207c3392c75eac3f2b18a266a6482d8345df1 (patch)
tree640f449b6e603d8cd5a01e94e40b12b8551d52d4 /src/look.zig
parent0b5ad7a30ba9e7c6eedcb426be294bd94219310f (diff)
downloadpardes-65b207c3392c75eac3f2b18a266a6482d8345df1.tar.gz
pardes-65b207c3392c75eac3f2b18a266a6482d8345df1.zip
Builtins (Find, Grep) qol improvements and ui fixes
Diffstat (limited to 'src/look.zig')
-rw-r--r--src/look.zig117
1 files changed, 117 insertions, 0 deletions
diff --git a/src/look.zig b/src/look.zig
index 2b788bde..a3d6ab30 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -243,6 +243,123 @@ pub fn find(arena: std.mem.Allocator, dir: []const u8, pat: []const u8, out: *st
}
}
+/// how much of one file Grep reads. The core is synchronous, so a tree with a
+/// core dump in it must not stall the keystroke: past this the tail of the file
+/// is simply not searched (`grep -R` would read it all).
+const grep_max_bytes = 256 * 1024;
+const grep_max_files = 20_000;
+
+/// every line of `text` holding `pat`, as `path:LINE:COL text` rows — the
+/// shared half of grep(), and the shape every result row in pardes has: the
+/// leading word is a look target, so n/N walk the hits. Returns the rows
+/// written, at most `budget`.
+fn grepText(arena: std.mem.Allocator, path: []const u8, text: []const u8, pat: []const u8, out: *std.ArrayList(u8), budget: usize) usize {
+ var n: usize = 0;
+ var line: usize = 0;
+ var it = std.mem.splitScalar(u8, text, '\n');
+ while (it.next()) |raw| {
+ line += 1;
+ if (n >= budget) break;
+ const at = std.ascii.indexOfIgnoreCase(raw, pat) orelse continue;
+ // one minified line can be the whole file: cut it, but never mid
+ // codepoint — a partial UTF-8 sequence reaches the renderer as a hit
+ // row and there is nothing sane for it to draw.
+ const ln = std.mem.trimEnd(u8, raw, " \t\r");
+ var cut = @min(ln.len, 200);
+ while (cut > 0 and cut < ln.len and ln[cut] & 0xc0 == 0x80) cut -= 1;
+ const row = std.fmt.allocPrint(arena, "{s}:{d}:{d} {s}\n", .{ path, line, at + 1, ln[0..cut] }) catch break;
+ out.appendSlice(arena, row) catch break;
+ n += 1;
+ }
+ return n;
+}
+
+/// `grep -R`, in-core: every LINE of every file under `dir` containing `pat`
+/// (plain case-insensitive substring, like every other search here), one row
+/// per hit into `out`. A row's path is RELATIVE to `base` — the directory of
+/// the pane that asked, which is also the one its results buffer is named in,
+/// so a row reads as the short name that pane would have typed and still looks
+/// up. A hit `base` does not contain (another pane's tree) keeps its absolute
+/// path, which resolves from anywhere. Same walk, same skip list and same three
+/// caps as find(), plus grep_max_bytes and a NUL sniff so a binary never lands
+/// in the results.
+pub fn grep(arena: std.mem.Allocator, gpa: std.mem.Allocator, dir: []const u8, base: []const u8, pat: []const u8, out: *std.ArrayList(u8)) void {
+ var hits: usize = 0;
+ if (!platform_has_fs) {
+ // web: the build-generated source archive IS the filesystem
+ for (embedded_sources.all) |s| {
+ if (hits >= find_max_hits) break;
+ hits += grepText(arena, s.path, s.contents, pat, out, find_max_hits - hits);
+ }
+ return;
+ }
+ const root_path = std.mem.trimEnd(u8, dir, "/");
+ const home = std.mem.trimEnd(u8, base, "/");
+ // the walk COLLECTS, then the read scans in sorted order: readdir order is
+ // undefined, and rows the same tree hands back in a different order twice
+ // running are rows n/N cannot be trusted to walk (find() sorts for the
+ // same reason). e.path dies at the next next(), so these are copies.
+ var files: std.ArrayList([]const u8) = .empty;
+ {
+ 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);
+ var w = root.walkSelectively(arena) catch return;
+ defer w.deinit();
+ var steps: usize = 0;
+ walk: while (steps < find_max_steps and files.items.len < grep_max_files) {
+ steps += 1;
+ const e = (w.next(io) catch continue) orelse break;
+ if (e.kind == .directory) {
+ if (e.depth() >= find_max_depth) continue;
+ for (find_skip) |s| if (std.mem.eql(u8, e.basename, s)) continue :walk;
+ w.enter(io, e) catch {};
+ continue;
+ }
+ if (e.kind != .file) continue;
+ const path = std.fmt.allocPrint(arena, "{s}/{s}", .{ root_path, e.path }) catch break;
+ files.append(arena, path) catch break;
+ }
+ }
+ std.mem.sort([]const u8, files.items, {}, struct {
+ fn lt(_: void, a: []const u8, b: []const u8) bool {
+ return std.mem.lessThan(u8, a, b);
+ }
+ }.lt);
+ // ONE buffer for every file: readFile is unbounded, and a synchronous
+ // search must not swallow a file it cannot afford to hold
+ const buf = gpa.alloc(u8, grep_max_bytes) catch return;
+ defer gpa.free(buf);
+ for (files.items) |path| {
+ if (hits >= find_max_hits) break;
+ var pathbuf: [4096]u8 = undefined;
+ const path_z = std.fmt.bufPrintSentinel(&pathbuf, "{s}", .{path}, 0) catch continue;
+ const fd = libc.open(path_z, .{ .ACCMODE = .RDONLY, .CLOEXEC = true });
+ if (fd < 0) continue;
+ var len: usize = 0;
+ while (len < buf.len) {
+ const n = libc.read(fd, buf[len..].ptr, buf.len - len);
+ if (n < 0) {
+ if (libc.errno(n) == .INTR) continue;
+ break;
+ }
+ if (n == 0) break;
+ len += @intCast(n);
+ }
+ _ = libc.close(fd);
+ const text = buf[0..len];
+ if (std.mem.indexOfScalar(u8, text[0..@min(len, 1024)], 0) != null) continue; // binary
+ // per PATH, not per root: one root can straddle the asking pane's
+ // directory (a shell at `/a` searching for a file pane at `/a/b`), and
+ // the rows inside it are the ones worth shortening
+ const shown = if (path.len > home.len and std.mem.startsWith(u8, path, home) and path[home.len] == '/')
+ path[home.len + 1 ..]
+ else
+ path;
+ hits += grepText(arena, shown, text, pat, out, find_max_hits - hits);
+ }
+}
+
/// 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 });