feat(tui): add syntax highlighting for code panes using syntect and a new highlight module

This commit is contained in:
2025-10-13 22:50:25 +02:00
parent ba9d083088
commit c9daf68fea
4 changed files with 123 additions and 5 deletions

View File

@@ -27,6 +27,8 @@ tree-sitter = "0.20"
tree-sitter-rust = "0.20"
dirs = { workspace = true }
toml = { workspace = true }
syntect = "5.3"
once_cell = "1.19"
# Async runtime
tokio = { workspace = true }

View File

@@ -0,0 +1,102 @@
use once_cell::sync::Lazy;
use ratatui::style::{Color as TuiColor, Modifier, Style as TuiStyle};
use std::path::{Path, PathBuf};
use syntect::easy::HighlightLines;
use syntect::highlighting::{FontStyle, Style as SynStyle, Theme, ThemeSet};
use syntect::parsing::{SyntaxReference, SyntaxSet};
static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_newlines);
static THEME_SET: Lazy<ThemeSet> = Lazy::new(ThemeSet::load_defaults);
static THEME: Lazy<Theme> = Lazy::new(|| {
THEME_SET
.themes
.get("base16-ocean.dark")
.cloned()
.or_else(|| THEME_SET.themes.values().next().cloned())
.unwrap_or_default()
});
fn select_syntax(path_hint: Option<&Path>) -> &'static SyntaxReference {
if let Some(path) = path_hint {
if let Ok(Some(syntax)) = SYNTAX_SET.find_syntax_for_file(path) {
return syntax;
}
if let Some(ext) = path.extension().and_then(|ext| ext.to_str())
&& let Some(syntax) = SYNTAX_SET.find_syntax_by_extension(ext)
{
return syntax;
}
if let Some(name) = path.file_name().and_then(|name| name.to_str())
&& let Some(syntax) = SYNTAX_SET.find_syntax_by_token(name)
{
return syntax;
}
}
SYNTAX_SET.find_syntax_plain_text()
}
fn path_hint_from_components(absolute: Option<&Path>, display: Option<&str>) -> Option<PathBuf> {
if let Some(abs) = absolute {
return Some(abs.to_path_buf());
}
display.map(PathBuf::from)
}
fn style_from_syntect(style: SynStyle) -> TuiStyle {
let mut tui_style = TuiStyle::default().fg(TuiColor::Rgb(
style.foreground.r,
style.foreground.g,
style.foreground.b,
));
let mut modifiers = Modifier::empty();
if style.font_style.contains(FontStyle::BOLD) {
modifiers |= Modifier::BOLD;
}
if style.font_style.contains(FontStyle::ITALIC) {
modifiers |= Modifier::ITALIC;
}
if style.font_style.contains(FontStyle::UNDERLINE) {
modifiers |= Modifier::UNDERLINED;
}
if !modifiers.is_empty() {
tui_style = tui_style.add_modifier(modifiers);
}
tui_style
}
pub fn build_highlighter(
absolute: Option<&Path>,
display: Option<&str>,
) -> HighlightLines<'static> {
let hint_path = path_hint_from_components(absolute, display);
let syntax = select_syntax(hint_path.as_deref());
HighlightLines::new(syntax, &THEME)
}
pub fn highlight_line(
highlighter: &mut HighlightLines<'static>,
line: &str,
) -> Vec<(TuiStyle, String)> {
let mut segments = Vec::new();
match highlighter.highlight_line(line, &SYNTAX_SET) {
Ok(result) => {
for (style, piece) in result {
let tui_style = style_from_syntect(style);
segments.push((tui_style, piece.to_string()));
}
}
Err(_) => {
segments.push((TuiStyle::default(), line.to_string()));
}
}
if segments.is_empty() {
segments.push((TuiStyle::default(), String::new()));
}
segments
}

View File

@@ -17,6 +17,7 @@ pub mod code_app;
pub mod commands;
pub mod config;
pub mod events;
pub mod highlight;
pub mod model_info_panel;
pub mod slash;
pub mod state;

View File

@@ -12,6 +12,7 @@ use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::chat_app::{ChatApp, HELP_TAB_COUNT, MessageRenderContext, ModelSelectorItemKind};
use crate::highlight;
use crate::state::{
CodePane, EditorTab, FileFilterMode, FileNode, LayoutNode, PaletteGroup, PaneId,
RepoSearchRowKind, SplitAxis, VisibleFileEntry,
@@ -2338,6 +2339,8 @@ fn render_code_pane(
.add_modifier(Modifier::ITALIC),
)));
} else {
let mut highlighter =
highlight::build_highlighter(pane.absolute_path(), pane.display_path());
for (idx, content) in pane.lines.iter().enumerate() {
let number = format!("{:>4} ", idx + 1);
let mut spans = vec![Span::styled(
@@ -2347,12 +2350,22 @@ fn render_code_pane(
.add_modifier(Modifier::DIM),
)];
let segments = highlight::highlight_line(&mut highlighter, content);
if segments.is_empty() {
let mut line_style = Style::default().fg(theme.text);
if !is_active {
line_style = line_style.add_modifier(Modifier::DIM);
}
spans.push(Span::styled(content.clone(), line_style));
} else {
for (segment_style, text) in segments {
let mut style = segment_style;
if !is_active {
style = style.add_modifier(Modifier::DIM);
}
spans.push(Span::styled(text, style));
}
}
lines.push(Line::from(spans));
}
}