Skip to content

Commit 8012003

Browse files
committed
Merge branch 'feature/20260429/better'
2 parents 7961063 + ccf846a commit 8012003

20 files changed

Lines changed: 2598 additions & 212 deletions

File tree

Cargo.lock

Lines changed: 110 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

perihelion-widgets/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ description = "Reusable ratatui widget library for Perihelion"
77
[dependencies]
88
ratatui = { version = ">=0.30", features = ["unstable-rendered-line-info"] }
99
pulldown-cmark = { version = "0.12", optional = true }
10+
syntect = { version = "5", default-features = false, features = ["default-fancy"], optional = true }
11+
once_cell = "1"
1012
unicode-width = "0.2"
1113
rand = "0.8"
1214
regex = "1"
1315

1416
[features]
1517
default = []
1618
markdown = ["pulldown-cmark"]
19+
markdown-highlight = ["markdown", "dep:syntect"]
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use once_cell::sync::Lazy;
2+
use ratatui::{
3+
style::{Color, Style},
4+
text::{Line, Span},
5+
};
6+
use syntect::easy::HighlightLines;
7+
use syntect::highlighting::ThemeSet;
8+
use syntect::parsing::SyntaxSet;
9+
10+
pub static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_newlines);
11+
pub static THEME_SET: Lazy<ThemeSet> = Lazy::new(ThemeSet::load_defaults);
12+
13+
/// │ 前缀的固定灰色,与 TUI 暗色背景视觉协调
14+
const PREFIX_COLOR: Color = Color::Rgb(130, 140, 150);
15+
16+
/// 对多行代码块进行语法高亮,返回着色后的 Line 列表。
17+
/// 当语言标签未识别时返回 None,调用方应回退到统一颜色渲染。
18+
pub fn highlight_code_block(lang: &str, lines: &[String]) -> Option<Vec<Line<'static>>> {
19+
let ss = &*SYNTAX_SET;
20+
let syntax = ss.find_syntax_by_token(lang)?;
21+
let theme = &THEME_SET.themes["base16-ocean.dark"];
22+
let mut highlighter = HighlightLines::new(syntax, theme);
23+
24+
let mut result = Vec::with_capacity(lines.len());
25+
for line_text in lines {
26+
let mut spans = Vec::new();
27+
spans.push(Span::styled("│ ".to_string(), Style::default().fg(PREFIX_COLOR)));
28+
29+
let ranges = highlighter.highlight_line(line_text, ss).ok()?;
30+
for (style, text) in ranges {
31+
let color = Color::Rgb(style.foreground.r, style.foreground.g, style.foreground.b);
32+
spans.push(Span::styled(text.to_string(), Style::default().fg(color)));
33+
}
34+
result.push(Line::from(spans));
35+
}
36+
Some(result)
37+
}
38+
39+
#[cfg(test)]
40+
mod tests {
41+
use super::*;
42+
43+
#[test]
44+
fn highlight_rust_code() {
45+
let result = highlight_code_block("rust", &["fn main() {}".to_string()]);
46+
assert!(result.is_some(), "rust 代码应被识别");
47+
let lines = result.unwrap();
48+
assert_eq!(lines.len(), 1);
49+
let has_prefix = lines[0].spans.iter().any(|s| s.content.contains("│"));
50+
assert!(has_prefix, "应有 │ 前缀");
51+
let has_syntax_color = lines[0].spans.iter().any(|s| {
52+
s.style.fg.map_or(false, |c| c != PREFIX_COLOR) && !s.content.contains("│")
53+
});
54+
assert!(has_syntax_color, "应有非前缀颜色的语法着色 span");
55+
}
56+
57+
#[test]
58+
fn highlight_unknown_lang() {
59+
let result = highlight_code_block("unknown_lang_xyz", &["hello".to_string()]);
60+
assert!(result.is_none(), "未识别语言应返回 None");
61+
}
62+
63+
#[test]
64+
fn highlight_empty_lang() {
65+
let result = highlight_code_block("", &["hello".to_string()]);
66+
assert!(result.is_none(), "空语言标签应返回 None");
67+
}
68+
69+
#[test]
70+
fn highlight_multiline() {
71+
let lines = vec![
72+
"fn main() {".to_string(),
73+
" println!(\"hello\");".to_string(),
74+
"}".to_string(),
75+
];
76+
let result = highlight_code_block("rust", &lines);
77+
assert!(result.is_some(), "多行 rust 代码应被识别");
78+
assert_eq!(result.unwrap().len(), 3, "输出行数应等于输入行数");
79+
}
80+
}

0 commit comments

Comments
 (0)