use nu_ansi_term::{Color, Style}; pub(crate) struct StyledText { pub(crate) buffer: Vec<(Style, String)>, } impl StyledText { pub fn new() -> Self { Self { buffer: Vec::new() } } pub fn push(&mut self, value: (Style, String)) { self.buffer.push(value); } } /// Syntax highlighter for the forge readline prompt. /// /// Applies visual styles to recognised input patterns as the user types: /// - Commands (`:foo` or `/foo` for backward compatibility) are rendered in /// yellow bold. /// - File mentions (`@[path]`) are rendered in cyan bold. /// - Shell pass-through commands (`!cmd`) are rendered in magenta. /// - All other text is rendered in the default terminal style. pub struct ForgeHighlighter; impl ForgeHighlighter { pub(crate) fn highlight(&self, line: &str, _cursor: usize) -> StyledText { let mut styled = StyledText::new(); if line.is_empty() { return styled; } // Command: highlight the command token (e.g. `:compact` or `/compact` for // compat) in yellow bold, then the remainder (arguments) without // special styling. if line.starts_with('/') || line.starts_with(':') { let end = line.find(|c: char| c.is_whitespace()).unwrap_or(line.len()); styled.push(( Style::new().bold().fg(Color::Yellow), line.get(..end).unwrap_or(line).to_string(), )); if end < line.len() && let Some(args) = line.get(end..) { highlight_mentions(args, &mut styled); } return styled; } // Shell pass-through: `!` rendered in magenta. if line.starts_with('!') { styled.push((Style::new().fg(Color::Magenta), line.to_string())); return styled; } // General message text — scan for `@[...]` file mentions and colour them cyan // bold. highlight_mentions(line, &mut styled); styled } } /// Walk through `line` and emit styled segments, colouring every `@[...]` /// mention (matching the ZSH pattern `@\[[^]]*\]`) in cyan bold and leaving /// surrounding text unstyled. /// /// Mirrors `ZSH_HIGHLIGHT_PATTERNS+=('@\[[^]]#\]' 'fg=cyan,bold')` exactly: /// - Requires `@[` opener. /// - Matches zero or more non-`]` characters inside the brackets. /// - Requires closing `]` — unterminated tags are left unstyled. fn highlight_mentions(line: &str, styled: &mut StyledText) { let mut remaining = line; while !remaining.is_empty() { // Find the next `@[` opener. match remaining.find("@[") { None => { // No more mentions — emit the rest as plain text. styled.push((Style::new(), remaining.to_string())); break; } Some(start) => { // Emit any plain text before the `@[`. if start > 0 && let Some(before) = remaining.get(..start) { styled.push((Style::new(), before.to_string())); } // `after_open` starts at `@[`. let after_open = match remaining.get(start..) { Some(s) => s, None => { styled.push((Style::new(), remaining.to_string())); break; } }; // Look for a `]` that is not immediately after `@[`. // The ZSH pattern `[^]#` matches zero-or-more non-`]` chars, // so `@[]` (empty brackets) also qualifies. // We search for `]` starting from position 2 (after `@[`). match after_open.get(2..).and_then(|s| s.find(']')) { None => { // No closing `]` — emit `@[` and the rest as plain text // to match ZSH behaviour (unterminated tag = no highlight). styled.push((Style::new(), after_open.to_string())); break; } Some(rel_close) => { // Absolute position of `]` within `after_open`. let close = 2 + rel_close; // Emit `@[...]` in cyan bold (inclusive of both brackets). let mention = after_open.get(..=close).unwrap_or(after_open); styled.push((Style::new().bold().fg(Color::Cyan), mention.to_string())); match after_open.get(close + 1..) { Some(rest) => remaining = rest, None => break, } } } } } } } #[cfg(test)] mod tests { use super::*; fn render(styled: &StyledText) -> String { styled.buffer.iter().map(|(_, s)| s.as_str()).collect() } fn styles(styled: &StyledText) -> Vec