on master help sign in
Correctly handle escaping in completion (#4316)
8584b38Armin Ronacher
helix-core/src/shellwords.rs+31
@@ -1,5 +1,22 @@
1use std::borrow::Cow;
1use std::borrow::Cow;
2
2
3+/// Auto escape for shellwords usage.
4+pub fn escape(input: &str) -> Cow<'_, str> {
5+ if !input.chars().any(|x| x.is_ascii_whitespace()) {
6+ Cow::Borrowed(input)
7+ } else if cfg!(unix) {
8+ Cow::Owned(input.chars().fold(String::new(), |mut buf, c| {
9+ if c.is_ascii_whitespace() {
10+ buf.push('\\');
11+ }
12+ buf.push(c);
13+ buf
14+ }))
15+ } else {
16+ Cow::Owned(format!("\"{}\"", input))
17+ }
18+}
19+
3/// Get the vec of escaped / quoted / doublequoted filenames from the input str
20/// Get the vec of escaped / quoted / doublequoted filenames from the input str
4pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
21pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
5 enum State {
22 enum State {
@@ -226,4 +243,18 @@ mod test {
226 ];
243 ];
227 assert_eq!(expected, result);
244 assert_eq!(expected, result);
228 }
245 }
246+
247+ #[cfg(unix)]
248+ fn test_escaping_unix() {
249+ assert_eq!(escape("foobar"), Cow::Borrowed("foobar"));
250+ assert_eq!(escape("foo bar"), Cow::Borrowed("foo\\ bar"));
251+ assert_eq!(escape("foo\tbar"), Cow::Borrowed("foo\\\tbar"));
252+ }
253+
254+ #[test]
255+ #[cfg(windows)]
256+ fn test_escaping_windows() {
257+ assert_eq!(escape("foobar"), Cow::Borrowed("foobar"));
258+ assert_eq!(escape("foo bar"), Cow::Borrowed("\"foo bar\""));
259+ }
229}
260}
helix-term/src/commands/typed.rs+4−5
@@ -2183,12 +2183,10 @@ pub(super) fn command_mode(cx: &mut Context) {
2183 static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
2183 static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
2184 Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
2184 Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
2185
2185
2186− // we use .this over split_whitespace() because we care about empty segments
2187− let parts = input.split(' ').collect::<Vec<&str>>();
2188−
2189 // simple heuristic: if there's no just one part, complete command name.
2186 // simple heuristic: if there's no just one part, complete command name.
2190 // if there's a space, per command completion kicks in.
2187 // if there's a space, per command completion kicks in.
2191− if parts.len() <= 1 {
2188+ // we use .this over split_whitespace() because we care about empty segments
2189+ if input.split(' ').count() <= 1 {
2192 let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST
2190 let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST
2193 .iter()
2191 .iter()
2194 .filter_map(|command| {
2192 .filter_map(|command| {
@@ -2204,12 +2202,13 @@ pub(super) fn command_mode(cx: &mut Context) {
2204 .map(|(name, _)| (0.., name.into()))
2202 .map(|(name, _)| (0.., name.into()))
2205 .collect()
2203 .collect()
2206 } else {
2204 } else {
2205+ let parts = shellwords::shellwords(input);
2207 let part = parts.last().unwrap();
2206 let part = parts.last().unwrap();
2208
2207
2209 if let Some(typed::TypableCommand {
2208 if let Some(typed::TypableCommand {
2210 completer: Some(completer),
2209 completer: Some(completer),
2211 ..
2210 ..
2212− }) = typed::TYPABLE_COMMAND_MAP.get(parts[0])
2211+ }) = typed::TYPABLE_COMMAND_MAP.get(&parts[0] as &str)
2213 {
2212 {
2214 completer(editor, part)
2213 completer(editor, part)
2215 .into_iter()
2214 .into_iter()
helix-term/src/ui/prompt.rs+5−1
@@ -1,5 +1,6 @@
1use crate::compositor::{Component, Compositor, Context, Event, EventResult};
1use crate::compositor::{Component, Compositor, Context, Event, EventResult};
2use crate::{alt, ctrl, key, shift, ui};
2use crate::{alt, ctrl, key, shift, ui};
3+use helix_core::shellwords;
3use helix_view::input::KeyEvent;
4use helix_view::input::KeyEvent;
4use helix_view::keyboard::KeyCode;
5use helix_view::keyboard::KeyCode;
5use std::{borrow::Cow, ops::RangeFrom};
6use std::{borrow::Cow, ops::RangeFrom};
@@ -335,7 +336,10 @@ impl Prompt {
335
336
336 let (range, item) = &self.completion[index];
337 let (range, item) = &self.completion[index];
337
338
339+ // since we are using shellwords to parse arguments, make sure
340+ // that whitespace in files is properly escaped.
341+ let item = shellwords::escape(item);
338− self.line.replace_range(range.clone(), item);
342+ self.line.replace_range(range.clone(), &item);
339
343
340 self.move_end();
344 self.move_end();
341 }
345 }