aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/completion.rs49
-rw-r--r--src/main.rs5
-rw-r--r--src/parse/mod.rs99
3 files changed, 124 insertions, 29 deletions
diff --git a/src/completion.rs b/src/completion.rs
index c628bd2..0e24d6d 100644
--- a/src/completion.rs
+++ b/src/completion.rs
@@ -1,4 +1,4 @@
-use crate::parse;
+use crate::parse::{self, CompletionContext};
use crate::{BString, Session};
use std::collections::HashMap;
use std::ffi::OsStr;
@@ -9,15 +9,21 @@ use std::sync::{Arc, Mutex};
use std::{env, fs};
pub struct Suggestion {
+ /// display string that is shown in the possibilities
pub display: BString,
+
+ /// *escaped* bytes that can be directly appended into terminal.
pub delta: BString,
}
fn _path_completion(
- mut prefix: BString,
+ cc: CompletionContext,
filter: &dyn Fn(&DirEntry) -> bool,
) -> std::io::Result<Vec<Suggestion>> {
+ let delim = cc.delim;
+ let mut prefix = cc.partial;
let mut partial_entry = BString::new();
+
while let Some(c) = prefix.last().cloned() {
if c == b'/' {
break;
@@ -40,12 +46,14 @@ fn _path_completion(
}
let name = entry.file_name().as_bytes().to_vec();
if name.starts_with(&partial_entry) {
- let mut delta = name[partial_entry.len()..].to_vec();
+ let mut delta = BString::new();
+ delim.escape(&name[partial_entry.len()..], &mut delta);
let is_dir = entry.metadata().map(|m| m.is_dir()).unwrap_or(false);
if is_dir {
delta.push(b'/');
} else {
+ delim.write_closing_delimiter(&mut delta);
delta.push(b' ');
}
@@ -59,8 +67,8 @@ fn _path_completion(
Ok(sugs)
}
-pub fn path_completion(prefix: BString) -> Vec<Suggestion> {
- match _path_completion(prefix, &|_| true) {
+pub fn path_completion(cc: CompletionContext) -> Vec<Suggestion> {
+ match _path_completion(cc, &|_| true) {
Ok(suggestions) => suggestions,
Err(err) => {
println!("path completion failed: {err:?}\r");
@@ -69,8 +77,8 @@ pub fn path_completion(prefix: BString) -> Vec<Suggestion> {
}
}
-pub fn path_exe_completion(prefix: BString) -> Vec<Suggestion> {
- match _path_completion(prefix, &|d| is_executable(&d.path())) {
+pub fn path_exe_completion(cc: CompletionContext) -> Vec<Suggestion> {
+ match _path_completion(cc, &|d| is_executable(&d.path())) {
Ok(suggestions) => suggestions,
Err(err) => {
println!("path completion failed: {err:?}\r");
@@ -134,7 +142,7 @@ pub fn populate_path_cache(session: Arc<Mutex<Session>>) {
session.lock().unwrap().path_cache = PathCache { binaries };
}
-pub fn command_completion(session: Arc<Mutex<Session>>, prefix: BString) -> Vec<Suggestion> {
+pub fn command_completion(session: Arc<Mutex<Session>>, cc: CompletionContext) -> Vec<Suggestion> {
let se = session.lock().unwrap();
let mut out = Vec::new();
for fun in se
@@ -143,18 +151,19 @@ pub fn command_completion(session: Arc<Mutex<Session>>, prefix: BString) -> Vec<
.chain(se.builtins.keys())
.chain(se.path_cache.binaries.keys())
{
- if fun.starts_with(&prefix) {
+ if fun.starts_with(&cc.partial) {
+ let mut delta = BString::new();
+ cc.delim.escape(&fun[cc.partial.len()..], &mut delta);
+ cc.delim.write_closing_delimiter(&mut delta);
+ delta.push(b' ');
+
out.push(Suggestion {
display: fun.to_vec(),
- delta: fun[prefix.len()..].to_vec(),
+ delta,
})
}
}
- for s in out.iter_mut() {
- s.delta.push(b' ');
- }
-
out
}
@@ -180,10 +189,12 @@ pub fn completion(session: Arc<Mutex<Session>>, cmd: &[u8]) -> CompletionResult
&mut crate::run::Executor::new_for_completion(session.clone()),
);
+ let kind = comp.kind.clone();
+
let mut suggestions = match comp.kind {
- parse::CompletionKind::Command => command_completion(session.clone(), comp.partial),
- parse::CompletionKind::PathCommand => path_exe_completion(comp.partial),
- parse::CompletionKind::Argument => path_completion(comp.partial),
+ parse::CompletionKind::Command => command_completion(session.clone(), comp),
+ parse::CompletionKind::PathCommand => path_exe_completion(comp),
+ parse::CompletionKind::Argument => path_completion(comp),
parse::CompletionKind::Variable => variable_completion(session.clone(), comp.partial),
parse::CompletionKind::None => return CompletionResult::empty(),
};
@@ -193,7 +204,7 @@ pub fn completion(session: Arc<Mutex<Session>>, cmd: &[u8]) -> CompletionResult
if suggestions.is_empty() {
return CompletionResult {
- kind: comp.kind,
+ kind,
..CompletionResult::empty()
};
}
@@ -214,7 +225,7 @@ pub fn completion(session: Arc<Mutex<Session>>, cmd: &[u8]) -> CompletionResult
let shared_prefix = shared_prefix.to_vec();
CompletionResult {
- kind: comp.kind,
+ kind,
suggestions,
shared_prefix,
}
diff --git a/src/main.rs b/src/main.rs
index a54f4fc..e10a8e3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -374,7 +374,10 @@ impl Session {
}
fn exec_rc_file(se: Arc<Mutex<Session>>) {
- let _ = run::source(se, basedir::config_dir().join(".pishrc").as_os_str().as_bytes());
+ let _ = run::source(
+ se,
+ basedir::config_dir().join(".pishrc").as_os_str().as_bytes(),
+ );
}
fn event_loop() {
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<const N: usize>(cmds: [Command<PreExpansion>; N]) -> Ast<PreExpansi
pub fn estr(x: &[u8]) -> ExpString {
ExpString {
parts: vec![StringPart::Boring(x.to_vec())],
- delim: b' ',
+ delim: StringDelimiter::None,
}
}
pub fn str<const N: usize>(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<StringPart>,
- 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<Option<u8>> {
@@ -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<Ast<PreExpansion>, (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()
}