///|
/// Directory and file names the file tools (grep, glob) prune during
/// traversal. Covers VCS internals and the common build/vendor directories;
/// dot-prefixed entries are additionally caught by `has_hidden_segment`.
/// Hosts override per tool via each tool's constructor.
pub fn default_ignore_patterns() -> Array[String] {
[
".git", ".hg", ".svn", "node_modules", "_build", "target", "dist", "__pycache__",
".venv",
]
}
///|
/// Does `path` match any ignore pattern? Same semantics as
/// `@workspace.matches_ignore` (kept dependency-free here): the path equals
/// the pattern, starts with `pattern + "/"`, or any path segment equals the
/// pattern. Patterns are plain names, no globs.
pub fn matches_ignore(path : String, patterns : Array[String]) -> Bool {
for pat in patterns {
if path == pat || path.has_prefix(pat + "/") {
return true
}
for seg in path.split("/") {
if seg == pat {
return true
}
}
}
false
}
///|
/// Does any segment of `path` start with `.`? Mirrors ripgrep's default
/// hidden-entry skipping; callers apply it below the scan base only, so an
/// explicitly requested base like `.config/x` still searches.
pub fn has_hidden_segment(path : String) -> Bool {
for seg in path.split("/") {
if seg != "" && seg.has_prefix(".") {
return true
}
}
false
}
///|
/// Segment glob match: `*` matches any characters within one path segment,
/// `**` matches zero or more whole segments (a trailing `**` matches
/// everything remaining). Used for grep's `glob` file filter and ignore
/// handling; richer syntax (`?`, `{a,b}`, classes) is not supported.
pub fn glob_match(pattern : String, path : String) -> Bool {
glob_match_segments(
pattern.split("/").to_array(),
0,
path.split("/").to_array(),
0,
)
}
///|
fn glob_match_segments(
pat : Array[StringView],
pi : Int,
path : Array[StringView],
si : Int,
) -> Bool {
if pi >= pat.length() {
return si >= path.length()
}
if pat[pi] == "**" {
if pi + 1 == pat.length() {
return true
}
let mut next = si
while next <= path.length() {
if glob_match_segments(pat, pi + 1, path, next) {
return true
}
next += 1
}
return false
}
if si >= path.length() {
return false
}
if glob_seg_match(pat[pi], path[si]) {
return glob_match_segments(pat, pi + 1, path, si + 1)
}
false
}
///|
fn glob_seg_match(pat_seg : StringView, path_seg : StringView) -> Bool {
if !pat_seg.contains("*") {
return pat_seg == path_seg
}
glob_wildcard_match(pat_seg.iter().collect(), 0, path_seg.iter().collect(), 0)
}
///|
fn glob_wildcard_match(
pat : Array[Char],
pi : Int,
text : Array[Char],
ti : Int,
) -> Bool {
if pi >= pat.length() {
return ti >= text.length()
}
if pat[pi] == '*' {
let mut next = ti
while next <= text.length() {
if glob_wildcard_match(pat, pi + 1, text, next) {
return true
}
next += 1
}
return false
}
if ti >= text.length() {
return false
}
if pat[pi] == text[ti] {
return glob_wildcard_match(pat, pi + 1, text, ti + 1)
}
false
}