Correctly handle escaping in completion (#4316)

* Correctly handle escaping in completion

* Added escaping tests
pull/4557/head
Armin Ronacher 2 years ago committed by GitHub
parent 3881fef39d
commit 8584b38cfb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,22 @@
use std::borrow::Cow; use std::borrow::Cow;
/// Auto escape for shellwords usage.
pub fn escape(input: &str) -> Cow<'_, str> {
if !input.chars().any(|x| x.is_ascii_whitespace()) {
Cow::Borrowed(input)
} else if cfg!(unix) {
Cow::Owned(input.chars().fold(String::new(), |mut buf, c| {
if c.is_ascii_whitespace() {
buf.push('\\');
}
buf.push(c);
buf
}))
} else {
Cow::Owned(format!("\"{}\"", input))
}
}
/// Get the vec of escaped / quoted / doublequoted filenames from the input str /// Get the vec of escaped / quoted / doublequoted filenames from the input str
pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> { pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
enum State { enum State {
@ -226,4 +243,18 @@ mod test {
]; ];
assert_eq!(expected, result); assert_eq!(expected, result);
} }
#[cfg(unix)]
fn test_escaping_unix() {
assert_eq!(escape("foobar"), Cow::Borrowed("foobar"));
assert_eq!(escape("foo bar"), Cow::Borrowed("foo\\ bar"));
assert_eq!(escape("foo\tbar"), Cow::Borrowed("foo\\\tbar"));
}
#[test]
#[cfg(windows)]
fn test_escaping_windows() {
assert_eq!(escape("foobar"), Cow::Borrowed("foobar"));
assert_eq!(escape("foo bar"), Cow::Borrowed("\"foo bar\""));
}
} }

@ -2183,12 +2183,10 @@ 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);
// we use .this over split_whitespace() because we care about empty segments
let parts = input.split(' ').collect::<Vec<&str>>();
// simple heuristic: if there's no just one part, complete command name. // simple heuristic: if there's no just one part, complete command name.
// if there's a space, per command completion kicks in. // if there's a space, per command completion kicks in.
if parts.len() <= 1 { // we use .this over split_whitespace() because we care about empty segments
if input.split(' ').count() <= 1 {
let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST
.iter() .iter()
.filter_map(|command| { .filter_map(|command| {
@ -2204,12 +2202,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);
let part = parts.last().unwrap(); let part = parts.last().unwrap();
if let Some(typed::TypableCommand { if let Some(typed::TypableCommand {
completer: Some(completer), completer: Some(completer),
.. ..
}) = typed::TYPABLE_COMMAND_MAP.get(parts[0]) }) = typed::TYPABLE_COMMAND_MAP.get(&parts[0] as &str)
{ {
completer(editor, part) completer(editor, part)
.into_iter() .into_iter()

@ -1,5 +1,6 @@
use crate::compositor::{Component, Compositor, Context, Event, EventResult}; use crate::compositor::{Component, Compositor, Context, Event, EventResult};
use crate::{alt, ctrl, key, shift, ui}; use crate::{alt, ctrl, key, shift, ui};
use helix_core::shellwords;
use helix_view::input::KeyEvent; use helix_view::input::KeyEvent;
use helix_view::keyboard::KeyCode; use helix_view::keyboard::KeyCode;
use std::{borrow::Cow, ops::RangeFrom}; use std::{borrow::Cow, ops::RangeFrom};
@ -335,7 +336,10 @@ impl Prompt {
let (range, item) = &self.completion[index]; let (range, item) = &self.completion[index];
self.line.replace_range(range.clone(), item); // since we are using shellwords to parse arguments, make sure
// that whitespace in files is properly escaped.
let item = shellwords::escape(item);
self.line.replace_range(range.clone(), &item);
self.move_end(); self.move_end();
} }

Loading…
Cancel
Save