shulkerscript-cli/src/util.rs

153 lines
4.3 KiB
Rust

use std::{
borrow::Cow,
collections::HashMap,
env,
path::{Path, PathBuf},
};
use inquire::{autocompletion::Replacement, Autocomplete};
use path_absolutize::Absolutize;
pub fn get_project_path<P>(base_path: P) -> Option<PathBuf>
where
P: AsRef<Path>,
{
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<PathBuf>;
}
impl<P> Relativize for P
where
P: AsRef<Path>,
{
fn relativize(&self) -> Option<PathBuf> {
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<String>,
cache: HashMap<String, Vec<String>>,
}
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::<Vec<_>>();
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::<Vec<_>>();
Ok(())
}
}
}
impl Autocomplete for PathAutocomplete {
fn get_suggestions(&mut self, input: &str) -> Result<Vec<String>, inquire::CustomUserError> {
self.update_input(input)?;
Ok(self.outputs.clone())
}
fn get_completion(
&mut self,
input: &str,
highlighted_suggestion: Option<String>,
) -> Result<inquire::autocompletion::Replacement, inquire::CustomUserError> {
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| current.is_empty() || entry.starts_with(current))
{
let completion = format!("{parent}/{first}");
self.update_input(&completion)?;
Ok(Replacement::Some(completion))
} else {
Ok(Replacement::None)
}
}
}