Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 0.3.20

- Skipped common build, dependency, cache, and VCS directories during grep unless the ignored directory is searched directly.

## 0.3.19

- Reported session save, list, and load failures instead of silently dropping persistence errors.
Expand Down
2 changes: 1 addition & 1 deletion rv.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rook"
version = "0.3.19"
version = "0.3.20"
edition = "v2"

[dependencies]
Expand Down
130 changes: 89 additions & 41 deletions src/tools/search.rv
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import std/string
import std/regex { compile, Regex }
import std/fs { walk, read, is_file, is_symlink, basename }
import std/fs { list_dir, read, is_file, is_dir, is_symlink, basename }
import "./paths" { resolve_path }

const MAX_MATCHES: Int = 50
Expand Down Expand Up @@ -47,57 +47,105 @@ fun _grep_with_regexes(
has_glob: Bool,
) -> String {
let files: List<String> = []
let ok = true
match walk(root) {
Ok(fs) -> {
files = fs
},
Err(e) -> {
ok = false
},
}
_collect_files(root, files, _has_ignored_segment(root))

let out = ""
if ok {
let count = 0
let i = 0
while i < files.len() && count < MAX_MATCHES {
let f = files[i]
if is_file(f) && !is_symlink(f) && (!has_glob || name_re.is_match(basename(f))) {
let content = ""
let rok = true
match read(f) {
Ok(s) -> {
content = s
},
Err(e) -> {
rok = false
},
}
if rok {
let lines = content.lines()
let ln = 0
while ln < lines.len() && count < MAX_MATCHES {
if re.is_match(lines[ln]) {
out = out.concat("${f}:${ln + 1}: ${lines[ln]}\n")
count = count + 1
}
ln = ln + 1
let count = 0
let i = 0
while i < files.len() && count < MAX_MATCHES {
let f = files[i]
if !has_glob || name_re.is_match(basename(f)) {
let content = ""
let rok = true
match read(f) {
Ok(s) -> {
content = s
},
Err(e) -> {
rok = false
},
}
if rok {
let lines = content.lines()
let ln = 0
while ln < lines.len() && count < MAX_MATCHES {
if re.is_match(lines[ln]) {
out = out.concat("${f}:${ln + 1}: ${lines[ln]}\n")
count = count + 1
}
ln = ln + 1
}
}
i = i + 1
}
if count == 0 {
out = "no matches for '${pattern}' under ${root}"
}
} else {
out = "error: cannot search ${root}"
i = i + 1
}
if count == 0 {
out = "no matches for '${pattern}' under ${root}"
}
name_re.free()
return out
}

fun _collect_files(path: String, out: List<String>, include_ignored: Bool) {
if is_symlink(path) {
return
}
if is_file(path) {
out.push(path)
return
}
if !is_dir(path) {
return
}
let names: List<String> = []
match list_dir(path) {
Ok(ns) -> {
names = ns
},
Err(e) -> {
return
},
}
let i = 0
while i < names.len() {
let name = names[i]
let full = "${path}/${name}"
if !is_symlink(full) {
if is_dir(full) {
if include_ignored || !_ignored(name) {
_collect_files(full, out, include_ignored)
}
} else if is_file(full) {
out.push(full)
}
}
i = i + 1
}
return
}

fun _has_ignored_segment(path: String) -> Bool {
let parts = path.split("/")
let i = 0
while i < parts.len() {
if _ignored(parts[i]) {
return true
}
i = i + 1
}
return false
}

fun _ignored(name: String) -> Bool {
if name == "node_modules" || name == "target" || name == "dist" || name == "build" || name == "out" || name == "vendor" || name == "__pycache__" {
return true
}
if name.starts_with(".") {
return name != ".agents" && name != ".github"
}
return false
}

// Translates a shell-style glob (`*` any run, `?` one char, everything else
// literal) into an anchored regex. An empty glob (no filter requested)
// becomes `.*` so it matches any name without ever being consulted.
Expand Down
44 changes: 44 additions & 0 deletions src/tools/search_test.rv
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,50 @@ fun test_grep_glob_filters_files() {
return
}

fun test_grep_skips_noise_directories_by_default() {
create_dir_all("_rook_gr6/src")
create_dir_all("_rook_gr6/target/debug")
create_dir_all("_rook_gr6/node_modules/pkg")
create_dir_all("_rook_gr6/.git")
fs_write("_rook_gr6/src/main.rv", "needle in source")
fs_write("_rook_gr6/target/debug/app.txt", "needle in build output")
fs_write("_rook_gr6/node_modules/pkg/index.js", "needle in dependency")
fs_write("_rook_gr6/.git/config", "needle in metadata")

let out = grep("needle", "_rook_gr6", "")
assert_true(out.contains("src/main.rv"))
assert_true(!out.contains("target"))
assert_true(!out.contains("node_modules"))
assert_true(!out.contains(".git"))

remove_file("_rook_gr6/src/main.rv")
remove_file("_rook_gr6/target/debug/app.txt")
remove_file("_rook_gr6/node_modules/pkg/index.js")
remove_file("_rook_gr6/.git/config")
remove_dir("_rook_gr6/src")
remove_dir("_rook_gr6/target/debug")
remove_dir("_rook_gr6/target")
remove_dir("_rook_gr6/node_modules/pkg")
remove_dir("_rook_gr6/node_modules")
remove_dir("_rook_gr6/.git")
remove_dir("_rook_gr6")
return
}

fun test_grep_can_search_explicit_ignored_directory() {
create_dir_all("_rook_gr7/target")
fs_write("_rook_gr7/target/out.txt", "needle in build output")

let out = grep("needle", "_rook_gr7/target", "")
assert_true(out.contains("target/out.txt"))
assert_true(out.contains("needle in build output"))

remove_file("_rook_gr7/target/out.txt")
remove_dir("_rook_gr7/target")
remove_dir("_rook_gr7")
return
}

fun test_grep_rejects_workspace_escape() {
assert_true(grep("needle", "..", "").contains("workspace"))
}
Loading