nushell/crates/nu-cli/src/shell/completer.rs
Jason Gedge f5fad393d0
Refactor completion trait (#2555)
* Remove completion code from help/value shells

* Tweak the `Completer` trait for nushell.

Previously, this trait was built around rustyline's completion traits, and for
`Shell` instances. Now it is built for individual completers inside of nushell
that will complete a specific location based on a partial string. For example,
for completing a partially typed command in the command position.
2020-09-16 16:37:43 +12:00

139 lines
5.0 KiB
Rust

use crate::completion::command::CommandCompleter;
use crate::completion::flag::FlagCompleter;
use crate::completion::path::{PathCompleter, PathSuggestion};
use crate::completion::{self, Completer, Suggestion};
use crate::context;
pub(crate) struct NuCompleter {}
impl NuCompleter {}
impl NuCompleter {
pub fn complete(
&self,
line: &str,
pos: usize,
context: &completion::Context,
) -> (usize, Vec<Suggestion>) {
use completion::engine::LocationType;
let nu_context: &context::Context = context.as_ref();
let lite_block = match nu_parser::lite_parse(line, 0) {
Ok(block) => Some(block),
Err(result) => result.partial,
};
let locations = lite_block
.map(|block| nu_parser::classify_block(&block, &nu_context.registry))
.map(|block| completion::engine::completion_location(line, &block.block, pos))
.unwrap_or_default();
if locations.is_empty() {
(pos, Vec::new())
} else {
let pos = locations[0].span.start();
let suggestions = locations
.into_iter()
.flat_map(|location| {
let partial = location.span.slice(line);
match location.item {
LocationType::Command => {
let command_completer = CommandCompleter;
command_completer.complete(context, partial)
}
LocationType::Flag(cmd) => {
let flag_completer = FlagCompleter { cmd };
flag_completer.complete(context, partial)
}
LocationType::Argument(cmd, _arg_name) => {
let path_completer = PathCompleter;
const QUOTE_CHARS: &[char] = &['\'', '"', '`'];
// TODO Find a better way to deal with quote chars. Can the completion
// engine relay this back to us? Maybe have two spans: inner and
// outer. The former is what we want to complete, the latter what
// we'd need to replace.
let (quote_char, partial) = if partial.starts_with(QUOTE_CHARS) {
let (head, tail) = partial.split_at(1);
(Some(head), tail)
} else {
(None, partial)
};
let partial = if let Some(quote_char) = quote_char {
if partial.ends_with(quote_char) {
&partial[..partial.len() - 1]
} else {
partial
}
} else {
partial
};
let completed_paths = path_completer.path_suggestions(partial);
match cmd.as_deref().unwrap_or("") {
"cd" => select_directory_suggestions(completed_paths),
_ => completed_paths,
}
.into_iter()
.map(|s| Suggestion {
replacement: requote(s.suggestion.replacement),
display: s.suggestion.display,
})
.collect()
}
LocationType::Variable => Vec::new(),
}
})
.collect();
(pos, suggestions)
}
}
}
fn select_directory_suggestions(completed_paths: Vec<PathSuggestion>) -> Vec<PathSuggestion> {
completed_paths
.into_iter()
.filter(|suggestion| {
suggestion
.path
.metadata()
.map(|md| md.is_dir())
.unwrap_or(false)
})
.collect()
}
fn requote(value: String) -> String {
let value = rustyline::completion::unescape(&value, Some('\\'));
let mut quotes = vec!['"', '\'', '`'];
let mut should_quote = false;
for c in value.chars() {
if c.is_whitespace() {
should_quote = true;
} else if let Some(index) = quotes.iter().position(|q| *q == c) {
should_quote = true;
quotes.swap_remove(index);
}
}
if should_quote {
if quotes.is_empty() {
// TODO we don't really have an escape character, so there isn't a great option right
// now. One possibility is `{{$(char backtick)}}`
value.to_string()
} else {
let quote = quotes[0];
format!("{}{}{}", quote, value, quote)
}
} else {
value.to_string()
}
}