Files
shed/src/prompt/readline/mod.rs
pagedmov 7d0614943c Tab completion has been implemented
more small highlighter tune ups

2>&1 style redirections now work properly
2026-02-18 21:53:36 -05:00

970 lines
29 KiB
Rust

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<dyn LineWriter>,
pub prompt: String,
pub highlighter: Highlighter,
pub completer: Completer,
pub mode: Box<dyn ViMode>,
pub repeat_action: Option<CmdReplay>,
pub repeat_motion: Option<MotionCmd>,
pub editor: LineBuf,
pub old_layout: Option<Layout>,
pub history: History,
pub needs_redraw: bool,
}
impl FernVi {
pub fn new(prompt: Option<String>) -> ShResult<Self> {
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<String>) {
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<ReadlineEvent> {
// 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<dyn ViMode> = 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<dyn ViMode> = 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<dyn ViMode> = 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<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() {
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.
/// 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<char> {
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
}