From 135439d763a670c41af6b302d47e07ddd498f735 Mon Sep 17 00:00:00 2001 From: vasilito Date: Sat, 20 Jun 2026 00:07:26 +0300 Subject: [PATCH] =?UTF-8?q?tlc:=20editor=20visual=20polish=20=E2=80=94=20c?= =?UTF-8?q?urrent-line=20highlight,=20bracket=20match,=20modified=20gutter?= =?UTF-8?q?=20mark?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Current-line highlight (bg lightened by 12 per channel on cursor line), bracket-match tracking (forward/backward search for ()[]{}<> pairs), modified-line gutter mark (▌ in warning color when buffer is dirty). bracket_flash field + find_matching_forward/backward methods on Editor. --- .../recipes/tui/tlc/source/src/editor/mod.rs | 63 +++++++++++++++++++ .../tui/tlc/source/src/editor/render.rs | 23 +++++-- 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/local/recipes/tui/tlc/source/src/editor/mod.rs b/local/recipes/tui/tlc/source/src/editor/mod.rs index 3bb5115208..f91ae91c75 100644 --- a/local/recipes/tui/tlc/source/src/editor/mod.rs +++ b/local/recipes/tui/tlc/source/src/editor/mod.rs @@ -131,6 +131,10 @@ pub struct Editor { /// Track scroll position to reset highlighter state on scroll. #[cfg(feature = "syntect")] last_render_top: usize, + /// Cached bracket-match pair (opening byte offset, closing byte + /// offset) for the flash highlight, or `None` when the cursor is + /// not on a bracket. + bracket_flash: Option<(usize, usize)>, } impl Editor { @@ -165,6 +169,7 @@ impl Editor { highlighter: hl, #[cfg(feature = "syntect")] last_render_top: 0, + bracket_flash: None, } } @@ -191,6 +196,7 @@ impl Editor { highlighter: None, #[cfg(feature = "syntect")] last_render_top: 0, + bracket_flash: None, } } @@ -241,6 +247,63 @@ impl Editor { self.modified } + fn update_bracket_flash(&mut self) { + let pos = self.cursor.position(); + let text = self.buffer.as_string(); + if pos >= text.len() { + self.bracket_flash = None; + return; + } + let ch = text[pos..].chars().next().unwrap(); + let open = "([{<"; + let close = ")]}>"; + let result = if let Some(idx) = open.find(ch) { + let target = close.as_bytes()[idx] as char; + Self::find_matching_forward(&text, pos, ch, target) + .map(|end| (pos, end)) + } else if let Some(idx) = close.find(ch) { + let target = open.as_bytes()[idx] as char; + Self::find_matching_backward(&text, pos, ch, target) + .map(|start| (start, pos)) + } else { + None + }; + self.bracket_flash = result; + } + + fn find_matching_forward(text: &str, start: usize, open: char, close: char) -> Option { + let mut depth = 1i32; + let mut offset = start + open.len_utf8(); + for (i, ch) in text[offset..].char_indices() { + let abs = offset + i; + if ch == open { + depth += 1; + } else if ch == close { + depth -= 1; + if depth == 0 { + return Some(abs); + } + } + offset = abs + ch.len_utf8(); + } + None + } + + fn find_matching_backward(text: &str, start: usize, close: char, open: char) -> Option { + let mut depth = 1i32; + for (i, ch) in text[..start].char_indices().rev() { + if ch == close { + depth += 1; + } else if ch == open { + depth -= 1; + if depth == 0 { + return Some(i); + } + } + } + None + } + /// Set a named bookmark at the current cursor location. pub fn set_bookmark(&mut self, name: char) -> Result<(), String> { let line = self.buffer_line_of(self.cursor.position()) as u32; diff --git a/local/recipes/tui/tlc/source/src/editor/render.rs b/local/recipes/tui/tlc/source/src/editor/render.rs index c756f96fc3..b46de72fab 100644 --- a/local/recipes/tui/tlc/source/src/editor/render.rs +++ b/local/recipes/tui/tlc/source/src/editor/render.rs @@ -28,6 +28,7 @@ impl Editor { // (e.g. commit_prompt's GotoLine/GotoCol) changed the // buffer cursor without calling cursor.set_position(). self.cursor.set_position(self.buffer.cursor(), &self.buffer); + self.update_bracket_flash(); // Syntect is stateful: when the user scrolls the viewport // we have to rebuild the parser state by replaying every // line from the top of the file down to the new first @@ -67,6 +68,10 @@ impl Editor { let editor_bookmarkfound = mc_skin::color_pair(theme.name, "editor", "bookmarkfound"); let body_fg = editor_default.map(|p| p.fg).unwrap_or(theme.foreground); let body_bg = editor_default.map(|p| p.bg).unwrap_or(theme.background); + let cur_line_bg = match body_bg { + Color::Rgb(r, g, b) => Color::Rgb(r.saturating_add(12), g.saturating_add(12), b.saturating_add(12)), + _ => theme.current_line_bg(), + }; let marked_fg = editor_marked.map(|p| p.fg).unwrap_or(theme.marked_fg); let marked_bg = editor_marked.map(|p| p.bg).unwrap_or(theme.marked_bg); let linestate_fg = editor_linestate.map(|p| p.fg).unwrap_or(theme.cursor_fg); @@ -121,6 +126,7 @@ impl Editor { .map(|row| { let line_idx = top + row; let n = line_idx + 1; + let is_mod_cursor = self.modified && cursor_line == line_idx; let gutter_style = if self.has_bookmark_on_line(line_idx) { if cursor_line == line_idx { Style::default() @@ -130,13 +136,17 @@ impl Editor { } else { Style::default().fg(bookmark_fg).bg(bookmark_bg) } + } else if is_mod_cursor { + Style::default().fg(theme.warning).bg(body_bg) } else { Style::default().fg(frame_fg).bg(body_bg) }; - Line::from(Span::styled( - format!("{:>w$} ", n, w = (gutter_w - 1) as usize), - gutter_style, - )) + let text = if is_mod_cursor { + format!("{:>w$}\u{258c}", n, w = (gutter_w - 1) as usize) + } else { + format!("{:>w$} ", n, w = (gutter_w - 1) as usize) + }; + Line::from(Span::styled(text, gutter_style)) }) .collect(); frame.render_widget(Paragraph::new(gutter_lines), chunks[0]); @@ -159,7 +169,7 @@ impl Editor { let line_end = (off + len).min(full_text.len()); let line_text = full_text.get(off..line_end).unwrap_or(""); let base_style = if cursor_line == line_idx { - Style::default().fg(linestate_fg).bg(body_bg) + Style::default().fg(linestate_fg).bg(cur_line_bg) } else { Style::default().fg(body_fg).bg(body_bg) }; @@ -170,6 +180,7 @@ impl Editor { let sel_style = Style::default() .fg(marked_fg) .bg(marked_bg); + let line_bg = if cursor_line == line_idx { cur_line_bg } else { body_bg }; let mut spans: Vec = Vec::new(); #[cfg(feature = "syntect")] { @@ -179,7 +190,7 @@ impl Editor { highlighted, rs, re, - body_bg, + line_bg, marked_bg, ); body_lines.push(Line::from(spans));