mirror of
https://github.com/helix-editor/helix.git
synced 2025-01-19 13:37:06 +04:00
Correctly handle escaping in completion (#4316)
* Correctly handle escaping in completion * Added escaping tests
This commit is contained in:
parent
3881fef39d
commit
8584b38cfb
@ -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 @@ fn test_lists() {
|
|||||||
];
|
];
|
||||||
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 @@ pub fn change_completion_selection(&mut self, direction: CompletionDirection) {
|
|||||||
|
|
||||||
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…
Reference in New Issue
Block a user