use super::*;
pub fn nearest_existing_dir(path: &Path) -> Option<PathBuf> {
let mut candidate = Some(path);
while let Some(dir) = candidate {
if dir.is_dir() {
return Some(dir.to_path_buf());
}
candidate = dir.parent();
}
None
}
pub fn typed_path(text: &str, base: &Path) -> PathBuf {
let home = || env::home_dir().unwrap_or_default();
let path = match text.strip_prefix('~') {
Some("") => home(),
Some(rest) if rest.starts_with(std::path::is_separator) => home().join(rest.trim_start_matches(std::path::is_separator)),
_ => PathBuf::from(text),
};
base.join(path)
}
pub fn tidy_path(path: &Path) -> PathBuf {
use std::path::Component;
let mut tidy = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
if !tidy.pop() {
tidy.push(component);
}
}
_ => tidy.push(component),
}
}
tidy
}
pub fn complete_path(text: &str, base: &Path, show_hidden: bool) -> Option<String> {
let split = text.rfind(std::path::is_separator).map_or(0, |index| index + 1);
let (head, prefix) = text.split_at(split);
let dir = if head.is_empty() { base.to_path_buf() } else { typed_path(head, base) };
let fold = !prefix.chars().any(char::is_uppercase);
let folded = |name: &str| -> Vec<char> { name.chars().map(|c| if fold { crate::find::fold_char(c) } else { c }).collect() };
let wanted = folded(prefix);
let hidden = show_hidden || prefix.starts_with('.');
let mut candidates: Vec<(String, bool)> = read_dir(&dir)
.ok()?
.filter_map(Result::ok)
.filter_map(|entry| {
let name = entry.file_name().into_string().ok()?;
let shown = hidden || !name.starts_with('.');
(shown && folded(&name).starts_with(&wanted)).then(|| (name, entry.path().is_dir()))
})
.collect();
candidates.sort();
let (first, first_is_dir) = candidates.first()?.clone();
let completed = if candidates.len() == 1 {
if first_is_dir { format!("{first}{}", std::path::MAIN_SEPARATOR) } else { first }
} else {
let first_chars: Vec<char> = first.chars().collect();
let folded_first = folded(&first);
let common = candidates[1..]
.iter()
.fold(first_chars.len(), |length, (name, _)| folded_first.iter().zip(folded(name)).take(length).take_while(|(a, b)| **a == *b).count());
let typed = wanted.len();
prefix.chars().chain(first_chars[typed.min(common)..common].iter().copied()).collect()
};
let result = format!("{head}{completed}");
(result != text && completed.chars().count() >= prefix.chars().count()).then_some(result)
}
pub fn get_current_dir() -> Result<PathBuf, Error> {
env::current_dir()
}
pub fn is_regular_file(path: &Path) -> bool {
fs::metadata(path).is_ok_and(|metadata| metadata.is_file())
}
pub(super) fn not_regular(path: &Path) -> Error {
Error::new(ErrorKind::InvalidInput, format!("Not a regular file: {}", path.display()))
}
pub fn is_plain_name(name: &str) -> bool {
let mut components = Path::new(name).components();
matches!(
(components.next(), components.next()),
(Some(std::path::Component::Normal(only)), None) if only == name
)
}
#[cfg(unix)]
pub fn is_same_entry(a: &Path, b: &Path) -> bool {
use std::os::unix::fs::MetadataExt;
match (a.symlink_metadata(), b.symlink_metadata()) {
(Ok(a), Ok(b)) => a.dev() == b.dev() && a.ino() == b.ino(),
_ => false,
}
}
#[cfg(windows)]
pub fn is_same_entry(a: &Path, b: &Path) -> bool {
let name = |path: &Path| path.file_name().map(|name| name.to_string_lossy().to_lowercase());
a.parent() == b.parent() && name(a).is_some() && name(a) == name(b) && path_exists(a)
}
#[cfg(not(any(unix, windows)))]
pub fn is_same_entry(a: &Path, b: &Path) -> bool {
a == b
}
pub fn path_exists(path: &Path) -> bool {
path.symlink_metadata().is_ok()
}
#[cfg(test)]
mod path_tests {
use super::*;
#[test]
fn dots_are_worked_out_as_written() {
let tidy = |path: &str| tidy_path(Path::new(path));
assert_eq!(tidy("/a/b/../c"), Path::new("/a/c"));
assert_eq!(tidy("/a/./b/"), Path::new("/a/b"));
assert_eq!(tidy("/a/b/../.."), Path::new("/"));
assert_eq!(tidy("/../.."), Path::new("/"));
assert_eq!(tidy("../x"), Path::new("../x"));
}
#[test]
fn tab_fills_out_a_name_as_far_as_the_entries_agree() {
let dir = std::env::temp_dir().join(format!("fm84-complete-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
for sub in ["Documents", "downloads", "music", ".config"] {
fs::create_dir_all(dir.join(sub)).unwrap();
}
fs::write(dir.join("music.txt"), "").unwrap();
let base = dir.display().to_string();
let complete = |text: &str, hidden: bool| complete_path(text, &dir, hidden);
let sep = std::path::MAIN_SEPARATOR;
assert_eq!(complete(&format!("{base}/Doc"), false), Some(format!("{base}/Documents{sep}")));
assert_eq!(complete(&format!("{base}/doc"), false), Some(format!("{base}/Documents{sep}")));
assert_eq!(complete(&format!("{base}/Dow"), false), None);
assert_eq!(complete(&format!("{base}/d"), false), Some(format!("{base}/do")));
assert_eq!(complete(&format!("{base}/mu"), false), Some(format!("{base}/music")));
assert_eq!(complete(&format!("{base}/music"), false), None);
assert_eq!(complete(&format!("{base}/x"), false), None);
assert_eq!(complete("Doc", false), Some(format!("Documents{sep}")));
assert_eq!(complete("c", false), None);
assert_eq!(complete("c", true), None);
assert_eq!(complete(".c", false), Some(format!(".config{sep}")));
assert_eq!(complete("", true).as_deref(), None);
assert_eq!(complete(&format!("{base}/nope/x"), false), None);
fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn the_git_branch_a_directory_is_on() {
let dir = std::env::temp_dir().join(format!("fm84-git-branch-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
let repo = dir.join("repo");
fs::create_dir_all(repo.join(".git")).unwrap();
fs::create_dir_all(repo.join("src").join("deep")).unwrap();
fs::write(repo.join(".git").join("HEAD"), "ref: refs/heads/feature/tabs\n").unwrap();
assert_eq!(git_branch(&repo).as_deref(), Some("feature/tabs"));
assert_eq!(git_branch(&repo.join("src").join("deep")).as_deref(), Some("feature/tabs"));
fs::write(repo.join(".git").join("HEAD"), "3d8eea0a1b2c3d4e5f60718293a4b5c6d7e8f901\n").unwrap();
assert_eq!(git_branch(&repo).as_deref(), Some("3d8eea0"));
let worktree = dir.join("worktree");
let worktree_git = dir.join("gitdirs").join("worktree");
fs::create_dir_all(&worktree).unwrap();
fs::create_dir_all(&worktree_git).unwrap();
fs::write(worktree.join(".git"), "gitdir: ../gitdirs/worktree\n").unwrap();
fs::write(worktree_git.join("HEAD"), "ref: refs/heads/main\n").unwrap();
assert_eq!(git_branch(&worktree).as_deref(), Some("main"));
let outside = dir.join("outside");
fs::create_dir_all(&outside).unwrap();
assert_eq!(git_branch(&outside), git_branch(&std::env::temp_dir()));
fs::remove_dir_all(&dir).unwrap();
}
}