use std::iter::FromIterator; use std::path::Path; use indexmap::set::IndexSet; use super::matchers::Matcher; use crate::completion::{Completer, Context, Suggestion}; use crate::context; pub struct CommandCompleter; impl Completer for CommandCompleter { fn complete(&self, ctx: &Context<'_>, partial: &str, matcher: &dyn Matcher) -> Vec { let context: &context::Context = ctx.as_ref(); let mut commands: IndexSet = IndexSet::from_iter(context.registry.names()); // Command suggestions can come from three possible sets: // 1. internal command names, // 2. external command names relative to PATH env var, and // 3. any other executable (that matches what's been typed so far). let path_executables = find_path_executables().unwrap_or_default(); // TODO quote these, if necessary commands.extend(path_executables.into_iter()); let mut suggestions: Vec<_> = commands .into_iter() .filter(|v| matcher.matches(partial, v)) .map(|v| Suggestion { replacement: v.clone(), display: v, }) .collect(); if partial != "" { let path_completer = crate::completion::path::PathCompleter; let path_results = path_completer.path_suggestions(partial, matcher); let iter = path_results.into_iter().filter_map(|path_suggestion| { let path = path_suggestion.path; if path.is_dir() || is_executable(&path) { Some(path_suggestion.suggestion) } else { None } }); suggestions.extend(iter); } suggestions } } // TODO create a struct for "is executable" and store this information in it so we don't recompute // on every dir entry #[cfg(windows)] fn pathext() -> Option> { std::env::var_os("PATHEXT").map(|v| { v.to_string_lossy() .split(';') // Cut off the leading '.' character .map(|ext| ext[1..].to_string()) .collect::>() }) } #[cfg(windows)] fn is_executable(path: &Path) -> bool { if let Ok(metadata) = path.metadata() { let file_type = metadata.file_type(); // If the entry isn't a file, it cannot be executable if !(file_type.is_file() || file_type.is_symlink()) { return false; } if let Some(extension) = path.extension() { if let Some(exts) = pathext() { exts.iter() .any(|ext| extension.to_string_lossy().eq_ignore_ascii_case(ext)) } else { false } } else { false } } else { false } } #[cfg(target_arch = "wasm32")] fn is_executable(_path: &Path) -> bool { false } #[cfg(unix)] fn is_executable(path: &Path) -> bool { use std::os::unix::fs::PermissionsExt; if let Ok(metadata) = path.metadata() { let filetype = metadata.file_type(); let permissions = metadata.permissions(); // The file is executable if it is a directory or a symlink and the permissions are set for // owner, group, or other (filetype.is_file() || filetype.is_symlink()) && (permissions.mode() & 0o111 != 0) } else { false } } // TODO cache these, but watch for changes to PATH fn find_path_executables() -> Option> { let path_var = std::env::var_os("PATH")?; let paths: Vec<_> = std::env::split_paths(&path_var).collect(); let mut executables: IndexSet = IndexSet::new(); for path in paths { if let Ok(mut contents) = std::fs::read_dir(path) { while let Some(Ok(item)) = contents.next() { if is_executable(&item.path()) { if let Ok(name) = item.file_name().into_string() { executables.insert(name); } } } } } Some(executables) }