use history::History; use keys::{KeyCode, KeyEvent, ModKeys}; use linebuf::{LineBuf, SelectAnchor, SelectMode}; use nix::libc::STDOUT_FILENO; use term::{get_win_size, KeyReader, Layout, LineWriter, PollReader, TermWriter}; use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd}; use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual}; use crate::{libsh::{ error::{ShErrKind, ShResult}, term::{Style, Styled}, }, parse::lex::{self, LexFlags, Tk, TkFlags, TkRule}, prompt::readline::{complete::{CompResult, Completer}, highlight::Highlighter}}; use crate::prelude::*; pub mod history; pub mod keys; pub mod layout; pub mod linebuf; pub mod register; pub mod term; pub mod vicmd; pub mod vimode; pub mod highlight; pub mod complete; pub mod markers { // token-level (derived from token class) pub const COMMAND: char = '\u{fdd0}'; pub const BUILTIN: char = '\u{fdd1}'; pub const ARG: char = '\u{fdd2}'; pub const KEYWORD: char = '\u{fdd3}'; pub const OPERATOR: char = '\u{fdd4}'; pub const REDIRECT: char = '\u{fdd5}'; pub const COMMENT: char = '\u{fdd6}'; pub const ASSIGNMENT: char = '\u{fdd7}'; pub const CMD_SEP: char = '\u{fde0}'; pub const CASE_PAT: char = '\u{fde1}'; pub const SUBSH: char = '\u{fde7}'; pub const SUBSH_END: char = '\u{fde8}'; // sub-token (needs scanning) pub const VAR_SUB: char = '\u{fdda}'; pub const VAR_SUB_END: char = '\u{fde3}'; pub const CMD_SUB: char = '\u{fdd8}'; pub const CMD_SUB_END: char = '\u{fde4}'; pub const PROC_SUB: char = '\u{fdd9}'; pub const PROC_SUB_END: char = '\u{fde9}'; pub const STRING_DQ: char = '\u{fddb}'; pub const STRING_DQ_END: char = '\u{fde5}'; pub const STRING_SQ: char = '\u{fddc}'; pub const STRING_SQ_END: char = '\u{fde6}'; pub const ESCAPE: char = '\u{fddd}'; pub const GLOB: char = '\u{fdde}'; pub const RESET: char = '\u{fde2}'; pub const END_MARKERS: [char;7] = [ VAR_SUB_END, CMD_SUB_END, PROC_SUB_END, STRING_DQ_END, STRING_SQ_END, SUBSH_END, RESET ]; pub const TOKEN_LEVEL: [char;10] = [ SUBSH, COMMAND, BUILTIN, ARG, KEYWORD, OPERATOR, REDIRECT, CMD_SEP, CASE_PAT, ASSIGNMENT, ]; pub const SUB_TOKEN: [char;6] = [ VAR_SUB, CMD_SUB, PROC_SUB, STRING_DQ, STRING_SQ, GLOB, ]; pub fn is_marker(c: char) -> bool { TOKEN_LEVEL.contains(&c) || SUB_TOKEN.contains(&c) || END_MARKERS.contains(&c) } } /// Non-blocking readline result pub enum ReadlineEvent { /// A complete line was entered Line(String), /// Ctrl+D on empty line - request to exit Eof, /// No complete input yet, need more bytes Pending, } pub struct FernVi { pub reader: PollReader, pub writer: Box, pub prompt: String, pub highlighter: Highlighter, pub completer: Completer, pub mode: Box, pub repeat_action: Option, pub repeat_motion: Option, pub editor: LineBuf, pub old_layout: Option, pub history: History, pub needs_redraw: bool, } impl FernVi { pub fn new(prompt: Option) -> ShResult { let mut new = Self { 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, repeat_action: None, repeat_motion: None, editor: LineBuf::new(), history: History::new()?, needs_redraw: true, }; new.print_line()?; Ok(new) } pub fn with_initial(mut self, initial: &str) -> Self { self.editor = LineBuf::new().with_initial(initial, 0); self.history.update_pending_cmd(self.editor.as_str()); self } /// Feed raw bytes from stdin into the reader's buffer pub fn feed_bytes(&mut self, bytes: &[u8]) { let test_input = "echo \"hello $USER\" | grep $(whoami)"; let annotated = annotate_input(test_input); self.reader.feed_bytes(bytes); } /// Mark that the display needs to be redrawn (e.g., after SIGWINCH) pub fn mark_dirty(&mut self) { self.needs_redraw = true; } /// Reset readline state for a new prompt pub fn reset(&mut self, prompt: Option) { if let Some(p) = prompt { self.prompt = p; } self.editor = Default::default(); self.mode = Box::new(ViInsert::new()); self.old_layout = None; self.needs_redraw = true; } /// Process any available input and return readline event /// This is non-blocking - returns Pending if no complete line yet pub fn process_input(&mut self) -> ShResult { // Redraw if needed if self.needs_redraw { self.print_line()?; self.needs_redraw = false; } // Process all available keys while let Some(key) = self.reader.read_key()? { if self.should_accept_hint(&key) { self.editor.accept_hint(); self.history.update_pending_cmd(self.editor.as_str()); self.needs_redraw = true; 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; }; cmd.alter_line_motion_if_no_verb(); if self.should_grab_history(&cmd) { self.scroll_history(cmd); self.needs_redraw = true; continue; } if cmd.should_submit() { self.editor.set_hint(None); self.print_line()?; self.writer.flush_write("\n")?; let buf = self.editor.take_buf(); // Save command to history self.history.push(buf.clone()); if let Err(e) = self.history.save() { eprintln!("Failed to save history: {e}"); } return Ok(ReadlineEvent::Line(buf)); } if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) { if self.editor.buffer.is_empty() { return Ok(ReadlineEvent::Eof); } else { self.editor.buffer.clear(); self.needs_redraw = true; continue; } } let before = self.editor.buffer.clone(); self.exec_cmd(cmd)?; let after = self.editor.as_str(); if before != after { self.history.update_pending_cmd(self.editor.as_str()); } let hint = self.history.get_hint(); self.editor.set_hint(hint); self.needs_redraw = true; } // Redraw if we processed any input if self.needs_redraw { self.print_line()?; self.needs_redraw = false; } Ok(ReadlineEvent::Pending) } pub fn get_layout(&mut self, line: &str) -> Layout { 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) { /* if self.history.cursor_entry().is_some_and(|ent| ent.is_new()) { let constraint = SearchConstraint::new(SearchKind::Prefix, self.editor.to_string()); self.history.constrain_entries(constraint); } */ let count = &cmd.motion().unwrap().0; let motion = &cmd.motion().unwrap().1; let entry = match motion { Motion::LineUpCharwise => { let Some(hist_entry) = self.history.scroll(-(*count as isize)) else { return; }; hist_entry } Motion::LineDownCharwise => { let Some(hist_entry) = self.history.scroll(*count as isize) else { return; }; hist_entry } _ => unreachable!(), }; let col = self.editor.saved_col.unwrap_or(self.editor.cursor_col()); let mut buf = LineBuf::new().with_initial(entry.command(), 0); let line_end = buf.end_of_line(); if let Some(dest) = self.mode.hist_scroll_start_pos() { match dest { To::Start => { /* Already at 0 */ } To::End => { // History entries cannot be empty // So this subtraction is safe (maybe) buf.cursor.add(line_end); } } } else { let target = (col).min(line_end); buf.cursor.add(target); } self.editor = buf } pub fn should_accept_hint(&self, event: &KeyEvent) -> bool { if self.editor.cursor_at_max() && self.editor.has_hint() { match self.mode.report_mode() { ModeReport::Replace | ModeReport::Insert => { matches!(event, KeyEvent(KeyCode::Right, ModKeys::NONE)) } ModeReport::Visual | ModeReport::Normal => { matches!(event, KeyEvent(KeyCode::Right, ModKeys::NONE)) || (self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty() && matches!(event, KeyEvent(KeyCode::Char('l'), ModKeys::NONE))) } _ => unimplemented!(), } } else { false } } pub fn should_grab_history(&mut self, cmd: &ViCmd) -> bool { cmd.verb().is_none() && (cmd .motion() .is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise))) && self.editor.start_of_line() == 0) || (cmd .motion() .is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise))) && self.editor.end_of_line() == self.editor.cursor_max() && !self.history.cursor_entry().is_some_and(|ent| ent.is_new())) } pub fn line_text(&mut self) -> String { let start = Instant::now(); let line = self.editor.to_string(); self.highlighter.load_input(&line); self.highlighter.highlight(); let highlighted = self.highlighter.take(); let hint = self.editor.get_hint_text(); let complete = format!("{highlighted}{hint}"); let end = start.elapsed(); complete } pub fn print_line(&mut self) -> ShResult<()> { let line = self.line_text(); let new_layout = self.get_layout(&line); if let Some(layout) = self.old_layout.as_ref() { self.writer.clear_rows(layout)?; } self .writer .redraw(&self.prompt, &line, &new_layout)?; self.writer.flush_write(&self.mode.cursor_style())?; self.old_layout = Some(new_layout); Ok(()) } pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> { let mut selecting = false; let mut is_insert_mode = false; if cmd.is_mode_transition() { let count = cmd.verb_count(); let mut mode: Box = match cmd.verb().unwrap().1 { Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => { is_insert_mode = true; Box::new(ViInsert::new().with_count(count as u16)) } Verb::NormalMode => Box::new(ViNormal::new()), Verb::ReplaceMode => Box::new(ViReplace::new()), Verb::VisualModeSelectLast => { if self.mode.report_mode() != ModeReport::Visual { self .editor .start_selecting(SelectMode::Char(SelectAnchor::End)); } let mut mode: Box = Box::new(ViVisual::new()); std::mem::swap(&mut mode, &mut self.mode); self.editor.set_cursor_clamp(self.mode.clamp_cursor()); return self.editor.exec_cmd(cmd); } Verb::VisualMode => { selecting = true; Box::new(ViVisual::new()) } _ => unreachable!(), }; std::mem::swap(&mut mode, &mut self.mode); if mode.is_repeatable() { self.repeat_action = mode.as_replay(); } // Set cursor clamp BEFORE executing the command so that motions // (like EndOfLine for 'A') can reach positions valid in the new mode self.editor.set_cursor_clamp(self.mode.clamp_cursor()); self.editor.exec_cmd(cmd)?; if selecting { self .editor .start_selecting(SelectMode::Char(SelectAnchor::End)); } else { self.editor.stop_selecting(); } if is_insert_mode { self.editor.mark_insert_mode_start_pos(); } else { self.editor.clear_insert_mode_start_pos(); } return Ok(()); } else if cmd.is_cmd_repeat() { let Some(replay) = self.repeat_action.clone() else { return Ok(()); }; let ViCmd { verb, .. } = cmd; let VerbCmd(count, _) = verb.unwrap(); match replay { CmdReplay::ModeReplay { cmds, mut repeat } => { if count > 1 { repeat = count as u16; } for _ in 0..repeat { let cmds = cmds.clone(); for cmd in cmds { self.editor.exec_cmd(cmd)? } } } CmdReplay::Single(mut cmd) => { if count > 1 { // Override the counts with the one passed to the '.' command if cmd.verb.is_some() { if let Some(v_mut) = cmd.verb.as_mut() { v_mut.0 = count } if let Some(m_mut) = cmd.motion.as_mut() { m_mut.0 = 1 } } else { return Ok(()); // it has to have a verb to be repeatable, // something weird happened } } self.editor.exec_cmd(cmd)?; } _ => unreachable!("motions should be handled in the other branch"), } return Ok(()); } else if cmd.is_motion_repeat() { match cmd.motion.as_ref().unwrap() { MotionCmd(count, Motion::RepeatMotion) => { let Some(motion) = self.repeat_motion.clone() else { return Ok(()); }; let repeat_cmd = ViCmd { register: RegisterName::default(), verb: None, motion: Some(motion), raw_seq: format!("{count};"), flags: CmdFlags::empty(), }; return self.editor.exec_cmd(repeat_cmd); } MotionCmd(count, Motion::RepeatMotionRev) => { let Some(motion) = self.repeat_motion.clone() else { return Ok(()); }; let mut new_motion = motion.invert_char_motion(); new_motion.0 = *count; let repeat_cmd = ViCmd { register: RegisterName::default(), verb: None, motion: Some(new_motion), raw_seq: format!("{count},"), flags: CmdFlags::empty(), }; return self.editor.exec_cmd(repeat_cmd); } _ => unreachable!(), } } if cmd.is_repeatable() { if self.mode.report_mode() == ModeReport::Visual { // The motion is assigned in the line buffer execution, so we also have to // assign it here in order to be able to repeat it let range = self.editor.select_range().unwrap(); cmd.motion = Some(MotionCmd(1, Motion::Range(range.0, range.1))) } self.repeat_action = Some(CmdReplay::Single(cmd.clone())); } if cmd.is_char_search() { self.repeat_motion = cmd.motion.clone() } self.editor.exec_cmd(cmd.clone())?; if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) { self.editor.stop_selecting(); let mut mode: Box = Box::new(ViNormal::new()); std::mem::swap(&mut mode, &mut self.mode); } Ok(()) } } /// Annotates shell input with invisible Unicode markers for syntax highlighting /// /// Takes raw shell input and inserts non-character markers (U+FDD0-U+FDEF range) /// around syntax elements. These markers indicate: /// - Token-level context (commands, arguments, operators, keywords) /// - Sub-token constructs (strings, variables, command substitutions, globs) /// /// The annotated string is suitable for processing by the highlighter, which /// interprets the markers and generates ANSI escape codes. /// /// # Strategy /// Tokens are processed in reverse order so that later insertions don't /// invalidate earlier positions. Each token is annotated independently. /// /// # Example /// ```text /// "echo $USER" -> "COMMAND echo RESET ARG VAR_SUB $USER VAR_SUB_END RESET" /// ``` /// (where COMMAND, RESET, etc. are invisible Unicode markers) pub fn annotate_input(input: &str) -> String { let mut annotated = input.to_string(); let input = Arc::new(input.to_string()); let tokens: Vec = 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() { 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 = 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. /// Token-level markers are derived directly from the lexer's token classification /// and represent complete tokens (operators, separators, etc.). /// /// Returns `None` for: /// - String tokens (which need sub-token scanning for variables, quotes, etc.) /// - Structural markers (SOI, EOI, Null) /// - Unimplemented features (comments, brace groups) pub fn marker_for(class: &TkRule) -> Option { match class { TkRule::Pipe | TkRule::ErrPipe | TkRule::And | TkRule::Or | TkRule::Bg => Some(markers::OPERATOR), TkRule::Sep => Some(markers::CMD_SEP), TkRule::Redir => Some(markers::REDIRECT), TkRule::CasePattern => Some(markers::CASE_PAT), TkRule::BraceGrpStart => todo!(), TkRule::BraceGrpEnd => todo!(), TkRule::Comment => todo!(), TkRule::Expanded { exp: _ } | TkRule::EOI | TkRule::SOI | TkRule::Null | TkRule::Str => None, } } 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 // - END markers last (inserted last, ends up leftmost) // Result: [END][TOGGLE][RESET] let sort_insertions = |insertions: &mut Vec<(usize, char)>| { insertions.sort_by(|a, b| { match b.0.cmp(&a.0) { std::cmp::Ordering::Equal => { let priority = |m: char| -> u8 { match m { markers::RESET => 0, markers::VAR_SUB | markers::VAR_SUB_END | markers::CMD_SUB | markers::CMD_SUB_END | markers::PROC_SUB | markers::PROC_SUB_END | markers::STRING_DQ | markers::STRING_DQ_END | markers::STRING_SQ | markers::STRING_SQ_END | markers::SUBSH_END => 2, markers::ARG => 3, _ => 1, } }; priority(a.1).cmp(&priority(b.1)) } other => other, } }); }; let in_context = |c: char, insertions: &[(usize, char)]| -> bool { let mut stack = insertions.to_vec(); stack.sort_by(|a, b| { match b.0.cmp(&a.0) { std::cmp::Ordering::Equal => { let priority = |m: char| -> u8 { match m { markers::RESET => 0, markers::VAR_SUB | markers::VAR_SUB_END | markers::CMD_SUB | markers::CMD_SUB_END | markers::PROC_SUB | markers::PROC_SUB_END | markers::STRING_DQ | markers::STRING_DQ_END | markers::STRING_SQ | markers::STRING_SQ_END | markers::SUBSH_END => 2, markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens _ => 1, } }; priority(a.1).cmp(&priority(b.1)) } other => other, } }); stack.retain(|(i, m)| *i <= token.span.start && !markers::END_MARKERS.contains(m)); let Some(ctx) = stack.last() else { return false; }; ctx.1 == c }; let mut insertions: Vec<(usize, char)> = vec![]; if token.class != TkRule::Str && let Some(marker) = marker_for(&token.class) { 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(')') { insertions.push((token.span.end, markers::SUBSH_END)); } insertions.push((token.span.start, markers::SUBSH)); return insertions; } let token_raw = token.span.as_str(); let mut token_chars = token_raw .char_indices() .peekable(); let span_start = token.span.start; let mut in_dub_qt = false; let mut in_sng_qt = false; let mut cmd_sub_depth = 0; let mut proc_sub_depth = 0; if token.flags.contains(TkFlags::BUILTIN) { insertions.insert(0, (span_start, markers::BUILTIN)); } else if token.flags.contains(TkFlags::IS_CMD) { insertions.insert(0, (span_start, markers::COMMAND)); } else if !token.flags.contains(TkFlags::KEYWORD) && !token.flags.contains(TkFlags::ASSIGN) { insertions.insert(0, (span_start, markers::ARG)); } if token.flags.contains(TkFlags::KEYWORD) { insertions.insert(0, (span_start, markers::KEYWORD)); } if token.flags.contains(TkFlags::ASSIGN) { insertions.insert(0, (span_start, markers::ASSIGNMENT)); } insertions.insert(0, (token.span.end, markers::RESET)); // reset at the end of the token while let Some((i,ch)) = token_chars.peek() { let index = *i; // we have to dereference this here because rustc is a very pedantic program match ch { ')' if cmd_sub_depth > 0 || proc_sub_depth > 0 => { token_chars.next(); // consume the paren if cmd_sub_depth > 0 { cmd_sub_depth -= 1; if cmd_sub_depth == 0 { insertions.push((span_start + index + 1, markers::CMD_SUB_END)); } } else if proc_sub_depth > 0 { proc_sub_depth -= 1; if proc_sub_depth == 0 { insertions.push((span_start + index + 1, markers::PROC_SUB_END)); } } } '$' if !in_sng_qt => { let dollar_pos = index; token_chars.next(); // consume the dollar if let Some((_, dollar_ch)) = token_chars.peek() { match dollar_ch { '(' => { cmd_sub_depth += 1; if cmd_sub_depth == 1 { // only mark top level command subs insertions.push((span_start + dollar_pos, markers::CMD_SUB)); } token_chars.next(); // consume the paren } '{' if cmd_sub_depth == 0 => { insertions.push((span_start + dollar_pos, markers::VAR_SUB)); token_chars.next(); // consume the brace let mut end_pos; // position after ${ while let Some((cur_i, br_ch)) = token_chars.peek() { end_pos = *cur_i; // TODO: implement better parameter expansion awareness here // this is a little too permissive if br_ch.is_ascii_alphanumeric() || *br_ch == '_' || *br_ch == '!' || *br_ch == '#' || *br_ch == '%' || *br_ch == ':' || *br_ch == '-' || *br_ch == '+' || *br_ch == '=' || *br_ch == '/' // parameter expansion symbols || *br_ch == '?' { token_chars.next(); } else if *br_ch == '}' { token_chars.next(); // consume the closing brace insertions.push((span_start + end_pos + 1, markers::VAR_SUB_END)); break; } else { // malformed, insert end at current position insertions.push((span_start + end_pos, markers::VAR_SUB_END)); break; } } } _ if cmd_sub_depth == 0 && (dollar_ch.is_ascii_alphanumeric() || *dollar_ch == '_') => { insertions.push((span_start + dollar_pos, markers::VAR_SUB)); let mut end_pos = dollar_pos + 1; // consume the var name while let Some((cur_i, var_ch)) = token_chars.peek() { if var_ch.is_ascii_alphanumeric() || *var_ch == '_' { end_pos = *cur_i + 1; token_chars.next(); } else { break; } } insertions.push((span_start + end_pos, markers::VAR_SUB_END)); } _ => { /* Just a plain dollar sign, no marker needed */ } } } } ch if cmd_sub_depth > 0 || proc_sub_depth > 0 => { // We are inside of a command sub or process sub right now // We don't mark any of this text. It will later be recursively annotated // by the syntax highlighter token_chars.next(); // consume the char with no special handling } '\\' if !in_sng_qt => { token_chars.next(); // consume the backslash if token_chars.peek().is_some() { token_chars.next(); // consume the escaped char } } '<' | '>' if !in_dub_qt && !in_sng_qt && cmd_sub_depth == 0 && proc_sub_depth == 0 => { token_chars.next(); if let Some((_, proc_sub_ch)) = token_chars.peek() && *proc_sub_ch == '(' { proc_sub_depth += 1; token_chars.next(); // consume the paren if proc_sub_depth == 1 { insertions.push((span_start + index, markers::PROC_SUB)); } } } '"' if !in_sng_qt => { if in_dub_qt { insertions.push((span_start + *i + 1, markers::STRING_DQ_END)); } else { insertions.push((span_start + *i, markers::STRING_DQ)); } in_dub_qt = !in_dub_qt; token_chars.next(); // consume the quote } '\'' if !in_dub_qt => { if in_sng_qt { insertions.push((span_start + *i + 1, markers::STRING_SQ_END)); } else { insertions.push((span_start + *i, markers::STRING_SQ)); } in_sng_qt = !in_sng_qt; token_chars.next(); // consume the quote } '[' if !in_dub_qt && !in_sng_qt => { token_chars.next(); // consume the opening bracket let start_pos = span_start + index; let mut is_glob_pat = false; const VALID_CHARS: &[char] = &['!', '^', '-']; while let Some((cur_i, ch)) = token_chars.peek() { if *ch == ']' { is_glob_pat = true; insertions.push((span_start + *cur_i + 1, markers::RESET)); insertions.push((span_start + *cur_i, markers::GLOB)); token_chars.next(); // consume the closing bracket break; } else if !ch.is_ascii_alphanumeric() && !VALID_CHARS.contains(ch) { token_chars.next(); break; } else { token_chars.next(); } } if is_glob_pat { insertions.push((start_pos + 1, markers::RESET)); insertions.push((start_pos, markers::GLOB)); } } '*' | '?' if (!in_dub_qt && !in_sng_qt) => { if !in_context(markers::COMMAND, &insertions) { insertions.push((span_start + *i + 1, markers::RESET)); insertions.push((span_start + *i, markers::GLOB)); } token_chars.next(); // consume the glob char } _ => { token_chars.next(); // consume the char with no special handling } } } sort_insertions(&mut insertions); insertions }