use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct IgnoreOpts {
pub vcs: bool,
pub hidden: bool,
}
impl IgnoreOpts {
pub const DEFAULT: Self = Self {
vcs: true,
hidden: true,
};
pub const SHOW_HIDDEN: Self = Self {
vcs: true,
hidden: false,
};
}
fn matches_glob(pattern: &str, name: &str) -> bool {
fn rec(p: &[u8], n: &[u8]) -> bool {
match (p.first(), n.first()) {
(None, None) => true,
(None, _) => false,
(Some(&b'*'), _) => {
if rec(&p[1..], n) {
return true;
}
if !n.is_empty() && rec(p, &n[1..]) {
return true;
}
false
}
(Some(&pc), Some(&nc)) if pc == nc => rec(&p[1..], &n[1..]),
_ => false,
}
}
rec(pattern.as_bytes(), name.as_bytes())
}
fn matches_any_hidden(name: &str, patterns: &[String]) -> bool {
patterns.iter().any(|p| matches_glob(p, name))
}
fn rel_path_has_hidden_segment(rel: &str, patterns: &[String]) -> bool {
rel.split('/').any(|seg| matches_any_hidden(seg, patterns))
}
pub fn workspace_dirs(
root: &Path,
ignore: IgnoreOpts,
hidden_patterns: &[String],
max_items: usize,
) -> Vec<String> {
let mut out = Vec::new();
collect_dirs(root, root, &mut out, 0, ignore, hidden_patterns, max_items);
out.sort();
out
}
pub fn workspace_files(
root: &Path,
ignore: IgnoreOpts,
hidden_patterns: &[String],
max_items: usize,
) -> Vec<String> {
let mut items = if ignore.vcs
&& let Some(paths) = crate::vcs::tracked_files(root)
{
paths
.into_iter()
.filter(|p| !ignore.hidden || !rel_path_has_hidden_segment(p, hidden_patterns))
.filter(|p| !is_symlink(&root.join(p)))
.take(max_items)
.collect()
} else {
let mut v = Vec::new();
collect_files(root, root, &mut v, 0, ignore, hidden_patterns, max_items);
v
};
items.sort();
items
}
fn is_symlink(path: &Path) -> bool {
fs::symlink_metadata(path)
.map(|m| m.file_type().is_symlink())
.unwrap_or(false)
}
fn collect_dirs(
root: &Path,
dir: &Path,
out: &mut Vec<String>,
depth: usize,
ignore: IgnoreOpts,
hidden_patterns: &[String],
max_items: usize,
) {
if depth > 12 || out.len() >= max_items {
return;
}
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
let name = match path.file_name().and_then(|s| s.to_str()) {
Some(n) => n.to_string(),
None => continue,
};
if ignore.hidden && matches_any_hidden(&name, hidden_patterns) {
continue;
}
let file_type = match entry.file_type() {
Ok(t) => t,
Err(_) => continue,
};
if file_type.is_symlink() || !file_type.is_dir() {
continue;
}
if let Some(s) = path.strip_prefix(root).ok().and_then(|p| p.to_str()) {
out.push(s.to_string());
}
collect_dirs(
root,
&path,
out,
depth + 1,
ignore,
hidden_patterns,
max_items,
);
}
}
fn collect_files(
root: &Path,
dir: &Path,
out: &mut Vec<String>,
depth: usize,
ignore: IgnoreOpts,
hidden_patterns: &[String],
max_items: usize,
) {
if depth > 12 || out.len() >= max_items {
return;
}
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
let name = match path.file_name().and_then(|s| s.to_str()) {
Some(n) => n.to_string(),
None => continue,
};
if ignore.hidden && matches_any_hidden(&name, hidden_patterns) {
continue;
}
let file_type = match entry.file_type() {
Ok(t) => t,
Err(_) => continue,
};
if file_type.is_symlink() {
continue;
}
if file_type.is_dir() {
collect_files(
root,
&path,
out,
depth + 1,
ignore,
hidden_patterns,
max_items,
);
continue;
}
if !file_type.is_file() {
continue;
}
let rel = path.strip_prefix(root).ok().and_then(|p| p.to_str());
if let Some(s) = rel {
out.push(s.to_string());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn glob_matches_literal_and_wildcard() {
assert!(matches_glob("node_modules", "node_modules"));
assert!(!matches_glob("node_modules", "my_node_modules_old"));
assert!(matches_glob(".*", ".gitignore"));
assert!(matches_glob(".*", ".env"));
assert!(!matches_glob(".*", "Cargo.toml"));
assert!(matches_glob("*.lock", "Cargo.lock"));
assert!(matches_glob("*.lock", ".lock"));
assert!(!matches_glob("*.lock", "Cargo.toml"));
assert!(matches_glob("*foo*", "abcfoo123"));
assert!(matches_glob("*foo*", "foo"));
assert!(matches_glob("", ""));
assert!(!matches_glob("", "x"));
assert!(matches_glob("*", ""));
assert!(matches_glob("*", "anything"));
}
#[test]
fn rel_path_hidden_segment_walks_segments() {
let pats = vec![".*".to_string(), "node_modules".to_string()];
assert!(rel_path_has_hidden_segment(
".github/workflows/ci.yml",
&pats
));
assert!(rel_path_has_hidden_segment("a/node_modules/b/c", &pats));
assert!(!rel_path_has_hidden_segment("src/main.rs", &pats));
assert!(!rel_path_has_hidden_segment("Cargo.toml", &pats));
}
}