Hack path component movement to skip escaped spaces

Path component movement is not aware of fish syntax -- and we should
be careful as we teach it some fish syntax, because it is expected
to be used on command lines that have unclosed quotes etc.

Tab completion typically uses backslashes to escape paths with spaces.
Using ctrl-w on such path components doesn't work well because it
stops at the escaped space.

Add a quick hack to change it to skip over backslashed spaces.  Note that
this isn't fully correct because it will treat backslashes inside
quotes the same way.  Not sure what we should do here.  We could have
ctrl-w erase all of this

	"this is"'only\ one 'path component

But that might be surprising.
Regardless of what we end up with, skipping over backslashed whitespace
seems totally fine, so add that now

Closes #2016
This commit is contained in:
Johannes Altmanninger
2025-12-16 11:55:05 +01:00
parent 5e401fc6ea
commit ebc140a3ea
2 changed files with 31 additions and 17 deletions

View File

@@ -2198,8 +2198,7 @@ fn move_word(
let mut buff_pos = el.position();
while buff_pos != boundary {
let idx = if move_right { buff_pos } else { buff_pos - 1 };
let c = el.at(idx);
if !state.consume_char(c) {
if !state.consume_char(el.text(), idx) {
break;
}
buff_pos = if move_right {
@@ -5329,8 +5328,7 @@ fn accept_autosuggestion(&mut self, amount: AutosuggestionPortion) {
let have = search_string_range.len();
let mut want = have;
while want < autosuggestion_text.len() {
let wc = autosuggestion_text.as_char_slice()[want];
if !state.consume_char(wc) {
if !state.consume_char(autosuggestion_text, want) {
break;
}
want += 1;

View File

@@ -6,6 +6,7 @@
use crate::future_feature_flags::{FeatureFlag, feature_test};
use crate::parse_constants::SOURCE_OFFSET_INVALID;
use crate::parser_keywords::parser_keywords_is_subcommand;
use crate::reader::is_backslashed;
use crate::redirection::RedirectionMode;
use crate::wchar::prelude::*;
use libc::{STDIN_FILENO, STDOUT_FILENO};
@@ -1179,10 +1180,11 @@ pub fn new(style: MoveWordStyle) -> Self {
MoveWordStateMachine { state: 0, style }
}
pub fn consume_char(&mut self, c: char) -> bool {
pub fn consume_char(&mut self, text: &wstr, idx: usize) -> bool {
let c = text.as_char_slice()[idx];
match self.style {
MoveWordStyle::Punctuation => self.consume_char_punctuation(c),
MoveWordStyle::PathComponents => self.consume_char_path_components(c),
MoveWordStyle::PathComponents => self.consume_char_path_components(text, idx, c),
MoveWordStyle::Whitespace => self.consume_char_whitespace(c),
}
}
@@ -1254,7 +1256,7 @@ fn consume_char_punctuation(&mut self, c: char) -> bool {
consumed
}
fn consume_char_path_components(&mut self, c: char) -> bool {
fn consume_char_path_components(&mut self, s: &wstr, idx: usize, c: char) -> bool {
const S_INITIAL_PUNCTUATION: u8 = 0;
const S_WHITESPACE: u8 = 1;
const S_SEPARATOR: u8 = 2;
@@ -1263,30 +1265,35 @@ fn consume_char_path_components(&mut self, c: char) -> bool {
const S_INITIAL_SEPARATOR: u8 = 5;
const S_END: u8 = 6;
let is_escaped = is_backslashed(s, idx);
let is_whitespace = c.is_whitespace() && !is_escaped;
let is_path_component_character =
is_path_component_character(c) || (c.is_whitespace() && is_escaped);
let mut consumed = false;
while self.state != S_END && !consumed {
match self.state {
S_INITIAL_PUNCTUATION => {
if !is_path_component_character(c) && !c.is_whitespace() {
if !is_path_component_character && !is_whitespace {
self.state = S_INITIAL_SEPARATOR;
} else {
if !is_path_component_character(c) {
if !is_path_component_character {
consumed = true;
}
self.state = S_WHITESPACE;
}
}
S_WHITESPACE => {
if c.is_whitespace() {
if is_whitespace {
consumed = true; // consumed whitespace
} else if c == '/' || is_path_component_character(c) {
} else if c == '/' || is_path_component_character {
self.state = S_SLASH; // path component
} else {
self.state = S_SEPARATOR; // path separator
}
}
S_SEPARATOR => {
if !c.is_whitespace() && !is_path_component_character(c) {
if !is_whitespace && !is_path_component_character {
consumed = true; // consumed separator
} else {
self.state = S_END;
@@ -1300,17 +1307,17 @@ fn consume_char_path_components(&mut self, c: char) -> bool {
}
}
S_PATH_COMPONENT_CHARACTERS => {
if is_path_component_character(c) {
if is_path_component_character {
consumed = true; // consumed string character except slash
} else {
self.state = S_END;
}
}
S_INITIAL_SEPARATOR => {
if is_path_component_character(c) {
if is_path_component_character {
consumed = true;
self.state = S_PATH_COMPONENT_CHARACTERS;
} else if c.is_whitespace() {
} else if is_whitespace {
self.state = S_END;
} else {
consumed = true;
@@ -1637,8 +1644,7 @@ fn validate_visitor(
} else {
idx
};
let c = command.as_char_slice()[char_idx];
let will_stop = !sm.consume_char(c, || is_backslashed(&command, char_idx));
let will_stop = !sm.consume_char(&command, char_idx);
let expected_stop = stops.contains(&idx);
if will_stop != expected_stop {
on_failure(&format!(
@@ -1731,6 +1737,16 @@ macro_rules! validate {
MoveWordStyle::PathComponents,
"^aa^@@ ^aa@@^a^"
);
validate!(
Direction::Left,
MoveWordStyle::PathComponents,
r#"^a\ ^b\ c/^d"^e\ f"^g"#
);
validate!(
Direction::Left,
MoveWordStyle::PathComponents,
r#"^a\"^bc^"#
);
validate!(Direction::Right, MoveWordStyle::Punctuation, "^a^ bcd^");
validate!(Direction::Right, MoveWordStyle::Punctuation, "a^b^ cde^");