aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Davis2022-11-04 01:55:13 +0000
committerBlaž Hrastnik2022-11-07 04:38:16 +0000
commit1536a6528968f38adfac2e991b29006f5ded5968 (patch)
tree73b1b0ebe81b03e03af6e8098c7c92259b728d03
parent48a3965ab43718ce2a49724cbcc294b04c328b81 (diff)
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.
-rw-r--r--helix-core/src/shellwords.rs84
-rw-r--r--helix-term/src/commands/typed.rs18
2 files changed, 86 insertions, 16 deletions
diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs
index e8c5945b..3375bef1 100644
--- a/helix-core/src/shellwords.rs
+++ b/helix-core/src/shellwords.rs
@@ -17,18 +17,18 @@ pub fn escape(input: &str) -> Cow<'_, str> {
}
}
+enum State {
+ OnWhitespace,
+ Unquoted,
+ UnquotedEscaped,
+ Quoted,
+ QuoteEscaped,
+ Dquoted,
+ DquoteEscaped,
+}
+
/// Get the vec of escaped / quoted / doublequoted filenames from the input str
pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
- enum State {
- OnWhitespace,
- Unquoted,
- UnquotedEscaped,
- Quoted,
- QuoteEscaped,
- Dquoted,
- DquoteEscaped,
- }
-
use State::*;
let mut state = Unquoted;
@@ -140,6 +140,70 @@ pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
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)]
mod test {
use super::*;
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
index f4dfce7a..304b30f9 100644
--- a/helix-term/src/commands/typed.rs
+++ b/helix-term/src/commands/typed.rs
@@ -2183,10 +2183,11 @@ pub(super) fn command_mode(cx: &mut Context) {
static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
- // simple heuristic: if there's no just one part, complete command name.
- // if there's a space, per command completion kicks in.
- // we use .this over split_whitespace() because we care about empty segments
- if input.split(' ').count() <= 1 {
+ let parts = shellwords::shellwords(input);
+ let ends_with_whitespace = shellwords::ends_with_whitespace(input);
+
+ 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
.iter()
.filter_map(|command| {
@@ -2202,8 +2203,13 @@ pub(super) fn command_mode(cx: &mut Context) {
.map(|(name, _)| (0.., name.into()))
.collect()
} else {
- let parts = shellwords::shellwords(input);
- let part = parts.last().unwrap();
+ // Otherwise, use the command's completer and the last shellword
+ // as completion input.
+ let part = if parts.len() == 1 {
+ &Cow::Borrowed("")
+ } else {
+ parts.last().unwrap()
+ };
if let Some(typed::TypableCommand {
completer: Some(completer),