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

View File

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