use fuzzy_matcher::FuzzyMatcher;
use fuzzy_matcher::skim::SkimMatcherV2;
use std::borrow::Cow;
use std::cmp::Ordering;
use std::ffi::OsString;
use std::io::{self, BufRead};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
const BUFREADER_SIZE: usize = 32768;
#[rustfmt::skip]
const EXCLUDES: &[&str] = &[
".git", ".hg", ".svn", ".rustup", ".cargo", "target", "node_modules", "dist",
"venv", ".venv", "__pycache__", ".DS_Store", "build", "out", "bin", "obj"
];
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct FindResult {
path: PathBuf,
score: i64,
}
impl Ord for FindResult {
fn cmp(&self, other: &Self) -> Ordering {
other.score.cmp(&self.score)
}
}
impl PartialOrd for FindResult {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl FindResult {
pub fn path(&self) -> &Path {
&self.path
}
pub fn score(&self) -> i64 {
self.score
}
pub fn relative(&self, base: &Path) -> String {
let rel = self.path.strip_prefix(base).unwrap_or(&self.path);
normalize_relative_path(rel)
}
}
#[derive(Debug, Clone)]
struct RawResult {
relative: String,
score: i64,
}
pub fn find(
base_dir: &Path,
query: &str,
out: &mut Vec<FindResult>,
cancel: Arc<AtomicBool>,
max_results: usize,
) -> io::Result<()> {
out.clear();
if query.is_empty() {
return Ok(());
}
let mut args: Vec<OsString> = vec![
OsString::from("."),
OsString::from(base_dir),
OsString::from("--type"),
OsString::from("f"),
OsString::from("--type"),
OsString::from("d"),
OsString::from("--hidden"),
];
for excl in EXCLUDES {
args.push(OsString::from("--exclude"));
args.push(OsString::from(excl));
}
args.push(OsString::from("--color"));
args.push(OsString::from("never"));
args.push(OsString::from("--max-results"));
args.push(OsString::from(max_results.to_string()));
let mut cmd = Command::new("fd");
cmd.args(&args).stdout(Stdio::piped());
let mut proc = match cmd.spawn() {
Ok(proc) => proc,
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
return Err(io::Error::other(
"fd was not found in PATH. Please install fd-find",
));
} else {
return Err(io::Error::other(format!("Failed to spawn fd: {}", e)));
}
}
};
let matcher = SkimMatcherV2::default();
let mut raw_results: Vec<RawResult> = Vec::with_capacity(max_results * 2);
let norm_query = normalize_separators(query);
let flat_query = flatten_separators(&norm_query);
if let Some(stdout) = proc.stdout.take() {
let reader = io::BufReader::with_capacity(BUFREADER_SIZE, stdout);
for line in reader.lines() {
if cancel.load(std::sync::atomic::Ordering::Relaxed) {
let _ = proc.kill();
let _ = proc.wait();
break;
}
let rel = line?;
let rel = rel.trim();
let norm_rel = normalize_separators(rel);
let flat_rel = flatten_separators(&norm_rel);
if let Some(score) = matcher.fuzzy_match(&flat_rel, &flat_query) {
raw_results.push(RawResult {
relative: norm_rel.into_owned(),
score,
});
}
}
let _ = proc.wait();
}
raw_results.sort_unstable_by(|a, b| b.score.cmp(&a.score));
raw_results.truncate(max_results);
out.reserve(raw_results.len());
for raw in raw_results {
let path = base_dir.join(&raw.relative);
out.push(FindResult {
path,
score: raw.score,
});
}
Ok(())
}
pub fn preview_bat(
path: &Path,
max_lines: usize,
bat_args: &[OsString],
) -> Result<Vec<String>, std::io::Error> {
let mut args = bat_args.to_vec();
args.push(path.as_os_str().to_os_string());
let output = Command::new("bat")
.args(&args)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.output()?;
if !output.status.success() {
return Err(std::io::Error::other("bat command failed"));
}
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(stdout.lines().take(max_lines).map(str::to_owned).collect())
}
fn normalize_relative_path(path: &Path) -> String {
let rel = path.to_string_lossy().into_owned();
#[cfg(windows)]
{
rel.replace('\\', "/")
}
#[cfg(not(windows))]
{
rel
}
}
fn normalize_separators<'a>(separator: &'a str) -> Cow<'a, str> {
if separator.contains('\\') {
Cow::Owned(separator.replace('\\', "/"))
} else {
Cow::Borrowed(separator)
}
}
fn flatten_separators(separator: &str) -> String {
let mut buf = String::with_capacity(separator.len());
for char in separator.chars() {
if char != '/' && char != '\\' {
buf.push(char);
}
}
buf
}