Fix whitespace handling in command-mode completion

8584b38cfb switched to shellwords for
completion in command-mode. This changes the conditions for choosing
whether to complete the command or use the command's completer.

This change processes the input as shellwords up-front and uses
shellword logic about whitespace to determine whether the command
or argument should be completed.
pull/4626/head
Michael Davis 2 years ago committed by Blaž Hrastnik
parent 48a3965ab4
commit 1536a65289

@ -17,8 +17,6 @@ pub fn escape(input: &str) -> Cow<'_, str> {
} }
} }
/// Get the vec of escaped / quoted / doublequoted filenames from the input str
pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
enum State { enum State {
OnWhitespace, OnWhitespace,
Unquoted, Unquoted,
@ -29,6 +27,8 @@ pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
DquoteEscaped, DquoteEscaped,
} }
/// Get the vec of escaped / quoted / doublequoted filenames from the input str
pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
use State::*; use State::*;
let mut state = Unquoted; let mut state = Unquoted;
@ -140,6 +140,70 @@ pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
args args
} }
/// Checks that the input ends with an ascii whitespace character which is
/// not escaped.
///
/// # Examples
///
/// ```rust
/// use helix_core::shellwords::ends_with_whitespace;
/// assert_eq!(ends_with_whitespace(" "), true);
/// assert_eq!(ends_with_whitespace(":open "), true);
/// assert_eq!(ends_with_whitespace(":open foo.txt "), true);
/// assert_eq!(ends_with_whitespace(":open"), false);
/// #[cfg(unix)]
/// assert_eq!(ends_with_whitespace(":open a\\ "), false);
/// #[cfg(unix)]
/// assert_eq!(ends_with_whitespace(":open a\\ b.txt"), false);
/// ```
pub fn ends_with_whitespace(input: &str) -> bool {
use State::*;
// Fast-lane: the input must end with a whitespace character
// regardless of quoting.
if !input.ends_with(|c: char| c.is_ascii_whitespace()) {
return false;
}
let mut state = Unquoted;
for c in input.chars() {
state = match state {
OnWhitespace => match c {
'"' => Dquoted,
'\'' => Quoted,
'\\' if cfg!(unix) => UnquotedEscaped,
'\\' => OnWhitespace,
c if c.is_ascii_whitespace() => OnWhitespace,
_ => Unquoted,
},
Unquoted => match c {
'\\' if cfg!(unix) => UnquotedEscaped,
'\\' => Unquoted,
c if c.is_ascii_whitespace() => OnWhitespace,
_ => Unquoted,
},
UnquotedEscaped => Unquoted,
Quoted => match c {
'\\' if cfg!(unix) => QuoteEscaped,
'\\' => Quoted,
'\'' => OnWhitespace,
_ => Quoted,
},
QuoteEscaped => Quoted,
Dquoted => match c {
'\\' if cfg!(unix) => DquoteEscaped,
'\\' => Dquoted,
'"' => OnWhitespace,
_ => Dquoted,
},
DquoteEscaped => Dquoted,
}
}
matches!(state, OnWhitespace)
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

@ -2183,10 +2183,11 @@ pub(super) fn command_mode(cx: &mut Context) {
static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> = static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default); Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
// simple heuristic: if there's no just one part, complete command name. let parts = shellwords::shellwords(input);
// if there's a space, per command completion kicks in. let ends_with_whitespace = shellwords::ends_with_whitespace(input);
// we use .this over split_whitespace() because we care about empty segments
if input.split(' ').count() <= 1 { if parts.is_empty() || (parts.len() == 1 && !ends_with_whitespace) {
// If the command has not been finished yet, complete commands.
let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST
.iter() .iter()
.filter_map(|command| { .filter_map(|command| {
@ -2202,8 +2203,13 @@ pub(super) fn command_mode(cx: &mut Context) {
.map(|(name, _)| (0.., name.into())) .map(|(name, _)| (0.., name.into()))
.collect() .collect()
} else { } else {
let parts = shellwords::shellwords(input); // Otherwise, use the command's completer and the last shellword
let part = parts.last().unwrap(); // as completion input.
let part = if parts.len() == 1 {
&Cow::Borrowed("")
} else {
parts.last().unwrap()
};
if let Some(typed::TypableCommand { if let Some(typed::TypableCommand {
completer: Some(completer), completer: Some(completer),

Loading…
Cancel
Save