Tab completion has been implemented

more small highlighter tune ups

2>&1 style redirections now work properly
This commit is contained in:
2026-02-18 21:53:36 -05:00
parent 01684cf8e5
commit 3b698628c6
22 changed files with 511 additions and 188 deletions

View File

@@ -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)
}

View 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
}
}

View File

@@ -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)

View File

@@ -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())

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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);

View File

@@ -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 {