aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--helix-core/src/shellwords.rs31
-rw-r--r--helix-term/src/commands/typed.rs9
-rw-r--r--helix-term/src/ui/prompt.rs6
3 files changed, 40 insertions, 6 deletions
diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs
index afc83496..6edf3cc7 100644
--- a/helix-core/src/shellwords.rs
+++ b/helix-core/src/shellwords.rs
@@ -1,5 +1,22 @@
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
pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
enum State {
@@ -226,4 +243,18 @@ mod test {
];
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\""));
+ }
}
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
index 0cf75ada..f4dfce7a 100644
--- a/helix-term/src/commands/typed.rs
+++ b/helix-term/src/commands/typed.rs
@@ -2183,12 +2183,10 @@ pub(super) fn command_mode(cx: &mut Context) {
static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
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.
// 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
.iter()
.filter_map(|command| {
@@ -2204,12 +2202,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();
if let Some(typed::TypableCommand {
completer: Some(completer),
..
- }) = typed::TYPABLE_COMMAND_MAP.get(parts[0])
+ }) = typed::TYPABLE_COMMAND_MAP.get(&parts[0] as &str)
{
completer(editor, part)
.into_iter()
diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs
index db3bd62d..d0991d3c 100644
--- a/helix-term/src/ui/prompt.rs
+++ b/helix-term/src/ui/prompt.rs
@@ -1,5 +1,6 @@
use crate::compositor::{Component, Compositor, Context, Event, EventResult};
use crate::{alt, ctrl, key, shift, ui};
+use helix_core::shellwords;
use helix_view::input::KeyEvent;
use helix_view::keyboard::KeyCode;
use std::{borrow::Cow, ops::RangeFrom};
@@ -335,7 +336,10 @@ impl Prompt {
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();
}