use crate::parse::{self, CompletionContext}; use crate::{BString, Session}; use std::collections::HashMap; use std::ffi::OsStr; use std::fs::DirEntry; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::{env, fs}; pub struct Suggestion { /// display string that is shown in the possibilities pub display: BString, /// *escaped* bytes that can be directly appended into terminal. pub delta: BString, } fn _path_completion( cc: CompletionContext, filter: &dyn Fn(&DirEntry) -> bool, ) -> std::io::Result> { let delim = cc.delim; let mut prefix = cc.partial; let mut partial_entry = BString::new(); while let Some(c) = prefix.last().cloned() { if c == b'/' { break; } partial_entry.push(c); prefix.pop(); } partial_entry.reverse(); let mut sugs = Vec::new(); if prefix.is_empty() { prefix.push(b'.'); } for entry in fs::read_dir(OsStr::from_bytes(&prefix))? { let entry = entry?; if !filter(&entry) { continue; } let name = entry.file_name().as_bytes().to_vec(); if name.starts_with(&partial_entry) { let mut delta = BString::new(); delim.escape(&name[partial_entry.len()..], &mut delta); let is_dir = entry.metadata().map(|m| m.is_dir()).unwrap_or(false); if is_dir { delta.push(b'/'); } else { delim.write_closing_delimiter(&mut delta); delta.push(b' '); } sugs.push(Suggestion { display: name, delta, }); } } Ok(sugs) } pub fn path_completion(cc: CompletionContext) -> Vec { match _path_completion(cc, &|_| true) { Ok(suggestions) => suggestions, Err(err) => { println!("path completion failed: {err:?}\r"); Vec::new() } } } pub fn path_exe_completion(cc: CompletionContext) -> Vec { match _path_completion(cc, &|d| is_executable(&d.path())) { Ok(suggestions) => suggestions, Err(err) => { println!("path completion failed: {err:?}\r"); Vec::new() } } } pub fn variable_completion(session: Arc>, prefix: BString) -> Vec { let se = session.lock().unwrap(); let mut out = Vec::new(); for var in se.vars.vars() { if var.starts_with(&prefix) { out.push(Suggestion { display: var.to_vec(), delta: var[prefix.len()..].to_vec(), }); } } drop(se); for var in env::vars_os() { let var = var.0.as_bytes(); if var.starts_with(&prefix) { out.push(Suggestion { display: var.to_vec(), delta: var[prefix.len()..].to_vec(), }); } } out } #[derive(Default)] pub struct PathCache { binaries: HashMap, } fn is_executable(path: &Path) -> bool { use std::os::unix::fs::PermissionsExt; fs::metadata(path) .map(|m| m.permissions().mode() & 0o111 != 0) .unwrap_or(false) } pub fn populate_path_cache(session: Arc>) { let path_var = env::var_os("PATH").unwrap(); let mut binaries = HashMap::new(); for dir in env::split_paths(&path_var) { if let Ok(entries) = fs::read_dir(&dir) { for entry in entries.flatten() { let path = entry.path(); if path.is_file() && is_executable(&path) { binaries.insert(path.file_name().unwrap().as_bytes().to_vec(), path); } } } } session.lock().unwrap().path_cache = PathCache { binaries }; } pub fn command_completion(session: Arc>, cc: CompletionContext) -> Vec { let se = session.lock().unwrap(); let mut out = Vec::new(); for fun in se .funs .keys() .chain(se.builtins.keys()) .chain(se.path_cache.binaries.keys()) { if fun.starts_with(&cc.partial) { let mut delta = BString::new(); cc.delim.escape(&fun[cc.partial.len()..], &mut delta); cc.delim.write_closing_delimiter(&mut delta); delta.push(b' '); out.push(Suggestion { display: fun.to_vec(), delta, }) } } out } pub struct CompletionResult { pub kind: parse::CompletionKind, pub suggestions: Vec, pub shared_prefix: BString, } impl CompletionResult { pub fn empty() -> Self { CompletionResult { kind: parse::CompletionKind::None, suggestions: Vec::new(), shared_prefix: BString::new(), } } } pub fn completion(session: Arc>, cmd: &[u8]) -> CompletionResult { let comp = parse::completion_context( cmd, &mut crate::run::Executor::new_for_completion(session.clone()), ); let kind = comp.kind.clone(); let mut suggestions = match comp.kind { parse::CompletionKind::Command => command_completion(session.clone(), comp), parse::CompletionKind::PathCommand => path_exe_completion(comp), parse::CompletionKind::Argument => path_completion(comp), parse::CompletionKind::Variable => variable_completion(session.clone(), comp.partial), parse::CompletionKind::None => return CompletionResult::empty(), }; suggestions.sort_by(|x, y| x.delta.cmp(&y.delta)); suggestions.dedup_by(|x, y| x.delta == y.delta); if suggestions.is_empty() { return CompletionResult { kind, ..CompletionResult::empty() }; } // find longest shared prefix let mut shared_prefix = &suggestions[0].delta[..]; for s in suggestions.iter() { let mut new = &shared_prefix[..0]; for i in 0..shared_prefix.len().min(s.delta.len()) { if shared_prefix[i] != s.delta[i] { break; } else { new = &s.delta[..=i]; } } shared_prefix = new; } let shared_prefix = shared_prefix.to_vec(); CompletionResult { kind, suggestions, shared_prefix, } }