Highlighter now handles highlighting visual mode selections instead of LineBuf

This commit is contained in:
2026-02-20 14:03:42 -05:00
parent a0cf2a7edd
commit 5721cdb7ca
4 changed files with 84 additions and 16 deletions

View File

@@ -15,6 +15,8 @@ impl<T: Display> Styled for T {}
pub enum Style { pub enum Style {
// Undoes all styles // Undoes all styles
Reset, Reset,
ResetFg,
ResetBg,
// Foreground Colors // Foreground Colors
Black, Black,
Red, Red,
@@ -66,6 +68,8 @@ impl Display for Style {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Style::Reset => write!(f, "\x1b[0m"), Style::Reset => write!(f, "\x1b[0m"),
Style::ResetFg => write!(f, "\x1b[39m"),
Style::ResetBg => write!(f, "\x1b[49m"),
// Foreground colors // Foreground colors
Style::Black => write!(f, "\x1b[30m"), Style::Black => write!(f, "\x1b[30m"),
@@ -127,6 +131,14 @@ impl StyleSet {
Self { styles: vec![] } Self { styles: vec![] }
} }
pub fn styles(&self) -> &[Style] {
&self.styles
}
pub fn styles_mut(&mut self) -> &mut Vec<Style> {
&mut self.styles
}
pub fn add_style(mut self, style: Style) -> Self { pub fn add_style(mut self, style: Style) -> Self {
if !self.styles.contains(&style) { if !self.styles.contains(&style) {
self.styles.push(style); self.styles.push(style);

View File

@@ -6,7 +6,7 @@ use std::{
use crate::{ use crate::{
libsh::term::{Style, StyleSet, Styled}, libsh::term::{Style, StyleSet, Styled},
prompt::readline::{annotate_input, markers}, prompt::readline::{annotate_input, markers::{self, is_marker}},
state::{read_logic, read_shopts}, state::{read_logic, read_shopts},
}; };
@@ -23,6 +23,7 @@ pub struct Highlighter {
linebuf_cursor_pos: usize, linebuf_cursor_pos: usize,
style_stack: Vec<StyleSet>, style_stack: Vec<StyleSet>,
last_was_reset: bool, last_was_reset: bool,
in_selection: bool
} }
impl Highlighter { impl Highlighter {
@@ -34,6 +35,7 @@ impl Highlighter {
linebuf_cursor_pos: 0, linebuf_cursor_pos: 0,
style_stack: Vec::new(), style_stack: Vec::new(),
last_was_reset: true, // start as true so we don't emit a leading reset last_was_reset: true, // start as true so we don't emit a leading reset
in_selection: false
} }
} }
@@ -43,10 +45,21 @@ impl Highlighter {
/// indicating token types and sub-token constructs (strings, variables, etc.) /// indicating token types and sub-token constructs (strings, variables, etc.)
pub fn load_input(&mut self, input: &str, linebuf_cursor_pos: usize) { pub fn load_input(&mut self, input: &str, linebuf_cursor_pos: usize) {
let input = annotate_input(input); let input = annotate_input(input);
log::debug!("Annotated input: {:?}", input);
self.input = input; self.input = input;
self.linebuf_cursor_pos = linebuf_cursor_pos; self.linebuf_cursor_pos = linebuf_cursor_pos;
} }
pub fn strip_markers(str: &str) -> String {
let mut out = String::new();
for ch in str.chars() {
if !is_marker(ch) {
out.push(ch);
}
}
out
}
/// Processes the annotated input and generates ANSI-styled output /// Processes the annotated input and generates ANSI-styled output
/// ///
/// Walks through the input character by character, interpreting markers and /// Walks through the input character by character, interpreting markers and
@@ -57,6 +70,14 @@ impl Highlighter {
let mut input_chars = input.chars().peekable(); let mut input_chars = input.chars().peekable();
while let Some(ch) = input_chars.next() { while let Some(ch) = input_chars.next() {
match ch { match ch {
markers::VISUAL_MODE_START => {
self.emit_style(Style::BgWhite | Style::Black);
self.in_selection = true;
}
markers::VISUAL_MODE_END => {
self.reapply_style();
self.in_selection = false;
}
markers::STRING_DQ_END markers::STRING_DQ_END
| markers::STRING_SQ_END | markers::STRING_SQ_END
| markers::VAR_SUB_END | markers::VAR_SUB_END
@@ -78,6 +99,7 @@ impl Highlighter {
markers::REDIRECT | markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold), markers::REDIRECT | markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
markers::ASSIGNMENT => { markers::ASSIGNMENT => {
let mut var_name = String::new(); let mut var_name = String::new();
@@ -116,7 +138,7 @@ impl Highlighter {
arg.push(ch); arg.push(ch);
} }
let style = if Self::is_filename(&arg) { let style = if Self::is_filename(&Self::strip_markers(&arg)) {
Style::White | Style::Underline Style::White | Style::Underline
} else { } else {
Style::White.into() Style::White.into()
@@ -136,7 +158,7 @@ impl Highlighter {
} }
cmd_name.push(ch); cmd_name.push(ch);
} }
let style = if Self::is_valid(&cmd_name) { let style = if Self::is_valid(&Self::strip_markers(&cmd_name)) {
Style::Green.into() Style::Green.into()
} else { } else {
Style::Red | Style::Bold Style::Red | Style::Bold
@@ -346,7 +368,11 @@ impl Highlighter {
/// ///
/// Unconditionally appends the ANSI escape sequence for the given style /// Unconditionally appends the ANSI escape sequence for the given style
/// and marks that we're no longer in a reset state. /// and marks that we're no longer in a reset state.
fn emit_style(&mut self, style: &StyleSet) { fn emit_style(&mut self, style: StyleSet) {
let mut style = style;
if !style.styles().contains(&Style::BgWhite) {
style = style.add_style(Style::BgBlack);
}
self.output.push_str(&style.to_string()); self.output.push_str(&style.to_string());
self.last_was_reset = false; self.last_was_reset = false;
} }
@@ -358,7 +384,9 @@ impl Highlighter {
pub fn push_style(&mut self, style: impl Into<StyleSet>) { pub fn push_style(&mut self, style: impl Into<StyleSet>) {
let set: StyleSet = style.into(); let set: StyleSet = style.into();
self.style_stack.push(set.clone()); self.style_stack.push(set.clone());
self.emit_style(&set); if !self.in_selection {
self.emit_style(set.clone());
}
} }
/// Pops a style from the stack and restores the previous style /// Pops a style from the stack and restores the previous style
@@ -371,7 +399,7 @@ impl Highlighter {
pub fn pop_style(&mut self) { pub fn pop_style(&mut self) {
self.style_stack.pop(); self.style_stack.pop();
if let Some(style) = self.style_stack.last().cloned() { if let Some(style) = self.style_stack.last().cloned() {
self.emit_style(&style); self.emit_style(style);
} else { } else {
self.emit_reset(); self.emit_reset();
} }
@@ -383,9 +411,19 @@ impl Highlighter {
/// the default terminal color between independent commands. /// the default terminal color between independent commands.
pub fn clear_styles(&mut self) { pub fn clear_styles(&mut self) {
self.style_stack.clear(); self.style_stack.clear();
self.emit_reset(); if !self.in_selection {
self.emit_reset();
}
} }
pub fn reapply_style(&mut self) {
if let Some(style) = self.style_stack.last().cloned() {
self.emit_style(style);
} else {
self.emit_reset();
}
}
/// Simple marker-to-ANSI replacement (unused in favor of stack-based /// Simple marker-to-ANSI replacement (unused in favor of stack-based
/// highlighting) /// highlighting)
/// ///

View File

@@ -15,7 +15,7 @@ use crate::{
error::ShResult, error::ShResult,
term::{Style, Styled}, term::{Style, Styled},
}, },
prelude::*, prompt::readline::register::write_register, prelude::*, prompt::readline::{markers, register::write_register},
}; };
const PUNCTUATION: [&str; 3] = ["?", "!", "."]; const PUNCTUATION: [&str; 3] = ["?", "!", "."];
@@ -2242,7 +2242,7 @@ impl LineBuf {
if has_consumed_hint { if has_consumed_hint {
let buf_end = if self.cursor.exclusive { let buf_end = if self.cursor.exclusive {
self.cursor.ret_add(1) self.cursor.ret_add_inclusive(1)
} else { } else {
self.cursor.get() self.cursor.get()
}; };
@@ -2873,17 +2873,22 @@ impl Display for LineBuf {
let start_byte = self.read_idx_byte_pos(start); let start_byte = self.read_idx_byte_pos(start);
let end_byte = self.read_idx_byte_pos(end); let end_byte = self.read_idx_byte_pos(end);
if start_byte >= full_buf.len() || end_byte >= full_buf.len() {
log::warn!("Selection range '{:?}' is out of bounds for buffer of length {}, clearing selection", (start, end), full_buf.len());
return write!(f, "{}", full_buf);
}
match mode.anchor() { match mode.anchor() {
SelectAnchor::Start => { SelectAnchor::Start => {
let mut inclusive = start_byte..=end_byte; let mut inclusive = start_byte..=end_byte;
if *inclusive.end() == full_buf.len() { if *inclusive.end() == full_buf.len() {
inclusive = start_byte..=end_byte.saturating_sub(1); inclusive = start_byte..=end_byte.saturating_sub(1);
} }
let selected = full_buf[inclusive.clone()].styled(Style::BgWhite | Style::Black); let selected = format!("{}{}{}", markers::VISUAL_MODE_START, &full_buf[inclusive.clone()], markers::VISUAL_MODE_END);
full_buf.replace_range(inclusive, &selected); full_buf.replace_range(inclusive, &selected);
} }
SelectAnchor::End => { SelectAnchor::End => {
let selected = full_buf[start..end].styled(Style::BgWhite | Style::Black); let selected = format!("{}{}{}", markers::VISUAL_MODE_START, &full_buf[start..end], markers::VISUAL_MODE_END);
full_buf.replace_range(start_byte..end_byte, &selected); full_buf.replace_range(start_byte..end_byte, &selected);
} }
} }

View File

@@ -59,6 +59,10 @@ pub mod markers {
pub const ESCAPE: Marker = '\u{fddd}'; pub const ESCAPE: Marker = '\u{fddd}';
pub const GLOB: Marker = '\u{fdde}'; pub const GLOB: Marker = '\u{fdde}';
// other
pub const VISUAL_MODE_START: Marker = '\u{fdea}';
pub const VISUAL_MODE_END: Marker = '\u{fdeb}';
pub const RESET: Marker = '\u{fde2}'; pub const RESET: Marker = '\u{fde2}';
pub const NULL: Marker = '\u{fdef}'; pub const NULL: Marker = '\u{fdef}';
@@ -77,8 +81,10 @@ pub mod markers {
]; ];
pub const SUB_TOKEN: [Marker; 6] = [VAR_SUB, CMD_SUB, PROC_SUB, STRING_DQ, STRING_SQ, GLOB]; pub const SUB_TOKEN: [Marker; 6] = [VAR_SUB, CMD_SUB, PROC_SUB, STRING_DQ, STRING_SQ, GLOB];
pub const MISC: [Marker; 3] = [ESCAPE, VISUAL_MODE_START, VISUAL_MODE_END];
pub fn is_marker(c: Marker) -> bool { pub fn is_marker(c: Marker) -> bool {
TOKEN_LEVEL.contains(&c) || SUB_TOKEN.contains(&c) || END_MARKERS.contains(&c) TOKEN_LEVEL.contains(&c) || SUB_TOKEN.contains(&c) || END_MARKERS.contains(&c) || MISC.contains(&c)
} }
} }
type Marker = char; type Marker = char;
@@ -263,7 +269,8 @@ impl FernVi {
if self.editor.buffer.is_empty() { if self.editor.buffer.is_empty() {
return Ok(ReadlineEvent::Eof); return Ok(ReadlineEvent::Eof);
} else { } else {
self.editor.buffer.clear(); self.editor = LineBuf::new();
self.mode = Box::new(ViInsert::new());
self.needs_redraw = true; self.needs_redraw = true;
continue; continue;
} }
@@ -369,6 +376,7 @@ impl FernVi {
self.highlighter.load_input(&line,self.editor.cursor_byte_pos()); self.highlighter.load_input(&line,self.editor.cursor_byte_pos());
self.highlighter.highlight(); self.highlighter.highlight();
let highlighted = self.highlighter.take(); let highlighted = self.highlighter.take();
log::info!("Highlighting line. highlighted: {:?}, hint: {:?}", highlighted, hint);
format!("{highlighted}{hint}") format!("{highlighted}{hint}")
} else { } else {
format!("{line}{hint}") format!("{line}{hint}")
@@ -691,7 +699,9 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
std::cmp::Ordering::Equal => { std::cmp::Ordering::Equal => {
let priority = |m: Marker| -> u8 { let priority = |m: Marker| -> u8 {
match m { match m {
markers::RESET => 0, markers::VISUAL_MODE_END
| markers::VISUAL_MODE_START
| markers::RESET => 0,
markers::VAR_SUB markers::VAR_SUB
| markers::VAR_SUB_END | markers::VAR_SUB_END
| markers::CMD_SUB | markers::CMD_SUB
@@ -720,7 +730,9 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
std::cmp::Ordering::Equal => { std::cmp::Ordering::Equal => {
let priority = |m: Marker| -> u8 { let priority = |m: Marker| -> u8 {
match m { match m {
markers::RESET => 0, markers::VISUAL_MODE_END
| markers::VISUAL_MODE_START
| markers::RESET => 0,
markers::VAR_SUB markers::VAR_SUB
| markers::VAR_SUB_END | markers::VAR_SUB_END
| markers::CMD_SUB | markers::CMD_SUB
@@ -732,7 +744,8 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
| markers::STRING_SQ | markers::STRING_SQ
| markers::STRING_SQ_END | markers::STRING_SQ_END
| markers::SUBSH_END => 2, | markers::SUBSH_END => 2,
markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens
| markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens
_ => 1, _ => 1,
} }
}; };