mirror of
https://github.com/helix-editor/helix.git
synced 2025-01-19 13:37:06 +04:00
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.
This commit is contained in:
parent
48a3965ab4
commit
1536a65289
@ -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 @@ enum State {
|
||||
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::*;
|
||||
|
@ -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),
|
||||
|
Loading…
Reference in New Issue
Block a user