Skip to content

Commit 4005fbb

Browse files
mitsuhikoShekhinah Memmel
authored andcommitted
Correctly handle escaping in completion (helix-editor#4316)
* Correctly handle escaping in completion * Added escaping tests
1 parent d754b96 commit 4005fbb

File tree

3 files changed

+40
-6
lines changed

3 files changed

+40
-6
lines changed

helix-core/src/shellwords.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
use std::borrow::Cow;
22

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+
320
/// Get the vec of escaped / quoted / doublequoted filenames from the input str
421
pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
522
enum State {
@@ -226,4 +243,18 @@ mod test {
226243
];
227244
assert_eq!(expected, result);
228245
}
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+
}
229260
}

helix-term/src/commands/typed.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2183,12 +2183,10 @@ 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-
// we use .this over split_whitespace() because we care about empty segments
2187-
let parts = input.split(' ').collect::<Vec<&str>>();
2188-
21892186
// simple heuristic: if there's no just one part, complete command name.
21902187
// 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 {
21922190
let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST
21932191
.iter()
21942192
.filter_map(|command| {
@@ -2204,12 +2202,13 @@ pub(super) fn command_mode(cx: &mut Context) {
22042202
.map(|(name, _)| (0.., name.into()))
22052203
.collect()
22062204
} else {
2205+
let parts = shellwords::shellwords(input);
22072206
let part = parts.last().unwrap();
22082207

22092208
if let Some(typed::TypableCommand {
22102209
completer: Some(completer),
22112210
..
2212-
}) = typed::TYPABLE_COMMAND_MAP.get(parts[0])
2211+
}) = typed::TYPABLE_COMMAND_MAP.get(&parts[0] as &str)
22132212
{
22142213
completer(editor, part)
22152214
.into_iter()

helix-term/src/ui/prompt.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::compositor::{Component, Compositor, Context, Event, EventResult};
22
use crate::{alt, ctrl, key, shift, ui};
3+
use helix_core::shellwords;
34
use helix_view::input::KeyEvent;
45
use helix_view::keyboard::KeyCode;
56
use std::{borrow::Cow, ops::RangeFrom};
@@ -335,7 +336,10 @@ impl Prompt {
335336

336337
let (range, item) = &self.completion[index];
337338

338-
self.line.replace_range(range.clone(), item);
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);
342+
self.line.replace_range(range.clone(), &item);
339343

340344
self.move_end();
341345
}

0 commit comments

Comments
 (0)