Support completions for a single hyphen argument. (#2388)

The parser sees this as a positional argument, but when requesting completions
this could be either a filename that starts with a hyphen, or it could be a
flag. This expands the completion engine's interface to return a vec of possible
completion locations instead of an optional one, because we want to show all
possibilities instead of assuming one or the either.
This commit is contained in:
Jason Gedge 2020-08-21 21:26:47 -04:00 committed by GitHub
parent ee26590011
commit 8f568f4fc5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 86 additions and 46 deletions

View file

@ -177,19 +177,35 @@ impl<'s> Flatten<'s> {
const BEFORE_COMMAND_CHARS: &[char] = &['|', '(', ';'];
/// Determines the completion location for a given block at the given cursor position
pub fn completion_location(line: &str, block: &Block, pos: usize) -> Option<CompletionLocation> {
pub fn completion_location(line: &str, block: &Block, pos: usize) -> Vec<CompletionLocation> {
let completion_engine = Flatten::new(line);
let locations = completion_engine.completion_locations(block);
if locations.is_empty() {
Some(LocationType::Command.spanned(Span::unknown()))
vec![LocationType::Command.spanned(Span::unknown())]
} else {
let mut prev = None;
for loc in locations {
// We don't use span.contains because we want to include the end. This handles the case
// where the cursor is just after the text (i.e., no space between cursor and text)
if loc.span.start() <= pos && pos <= loc.span.end() {
return Some(loc);
// The parser sees the "-" in `cmd -` as an argument, but the user is likely
// expecting a flag.
return match loc.item {
LocationType::Argument(ref cmd, _) => {
if loc.span.slice(line) == "-" {
let cmd = cmd.clone();
let span = loc.span;
vec![
loc,
LocationType::Flag(cmd.unwrap_or_default()).spanned(span),
]
} else {
vec![loc]
}
}
_ => vec![loc],
};
} else if pos < loc.span.start() {
break;
}
@ -202,14 +218,14 @@ pub fn completion_location(line: &str, block: &Block, pos: usize) -> Option<Comp
// is after some character that would imply we're in the command position.
let start = prev.span.end();
if line[start..pos].contains(BEFORE_COMMAND_CHARS) {
Some(LocationType::Command.spanned(Span::unknown()))
vec![LocationType::Command.spanned(Span::unknown())]
} else {
// TODO this should be able to be mapped to a command
Some(LocationType::Argument(None, None).spanned(Span::unknown()))
vec![LocationType::Argument(None, None).spanned(Span::unknown())]
}
} else {
// Cursor is before any possible completion location, so must be a command
Some(LocationType::Command.spanned(Span::unknown()))
vec![LocationType::Command.spanned(Span::unknown())]
}
}
}
@ -253,10 +269,13 @@ mod tests {
line: &str,
registry: &dyn SignatureRegistry,
pos: usize,
) -> Option<LocationType> {
) -> Vec<LocationType> {
let lite_block = lite_parse(line, 0).expect("lite_parse");
let block = classify_block(&lite_block, registry);
super::completion_location(line, &block.block, pos).map(|v| v.item)
super::completion_location(line, &block.block, pos)
.into_iter()
.map(|v| v.item)
.collect()
}
#[test]
@ -267,7 +286,7 @@ mod tests {
assert_eq!(
completion_location(line, &registry, 10),
Some(LocationType::Command),
vec![LocationType::Command],
);
}
@ -278,7 +297,7 @@ mod tests {
assert_eq!(
completion_location(line, &registry, 10),
Some(LocationType::Command),
vec![LocationType::Command],
);
}
@ -289,7 +308,7 @@ mod tests {
assert_eq!(
completion_location(line, &registry, 4),
Some(LocationType::Command),
vec![LocationType::Command],
);
}
@ -300,7 +319,7 @@ mod tests {
assert_eq!(
completion_location(line, &registry, 13),
Some(LocationType::Variable),
vec![LocationType::Variable],
);
}
@ -315,7 +334,25 @@ mod tests {
assert_eq!(
completion_location(line, &registry, 7),
Some(LocationType::Flag("du".to_string())),
vec![LocationType::Flag("du".to_string())],
);
}
#[test]
fn completes_flags_with_just_a_single_hyphen() {
let registry: VecRegistry = vec![Signature::build("du")
.switch("recursive", "the values to echo", None)
.rest(SyntaxShape::Any, "blah")]
.into();
let line = "du -";
assert_eq!(
completion_location(line, &registry, 3),
vec![
LocationType::Argument(Some("du".to_string()), None),
LocationType::Flag("du".to_string()),
],
);
}
@ -327,7 +364,7 @@ mod tests {
assert_eq!(
completion_location(line, &registry, 6),
Some(LocationType::Argument(Some("echo".to_string()), None)),
vec![LocationType::Argument(Some("echo".to_string()), None)],
);
}
}

View file

@ -20,41 +20,44 @@ impl NuCompleter {
Err(result) => result.partial,
};
let location = lite_block
let locations = lite_block
.map(|block| nu_parser::classify_block(&block, &nu_context.registry))
.and_then(|block| {
crate::completion::engine::completion_location(line, &block.block, pos)
});
.map(|block| crate::completion::engine::completion_location(line, &block.block, pos))
.unwrap_or_default();
if let Some(location) = location {
let partial = location.span.slice(line);
let suggestions = match location.item {
LocationType::Command => {
let command_completer = crate::completion::command::Completer {};
command_completer.complete(context, partial)
}
LocationType::Flag(cmd) => {
let flag_completer = crate::completion::flag::Completer {};
flag_completer.complete(context, cmd, partial)
}
LocationType::Argument(_cmd, _arg_name) => {
// TODO use cmd and arg_name to narrow things down further
let path_completer = crate::completion::path::Completer::new();
path_completer.complete(context, partial)
}
LocationType::Variable => Vec::new(),
}
.into_iter()
.map(requote)
.collect();
(location.span.start(), suggestions)
} else {
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 = crate::completion::command::Completer {};
command_completer.complete(context, partial)
}
LocationType::Flag(cmd) => {
let flag_completer = crate::completion::flag::Completer {};
flag_completer.complete(context, cmd, partial)
}
LocationType::Argument(_cmd, _arg_name) => {
// TODO use cmd and arg_name to narrow things down further
let path_completer = crate::completion::path::Completer::new();
path_completer.complete(context, partial)
}
LocationType::Variable => Vec::new(),
}
.into_iter()
.map(requote)
})
.collect();
(pos, suggestions)
}
}
}