Tab completion has been implemented
more small highlighter tune ups 2>&1 style redirections now work properly
This commit is contained in:
@@ -85,7 +85,6 @@ pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResul
|
||||
write(stdout, alias_output.as_bytes())?; // Write it
|
||||
} else {
|
||||
for (arg, span) in argv {
|
||||
log::debug!("{arg:?}");
|
||||
if read_logic(|l| l.get_alias(&arg)).is_none() {
|
||||
return Err(ShErr::full(
|
||||
ShErrKind::SyntaxErr,
|
||||
|
||||
@@ -32,7 +32,6 @@ pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
|
||||
code = status;
|
||||
}
|
||||
|
||||
log::debug!("{code:?}");
|
||||
|
||||
let kind = match kind {
|
||||
LoopContinue(_) => LoopContinue(code),
|
||||
|
||||
@@ -247,9 +247,6 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
|
||||
let rhs = rhs.expand()?.get_words().join(" ");
|
||||
conjunct_op = conjunct;
|
||||
let test_op = operator.as_str().parse::<TestOp>()?;
|
||||
log::debug!("{lhs:?}");
|
||||
log::debug!("{rhs:?}");
|
||||
log::debug!("{test_op:?}");
|
||||
match test_op {
|
||||
TestOp::Unary(_) => {
|
||||
return Err(ShErr::Full {
|
||||
@@ -298,7 +295,6 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
|
||||
}
|
||||
}
|
||||
};
|
||||
log::debug!("{last_result:?}");
|
||||
|
||||
if let Some(op) = conjunct_op {
|
||||
match op {
|
||||
@@ -316,6 +312,5 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
|
||||
last_result = result;
|
||||
}
|
||||
}
|
||||
log::debug!("{last_result:?}");
|
||||
Ok(last_result)
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ pub struct Expander {
|
||||
impl Expander {
|
||||
pub fn new(raw: Tk) -> ShResult<Self> {
|
||||
let raw = raw.span.as_str();
|
||||
Self::from_raw(&raw)
|
||||
Self::from_raw(raw)
|
||||
}
|
||||
pub fn from_raw(raw: &str) -> ShResult<Self> {
|
||||
let raw = expand_braces_full(raw)?.join(" ");
|
||||
@@ -69,10 +69,24 @@ impl Expander {
|
||||
pub fn expand(&mut self) -> ShResult<Vec<String>> {
|
||||
let mut chars = self.raw.chars().peekable();
|
||||
self.raw = expand_raw(&mut chars)?;
|
||||
|
||||
let has_trailing_slash = self.raw.ends_with('/');
|
||||
let has_leading_dot_slash = self.raw.starts_with("./");
|
||||
|
||||
if let Ok(glob_exp) = expand_glob(&self.raw)
|
||||
&& !glob_exp.is_empty() {
|
||||
self.raw = glob_exp;
|
||||
}
|
||||
|
||||
if has_trailing_slash && !self.raw.ends_with('/') {
|
||||
// glob expansion can remove trailing slashes and leading dot-slashes, but we want to preserve them
|
||||
// so that things like tab completion don't break
|
||||
self.raw.push('/');
|
||||
}
|
||||
if has_leading_dot_slash && !self.raw.starts_with("./") {
|
||||
self.raw.insert_str(0, "./");
|
||||
}
|
||||
|
||||
Ok(self.split_words())
|
||||
}
|
||||
pub fn split_words(&mut self) -> Vec<String> {
|
||||
@@ -462,14 +476,12 @@ pub fn expand_raw(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
|
||||
result.push_str(&fd_path);
|
||||
}
|
||||
VAR_SUB => {
|
||||
log::info!("{chars:?}");
|
||||
let expanded = expand_var(chars)?;
|
||||
result.push_str(&expanded);
|
||||
}
|
||||
_ => result.push(ch),
|
||||
}
|
||||
}
|
||||
log::debug!("expand_raw result: {result:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -481,12 +493,19 @@ pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
|
||||
SUBSH if var_name.is_empty() => {
|
||||
chars.next(); // now safe to consume
|
||||
let mut subsh_body = String::new();
|
||||
let mut found_end = false;
|
||||
while let Some(c) = chars.next() {
|
||||
if c == SUBSH {
|
||||
found_end = true;
|
||||
break;
|
||||
}
|
||||
subsh_body.push(c);
|
||||
}
|
||||
if !found_end {
|
||||
// if there isnt a closing SUBSH, we are probably in some tab completion context
|
||||
// and we got passed some unfinished input. Just treat it as literal text
|
||||
return Ok(format!("$({subsh_body}"));
|
||||
}
|
||||
let expanded = expand_cmd_sub(&subsh_body)?;
|
||||
return Ok(expanded);
|
||||
}
|
||||
@@ -512,14 +531,10 @@ pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
|
||||
return Ok(NULL_EXPAND.to_string());
|
||||
}
|
||||
|
||||
log::debug!("{val:?}");
|
||||
return Ok(val);
|
||||
}
|
||||
ch if is_hard_sep(ch) || !(ch.is_alphanumeric() || ch == '_' || ch == '-') => {
|
||||
let val = read_vars(|v| v.get_var(&var_name));
|
||||
log::info!("{var_name:?}");
|
||||
log::info!("{val:?}");
|
||||
log::info!("{ch:?}");
|
||||
return Ok(val);
|
||||
}
|
||||
_ => {
|
||||
@@ -530,7 +545,6 @@ pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
|
||||
}
|
||||
if !var_name.is_empty() {
|
||||
let var_val = read_vars(|v| v.get_var(&var_name));
|
||||
log::info!("{var_val:?}");
|
||||
Ok(var_val)
|
||||
} else {
|
||||
Ok(String::new())
|
||||
@@ -781,7 +795,6 @@ pub fn expand_proc_sub(raw: &str, is_input: bool) -> ShResult<String> {
|
||||
ForkResult::Parent { child } => {
|
||||
write_jobs(|j| j.register_fd(child, register_fd));
|
||||
let registered = read_jobs(|j| j.registered_fds().to_vec());
|
||||
log::debug!("{registered:?}");
|
||||
// Do not wait; process may run in background
|
||||
Ok(path)
|
||||
}
|
||||
@@ -790,8 +803,6 @@ pub fn expand_proc_sub(raw: &str, is_input: bool) -> ShResult<String> {
|
||||
|
||||
/// Get the command output of a given command input as a String
|
||||
pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
|
||||
log::debug!("in expand_cmd_sub");
|
||||
log::debug!("{raw:?}");
|
||||
if raw.starts_with('(') && raw.ends_with(')')
|
||||
&& let Ok(output) = expand_arithmetic(raw) {
|
||||
return Ok(output); // It's actually an arithmetic sub
|
||||
@@ -815,7 +826,6 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
|
||||
std::mem::drop(cmd_sub_io_frame); // Closes the write pipe
|
||||
|
||||
// Read output first (before waiting) to avoid deadlock if child fills pipe buffer
|
||||
log::debug!("filling buffer");
|
||||
loop {
|
||||
match io_buf.fill_buffer() {
|
||||
Ok(()) => break,
|
||||
@@ -823,7 +833,6 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
log::debug!("done");
|
||||
|
||||
// Wait for child with EINTR retry
|
||||
let status = loop {
|
||||
@@ -1105,7 +1114,6 @@ pub fn unescape_math(raw: &str) -> String {
|
||||
let mut result = String::new();
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
log::debug!("{result:?}");
|
||||
match ch {
|
||||
'\\' => {
|
||||
if let Some(next_ch) = chars.next() {
|
||||
@@ -1148,7 +1156,6 @@ pub fn unescape_math(raw: &str) -> String {
|
||||
_ => result.push(ch),
|
||||
}
|
||||
}
|
||||
log::info!("{result:?}");
|
||||
result
|
||||
}
|
||||
|
||||
@@ -1302,9 +1309,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("{rest:?}");
|
||||
if let Ok(expansion) = rest.parse::<ParamExp>() {
|
||||
log::debug!("{expansion:?}");
|
||||
match expansion {
|
||||
ParamExp::Len => unreachable!(),
|
||||
ParamExp::DefaultUnsetOrNull(default) => {
|
||||
@@ -1523,7 +1528,6 @@ fn glob_to_regex(glob: &str, anchored: bool) -> Regex {
|
||||
if anchored {
|
||||
regex.push('$');
|
||||
}
|
||||
log::debug!("{regex:?}");
|
||||
Regex::new(®ex).unwrap()
|
||||
}
|
||||
|
||||
@@ -1946,7 +1950,6 @@ pub fn expand_prompt(raw: &str) -> ShResult<String> {
|
||||
PromptTk::FailureSymbol => todo!(),
|
||||
PromptTk::JobCount => todo!(),
|
||||
PromptTk::Function(f) => {
|
||||
log::debug!("Expanding prompt function: {}", f);
|
||||
let output = expand_cmd_sub(&f)?;
|
||||
result.push_str(&output);
|
||||
}
|
||||
|
||||
@@ -106,7 +106,6 @@ pub fn get_opts_from_tokens(tokens: Vec<Tk>, opt_specs: &[OptSpec]) -> (Vec<Tk>,
|
||||
}
|
||||
if !pushed {
|
||||
non_opts.push(token.clone());
|
||||
log::warn!("Unexpected flag '{opt}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,6 @@ impl ChildProc {
|
||||
if let Some(pgid) = pgid {
|
||||
child.set_pgid(pgid).ok();
|
||||
}
|
||||
log::trace!("new child: {:?}", child);
|
||||
Ok(child)
|
||||
}
|
||||
pub fn pid(&self) -> Pid {
|
||||
@@ -520,11 +519,7 @@ impl Job {
|
||||
}
|
||||
pub fn wait_pgrp(&mut self) -> ShResult<Vec<WtStat>> {
|
||||
let mut stats = vec![];
|
||||
log::trace!("waiting on children");
|
||||
log::trace!("{:?}", self.children);
|
||||
for child in self.children.iter_mut() {
|
||||
log::trace!("shell pid {}", Pid::this());
|
||||
log::trace!("child pid {}", child.pid);
|
||||
if child.pid == Pid::this() {
|
||||
// TODO: figure out some way to get the exit code of builtins
|
||||
let code = state::get_status();
|
||||
@@ -667,7 +662,6 @@ pub fn wait_fg(job: Job) -> ShResult<()> {
|
||||
if job.children().is_empty() {
|
||||
return Ok(()); // Nothing to do
|
||||
}
|
||||
log::trace!("Waiting on foreground job");
|
||||
let mut code = 0;
|
||||
let mut was_stopped = false;
|
||||
attach_tty(job.pgid())?;
|
||||
@@ -699,7 +693,6 @@ pub fn wait_fg(job: Job) -> ShResult<()> {
|
||||
}
|
||||
take_term()?;
|
||||
set_status(code);
|
||||
log::trace!("exit code: {}", code);
|
||||
enable_reaping();
|
||||
Ok(())
|
||||
}
|
||||
@@ -719,7 +712,6 @@ pub fn attach_tty(pgid: Pid) -> ShResult<()> {
|
||||
if !isatty(0).unwrap_or(false) || pgid == term_ctlr() || killpg(pgid, None).is_err() {
|
||||
return Ok(());
|
||||
}
|
||||
log::trace!("Attaching tty to pgid: {}", pgid);
|
||||
|
||||
if pgid == getpgrp() && term_ctlr() != getpgrp() {
|
||||
kill(term_ctlr(), Signal::SIGTTOU).ok();
|
||||
@@ -745,7 +737,6 @@ pub fn attach_tty(pgid: Pid) -> ShResult<()> {
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
log::error!("error while switching term control: {}", e);
|
||||
tcsetpgrp(borrow_fd(0), getpgrp())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -84,7 +84,6 @@ impl TkVecUtils<Tk> for Vec<Tk> {
|
||||
}
|
||||
fn debug_tokens(&self) {
|
||||
for token in self {
|
||||
log::debug!("token: {}", token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,8 +208,6 @@ fn fern_interactive() -> ShResult<()> {
|
||||
// Reset for next command with fresh prompt
|
||||
readline.reset(get_prompt().ok());
|
||||
let real_end = start.elapsed();
|
||||
log::info!("Command execution time: {:.2?}", command_run_time);
|
||||
log::info!("Total round trip time: {:.2?}", real_end);
|
||||
}
|
||||
Ok(ReadlineEvent::Eof) => {
|
||||
// Ctrl+D on empty line
|
||||
|
||||
@@ -138,7 +138,6 @@ impl Dispatcher {
|
||||
}
|
||||
}
|
||||
pub fn begin_dispatch(&mut self) -> ShResult<()> {
|
||||
log::trace!("beginning dispatch");
|
||||
while let Some(node) = self.nodes.pop_front() {
|
||||
let blame = node.get_span();
|
||||
self.dispatch_node(node).try_blame(blame)?;
|
||||
@@ -401,10 +400,23 @@ impl Dispatcher {
|
||||
Ok(())
|
||||
}
|
||||
fn exec_for(&mut self, for_stmt: Node) -> ShResult<()> {
|
||||
|
||||
let NdRule::ForNode { vars, arr, body } = for_stmt.class else {
|
||||
unreachable!();
|
||||
};
|
||||
|
||||
let to_expanded_strings = |tks: Vec<Tk>| -> ShResult<Vec<String>> {
|
||||
Ok(tks.into_iter()
|
||||
.map(|tk| tk.expand().map(|tk| tk.get_words()))
|
||||
.collect::<ShResult<Vec<Vec<String>>>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>())
|
||||
};
|
||||
|
||||
// Expand all array variables
|
||||
let arr: Vec<String> = to_expanded_strings(arr)?;
|
||||
let vars: Vec<String> = to_expanded_strings(vars)?;
|
||||
|
||||
let mut for_guard = VarCtxGuard::new(
|
||||
vars.iter().map(|v| v.to_string()).collect()
|
||||
);
|
||||
@@ -415,7 +427,7 @@ impl Dispatcher {
|
||||
.redirect()?;
|
||||
|
||||
'outer: for chunk in arr.chunks(vars.len()) {
|
||||
let empty = Tk::default();
|
||||
let empty = String::new();
|
||||
let chunk_iter = vars.iter().zip(
|
||||
chunk.iter().chain(std::iter::repeat(&empty)),
|
||||
);
|
||||
@@ -540,7 +552,6 @@ impl Dispatcher {
|
||||
return self.dispatch_cmd(cmd);
|
||||
}
|
||||
|
||||
log::trace!("doing builtin");
|
||||
let result = match cmd_raw.span.as_str() {
|
||||
"echo" => echo(cmd, io_stack_mut, curr_job_mut),
|
||||
"cd" => cd(cmd, curr_job_mut),
|
||||
|
||||
@@ -47,6 +47,11 @@ impl Span {
|
||||
pub fn range(&self) -> Range<usize> {
|
||||
self.range.clone()
|
||||
}
|
||||
/// With great power comes great responsibility
|
||||
/// Only use this in the most dire of circumstances
|
||||
pub fn set_range(&mut self, range: Range<usize>) {
|
||||
self.range = range;
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows simple access to the underlying range wrapped by the span
|
||||
@@ -176,7 +181,6 @@ bitflags! {
|
||||
|
||||
impl LexStream {
|
||||
pub fn new(source: Arc<String>, flags: LexFlags) -> Self {
|
||||
log::trace!("new lex stream");
|
||||
let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD;
|
||||
Self {
|
||||
source,
|
||||
@@ -260,7 +264,7 @@ impl LexStream {
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
if !found_fd {
|
||||
if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
|
||||
return Some(Err(ShErr::full(
|
||||
ShErrKind::ParseErr,
|
||||
"Invalid redirection",
|
||||
@@ -790,7 +794,6 @@ impl Iterator for LexStream {
|
||||
match self.read_string() {
|
||||
Ok(tk) => tk,
|
||||
Err(e) => {
|
||||
log::error!("{e:?}");
|
||||
return Some(Err(e));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -860,6 +860,10 @@ impl ParseStream {
|
||||
let redir_bldr = redir_bldr.with_io_mode(io_mode);
|
||||
let redir = redir_bldr.build();
|
||||
redirs.push(redir);
|
||||
} else {
|
||||
// io_mode is already set (e.g., for fd redirections like 2>&1)
|
||||
let redir = redir_bldr.build();
|
||||
redirs.push(redir);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -1346,6 +1350,10 @@ impl ParseStream {
|
||||
let redir_bldr = redir_bldr.with_io_mode(io_mode);
|
||||
let redir = redir_bldr.build();
|
||||
redirs.push(redir);
|
||||
} else {
|
||||
// io_mode is already set (e.g., for fd redirections like 2>&1)
|
||||
let redir = redir_bldr.build();
|
||||
redirs.push(redir);
|
||||
}
|
||||
}
|
||||
_ => unimplemented!("Unexpected token rule `{:?}` in parse_cmd()", tk.class),
|
||||
|
||||
@@ -137,9 +137,7 @@ impl<R: Read> IoBuf<R> {
|
||||
pub fn fill_buffer(&mut self) -> io::Result<()> {
|
||||
let mut temp_buf = vec![0; 1024]; // Read in chunks
|
||||
loop {
|
||||
log::debug!("reading bytes");
|
||||
let bytes_read = self.reader.read(&mut temp_buf)?;
|
||||
log::debug!("{bytes_read:?}");
|
||||
if bytes_read == 0 {
|
||||
break; // EOF reached
|
||||
}
|
||||
@@ -220,11 +218,9 @@ impl<'e> IoFrame {
|
||||
self.save();
|
||||
for redir in &mut self.redirs {
|
||||
let io_mode = &mut redir.io_mode;
|
||||
log::debug!("{io_mode:?}");
|
||||
if let IoMode::File { .. } = io_mode {
|
||||
*io_mode = io_mode.clone().open_file()?;
|
||||
};
|
||||
log::debug!("{io_mode:?}");
|
||||
let tgt_fd = io_mode.tgt_fd();
|
||||
let src_fd = io_mode.src_fd();
|
||||
dup2(src_fd, tgt_fd)?;
|
||||
|
||||
@@ -17,7 +17,6 @@ pub fn get_prompt() -> ShResult<String> {
|
||||
return expand_prompt(default);
|
||||
};
|
||||
let sanitized = format!("\\e[0m{prompt}");
|
||||
log::debug!("Using prompt: {}", sanitized.replace("\n", "\\n"));
|
||||
|
||||
expand_prompt(&sanitized)
|
||||
}
|
||||
|
||||
318
src/prompt/readline/complete.rs
Normal file
318
src/prompt/readline/complete.rs
Normal file
@@ -0,0 +1,318 @@
|
||||
use std::{env, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc};
|
||||
|
||||
use crate::{builtin::BUILTINS, libsh::error::ShResult, parse::lex::{self, LexFlags, Tk, TkFlags}, prompt::readline::{annotate_input, annotate_input_recursive, get_insertions, markers::{self, is_marker}}, state::read_logic};
|
||||
|
||||
pub enum CompCtx {
|
||||
CmdName,
|
||||
FileName
|
||||
}
|
||||
|
||||
pub enum CompResult {
|
||||
NoMatch,
|
||||
Single {
|
||||
result: String
|
||||
},
|
||||
Many {
|
||||
candidates: Vec<String>
|
||||
}
|
||||
}
|
||||
|
||||
impl CompResult {
|
||||
pub fn from_candidates(candidates: Vec<String>) -> Self {
|
||||
if candidates.is_empty() {
|
||||
Self::NoMatch
|
||||
} else if candidates.len() == 1 {
|
||||
Self::Single { result: candidates[0].clone() }
|
||||
} else {
|
||||
Self::Many { candidates }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Completer {
|
||||
pub candidates: Vec<String>,
|
||||
pub selected_idx: usize,
|
||||
pub original_input: String,
|
||||
pub token_span: (usize, usize),
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
impl Completer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
candidates: vec![],
|
||||
selected_idx: 0,
|
||||
original_input: String::new(),
|
||||
token_span: (0, 0),
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn slice_line(line: &str, cursor_pos: usize) -> (&str, &str) {
|
||||
let (before_cursor, after_cursor) = line.split_at(cursor_pos);
|
||||
(before_cursor, after_cursor)
|
||||
}
|
||||
|
||||
fn get_completion_context(&self, line: &str, cursor_pos: usize) -> (bool, usize) {
|
||||
let annotated = annotate_input_recursive(line);
|
||||
log::debug!("Annotated input for completion context: {:?}", annotated);
|
||||
let mut in_cmd = false;
|
||||
let mut same_position = false; // so that arg markers do not overwrite command markers if they are in the same spot
|
||||
let mut ctx_start = 0;
|
||||
let mut pos = 0;
|
||||
|
||||
for ch in annotated.chars() {
|
||||
match ch {
|
||||
_ if is_marker(ch) => {
|
||||
match ch {
|
||||
markers::COMMAND | markers::BUILTIN => {
|
||||
log::debug!("Found command marker at position {}", pos);
|
||||
ctx_start = pos;
|
||||
same_position = true;
|
||||
in_cmd = true;
|
||||
}
|
||||
markers::ARG => {
|
||||
log::debug!("Found argument marker at position {}", pos);
|
||||
if !same_position {
|
||||
ctx_start = pos;
|
||||
in_cmd = false;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
same_position = false;
|
||||
pos += 1; // we hit a normal character, advance our position
|
||||
if pos >= cursor_pos {
|
||||
log::debug!("Cursor is at position {}, current context: {}", pos, if in_cmd { "command" } else { "argument" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(in_cmd, ctx_start)
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.candidates.clear();
|
||||
self.selected_idx = 0;
|
||||
self.original_input.clear();
|
||||
self.token_span = (0, 0);
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
pub fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult<Option<String>> {
|
||||
if self.active {
|
||||
Ok(Some(self.cycle_completion(direction)))
|
||||
} else {
|
||||
self.start_completion(line, cursor_pos)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected_candidate(&self) -> Option<String> {
|
||||
self.candidates.get(self.selected_idx).cloned()
|
||||
}
|
||||
|
||||
pub fn cycle_completion(&mut self, direction: i32) -> String {
|
||||
if self.candidates.is_empty() {
|
||||
return self.original_input.clone();
|
||||
}
|
||||
|
||||
let len = self.candidates.len();
|
||||
self.selected_idx = (self.selected_idx as i32 + direction).rem_euclid(len as i32) as usize;
|
||||
|
||||
self.get_completed_line()
|
||||
}
|
||||
|
||||
pub fn start_completion(&mut self, line: String, cursor_pos: usize) -> ShResult<Option<String>> {
|
||||
let result = self.get_candidates(line.clone(), cursor_pos)?;
|
||||
match result {
|
||||
CompResult::Many { candidates } => {
|
||||
self.candidates = candidates.clone();
|
||||
self.selected_idx = 0;
|
||||
self.original_input = line;
|
||||
self.active = true;
|
||||
|
||||
Ok(Some(self.get_completed_line()))
|
||||
}
|
||||
CompResult::Single { result } => {
|
||||
self.candidates = vec![result.clone()];
|
||||
self.selected_idx = 0;
|
||||
self.original_input = line;
|
||||
self.active = false;
|
||||
|
||||
Ok(Some(self.get_completed_line()))
|
||||
}
|
||||
CompResult::NoMatch => Ok(None)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_completed_line(&self) -> String {
|
||||
if self.candidates.is_empty() {
|
||||
return self.original_input.clone();
|
||||
}
|
||||
|
||||
let selected = &self.candidates[self.selected_idx];
|
||||
let (start, end) = self.token_span;
|
||||
format!("{}{}{}", &self.original_input[..start], selected, &self.original_input[end..])
|
||||
}
|
||||
|
||||
pub fn get_candidates(&mut self, line: String, cursor_pos: usize) -> ShResult<CompResult> {
|
||||
let source = Arc::new(line.clone());
|
||||
let tokens = lex::LexStream::new(source, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()?;
|
||||
|
||||
let Some(mut cur_token) = tokens.into_iter().find(|tk| {
|
||||
let start = tk.span.start;
|
||||
let end = tk.span.end;
|
||||
(start..=end).contains(&cursor_pos)
|
||||
}) else {
|
||||
log::debug!("No token found at cursor position");
|
||||
let candidates = Self::complete_filename("./"); // Default to filename completion if no token is found
|
||||
let end_pos = line.len();
|
||||
self.token_span = (end_pos, end_pos);
|
||||
return Ok(CompResult::from_candidates(candidates));
|
||||
};
|
||||
|
||||
self.token_span = (cur_token.span.start, cur_token.span.end);
|
||||
|
||||
|
||||
// Look for marker at the START of what we're completing, not at cursor
|
||||
let (is_cmd, token_start) = self.get_completion_context(&line, cursor_pos);
|
||||
self.token_span.0 = token_start; // Update start of token span based on context
|
||||
log::debug!("Completion context: {}, token span: {:?}, token_start: {}", if is_cmd { "command" } else { "argument" }, self.token_span, token_start);
|
||||
cur_token.span.set_range(self.token_span.0..self.token_span.1); // Update token span to reflect context
|
||||
|
||||
// If token contains '=', only complete after the '='
|
||||
let token_str = cur_token.span.as_str();
|
||||
if let Some(eq_pos) = token_str.rfind('=') {
|
||||
// Adjust span to only replace the part after '='
|
||||
self.token_span.0 = cur_token.span.start + eq_pos + 1;
|
||||
}
|
||||
|
||||
let expanded_tk = cur_token.expand()?;
|
||||
let expanded_words = expanded_tk.get_words().into_iter().collect::<Vec<_>>();
|
||||
let expanded = expanded_words.join("\\ ");
|
||||
|
||||
let candidates = if is_cmd {
|
||||
log::debug!("Completing command: {}", &expanded);
|
||||
Self::complete_command(&expanded)?
|
||||
} else {
|
||||
log::debug!("Completing filename: {}", &expanded);
|
||||
Self::complete_filename(&expanded)
|
||||
};
|
||||
|
||||
Ok(CompResult::from_candidates(candidates))
|
||||
}
|
||||
|
||||
fn complete_command(start: &str) -> ShResult<Vec<String>> {
|
||||
let mut candidates = vec![];
|
||||
|
||||
let path = env::var("PATH").unwrap_or_default();
|
||||
let paths = path.split(':').map(PathBuf::from).collect::<Vec<_>>();
|
||||
for path in paths {
|
||||
// Skip directories that don't exist (common in PATH)
|
||||
let Ok(entries) = std::fs::read_dir(path) else { continue; };
|
||||
for entry in entries {
|
||||
let Ok(entry) = entry else { continue; };
|
||||
let Ok(meta) = entry.metadata() else { continue; };
|
||||
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
if meta.is_file()
|
||||
&& (meta.permissions().mode() & 0o111) != 0
|
||||
&& file_name.starts_with(start) {
|
||||
candidates.push(file_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let builtin_candidates = BUILTINS
|
||||
.iter()
|
||||
.filter(|b| b.starts_with(start))
|
||||
.map(|s| s.to_string());
|
||||
|
||||
candidates.extend(builtin_candidates);
|
||||
|
||||
read_logic(|l| {
|
||||
let func_table = l.funcs();
|
||||
let matches = func_table
|
||||
.keys()
|
||||
.filter(|k| k.starts_with(start))
|
||||
.map(|k| k.to_string());
|
||||
|
||||
candidates.extend(matches);
|
||||
|
||||
let aliases = l.aliases();
|
||||
let matches = aliases
|
||||
.keys()
|
||||
.filter(|k| k.starts_with(start))
|
||||
.map(|k| k.to_string());
|
||||
|
||||
candidates.extend(matches);
|
||||
});
|
||||
|
||||
// Deduplicate (same command may appear in multiple PATH dirs)
|
||||
candidates.sort();
|
||||
candidates.dedup();
|
||||
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
fn complete_filename(start: &str) -> Vec<String> {
|
||||
let mut candidates = vec![];
|
||||
|
||||
// If completing after '=', only use the part after it
|
||||
let start = if let Some(eq_pos) = start.rfind('=') {
|
||||
&start[eq_pos + 1..]
|
||||
} else {
|
||||
start
|
||||
};
|
||||
|
||||
// Split path into directory and filename parts
|
||||
// Use "." if start is empty (e.g., after "foo=")
|
||||
let path = PathBuf::from(if start.is_empty() { "." } else { start });
|
||||
let (dir, prefix) = if start.ends_with('/') || start.is_empty() {
|
||||
// Completing inside a directory: "src/" → dir="src/", prefix=""
|
||||
(path, "")
|
||||
} else if let Some(parent) = path.parent()
|
||||
&& !parent.as_os_str().is_empty() {
|
||||
// Has directory component: "src/ma" → dir="src", prefix="ma"
|
||||
(parent.to_path_buf(), path.file_name().unwrap().to_str().unwrap_or(""))
|
||||
} else {
|
||||
// No directory: "fil" → dir=".", prefix="fil"
|
||||
(PathBuf::from("."), start)
|
||||
};
|
||||
|
||||
let Ok(entries) = std::fs::read_dir(&dir) else {
|
||||
return candidates;
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let file_name = entry.file_name();
|
||||
let file_str = file_name.to_string_lossy();
|
||||
|
||||
// Skip hidden files unless explicitly requested
|
||||
if !prefix.starts_with('.') && file_str.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if file_str.starts_with(prefix) {
|
||||
// Reconstruct full path
|
||||
let mut full_path = dir.join(&file_name);
|
||||
|
||||
// Add trailing slash for directories
|
||||
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
|
||||
full_path.push(""); // adds trailing /
|
||||
}
|
||||
|
||||
candidates.push(full_path.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
candidates.sort();
|
||||
candidates
|
||||
}
|
||||
}
|
||||
@@ -173,7 +173,6 @@ impl Highlighter {
|
||||
input_chars.next(); // consume the end marker
|
||||
break;
|
||||
} else if markers::is_marker(*ch) {
|
||||
log::warn!("Unhandled marker character in variable substitution: U+{:04X}", *ch as u32);
|
||||
input_chars.next(); // skip the marker
|
||||
continue;
|
||||
}
|
||||
@@ -187,7 +186,6 @@ impl Highlighter {
|
||||
}
|
||||
_ => {
|
||||
if markers::is_marker(ch) {
|
||||
log::warn!("Unhandled marker character in highlighter: U+{:04X}", ch as u32);
|
||||
} else {
|
||||
self.output.push(ch);
|
||||
self.last_was_reset = false;
|
||||
@@ -202,7 +200,6 @@ impl Highlighter {
|
||||
/// Clears the input buffer, style stack, and returns the generated output
|
||||
/// containing ANSI escape codes. The highlighter is ready for reuse after this.
|
||||
pub fn take(&mut self) -> String {
|
||||
log::info!("Highlighting result: {:?}", self.output);
|
||||
self.input.clear();
|
||||
self.clear_styles();
|
||||
std::mem::take(&mut self.output)
|
||||
|
||||
@@ -303,7 +303,6 @@ impl History {
|
||||
}
|
||||
|
||||
pub fn constrain_entries(&mut self, constraint: SearchConstraint) {
|
||||
log::debug!("{constraint:?}");
|
||||
let SearchConstraint { kind, term } = constraint;
|
||||
match kind {
|
||||
SearchKind::Prefix => {
|
||||
@@ -318,7 +317,6 @@ impl History {
|
||||
.collect();
|
||||
|
||||
self.search_mask = dedupe_entries(&filtered);
|
||||
log::debug!("search mask len: {}", self.search_mask.len());
|
||||
}
|
||||
self.cursor = self.search_mask.len().saturating_sub(1);
|
||||
}
|
||||
@@ -328,12 +326,10 @@ impl History {
|
||||
|
||||
pub fn hint_entry(&self) -> Option<&HistEntry> {
|
||||
let second_to_last = self.search_mask.len().checked_sub(2)?;
|
||||
log::info!("search mask: {:?}", self.search_mask.iter().map(|e| e.command()).collect::<Vec<_>>());
|
||||
self.search_mask.get(second_to_last)
|
||||
}
|
||||
|
||||
pub fn get_hint(&self) -> Option<String> {
|
||||
log::info!("checking cursor entry: {:?}", self.cursor_entry());
|
||||
if self
|
||||
.cursor_entry()
|
||||
.is_some_and(|ent| ent.is_new() && !ent.command().is_empty())
|
||||
|
||||
@@ -368,7 +368,6 @@ impl LineBuf {
|
||||
} else {
|
||||
self.hint = None
|
||||
}
|
||||
log::debug!("{:?}", self.hint)
|
||||
}
|
||||
pub fn accept_hint(&mut self) {
|
||||
let Some(hint) = self.hint.take() else { return };
|
||||
@@ -406,7 +405,6 @@ impl LineBuf {
|
||||
#[track_caller]
|
||||
pub fn update_graphemes(&mut self) {
|
||||
let indices: Vec<_> = self.buffer.grapheme_indices(true).map(|(i, _)| i).collect();
|
||||
log::debug!("{:?}", std::panic::Location::caller());
|
||||
self.cursor.set_max(indices.len());
|
||||
self.grapheme_indices = Some(indices)
|
||||
}
|
||||
@@ -577,7 +575,6 @@ impl LineBuf {
|
||||
let end = self.grapheme_indices()[end];
|
||||
self.buffer.drain(start..end).collect()
|
||||
};
|
||||
log::debug!("{drained:?}");
|
||||
self.update_graphemes();
|
||||
drained
|
||||
}
|
||||
@@ -1073,7 +1070,6 @@ impl LineBuf {
|
||||
let Some(gr) = self.grapheme_at(idx) else {
|
||||
break;
|
||||
};
|
||||
log::debug!("{gr:?}");
|
||||
if is_whitespace(gr) {
|
||||
end += 1;
|
||||
} else {
|
||||
@@ -1203,7 +1199,6 @@ impl LineBuf {
|
||||
let Some(gr) = self.grapheme_at(idx) else {
|
||||
break;
|
||||
};
|
||||
log::debug!("{gr:?}");
|
||||
if is_whitespace(gr) {
|
||||
end += 1;
|
||||
} else {
|
||||
@@ -1901,10 +1896,7 @@ impl LineBuf {
|
||||
let Some(line) = self.slice(start..end).map(|s| s.to_string()) else {
|
||||
return MotionKind::Null;
|
||||
};
|
||||
log::debug!("{target_col:?}");
|
||||
log::debug!("{target_col:?}");
|
||||
let mut target_pos = self.grapheme_index_for_display_col(&line, target_col);
|
||||
log::debug!("{target_pos:?}");
|
||||
if self.cursor.exclusive
|
||||
&& line.ends_with("\n")
|
||||
&& self.grapheme_at(target_pos) == Some("\n")
|
||||
@@ -2107,7 +2099,6 @@ impl LineBuf {
|
||||
Motion::BackwardChar => target.sub(1),
|
||||
Motion::ForwardChar => {
|
||||
if self.cursor.exclusive && self.grapheme_at(target.ret_add(1)) == Some("\n") {
|
||||
log::debug!("returning null");
|
||||
return MotionKind::Null;
|
||||
}
|
||||
target.add(1);
|
||||
@@ -2116,7 +2107,6 @@ impl LineBuf {
|
||||
_ => unreachable!(),
|
||||
}
|
||||
if self.grapheme_at(target.get()) == Some("\n") {
|
||||
log::debug!("returning null outside of match");
|
||||
return MotionKind::Null;
|
||||
}
|
||||
}
|
||||
@@ -2132,7 +2122,6 @@ impl LineBuf {
|
||||
}) else {
|
||||
return MotionKind::Null;
|
||||
};
|
||||
log::debug!("{:?}", self.slice(start..end));
|
||||
|
||||
let target_col = if let Some(col) = self.saved_col {
|
||||
col
|
||||
@@ -2145,10 +2134,7 @@ impl LineBuf {
|
||||
let Some(line) = self.slice(start..end).map(|s| s.to_string()) else {
|
||||
return MotionKind::Null;
|
||||
};
|
||||
log::debug!("{target_col:?}");
|
||||
log::debug!("{target_col:?}");
|
||||
let mut target_pos = self.grapheme_index_for_display_col(&line, target_col);
|
||||
log::debug!("{target_pos:?}");
|
||||
if self.cursor.exclusive
|
||||
&& line.ends_with("\n")
|
||||
&& self.grapheme_at(target_pos) == Some("\n")
|
||||
@@ -2173,8 +2159,6 @@ impl LineBuf {
|
||||
}) else {
|
||||
return MotionKind::Null;
|
||||
};
|
||||
log::debug!("{start:?}, {end:?}");
|
||||
log::debug!("{:?}", self.slice(start..end));
|
||||
|
||||
let target_col = if let Some(col) = self.saved_col {
|
||||
col
|
||||
@@ -2239,9 +2223,6 @@ impl LineBuf {
|
||||
|
||||
let has_consumed_hint = (self.cursor.exclusive && self.cursor.get() >= last_grapheme_pos)
|
||||
|| (!self.cursor.exclusive && self.cursor.get() > last_grapheme_pos);
|
||||
log::debug!("{has_consumed_hint:?}");
|
||||
log::debug!("{:?}", self.cursor.get());
|
||||
log::debug!("{last_grapheme_pos:?}");
|
||||
|
||||
if has_consumed_hint {
|
||||
let buf_end = if self.cursor.exclusive {
|
||||
@@ -2403,7 +2384,6 @@ impl LineBuf {
|
||||
} else {
|
||||
let drained = self.drain(start, end);
|
||||
self.update_graphemes();
|
||||
log::debug!("{:?}", self.cursor);
|
||||
drained
|
||||
};
|
||||
register.write_to_register(register_text);
|
||||
|
||||
@@ -9,7 +9,7 @@ use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVis
|
||||
use crate::{libsh::{
|
||||
error::{ShErrKind, ShResult},
|
||||
term::{Style, Styled},
|
||||
}, parse::lex::{self, LexFlags, Tk, TkFlags, TkRule}, prompt::readline::highlight::Highlighter};
|
||||
}, parse::lex::{self, LexFlags, Tk, TkFlags, TkRule}, prompt::readline::{complete::{CompResult, Completer}, highlight::Highlighter}};
|
||||
use crate::prelude::*;
|
||||
|
||||
pub mod history;
|
||||
@@ -21,6 +21,7 @@ pub mod term;
|
||||
pub mod vicmd;
|
||||
pub mod vimode;
|
||||
pub mod highlight;
|
||||
pub mod complete;
|
||||
|
||||
pub mod markers {
|
||||
// token-level (derived from token class)
|
||||
@@ -101,15 +102,20 @@ pub enum ReadlineEvent {
|
||||
pub struct FernVi {
|
||||
pub reader: PollReader,
|
||||
pub writer: Box<dyn LineWriter>,
|
||||
|
||||
pub prompt: String,
|
||||
pub highlighter: Highlighter,
|
||||
pub completer: Completer,
|
||||
|
||||
pub mode: Box<dyn ViMode>,
|
||||
pub old_layout: Option<Layout>,
|
||||
pub repeat_action: Option<CmdReplay>,
|
||||
pub repeat_motion: Option<MotionCmd>,
|
||||
pub editor: LineBuf,
|
||||
|
||||
pub old_layout: Option<Layout>,
|
||||
pub history: History,
|
||||
needs_redraw: bool,
|
||||
|
||||
pub needs_redraw: bool,
|
||||
}
|
||||
|
||||
impl FernVi {
|
||||
@@ -118,6 +124,7 @@ impl FernVi {
|
||||
reader: PollReader::new(),
|
||||
writer: Box::new(TermWriter::new(STDOUT_FILENO)),
|
||||
prompt: prompt.unwrap_or("$ ".styled(Style::Green)),
|
||||
completer: Completer::new(),
|
||||
highlighter: Highlighter::new(),
|
||||
mode: Box::new(ViInsert::new()),
|
||||
old_layout: None,
|
||||
@@ -139,10 +146,8 @@ impl FernVi {
|
||||
|
||||
/// Feed raw bytes from stdin into the reader's buffer
|
||||
pub fn feed_bytes(&mut self, bytes: &[u8]) {
|
||||
log::info!("Feeding bytes: {:?}", bytes.iter().map(|b| *b as char).collect::<String>());
|
||||
let test_input = "echo \"hello $USER\" | grep $(whoami)";
|
||||
let annotated = annotate_input(test_input);
|
||||
log::info!("Annotated test input: {:?}", annotated);
|
||||
self.reader.feed_bytes(bytes);
|
||||
}
|
||||
|
||||
@@ -173,7 +178,6 @@ impl FernVi {
|
||||
|
||||
// Process all available keys
|
||||
while let Some(key) = self.reader.read_key()? {
|
||||
log::debug!("{key:?}");
|
||||
|
||||
if self.should_accept_hint(&key) {
|
||||
self.editor.accept_hint();
|
||||
@@ -182,10 +186,42 @@ impl FernVi {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let KeyEvent(KeyCode::Tab, mod_keys) = key {
|
||||
let direction = match mod_keys {
|
||||
ModKeys::SHIFT => -1,
|
||||
_ => 1,
|
||||
};
|
||||
let line = self.editor.as_str().to_string();
|
||||
let cursor_pos = self.editor.cursor_byte_pos();
|
||||
|
||||
match self.completer.complete(line, cursor_pos, direction)? {
|
||||
Some(mut line) => {
|
||||
let span_start = self.completer.token_span.0;
|
||||
let new_cursor = span_start + self.completer.selected_candidate().map(|c| c.len()).unwrap_or_default();
|
||||
|
||||
self.editor.set_buffer(line);
|
||||
self.editor.cursor.set(new_cursor);
|
||||
|
||||
self.history.update_pending_cmd(self.editor.as_str());
|
||||
let hint = self.history.get_hint();
|
||||
self.editor.set_hint(hint);
|
||||
}
|
||||
None => {
|
||||
self.writer.flush_write("\x07")?; // Bell character
|
||||
}
|
||||
}
|
||||
|
||||
self.needs_redraw = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// if we are here, we didnt press tab
|
||||
// so we should reset the completer state
|
||||
self.completer.reset();
|
||||
|
||||
let Some(mut cmd) = self.mode.handle_key(key) else {
|
||||
continue;
|
||||
};
|
||||
log::debug!("{cmd:?}");
|
||||
cmd.alter_line_motion_if_no_verb();
|
||||
|
||||
if self.should_grab_history(&cmd) {
|
||||
@@ -240,13 +276,11 @@ impl FernVi {
|
||||
}
|
||||
|
||||
pub fn get_layout(&mut self, line: &str) -> Layout {
|
||||
log::debug!("{line:?}");
|
||||
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
|
||||
let (cols, _) = get_win_size(STDIN_FILENO);
|
||||
Layout::from_parts(/* tab_stop: */ 8, cols, &self.prompt, to_cursor, line)
|
||||
}
|
||||
pub fn scroll_history(&mut self, cmd: ViCmd) {
|
||||
log::debug!("scrolling");
|
||||
/*
|
||||
if self.history.cursor_entry().is_some_and(|ent| ent.is_new()) {
|
||||
let constraint = SearchConstraint::new(SearchKind::Prefix, self.editor.to_string());
|
||||
@@ -255,23 +289,17 @@ impl FernVi {
|
||||
*/
|
||||
let count = &cmd.motion().unwrap().0;
|
||||
let motion = &cmd.motion().unwrap().1;
|
||||
log::debug!("{count:?}, {motion:?}");
|
||||
log::debug!("{:?}", self.history.masked_entries());
|
||||
let entry = match motion {
|
||||
Motion::LineUpCharwise => {
|
||||
let Some(hist_entry) = self.history.scroll(-(*count as isize)) else {
|
||||
return;
|
||||
};
|
||||
log::debug!("found entry");
|
||||
log::debug!("{:?}", hist_entry.command());
|
||||
hist_entry
|
||||
}
|
||||
Motion::LineDownCharwise => {
|
||||
let Some(hist_entry) = self.history.scroll(*count as isize) else {
|
||||
return;
|
||||
};
|
||||
log::debug!("found entry");
|
||||
log::debug!("{:?}", hist_entry.command());
|
||||
hist_entry
|
||||
}
|
||||
_ => unreachable!(),
|
||||
@@ -296,8 +324,6 @@ impl FernVi {
|
||||
self.editor = buf
|
||||
}
|
||||
pub fn should_accept_hint(&self, event: &KeyEvent) -> bool {
|
||||
log::debug!("{:?}", self.editor.cursor_at_max());
|
||||
log::debug!("{:?}", self.editor.cursor);
|
||||
if self.editor.cursor_at_max() && self.editor.has_hint() {
|
||||
match self.mode.report_mode() {
|
||||
ModeReport::Replace | ModeReport::Insert => {
|
||||
@@ -337,7 +363,6 @@ impl FernVi {
|
||||
let hint = self.editor.get_hint_text();
|
||||
let complete = format!("{highlighted}{hint}");
|
||||
let end = start.elapsed();
|
||||
log::info!("Line styling done in: {:.2?}", end);
|
||||
complete
|
||||
}
|
||||
|
||||
@@ -538,15 +563,95 @@ pub fn annotate_input(input: &str) -> String {
|
||||
let input = Arc::new(input.to_string());
|
||||
let tokens: Vec<Tk> = lex::LexStream::new(input, LexFlags::LEX_UNFINISHED)
|
||||
.flatten()
|
||||
.filter(|tk| !matches!(tk.class, TkRule::SOI | TkRule::EOI | TkRule::Null))
|
||||
.collect();
|
||||
|
||||
for tk in tokens.into_iter().rev() {
|
||||
annotate_token(&mut annotated, tk);
|
||||
let insertions = annotate_token(tk);
|
||||
for (pos, marker) in insertions {
|
||||
let pos = pos.max(0).min(annotated.len());
|
||||
annotated.insert(pos, marker);
|
||||
}
|
||||
}
|
||||
|
||||
annotated
|
||||
}
|
||||
|
||||
/// Recursively annotates nested constructs in the input string
|
||||
pub fn annotate_input_recursive(input: &str) -> String {
|
||||
let mut annotated = annotate_input(input);
|
||||
let mut chars = annotated.char_indices().peekable();
|
||||
let mut changes = vec![];
|
||||
|
||||
while let Some((pos,ch)) = chars.next() {
|
||||
match ch {
|
||||
markers::CMD_SUB |
|
||||
markers::SUBSH |
|
||||
markers::PROC_SUB => {
|
||||
let mut body = String::new();
|
||||
let span_start = pos + ch.len_utf8();
|
||||
let mut span_end = span_start;
|
||||
let closing_marker = match ch {
|
||||
markers::CMD_SUB => markers::CMD_SUB_END,
|
||||
markers::SUBSH => markers::SUBSH_END,
|
||||
markers::PROC_SUB => markers::PROC_SUB_END,
|
||||
_ => unreachable!()
|
||||
};
|
||||
while let Some((sub_pos,sub_ch)) = chars.next() {
|
||||
match sub_ch {
|
||||
_ if sub_ch == closing_marker => {
|
||||
span_end = sub_pos;
|
||||
break;
|
||||
}
|
||||
_ => body.push(sub_ch),
|
||||
}
|
||||
}
|
||||
let prefix = match ch {
|
||||
markers::PROC_SUB => {
|
||||
match chars.peek().map(|(_, c)| *c) {
|
||||
Some('>') => ">(",
|
||||
Some('<') => "<(",
|
||||
_ => {
|
||||
log::error!("Unexpected character after PROC_SUB marker: expected '>' or '<'");
|
||||
"<("
|
||||
}
|
||||
}
|
||||
}
|
||||
markers::CMD_SUB => "$(",
|
||||
markers::SUBSH => "(",
|
||||
_ => unreachable!()
|
||||
};
|
||||
|
||||
body = body.trim_start_matches(prefix).to_string();
|
||||
let annotated_body = annotate_input_recursive(&body);
|
||||
let final_str = format!("{prefix}{annotated_body})");
|
||||
changes.push((span_start, span_end, final_str));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
for change in changes.into_iter().rev() {
|
||||
let (start, end, replacement) = change;
|
||||
annotated.replace_range(start..end, &replacement);
|
||||
}
|
||||
|
||||
annotated
|
||||
}
|
||||
|
||||
pub fn get_insertions(input: &str) -> Vec<(usize, char)> {
|
||||
let input = Arc::new(input.to_string());
|
||||
let tokens: Vec<Tk> = lex::LexStream::new(input, LexFlags::LEX_UNFINISHED)
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
let mut insertions = vec![];
|
||||
for tk in tokens.into_iter().rev() {
|
||||
insertions.extend(annotate_token(tk));
|
||||
}
|
||||
insertions
|
||||
}
|
||||
|
||||
/// Maps token class to its corresponding marker character
|
||||
///
|
||||
/// Returns the appropriate Unicode marker for token-level syntax elements.
|
||||
@@ -578,43 +683,7 @@ pub fn marker_for(class: &TkRule) -> Option<char> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Annotates a single token with markers for both token-level and sub-token constructs
|
||||
///
|
||||
/// This is the core annotation function that handles the complexity of shell syntax.
|
||||
/// It uses a two-phase approach:
|
||||
///
|
||||
/// # Phase 1: Analysis (Delayed Insertion)
|
||||
/// Scans through the token character by character, recording marker insertions
|
||||
/// as `(position, marker)` pairs in a list. This avoids borrowing issues and
|
||||
/// allows context queries during the scan.
|
||||
///
|
||||
/// The analysis phase handles:
|
||||
/// - **Strings**: Single/double quoted regions (with escaping rules)
|
||||
/// - **Variables**: `$VAR` and `${VAR}` expansions
|
||||
/// - **Command substitutions**: `$(...)` with depth tracking
|
||||
/// - **Process substitutions**: `<(...)` and `>(...)`
|
||||
/// - **Globs**: `*`, `?`, `[...]` patterns (context-aware)
|
||||
/// - **Escapes**: Backslash escaping
|
||||
///
|
||||
/// # Phase 2: Application (Sorted Insertion)
|
||||
/// Markers are sorted by position (descending) to avoid index invalidation when
|
||||
/// inserting into the string. At the same position, markers are ordered:
|
||||
/// 1. RESET (rightmost)
|
||||
/// 2. Regular markers (middle)
|
||||
/// 3. END markers (leftmost)
|
||||
///
|
||||
/// This produces the pattern: `[END][TOGGLE][RESET]` at boundaries.
|
||||
///
|
||||
/// # Context Tracking
|
||||
/// The `in_context` closure queries the insertion list to determine the active
|
||||
/// syntax context at the current position. This enables context-aware decisions
|
||||
/// like "only highlight globs in arguments, not in command names".
|
||||
///
|
||||
/// # Depth Tracking
|
||||
/// Nested constructs like `$(echo $(date))` are tracked with depth counters.
|
||||
/// Only the outermost construct is marked; inner content is handled recursively
|
||||
/// by the highlighter.
|
||||
pub fn annotate_token(input: &mut String, token: Tk) {
|
||||
pub fn annotate_token(token: Tk) -> Vec<(usize, char)> {
|
||||
// Sort by position descending, with priority ordering at same position:
|
||||
// - RESET first (inserted first, ends up rightmost)
|
||||
// - Regular markers middle
|
||||
@@ -686,18 +755,21 @@ pub fn annotate_token(input: &mut String, token: Tk) {
|
||||
ctx.1 == c
|
||||
};
|
||||
|
||||
let mut insertions: Vec<(usize, char)> = vec![];
|
||||
|
||||
|
||||
if token.class != TkRule::Str
|
||||
&& let Some(marker) = marker_for(&token.class) {
|
||||
input.insert(token.span.end, markers::RESET);
|
||||
input.insert(token.span.start, marker);
|
||||
return;
|
||||
insertions.push((token.span.end, markers::RESET));
|
||||
insertions.push((token.span.start, marker));
|
||||
return insertions;
|
||||
} else if token.flags.contains(TkFlags::IS_SUBSH) {
|
||||
let token_raw = token.span.as_str();
|
||||
if token_raw.ends_with(')') {
|
||||
input.insert(token.span.end, markers::SUBSH_END);
|
||||
insertions.push((token.span.end, markers::SUBSH_END));
|
||||
}
|
||||
input.insert(token.span.start, markers::SUBSH);
|
||||
return;
|
||||
insertions.push((token.span.start, markers::SUBSH));
|
||||
return insertions;
|
||||
}
|
||||
|
||||
|
||||
@@ -713,8 +785,6 @@ pub fn annotate_token(input: &mut String, token: Tk) {
|
||||
let mut cmd_sub_depth = 0;
|
||||
let mut proc_sub_depth = 0;
|
||||
|
||||
let mut insertions: Vec<(usize, char)> = vec![];
|
||||
|
||||
if token.flags.contains(TkFlags::BUILTIN) {
|
||||
insertions.insert(0, (span_start, markers::BUILTIN));
|
||||
} else if token.flags.contains(TkFlags::IS_CMD) {
|
||||
@@ -895,9 +965,5 @@ pub fn annotate_token(input: &mut String, token: Tk) {
|
||||
|
||||
sort_insertions(&mut insertions);
|
||||
|
||||
for (pos, marker) in insertions {
|
||||
log::info!("Inserting marker {marker:?} at position {pos}");
|
||||
let pos = pos.max(0).min(input.len());
|
||||
input.insert(pos, marker);
|
||||
}
|
||||
insertions
|
||||
}
|
||||
|
||||
@@ -239,13 +239,10 @@ impl TermBuffer {
|
||||
impl Read for TermBuffer {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
assert!(isatty(self.tty).is_ok_and(|r| r));
|
||||
log::debug!("TermBuffer::read() ENTERING read syscall");
|
||||
let result = nix::unistd::read(self.tty, buf);
|
||||
log::debug!("TermBuffer::read() EXITED read syscall: {:?}", result);
|
||||
match result {
|
||||
Ok(n) => Ok(n),
|
||||
Err(Errno::EINTR) => {
|
||||
log::debug!("TermBuffer::read() returning EINTR");
|
||||
Err(Errno::EINTR.into())
|
||||
}
|
||||
Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)),
|
||||
@@ -409,6 +406,10 @@ impl Perform for KeyCollector {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::End, mods)
|
||||
}
|
||||
// Shift+Tab: CSI Z
|
||||
([], 'Z') => {
|
||||
KeyEvent(KeyCode::Tab, ModKeys::SHIFT)
|
||||
}
|
||||
// Special keys with tilde: CSI num ~ or CSI num;mod ~
|
||||
([], '~') => {
|
||||
let key_num = params.first().copied().unwrap_or(0);
|
||||
@@ -643,7 +644,6 @@ impl KeyReader for TermReader {
|
||||
|
||||
loop {
|
||||
let byte = self.next_byte()?;
|
||||
log::debug!("read byte: {:?}", byte as char);
|
||||
collected.push(byte);
|
||||
|
||||
// If it's an escape seq, delegate to ESC sequence handler
|
||||
@@ -706,7 +706,6 @@ impl Layout {
|
||||
to_cursor: &str,
|
||||
to_end: &str,
|
||||
) -> Self {
|
||||
log::debug!("{to_cursor:?}");
|
||||
let prompt_end = Self::calc_pos(tab_stop, term_width, prompt, Pos { col: 0, row: 0 });
|
||||
let cursor = Self::calc_pos(tab_stop, term_width, to_cursor, prompt_end);
|
||||
let end = Self::calc_pos(tab_stop, term_width, to_end, prompt_end);
|
||||
|
||||
@@ -348,14 +348,11 @@ impl ViNormal {
|
||||
/// End the parse and clear the pending sequence
|
||||
#[track_caller]
|
||||
pub fn quit_parse(&mut self) -> Option<ViCmd> {
|
||||
log::debug!("{:?}", std::panic::Location::caller());
|
||||
log::warn!("exiting parse early with sequence: {}", self.pending_seq);
|
||||
self.clear_cmd();
|
||||
None
|
||||
}
|
||||
pub fn try_parse(&mut self, ch: char) -> Option<ViCmd> {
|
||||
self.pending_seq.push(ch);
|
||||
log::debug!("parsing {}", ch);
|
||||
let mut chars = self.pending_seq.chars().peekable();
|
||||
|
||||
/*
|
||||
@@ -998,8 +995,6 @@ impl ViNormal {
|
||||
};
|
||||
|
||||
if chars.peek().is_some() {
|
||||
log::warn!("Unused characters in Vi command parse!");
|
||||
log::warn!("{:?}", chars)
|
||||
}
|
||||
|
||||
let verb_ref = verb.as_ref().map(|v| &v.1);
|
||||
@@ -1145,8 +1140,6 @@ impl ViVisual {
|
||||
/// End the parse and clear the pending sequence
|
||||
#[track_caller]
|
||||
pub fn quit_parse(&mut self) -> Option<ViCmd> {
|
||||
log::debug!("{:?}", std::panic::Location::caller());
|
||||
log::warn!("exiting parse early with sequence: {}", self.pending_seq);
|
||||
self.clear_cmd();
|
||||
None
|
||||
}
|
||||
@@ -1630,7 +1623,6 @@ impl ViVisual {
|
||||
));
|
||||
}
|
||||
ch if ch == 'i' || ch == 'a' => {
|
||||
log::debug!("in text_obj parse");
|
||||
let bound = match ch {
|
||||
'i' => Bound::Inside,
|
||||
'a' => Bound::Around,
|
||||
@@ -1654,7 +1646,6 @@ impl ViVisual {
|
||||
_ => return self.quit_parse(),
|
||||
};
|
||||
chars = chars_clone;
|
||||
log::debug!("{obj:?}, {bound:?}");
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::TextObj(obj)));
|
||||
}
|
||||
_ => return self.quit_parse(),
|
||||
@@ -1662,13 +1653,10 @@ impl ViVisual {
|
||||
};
|
||||
|
||||
if chars.peek().is_some() {
|
||||
log::warn!("Unused characters in Vi command parse!");
|
||||
log::warn!("{:?}", chars)
|
||||
}
|
||||
|
||||
let verb_ref = verb.as_ref().map(|v| &v.1);
|
||||
let motion_ref = motion.as_ref().map(|m| &m.1);
|
||||
log::debug!("{verb_ref:?}, {motion_ref:?}");
|
||||
|
||||
match self.validate_combination(verb_ref, motion_ref) {
|
||||
CmdState::Complete => Some(ViCmd {
|
||||
|
||||
@@ -29,38 +29,30 @@ pub fn signals_pending() -> bool {
|
||||
|
||||
pub fn check_signals() -> ShResult<()> {
|
||||
if GOT_SIGINT.swap(false, Ordering::SeqCst) {
|
||||
log::debug!("check_signals: processing SIGINT");
|
||||
interrupt()?;
|
||||
return Err(ShErr::simple(ShErrKind::ClearReadline, ""));
|
||||
}
|
||||
if GOT_SIGHUP.swap(false, Ordering::SeqCst) {
|
||||
log::debug!("check_signals: processing SIGHUP");
|
||||
hang_up(0);
|
||||
}
|
||||
if GOT_SIGTSTP.swap(false, Ordering::SeqCst) {
|
||||
log::debug!("check_signals: processing SIGTSTP");
|
||||
terminal_stop()?;
|
||||
}
|
||||
if REAPING_ENABLED.load(Ordering::SeqCst) && GOT_SIGCHLD.swap(false, Ordering::SeqCst) {
|
||||
log::debug!("check_signals: processing SIGCHLD (reaping enabled)");
|
||||
wait_child()?;
|
||||
} else if GOT_SIGCHLD.load(Ordering::SeqCst) {
|
||||
log::debug!("check_signals: SIGCHLD pending but reaping disabled");
|
||||
}
|
||||
if SHOULD_QUIT.load(Ordering::SeqCst) {
|
||||
let code = QUIT_CODE.load(Ordering::SeqCst);
|
||||
log::debug!("check_signals: SHOULD_QUIT set, exiting with code {}", code);
|
||||
return Err(ShErr::simple(ShErrKind::CleanExit(code), "exit"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn disable_reaping() {
|
||||
log::debug!("disable_reaping: turning off SIGCHLD processing");
|
||||
REAPING_ENABLED.store(false, Ordering::SeqCst);
|
||||
}
|
||||
pub fn enable_reaping() {
|
||||
log::debug!("enable_reaping: turning on SIGCHLD processing");
|
||||
REAPING_ENABLED.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
@@ -166,13 +158,10 @@ extern "C" fn handle_sigint(_: libc::c_int) {
|
||||
}
|
||||
|
||||
pub fn interrupt() -> ShResult<()> {
|
||||
log::debug!("interrupt: checking for fg job to send SIGINT");
|
||||
write_jobs(|j| {
|
||||
if let Some(job) = j.get_fg_mut() {
|
||||
log::debug!("interrupt: sending SIGINT to fg job pgid {}", job.pgid());
|
||||
job.killpg(Signal::SIGINT)
|
||||
} else {
|
||||
log::debug!("interrupt: no fg job, clearing readline");
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
@@ -188,28 +177,22 @@ extern "C" fn handle_sigchld(_: libc::c_int) {
|
||||
}
|
||||
|
||||
pub fn wait_child() -> ShResult<()> {
|
||||
log::debug!("wait_child: starting reap loop");
|
||||
let flags = WtFlag::WNOHANG | WtFlag::WSTOPPED;
|
||||
while let Ok(status) = waitpid(None, Some(flags)) {
|
||||
match status {
|
||||
WtStat::Exited(pid, code) => {
|
||||
log::debug!("wait_child: pid {} exited with code {}", pid, code);
|
||||
child_exited(pid, status)?;
|
||||
}
|
||||
WtStat::Signaled(pid, signal, _) => {
|
||||
log::debug!("wait_child: pid {} signaled with {:?}", pid, signal);
|
||||
child_signaled(pid, signal)?;
|
||||
}
|
||||
WtStat::Stopped(pid, signal) => {
|
||||
log::debug!("wait_child: pid {} stopped with {:?}", pid, signal);
|
||||
child_stopped(pid, signal)?;
|
||||
}
|
||||
WtStat::Continued(pid) => {
|
||||
log::debug!("wait_child: pid {} continued", pid);
|
||||
child_continued(pid)?;
|
||||
}
|
||||
WtStat::StillAlive => {
|
||||
log::debug!("wait_child: no more children to reap");
|
||||
break;
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
|
||||
@@ -315,10 +315,7 @@ impl LogTab {
|
||||
self.aliases.get(name).cloned()
|
||||
}
|
||||
pub fn remove_alias(&mut self, name: &str) {
|
||||
log::debug!("{:?}", self.aliases);
|
||||
log::debug!("{name:?}");
|
||||
self.aliases.remove(name);
|
||||
log::debug!("{:?}", self.aliases);
|
||||
}
|
||||
pub fn clear_aliases(&mut self) {
|
||||
self.aliases.clear()
|
||||
@@ -655,7 +652,6 @@ impl VarTab {
|
||||
}
|
||||
}
|
||||
pub fn var_exists(&self, var_name: &str) -> bool {
|
||||
log::debug!("checking existence of {}", var_name);
|
||||
if let Ok(param) = var_name.parse::<ShellParam>() {
|
||||
return self.params.contains_key(¶m);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user