use super::matchers::Matcher; use crate::{Completer, CompletionContext, Suggestion}; use indexmap::set::IndexSet; #[cfg(feature = "is_executable")] #[allow(unused)] use is_executable::IsExecutable; use nu_test_support::NATIVE_PATH_ENV_VAR; use std::iter::FromIterator; use std::path::Path; pub struct CommandCompleter; impl Completer for CommandCompleter where Context: CompletionContext, { fn complete(&self, ctx: &Context, partial: &str, matcher: &dyn Matcher) -> Vec { let registry = ctx.signature_registry(); let mut commands: IndexSet = IndexSet::from_iter(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.is_empty() { let path_completer = crate::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 } } #[cfg(windows)] fn is_executable(path: &Path) -> bool { // This call to a crate essentially checks the PATHEXT on Windows and does some // low level WinAPI calls to determine if the file is executable. It seems quite // a bit faster than calling path.metadata(). path.is_executable() } #[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(NATIVE_PATH_ENV_VAR)?; 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) }