feat(tui): add syntax highlighting for code panes using syntect and a new highlight module
This commit is contained in:
@@ -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 }
|
||||
|
||||
102
crates/owlen-tui/src/highlight.rs
Normal file
102
crates/owlen-tui/src/highlight.rs
Normal 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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 mut line_style = Style::default().fg(theme.text);
|
||||
if !is_active {
|
||||
line_style = line_style.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));
|
||||
}
|
||||
}
|
||||
|
||||
spans.push(Span::styled(content.clone(), line_style));
|
||||
lines.push(Line::from(spans));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user