From e56e2d1f9206102068c402a0cadbe62284816586 Mon Sep 17 00:00:00 2001 From: Jonas Maier Date: Mon, 4 May 2026 23:28:17 +0200 Subject: completion now should not introduce strings that do not parse as strings --- src/parse/mod.rs | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 90 insertions(+), 9 deletions(-) (limited to 'src/parse') diff --git a/src/parse/mod.rs b/src/parse/mod.rs index e061d67..02012dc 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -98,14 +98,14 @@ pub fn pipes(cmds: [Command; N]) -> Ast ExpString { ExpString { parts: vec![StringPart::Boring(x.to_vec())], - delim: b' ', + delim: StringDelimiter::None, } } pub fn str(parts: [StringPart; N]) -> ExpString { ExpString { parts: parts.to_vec(), - delim: b' ', + delim: StringDelimiter::None, } } @@ -480,7 +480,7 @@ impl StringPart { /// `"hi ${var} $(cmd) "` gets mapped to `[Boring("hi "), Var("var"), String(" "), Cmd(...), Boring(" ")]` pub struct ExpString { parts: Vec, - delim: u8, + delim: StringDelimiter, } impl ExpString { @@ -559,8 +559,8 @@ impl Parse for VarName { } } -#[derive(Clone, Debug)] -enum StringDelimiter { +#[derive(Clone, Debug, PartialEq)] +pub enum StringDelimiter { /// no delimiter, i.e. when parsing a simple command like `echo foo` None, @@ -593,6 +593,18 @@ enum StringDelimiter { StrictCustom(BString), } +trait PushAll { + fn push_all(&mut self, other: &bstr); +} + +impl PushAll for BString { + fn push_all(&mut self, other: &bstr) { + for &c in other { + self.push(c); + } + } +} + /// gets the largest ident this slice starts with, might be empty fn peek_ident(b: &[u8]) -> &[u8] { if b.is_empty() || !b[0].is_ascii_alphabetic() { @@ -698,6 +710,61 @@ impl StringDelimiter { fn is_none(&self) -> bool { matches!(self, Self::None) } + + /// assuming that `s` will be placed in the middle of some specifically delimited string + pub fn escape(&self, mut s: &bstr, out: &mut BString) { + while !s.is_empty() { + let first = s[0]; + match self { + StringDelimiter::None => { + if matches!(first, b' ' | b'$' | b'\\' | b'\'' | b'"') { + out.push(b'\\'); + } + } + StringDelimiter::Interp | StringDelimiter::InterpCustom(_) => { + if matches!(first, b'$' | b'\\' | b'"') { + out.push(b'\\'); + } + } + StringDelimiter::Strict => { + if first == b'\'' { + out.push_all(b"'\\'"); + } + } + StringDelimiter::StrictCustom(delim) => { + if s.starts_with(b"'''") && s[3..].starts_with(delim) { + out.push_all(b"'''"); + out.push_all(delim); + out.push_all(b"\\'\\'\\'"); + out.push_all(delim); + out.push_all(b"''"); + out.push_all(delim); + out.push_all(b"'''"); + s = &s[3 + delim.len()..]; + continue; + } + } + } + out.push(first); + s = &s[1..]; + } + } + + pub fn write_closing_delimiter(&self, out: &mut BString) { + match self { + StringDelimiter::None => (), + StringDelimiter::Interp => out.push(b'"'), + StringDelimiter::Strict => out.push(b'\''), + StringDelimiter::InterpCustom(delim) => { + out.push_all(b"\"\"\""); + out.push_all(&delim); + } + StringDelimiter::StrictCustom(delim) => { + out.push_all(b"'''"); + out.push_all(&delim); + } + } + } } fn parse_escape_code(b: &mut Cursor<'_>) -> Result> { @@ -752,11 +819,15 @@ impl Parse for ExpString { let mut already_parsed = false; + let mut last_delim = StringDelimiter::None; + 'outer: loop { let Some(delim) = StringDelimiter::try_begin(b) else { break; }; + last_delim = delim.clone(); + already_parsed = true; while !delim.try_end(b) { @@ -879,8 +950,11 @@ impl Parse for ExpString { } } - if b.is_completion() || already_parsed { - Ok(Self { parts, delim: b' ' }) + if already_parsed { + Ok(Self { + parts, + delim: last_delim, + }) } else { Err(ParseError::NotAString) } @@ -978,7 +1052,7 @@ pub fn do_parse(x: &[u8]) -> Res, (ParseError, &[u8])> { } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum CompletionKind { Command, PathCommand, @@ -990,6 +1064,7 @@ pub enum CompletionKind { pub struct CompletionContext { pub kind: CompletionKind, pub partial: BString, + pub delim: StringDelimiter, } impl CompletionContext { @@ -997,6 +1072,7 @@ impl CompletionContext { Self { kind: CompletionKind::None, partial: BString::new(), + delim: StringDelimiter::None, } } } @@ -1029,6 +1105,7 @@ impl ExpString { CompletionContext { kind: CompletionKind::Variable, partial: var.name.name.clone(), + delim: self.delim.clone(), } } else if let Some(StringPart::Cmd(cmd)) = self.parts.last() && !cmd.already_complete @@ -1038,7 +1115,11 @@ impl ExpString { if s.contains(&b'/') && kind == CompletionKind::Command { kind = CompletionKind::PathCommand; } - CompletionContext { kind, partial: s } + CompletionContext { + kind, + partial: s, + delim: self.delim.clone(), + } } else { CompletionContext::none() } -- cgit v1.2.3