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 {
// Undoes all styles
Reset,
ResetFg,
ResetBg,
// Foreground Colors
Black,
Red,
@@ -66,6 +68,8 @@ impl Display for Style {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Style::Reset => write!(f, "\x1b[0m"),
Style::ResetFg => write!(f, "\x1b[39m"),
Style::ResetBg => write!(f, "\x1b[49m"),
// Foreground colors
Style::Black => write!(f, "\x1b[30m"),
@@ -127,6 +131,14 @@ impl StyleSet {
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 {
if !self.styles.contains(&style) {
self.styles.push(style);

View File

@@ -6,7 +6,7 @@ use std::{
use crate::{
libsh::term::{Style, StyleSet, Styled},
prompt::readline::{annotate_input, markers},
prompt::readline::{annotate_input, markers::{self, is_marker}},
state::{read_logic, read_shopts},
};
@@ -23,6 +23,7 @@ pub struct Highlighter {
linebuf_cursor_pos: usize,
style_stack: Vec<StyleSet>,
last_was_reset: bool,
in_selection: bool
}
impl Highlighter {
@@ -34,6 +35,7 @@ impl Highlighter {
linebuf_cursor_pos: 0,
style_stack: Vec::new(),
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.)
pub fn load_input(&mut self, input: &str, linebuf_cursor_pos: usize) {
let input = annotate_input(input);
log::debug!("Annotated input: {:?}", input);
self.input = input;
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
///
/// Walks through the input character by character, interpreting markers and
@@ -57,6 +70,14 @@ impl Highlighter {
let mut input_chars = input.chars().peekable();
while let Some(ch) = input_chars.next() {
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_SQ_END
| markers::VAR_SUB_END
@@ -78,6 +99,7 @@ impl Highlighter {
markers::REDIRECT | markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
markers::ASSIGNMENT => {
let mut var_name = String::new();
@@ -116,7 +138,7 @@ impl Highlighter {
arg.push(ch);
}
let style = if Self::is_filename(&arg) {
let style = if Self::is_filename(&Self::strip_markers(&arg)) {
Style::White | Style::Underline
} else {
Style::White.into()
@@ -136,7 +158,7 @@ impl Highlighter {
}
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()
} else {
Style::Red | Style::Bold
@@ -346,7 +368,11 @@ impl Highlighter {
///
/// Unconditionally appends the ANSI escape sequence for the given style
/// 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.last_was_reset = false;
}
@@ -358,7 +384,9 @@ impl Highlighter {
pub fn push_style(&mut self, style: impl Into<StyleSet>) {
let set: StyleSet = style.into();
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
@@ -371,7 +399,7 @@ impl Highlighter {
pub fn pop_style(&mut self) {
self.style_stack.pop();
if let Some(style) = self.style_stack.last().cloned() {
self.emit_style(&style);
self.emit_style(style);
} else {
self.emit_reset();
}
@@ -383,9 +411,19 @@ impl Highlighter {
/// the default terminal color between independent commands.
pub fn clear_styles(&mut self) {
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
/// highlighting)
///

View File

@@ -15,7 +15,7 @@ use crate::{
error::ShResult,
term::{Style, Styled},
},
prelude::*, prompt::readline::register::write_register,
prelude::*, prompt::readline::{markers, register::write_register},
};
const PUNCTUATION: [&str; 3] = ["?", "!", "."];
@@ -2242,7 +2242,7 @@ impl LineBuf {
if has_consumed_hint {
let buf_end = if self.cursor.exclusive {
self.cursor.ret_add(1)
self.cursor.ret_add_inclusive(1)
} else {
self.cursor.get()
};
@@ -2873,17 +2873,22 @@ impl Display for LineBuf {
let start_byte = self.read_idx_byte_pos(start);
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() {
SelectAnchor::Start => {
let mut inclusive = start_byte..=end_byte;
if *inclusive.end() == full_buf.len() {
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);
}
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);
}
}

View File

@@ -59,6 +59,10 @@ pub mod markers {
pub const ESCAPE: Marker = '\u{fddd}';
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 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 MISC: [Marker; 3] = [ESCAPE, VISUAL_MODE_START, VISUAL_MODE_END];
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;
@@ -263,7 +269,8 @@ impl FernVi {
if self.editor.buffer.is_empty() {
return Ok(ReadlineEvent::Eof);
} else {
self.editor.buffer.clear();
self.editor = LineBuf::new();
self.mode = Box::new(ViInsert::new());
self.needs_redraw = true;
continue;
}
@@ -369,6 +376,7 @@ impl FernVi {
self.highlighter.load_input(&line,self.editor.cursor_byte_pos());
self.highlighter.highlight();
let highlighted = self.highlighter.take();
log::info!("Highlighting line. highlighted: {:?}, hint: {:?}", highlighted, hint);
format!("{highlighted}{hint}")
} else {
format!("{line}{hint}")
@@ -691,7 +699,9 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
std::cmp::Ordering::Equal => {
let priority = |m: Marker| -> u8 {
match m {
markers::RESET => 0,
markers::VISUAL_MODE_END
| markers::VISUAL_MODE_START
| markers::RESET => 0,
markers::VAR_SUB
| markers::VAR_SUB_END
| markers::CMD_SUB
@@ -720,7 +730,9 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
std::cmp::Ordering::Equal => {
let priority = |m: Marker| -> u8 {
match m {
markers::RESET => 0,
markers::VISUAL_MODE_END
| markers::VISUAL_MODE_START
| markers::RESET => 0,
markers::VAR_SUB
| markers::VAR_SUB_END
| markers::CMD_SUB
@@ -732,7 +744,8 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
| markers::STRING_SQ
| markers::STRING_SQ_END
| 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,
}
};