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"
|
tree-sitter-rust = "0.20"
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
|
syntect = "5.3"
|
||||||
|
once_cell = "1.19"
|
||||||
|
|
||||||
# Async runtime
|
# Async runtime
|
||||||
tokio = { workspace = true }
|
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 commands;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
|
pub mod highlight;
|
||||||
pub mod model_info_panel;
|
pub mod model_info_panel;
|
||||||
pub mod slash;
|
pub mod slash;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
|||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use crate::chat_app::{ChatApp, HELP_TAB_COUNT, MessageRenderContext, ModelSelectorItemKind};
|
use crate::chat_app::{ChatApp, HELP_TAB_COUNT, MessageRenderContext, ModelSelectorItemKind};
|
||||||
|
use crate::highlight;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
CodePane, EditorTab, FileFilterMode, FileNode, LayoutNode, PaletteGroup, PaneId,
|
CodePane, EditorTab, FileFilterMode, FileNode, LayoutNode, PaletteGroup, PaneId,
|
||||||
RepoSearchRowKind, SplitAxis, VisibleFileEntry,
|
RepoSearchRowKind, SplitAxis, VisibleFileEntry,
|
||||||
@@ -2338,6 +2339,8 @@ fn render_code_pane(
|
|||||||
.add_modifier(Modifier::ITALIC),
|
.add_modifier(Modifier::ITALIC),
|
||||||
)));
|
)));
|
||||||
} else {
|
} else {
|
||||||
|
let mut highlighter =
|
||||||
|
highlight::build_highlighter(pane.absolute_path(), pane.display_path());
|
||||||
for (idx, content) in pane.lines.iter().enumerate() {
|
for (idx, content) in pane.lines.iter().enumerate() {
|
||||||
let number = format!("{:>4} ", idx + 1);
|
let number = format!("{:>4} ", idx + 1);
|
||||||
let mut spans = vec![Span::styled(
|
let mut spans = vec![Span::styled(
|
||||||
@@ -2347,12 +2350,22 @@ fn render_code_pane(
|
|||||||
.add_modifier(Modifier::DIM),
|
.add_modifier(Modifier::DIM),
|
||||||
)];
|
)];
|
||||||
|
|
||||||
let mut line_style = Style::default().fg(theme.text);
|
let segments = highlight::highlight_line(&mut highlighter, content);
|
||||||
if !is_active {
|
if segments.is_empty() {
|
||||||
line_style = line_style.add_modifier(Modifier::DIM);
|
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));
|
lines.push(Line::from(spans));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user