nushell/crates/nu-cli/src/shell/completer.rs

279 lines
9.8 KiB
Rust
Raw Normal View History

use crate::context::CommandRegistry;
use crate::data::config;
use crate::prelude::*;
2019-05-26 08:54:41 +02:00
use derive_new::new;
#[cfg(windows)]
use ichwh::IchwhError;
use ichwh::IchwhResult;
use indexmap::set::IndexSet;
2019-08-09 22:49:43 +02:00
use rustyline::completion::{Completer, FilenameCompleter};
use std::fs::{read_dir, DirEntry};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
Restructure and streamline token expansion (#1123) Restructure and streamline token expansion The purpose of this commit is to streamline the token expansion code, by removing aspects of the code that are no longer relevant, removing pointless duplication, and eliminating the need to pass the same arguments to `expand_syntax`. The first big-picture change in this commit is that instead of a handful of `expand_` functions, which take a TokensIterator and ExpandContext, a smaller number of methods on the `TokensIterator` do the same job. The second big-picture change in this commit is fully eliminating the coloring traits, making coloring a responsibility of the base expansion implementations. This also means that the coloring tracer is merged into the expansion tracer, so you can follow a single expansion and see how the expansion process produced colored tokens. One side effect of this change is that the expander itself is marginally more error-correcting. The error correction works by switching from structured expansion to `BackoffColoringMode` when an unexpected token is found, which guarantees that all spans of the source are colored, but may not be the most optimal error recovery strategy. That said, because `BackoffColoringMode` only extends as far as a closing delimiter (`)`, `]`, `}`) or pipe (`|`), it does result in fairly granular correction strategy. The current code still produces an `Err` (plus a complete list of colored shapes) from the parsing process if any errors are encountered, but this could easily be addressed now that the underlying expansion is error-correcting. This commit also colors any spans that are syntax errors in red, and causes the parser to include some additional information about what tokens were expected at any given point where an error was encountered, so that completions and hinting could be more robust in the future. Co-authored-by: Jonathan Turner <jonathandturner@users.noreply.github.com> Co-authored-by: Andrés N. Robalino <andres@androbtech.com>
2020-01-21 23:45:03 +01:00
use std::path::PathBuf;
2019-05-16 23:43:36 +02:00
2019-05-26 08:54:41 +02:00
#[derive(new)]
pub(crate) struct NuCompleter {
pub file_completer: FilenameCompleter,
2019-08-10 07:02:15 +02:00
pub commands: CommandRegistry,
Restructure and streamline token expansion (#1123) Restructure and streamline token expansion The purpose of this commit is to streamline the token expansion code, by removing aspects of the code that are no longer relevant, removing pointless duplication, and eliminating the need to pass the same arguments to `expand_syntax`. The first big-picture change in this commit is that instead of a handful of `expand_` functions, which take a TokensIterator and ExpandContext, a smaller number of methods on the `TokensIterator` do the same job. The second big-picture change in this commit is fully eliminating the coloring traits, making coloring a responsibility of the base expansion implementations. This also means that the coloring tracer is merged into the expansion tracer, so you can follow a single expansion and see how the expansion process produced colored tokens. One side effect of this change is that the expander itself is marginally more error-correcting. The error correction works by switching from structured expansion to `BackoffColoringMode` when an unexpected token is found, which guarantees that all spans of the source are colored, but may not be the most optimal error recovery strategy. That said, because `BackoffColoringMode` only extends as far as a closing delimiter (`)`, `]`, `}`) or pipe (`|`), it does result in fairly granular correction strategy. The current code still produces an `Err` (plus a complete list of colored shapes) from the parsing process if any errors are encountered, but this could easily be addressed now that the underlying expansion is error-correcting. This commit also colors any spans that are syntax errors in red, and causes the parser to include some additional information about what tokens were expected at any given point where an error was encountered, so that completions and hinting could be more robust in the future. Co-authored-by: Jonathan Turner <jonathandturner@users.noreply.github.com> Co-authored-by: Andrés N. Robalino <andres@androbtech.com>
2020-01-21 23:45:03 +01:00
pub homedir: Option<PathBuf>,
}
2019-05-16 23:43:36 +02:00
#[derive(PartialEq, Eq, Debug)]
enum ReplacementLocation {
Command,
Other,
}
2019-08-09 06:51:21 +02:00
impl NuCompleter {
pub fn complete(
2019-05-16 23:43:36 +02:00
&self,
line: &str,
pos: usize,
context: &rustyline::Context,
2019-08-09 21:42:23 +02:00
) -> rustyline::Result<(usize, Vec<rustyline::completion::Pair>)> {
Restructure and streamline token expansion (#1123) Restructure and streamline token expansion The purpose of this commit is to streamline the token expansion code, by removing aspects of the code that are no longer relevant, removing pointless duplication, and eliminating the need to pass the same arguments to `expand_syntax`. The first big-picture change in this commit is that instead of a handful of `expand_` functions, which take a TokensIterator and ExpandContext, a smaller number of methods on the `TokensIterator` do the same job. The second big-picture change in this commit is fully eliminating the coloring traits, making coloring a responsibility of the base expansion implementations. This also means that the coloring tracer is merged into the expansion tracer, so you can follow a single expansion and see how the expansion process produced colored tokens. One side effect of this change is that the expander itself is marginally more error-correcting. The error correction works by switching from structured expansion to `BackoffColoringMode` when an unexpected token is found, which guarantees that all spans of the source are colored, but may not be the most optimal error recovery strategy. That said, because `BackoffColoringMode` only extends as far as a closing delimiter (`)`, `]`, `}`) or pipe (`|`), it does result in fairly granular correction strategy. The current code still produces an `Err` (plus a complete list of colored shapes) from the parsing process if any errors are encountered, but this could easily be addressed now that the underlying expansion is error-correcting. This commit also colors any spans that are syntax errors in red, and causes the parser to include some additional information about what tokens were expected at any given point where an error was encountered, so that completions and hinting could be more robust in the future. Co-authored-by: Jonathan Turner <jonathandturner@users.noreply.github.com> Co-authored-by: Andrés N. Robalino <andres@androbtech.com>
2020-01-21 23:45:03 +01:00
let commands: Vec<String> = self.commands.names();
2019-09-02 20:06:25 +02:00
let line_chars: Vec<_> = line[..pos].chars().collect();
2019-12-08 07:23:31 +01:00
let (replace_pos, replace_loc) = self.get_replace_pos(line, pos);
2019-12-08 07:42:43 +01:00
let mut completions;
2019-12-08 06:58:53 +01:00
2019-12-08 07:23:31 +01:00
// See if we're a flag
2019-12-08 17:36:24 +01:00
if pos > 0 && replace_pos < line_chars.len() && line_chars[replace_pos] == '-' {
if let Ok(lite_block) = nu_parser::lite_parse(line, 0) {
completions =
self.get_matching_arguments(&lite_block, &line_chars, line, replace_pos, pos);
} else {
completions = self.file_completer.complete(line, pos, context)?.1;
}
2019-12-08 06:58:53 +01:00
} else {
completions = self.file_completer.complete(line, pos, context)?.1;
for completion in &mut completions {
if completion.replacement.contains("\\ ") {
completion.replacement = completion.replacement.replace("\\ ", " ");
}
if completion.replacement.contains("\\(") {
completion.replacement = completion.replacement.replace("\\(", "(");
}
if completion.replacement.contains(' ') || completion.replacement.contains('(') {
if !completion.replacement.starts_with('\"') {
completion.replacement = format!("\"{}", completion.replacement);
}
if !completion.replacement.ends_with('\"') {
completion.replacement = format!("{}\"", completion.replacement);
}
}
}
};
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;
}
2019-05-18 16:06:01 +02:00
}
}
if matched {
completions.push(rustyline::completion::Pair {
display: exe.to_string(),
replacement: exe.to_string(),
});
}
2019-05-18 16:06:01 +02:00
}
}
for completion in &mut completions {
// If the cursor is at a double-quote, remove the double-quote in the replacement
// This prevents duplicate quotes
let cursor_char = line.chars().nth(pos);
if cursor_char.unwrap_or(' ') == '"' && completion.replacement.ends_with('"') {
completion.replacement.pop();
}
}
Ok((replace_pos, completions))
2019-05-16 23:43:36 +02:00
}
2019-12-08 07:42:43 +01:00
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() {
let name_span = command.name.span;
if name_span.start() <= pos && name_span.end() >= pos {
replace_pos = name_span.start();
parsed_pos = true;
loc = ReplacementLocation::Command;
break 'outer;
}
for arg in command.args.iter() {
if arg.span.start() <= pos && arg.span.end() >= pos {
replace_pos = arg.span.start();
parsed_pos = true;
break 'outer;
}
}
}
}
}
if !parsed_pos {
// If the command won't parse, naively detect the completion start point
while replace_pos > 0 {
if line_chars[replace_pos - 1] == ' ' {
break;
}
replace_pos -= 1;
}
}
(replace_pos, loc)
}
2019-12-08 07:42:43 +01:00
fn get_matching_arguments(
&self,
lite_block: &nu_parser::LiteBlock,
2019-12-08 07:42:43 +01:00
line_chars: &[char],
line: &str,
replace_pos: usize,
pos: usize,
) -> Vec<rustyline::completion::Pair> {
let mut matching_arguments = vec![];
let mut line_copy = line.to_string();
let substring = line_chars[replace_pos..pos].iter().collect::<String>();
let replace_string = (replace_pos..pos).map(|_| " ").collect::<String>();
line_copy.replace_range(replace_pos..pos, &replace_string);
let result = nu_parser::classify_block(&lite_block, &self.commands);
for pipeline in &result.block.block {
for command in &pipeline.list {
if let nu_protocol::hir::ClassifiedCommand::Internal(
nu_protocol::hir::InternalCommand { args, .. },
) = command
{
if replace_pos >= args.span.start() && replace_pos <= args.span.end() {
if let Some(named) = &args.named {
for (name, _) in named.iter() {
let full_flag = format!("--{}", name);
if full_flag.starts_with(&substring) {
matching_arguments.push(rustyline::completion::Pair {
display: full_flag.clone(),
replacement: full_flag,
});
}
2019-12-08 07:42:43 +01:00
}
}
}
}
}
}
matching_arguments
}
// These is_executable/pathext implementations are copied from ichwh and modified
// to not be async
#[cfg(windows)]
fn pathext(&self) -> IchwhResult<Vec<String>> {
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::<Vec<_>>())
}
#[cfg(windows)]
fn is_executable(&self, file: &DirEntry) -> IchwhResult<bool> {
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<bool> {
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<IndexSet<String>> {
let path_var = std::env::var_os("PATH")?;
let paths: Vec<_> = std::env::split_paths(&path_var).collect();
let mut executables: IndexSet<String> = 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)
}
2019-05-16 23:43:36 +02:00
}