use std::{ borrow::Cow, collections::HashMap, env, path::{Path, PathBuf}, }; use inquire::{autocompletion::Replacement, Autocomplete}; use path_absolutize::Absolutize; pub fn get_project_path

(base_path: P) -> Option where P: AsRef, { let base_path = base_path.as_ref(); if base_path.is_absolute() { Cow::Borrowed(base_path) } else { base_path.absolutize().ok()? } .ancestors() .find(|p| p.join("pack.toml").exists()) .map(|p| p.relativize().unwrap_or_else(|| p.to_path_buf())) } pub trait Relativize { fn relativize(&self) -> Option; } impl

Relativize for P where P: AsRef, { fn relativize(&self) -> Option { let cwd = env::current_dir().ok()?; pathdiff::diff_paths(self, cwd) } } #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct PathAutocomplete { parent: String, current: String, outputs: Vec, cache: HashMap>, } impl PathAutocomplete { pub fn new() -> Self { Self::default() } fn split_input(input: &str) -> (&str, &str) { let (parent, current) = if input.ends_with('/') { (input.trim_end_matches('/'), "") } else if let Some((parent, current)) = input.rsplit_once('/') { if parent.is_empty() { ("/", current) } else { (parent, current) } } else { ("", input) }; let parent = if parent.is_empty() { "." } else { parent }; (parent, current) } fn get_cached(&mut self, parent: &str) -> Result<&[String], &'static str> { if !self.cache.contains_key(parent) { tracing::trace!("Cache miss for \"{}\", reading dir", parent); let parent_path = PathBuf::from(parent); if !parent_path.exists() || !parent_path.is_dir() { return Err("Path does not exist"); } let entries = parent_path .read_dir() .map_err(|_| "Could not read dir")? .filter_map(|entry| { entry.ok().and_then(|entry| { Some( entry.file_name().into_string().ok()?.to_string() + if entry.path().is_dir() { "/" } else { "" }, ) }) }) .collect::>(); self.cache.insert(parent.to_string(), entries); } Ok(self .cache .get(parent) .expect("Previous caching did not work")) } fn update_input(&mut self, input: &str) -> Result<(), inquire::CustomUserError> { let (parent, current) = Self::split_input(input); if self.parent == parent && self.current == current { Ok(()) } else { self.parent = parent.to_string(); self.current = current.to_string(); self.outputs = self .get_cached(parent)? .iter() .filter(|entry| entry.starts_with(current)) .cloned() .collect::>(); Ok(()) } } } impl Autocomplete for PathAutocomplete { fn get_suggestions(&mut self, input: &str) -> Result, inquire::CustomUserError> { self.update_input(input)?; Ok(self.outputs.clone()) } fn get_completion( &mut self, input: &str, highlighted_suggestion: Option, ) -> Result { let (parent, current) = Self::split_input(input); if let Some(highlighted) = highlighted_suggestion { let completion = format!("{parent}/{highlighted}"); self.update_input(&completion)?; Ok(Replacement::Some(completion)) } else if let Some(first) = self .get_cached(parent)? .iter() .find(|entry| entry.starts_with(current)) { let completion = format!("{parent}/{first}"); self.update_input(&completion)?; Ok(Replacement::Some(completion)) } else { Ok(Replacement::None) } } }