Show directories and executable for command completion. (#2446)

* Show directories and executable for command completion.

Previously we chose from two sets for completing the command position:

1. internal commands, and
2. executables relative to the PATH environment variable.

We now also show directories/executables that match the relative/absolute path
that has been partially typed.

* Fix for Windows
This commit is contained in:
Jason Gedge 2020-08-29 23:31:42 -04:00 committed by GitHub
parent 0390ec97f4
commit 6535ae3d6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 31 additions and 14 deletions

View File

@ -1,5 +1,5 @@
use std::fs::{read_dir, DirEntry};
use std::iter::FromIterator; use std::iter::FromIterator;
use std::path::Path;
use indexmap::set::IndexSet; use indexmap::set::IndexSet;
@ -13,22 +13,41 @@ impl Completer {
let context: &context::Context = ctx.as_ref(); let context: &context::Context = ctx.as_ref();
let mut commands: IndexSet<String> = IndexSet::from_iter(context.registry.names()); let mut commands: IndexSet<String> = 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(); let path_executables = find_path_executables().unwrap_or_default();
// TODO quote these, if necessary // TODO quote these, if necessary
commands.extend(path_executables.into_iter()); commands.extend(path_executables.into_iter());
commands let mut suggestions: Vec<_> = commands
.into_iter() .into_iter()
.filter(|v| v.starts_with(partial)) .filter(|v| v.starts_with(partial))
.map(|v| Suggestion { .map(|v| Suggestion {
replacement: format!("{} ", v), replacement: format!("{} ", v),
display: v, display: v,
}) })
.collect() .collect();
if partial != "" {
let path_completer = crate::completion::path::Completer::new();
let path_results = path_completer.complete(ctx, partial);
suggestions.extend(path_results.into_iter().filter(|suggestion| {
let path = Path::new(&suggestion.replacement);
path.is_dir() || is_executable(&path)
}));
}
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)] #[cfg(windows)]
fn pathext() -> Option<Vec<String>> { fn pathext() -> Option<Vec<String>> {
std::env::var_os("PATHEXT").map(|v| { std::env::var_os("PATHEXT").map(|v| {
@ -41,8 +60,8 @@ fn pathext() -> Option<Vec<String>> {
} }
#[cfg(windows)] #[cfg(windows)]
fn is_executable(file: &DirEntry) -> bool { fn is_executable(path: &Path) -> bool {
if let Ok(metadata) = file.metadata() { if let Ok(metadata) = path.metadata() {
let file_type = metadata.file_type(); let file_type = metadata.file_type();
// If the entry isn't a file, it cannot be executable // If the entry isn't a file, it cannot be executable
@ -50,7 +69,7 @@ fn is_executable(file: &DirEntry) -> bool {
return false; return false;
} }
if let Some(extension) = file.path().extension() { if let Some(extension) = path.extension() {
if let Some(exts) = pathext() { if let Some(exts) = pathext() {
exts.iter() exts.iter()
.any(|ext| extension.to_string_lossy().eq_ignore_ascii_case(ext)) .any(|ext| extension.to_string_lossy().eq_ignore_ascii_case(ext))
@ -66,17 +85,15 @@ fn is_executable(file: &DirEntry) -> bool {
} }
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
fn is_executable(_file: &DirEntry) -> bool { fn is_executable(_path: &Path) -> bool {
false false
} }
#[cfg(unix)] #[cfg(unix)]
fn is_executable(file: &DirEntry) -> bool { fn is_executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
let metadata = file.metadata(); if let Ok(metadata) = path.metadata() {
if let Ok(metadata) = metadata {
let filetype = metadata.file_type(); let filetype = metadata.file_type();
let permissions = metadata.permissions(); let permissions = metadata.permissions();
@ -95,9 +112,9 @@ fn find_path_executables() -> Option<IndexSet<String>> {
let mut executables: IndexSet<String> = IndexSet::new(); let mut executables: IndexSet<String> = IndexSet::new();
for path in paths { for path in paths {
if let Ok(mut contents) = read_dir(path) { if let Ok(mut contents) = std::fs::read_dir(path) {
while let Some(Ok(item)) = contents.next() { while let Some(Ok(item)) = contents.next() {
if is_executable(&item) { if is_executable(&item.path()) {
if let Ok(name) = item.file_name().into_string() { if let Ok(name) = item.file_name().into_string() {
executables.insert(name); executables.insert(name);
} }

View File

@ -36,7 +36,7 @@ impl NuCompleter {
let partial = location.span.slice(line); let partial = location.span.slice(line);
match location.item { match location.item {
LocationType::Command => { LocationType::Command => {
let command_completer = crate::completion::command::Completer {}; let command_completer = crate::completion::command::Completer;
command_completer.complete(context, partial) command_completer.complete(context, partial)
} }