on master help sign in
Fix whitespace handling in command-mode completion

8584b38cfbe6ffe3e5d539ad953c413e44e90bfa 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.

commit1536a6528968f38adfac2e991b29006f5ded5968 authorMichael Davis <mcarsondavis@gmail.com> dateThu 3 Nov 2022, 20:55 (4 years ago) parent48a3965
2 files changed+86−16
helix-core/src/shellwords.rs+74−10 helix-term/src/commands/typed.rs+12−6
helix-core/src/shellwords.rs+74−10
@@ -17,18 +17,18 @@ pub fn escape(input: &str) -> Cow<'_, str> {
1717 }
1818}
1919
20+enum State {
21+ OnWhitespace,
22+ Unquoted,
23+ UnquotedEscaped,
24+ Quoted,
25+ QuoteEscaped,
26+ Dquoted,
27+ DquoteEscaped,
28+}
29+
2030/// Get the vec of escaped / quoted / doublequoted filenames from the input str
2131pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
22− enum State {
23− OnWhitespace,
24− Unquoted,
25− UnquotedEscaped,
26− Quoted,
27− QuoteEscaped,
28− Dquoted,
29− DquoteEscaped,
30− }
31−
3232 use State::*;
3333
3434 let mut state = Unquoted;
@@ -140,6 +140,70 @@ pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
140140 args
141141}
142142
143+/// Checks that the input ends with an ascii whitespace character which is
144+/// not escaped.
145+///
146+/// # Examples
147+///
148+/// ```rust
149+/// use helix_core::shellwords::ends_with_whitespace;
150+/// assert_eq!(ends_with_whitespace(" "), true);
151+/// assert_eq!(ends_with_whitespace(":open "), true);
152+/// assert_eq!(ends_with_whitespace(":open foo.txt "), true);
153+/// assert_eq!(ends_with_whitespace(":open"), false);
154+/// #[cfg(unix)]
155+/// assert_eq!(ends_with_whitespace(":open a\\ "), false);
156+/// #[cfg(unix)]
157+/// assert_eq!(ends_with_whitespace(":open a\\ b.txt"), false);
158+/// ```
159+pub fn ends_with_whitespace(input: &str) -> bool {
160+ use State::*;
161+
162+ // Fast-lane: the input must end with a whitespace character
163+ // regardless of quoting.
164+ if !input.ends_with(|c: char| c.is_ascii_whitespace()) {
165+ return false;
166+ }
167+
168+ let mut state = Unquoted;
169+
170+ for c in input.chars() {
171+ state = match state {
172+ OnWhitespace => match c {
173+ '"' => Dquoted,
174+ '\'' => Quoted,
175+ '\\' if cfg!(unix) => UnquotedEscaped,
176+ '\\' => OnWhitespace,
177+ c if c.is_ascii_whitespace() => OnWhitespace,
178+ _ => Unquoted,
179+ },
180+ Unquoted => match c {
181+ '\\' if cfg!(unix) => UnquotedEscaped,
182+ '\\' => Unquoted,
183+ c if c.is_ascii_whitespace() => OnWhitespace,
184+ _ => Unquoted,
185+ },
186+ UnquotedEscaped => Unquoted,
187+ Quoted => match c {
188+ '\\' if cfg!(unix) => QuoteEscaped,
189+ '\\' => Quoted,
190+ '\'' => OnWhitespace,
191+ _ => Quoted,
192+ },
193+ QuoteEscaped => Quoted,
194+ Dquoted => match c {
195+ '\\' if cfg!(unix) => DquoteEscaped,
196+ '\\' => Dquoted,
197+ '"' => OnWhitespace,
198+ _ => Dquoted,
199+ },
200+ DquoteEscaped => Dquoted,
201+ }
202+ }
203+
204+ matches!(state, OnWhitespace)
205+}
206+
143207#[cfg(test)]
144208mod test {
145209 use super::*;
helix-term/src/commands/typed.rs+12−6
@@ -2183,10 +2183,11 @@ pub(super) fn command_mode(cx: &mut Context) {
21832183 static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
21842184 Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
21852185
2186− // simple heuristic: if there's no just one part, complete command name.
2187− // if there's a space, per command completion kicks in.
2188− // we use .this over split_whitespace() because we care about empty segments
2189− if input.split(' ').count() <= 1 {
2186+ let parts = shellwords::shellwords(input);
2187+ let ends_with_whitespace = shellwords::ends_with_whitespace(input);
2188+
2189+ if parts.is_empty() || (parts.len() == 1 && !ends_with_whitespace) {
2190+ // If the command has not been finished yet, complete commands.
21902191 let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST
21912192 .iter()
21922193 .filter_map(|command| {
@@ -2202,8 +2203,13 @@ pub(super) fn command_mode(cx: &mut Context) {
22022203 .map(|(name, _)| (0.., name.into()))
22032204 .collect()
22042205 } else {
2205− let parts = shellwords::shellwords(input);
2206− let part = parts.last().unwrap();
2206+ // Otherwise, use the command's completer and the last shellword
2207+ // as completion input.
2208+ let part = if parts.len() == 1 {
2209+ &Cow::Borrowed("")
2210+ } else {
2211+ parts.last().unwrap()
2212+ };
22072213
22082214 if let Some(typed::TypableCommand {
22092215 completer: Some(completer),