From 74c2daf6658326287461f094aa20ddf32a8291c9 Mon Sep 17 00:00:00 2001 From: Aaron Christiansen Date: Sun, 24 May 2020 01:27:52 +0100 Subject: [PATCH] Add completion for binaries on PATH (#1866) --- crates/nu-cli/src/shell/completer.rs | 144 +++++++++++++++++++++++---- docs/commands/config.md | 23 +++-- 2 files changed, 135 insertions(+), 32 deletions(-) diff --git a/crates/nu-cli/src/shell/completer.rs b/crates/nu-cli/src/shell/completer.rs index ba38cc6cc0..1dad53abd9 100644 --- a/crates/nu-cli/src/shell/completer.rs +++ b/crates/nu-cli/src/shell/completer.rs @@ -1,7 +1,16 @@ use crate::context::CommandRegistry; +use crate::data::config; +use crate::prelude::*; use derive_new::new; +#[cfg(windows)] +use ichwh::IchwhError; +use ichwh::IchwhResult; +use indexmap::set::IndexSet; use rustyline::completion::{Completer, FilenameCompleter}; +use std::fs::{read_dir, DirEntry}; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; use std::path::PathBuf; #[derive(new)] @@ -11,6 +20,12 @@ pub(crate) struct NuCompleter { pub homedir: Option, } +#[derive(PartialEq, Eq, Debug)] +enum ReplacementLocation { + Command, + Other, +} + impl NuCompleter { pub fn complete( &self, @@ -22,7 +37,7 @@ impl NuCompleter { let line_chars: Vec<_> = line[..pos].chars().collect(); - let replace_pos = self.get_replace_pos(line, pos); + let (replace_pos, replace_loc) = self.get_replace_pos(line, pos); let mut completions; @@ -56,27 +71,46 @@ impl NuCompleter { } }; - for command in commands.iter() { - let mut pos = replace_pos; - let mut matched = false; - if pos < line_chars.len() { - for chr in command.chars() { - if line_chars[pos] != chr { - break; - } - pos += 1; - if pos == line_chars.len() { - matched = true; - break; + let complete_from_path = match config::config(Tag::unknown()) { + Ok(conf) => match conf.get("complete_from_path") { + Some(val) => val.is_true(), + _ => true, + }, + _ => true, + }; + + // Only complete executables or commands if the thing we're completing + // is syntactically a command + if replace_loc == ReplacementLocation::Command { + let mut all_executables: IndexSet<_> = commands.iter().map(|x| x.to_string()).collect(); + if complete_from_path { + let path_executables = self.find_path_executables().unwrap_or_default(); + for path_exe in path_executables { + all_executables.insert(path_exe); + } + }; + for exe in all_executables.iter() { + let mut pos = replace_pos; + let mut matched = false; + if pos < line_chars.len() { + for chr in exe.chars() { + if line_chars[pos] != chr { + break; + } + pos += 1; + if pos == line_chars.len() { + matched = true; + break; + } } } - } - if matched { - completions.push(rustyline::completion::Pair { - display: command.clone(), - replacement: command.clone(), - }); + if matched { + completions.push(rustyline::completion::Pair { + display: exe.to_string(), + replacement: exe.to_string(), + }); + } } } @@ -92,10 +126,11 @@ impl NuCompleter { Ok((replace_pos, completions)) } - fn get_replace_pos(&self, line: &str, pos: usize) -> usize { + fn get_replace_pos(&self, line: &str, pos: usize) -> (usize, ReplacementLocation) { let line_chars: Vec<_> = line[..pos].chars().collect(); let mut replace_pos = line_chars.len(); let mut parsed_pos = false; + let mut loc = ReplacementLocation::Other; if let Ok(lite_block) = nu_parser::lite_parse(line, 0) { 'outer: for pipeline in lite_block.block.iter() { for command in pipeline.commands.iter() { @@ -103,6 +138,7 @@ impl NuCompleter { if name_span.start() <= pos && name_span.end() >= pos { replace_pos = name_span.start(); parsed_pos = true; + loc = ReplacementLocation::Command; break 'outer; } @@ -127,7 +163,7 @@ impl NuCompleter { } } - replace_pos + (replace_pos, loc) } fn get_matching_arguments( @@ -173,4 +209,70 @@ impl NuCompleter { matching_arguments } + + // These is_executable/pathext implementations are copied from ichwh and modified + // to not be async + + #[cfg(windows)] + fn pathext(&self) -> IchwhResult> { + Ok(std::env::var_os("PATHEXT") + .ok_or(IchwhError::PathextNotDefined)? + .to_string_lossy() + .split(';') + // Cut off the leading '.' character + .map(|ext| ext[1..].to_string()) + .collect::>()) + } + + #[cfg(windows)] + fn is_executable(&self, file: &DirEntry) -> IchwhResult { + let file_type = file.metadata()?.file_type(); + + // If the entry isn't a file, it cannot be executable + if !(file_type.is_file() || file_type.is_symlink()) { + return Ok(false); + } + + if let Some(extension) = file.path().extension() { + let exts = self.pathext()?; + + Ok(exts + .iter() + .any(|ext| extension.to_string_lossy().eq_ignore_ascii_case(ext))) + } else { + Ok(false) + } + } + + #[cfg(unix)] + fn is_executable(&self, file: &DirEntry) -> IchwhResult { + let metadata = file.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 + Ok((filetype.is_file() || filetype.is_symlink()) && (permissions.mode() & 0o111 != 0)) + } + + fn find_path_executables(&self) -> 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) = read_dir(path) { + while let Some(Ok(item)) = contents.next() { + if let Ok(true) = self.is_executable(&item) { + if let Ok(name) = item.file_name().into_string() { + executables.insert(name); + } + } + } + } + } + + Some(executables) + } } diff --git a/docs/commands/config.md b/docs/commands/config.md index 062e125730..94ff2772ee 100644 --- a/docs/commands/config.md +++ b/docs/commands/config.md @@ -29,17 +29,18 @@ Syntax: `config {flags}` ### Variables -| Variable | Type | Description | -| --------------- | ---------------------- | -------------------------------------------------------------- | -| path | table of strings | PATH to use to find binaries | -| env | row | the environment variables to pass to external commands | -| ctrlc_exit | boolean | whether or not to exit Nu after multiple ctrl-c presses | -| table_mode | "light" or other | enable lightweight or normal tables | -| edit_mode | "vi" or "emacs" | changes line editing to "vi" or "emacs" mode | -| key_timeout | integer (milliseconds) | vi: the delay to wait for a longer key sequence after ESC | -| history_size | integer | maximum entries that will be stored in history (100,000 default) | -| completion_mode | "circular" or "list" | changes completion type to "circular" (default) or "list" mode | -| no_auto_pivot | boolean | whether or not to automatically pivot single-row results | +| Variable | Type | Description | +| ------------------ | ---------------------- | ------------------------------------------------------------------- | +| path | table of strings | PATH to use to find binaries | +| env | row | the environment variables to pass to external commands | +| ctrlc_exit | boolean | whether or not to exit Nu after multiple ctrl-c presses | +| table_mode | "light" or other | enable lightweight or normal tables | +| edit_mode | "vi" or "emacs" | changes line editing to "vi" or "emacs" mode | +| key_timeout | integer (milliseconds) | vi: the delay to wait for a longer key sequence after ESC | +| history_size | integer | maximum entries that will be stored in history (100,000 default) | +| completion_mode | "circular" or "list" | changes completion type to "circular" (default) or "list" mode | +| no_auto_pivot | boolean | whether or not to automatically pivot single-row results | +| complete_from_path | boolean | whether or not to complete names of binaries on PATH (default true) | ## Examples