//! 代码阅读器模块 //! //! 支持语法高亮、代码折叠、行号显示等功能 use anyhow::Result; use serde::{Deserialize, Serialize}; use syntect::easy::HighlightLines; use syntect::highlighting::{ThemeSet, Style}; use syntect::parsing::SyntaxSet; use syntect::html::{styled_line_to_highlighted_html, IncludeBackground}; /// 代码语言 #[derive(Debug, Clone, Serialize, Deserialize)] pub enum CodeLanguage { Rust, JavaScript, TypeScript, Python, Go, Java, C, Cpp, CSharp, Ruby, Swift, Kotlin, Scala, Shell, Sql, Html, Css, Json, Yaml, Xml, Markdown, Unknown, } impl CodeLanguage { /// 从文件扩展名判断语言 pub fn from_extension(ext: &str) -> Self { match ext.to_lowercase().as_str() { "rs" => CodeLanguage::Rust, "js" | "mjs" | "cjs" => CodeLanguage::JavaScript, "ts" | "tsx" => CodeLanguage::TypeScript, "py" | "pyw" => CodeLanguage::Python, "go" => CodeLanguage::Go, "java" => CodeLanguage::Java, "c" | "h" => CodeLanguage::C, "cpp" | "cc" | "cxx" | "hpp" => CodeLanguage::Cpp, "cs" => CodeLanguage::CSharp, "rb" => CodeLanguage::Ruby, "swift" => CodeLanguage::Swift, "kt" | "kts" => CodeLanguage::Kotlin, "scala" | "sc" => CodeLanguage::Scala, "sh" | "bash" | "zsh" | "fish" => CodeLanguage::Shell, "sql" => CodeLanguage::Sql, "html" | "htm" => CodeLanguage::Html, "css" | "scss" | "sass" | "less" => CodeLanguage::Css, "json" => CodeLanguage::Json, "yaml" | "yml" => CodeLanguage::Yaml, "xml" | "svg" => CodeLanguage::Xml, "md" | "markdown" => CodeLanguage::Markdown, _ => CodeLanguage::Unknown, } } /// 获取 syntect 语法名称 pub fn to_syntect_name(&self) -> &'static str { match self { CodeLanguage::Rust => "Rust", CodeLanguage::JavaScript => "JavaScript", CodeLanguage::TypeScript => "TypeScript", CodeLanguage::Python => "Python", CodeLanguage::Go => "Go", CodeLanguage::Java => "Java", CodeLanguage::C => "C", CodeLanguage::Cpp => "C++", CodeLanguage::CSharp => "C#", CodeLanguage::Ruby => "Ruby", CodeLanguage::Swift => "Swift", CodeLanguage::Kotlin => "Kotlin", CodeLanguage::Scala => "Scala", CodeLanguage::Shell => "Bash (shell)", CodeLanguage::Sql => "SQL", CodeLanguage::Html => "HTML", CodeLanguage::Css => "CSS", CodeLanguage::Json => "JSON", CodeLanguage::Yaml => "YAML", CodeLanguage::Xml => "XML", CodeLanguage::Markdown => "Markdown", CodeLanguage::Unknown => "Plain Text", } } /// 获取语言显示名称 pub fn display_name(&self) -> &'static str { match self { CodeLanguage::Rust => "Rust", CodeLanguage::JavaScript => "JavaScript", CodeLanguage::TypeScript => "TypeScript", CodeLanguage::Python => "Python", CodeLanguage::Go => "Go", CodeLanguage::Java => "Java", CodeLanguage::C => "C", CodeLanguage::Cpp => "C++", CodeLanguage::CSharp => "C#", CodeLanguage::Ruby => "Ruby", CodeLanguage::Swift => "Swift", CodeLanguage::Kotlin => "Kotlin", CodeLanguage::Scala => "Scala", CodeLanguage::Shell => "Shell", CodeLanguage::Sql => "SQL", CodeLanguage::Html => "HTML", CodeLanguage::Css => "CSS", CodeLanguage::Json => "JSON", CodeLanguage::Yaml => "YAML", CodeLanguage::Xml => "XML", CodeLanguage::Markdown => "Markdown", CodeLanguage::Unknown => "Unknown", } } } /// 代码行 #[derive(Debug, Clone)] pub struct CodeLine { pub number: usize, pub content: String, pub highlighted_html: String, pub is_folded: bool, } /// 代码文档 #[derive(Debug, Clone)] pub struct CodeDocument { pub title: String, pub path: String, pub language: CodeLanguage, pub lines: Vec, pub total_lines: usize, } /// 代码阅读器 pub struct CodeReader { syntax_set: SyntaxSet, theme_set: ThemeSet, } impl CodeReader { /// 创建代码阅读器 pub fn new() -> Result { // 使用内置的语法和主题 let syntax_set = SyntaxSet::load_defaults_newlines(); let theme_set = ThemeSet::load_defaults(); Ok(Self { syntax_set, theme_set, }) } /// 解析代码文件 pub fn parse(&self, path: &str, content: &str) -> Result { let path_obj = std::path::Path::new(path); let ext = path_obj.extension() .and_then(|e| e.to_str()) .unwrap_or(""); let language = CodeLanguage::from_extension(ext); let title = path_obj.file_stem() .and_then(|s| s.to_str()) .unwrap_or("Untitled") .to_string(); let lines = self.highlight_code(content, &language); let total_lines = lines.len(); Ok(CodeDocument { title, path: path.to_string(), language, lines, total_lines, }) } /// 语法高亮 fn highlight_code(&self, code: &str, language: &CodeLanguage) -> Vec { let syntax = self.syntax_set .find_syntax_by_name(language.to_syntect_name()) .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text()); let theme = &self.theme_set.themes["base16-ocean.dark"]; let mut highlighter = HighlightLines::new(syntax, theme); let mut lines = Vec::new(); for (index, line) in code.lines().enumerate() { let line_number = index + 1; // 语法高亮 let ranges = highlighter.highlight_line(line, &self.syntax_set) .unwrap_or_else(|_| vec![]); // 转换为 HTML let html = styled_line_to_highlighted_html( &ranges[..], IncludeBackground::No ).unwrap_or_else(|_| line.to_string()); lines.push(CodeLine { number: line_number, content: line.to_string(), highlighted_html: html, is_folded: false, }); } lines } /// 渲染代码文档为 HTML pub fn render(&self, doc: &CodeDocument) -> Result { let mut html = String::new(); html.push_str("\n\n\n"); html.push_str("\n"); html.push_str(&format!("{}\n", doc.title)); html.push_str("\n\n\n"); html.push_str("
\n"); // 语言标识 html.push_str(&format!( "
\n {}\n {} lines\n
\n", doc.language.display_name(), doc.total_lines )); // 代码内容 html.push_str("
\n");
        
        for line in &doc.lines {
            if line.is_folded {
                continue;
            }
            
            html.push_str(&format!(
                "
\n {}\n {}\n
\n", line.number, line.number, line.highlighted_html )); } html.push_str("
\n"); html.push_str("
\n"); html.push_str("\n"); Ok(html) } /// 获取代码样式 fn get_code_css(language: &CodeLanguage) -> &'static str { r#" :root { --bg-primary: #1a1a2e; --bg-secondary: #16213e; --bg-line-number: #0f3460; --text-primary: #eaeaea; --text-muted: #6c757d; --border-color: #2a2a4a; --accent-color: #00adb5; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace; font-size: 14px; line-height: 1.6; background-color: var(--bg-primary); color: var(--text-primary); } .code-container { max-width: 1200px; margin: 20px auto; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 8px; overflow: hidden; } .code-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 20px; background: var(--bg-line-number); border-bottom: 1px solid var(--border-color); } .language-badge { font-size: 12px; font-weight: 600; color: var(--accent-color); text-transform: uppercase; } .line-count { font-size: 12px; color: var(--text-muted); } .code-content { overflow-x: auto; padding: 0; margin: 0; } .code-line { display: flex; min-height: 1.6em; } .code-line:hover { background: rgba(255, 255, 255, 0.05); } .line-number { display: inline-block; width: 50px; padding: 0 10px; text-align: right; color: var(--text-muted); background: var(--bg-line-number); border-right: 1px solid var(--border-color); user-select: none; flex-shrink: 0; } .line-content { flex: 1; padding: 0 15px; white-space: pre; } /* 语法高亮颜色 */ .c { color: #6c757d; font-style: italic; } .k { color: #ff79c6; font-weight: bold; } .o { color: #ff79c6; } .cm { color: #6c757d; font-style: italic; } .kd { color: #ff79c6; font-weight: bold; } .kn { color: #ff79c6; font-weight: bold; } .kp { color: #ff79c6; font-weight: bold; } .kr { color: #ff79c6; font-weight: bold; } .kt { color: #8be9fd; font-style: italic; } .n { color: #f8f8f2; } .na { color: #50fa7b; } .nb { color: #8be9fd; font-style: italic; } .nc { color: #50fa7b; font-weight: bold; } .no { color: #f1fa8c; } .nd { color: #bd93f9; } .ni { color: #f8f8f2; } .ne { color: #50fa7b; font-weight: bold; } .nf { color: #50fa7b; } .nl { color: #8be9fd; font-style: italic; } .nn { color: #f8f8f2; } .nt { color: #ff79c6; } .nv { color: #f8f8f2; } .s { color: #f1fa8c; } .s1 { color: #f1fa8c; } .s2 { color: #f1fa8c; } .se { color: #f1fa8c; } .sh { color: #f1fa8c; } .si { color: #f1fa8c; } .sx { color: #f1fa8c; } .m { color: #bd93f9; } .mi { color: #bd93f9; } .mf { color: #bd93f9; } /* 滚动条 */ ::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar-track { background: var(--bg-primary); } ::-webkit-scrollbar-thumb { background: var(--bg-line-number); border-radius: 5px; } ::-webkit-scrollbar-thumb:hover { background: var(--accent-color); } "# } /// 折叠代码行 pub fn fold_lines(&mut self, doc: &mut CodeDocument, start: usize, end: usize) { for line in &mut doc.lines { if line.number >= start && line.number <= end { line.is_folded = true; } } } /// 搜索代码 pub fn search(&self, doc: &CodeDocument, query: &str) -> Vec { let mut results = Vec::new(); let query_lower = query.to_lowercase(); for line in &doc.lines { if line.content.to_lowercase().contains(&query_lower) { results.push(line.number); } } results } } impl Default for CodeReader { fn default() -> Self { Self::new().expect("Failed to create CodeReader") } } #[cfg(test)] mod tests { use super::*; #[test] fn test_language_detection() { assert!(matches!(CodeLanguage::from_extension("rs"), CodeLanguage::Rust)); assert!(matches!(CodeLanguage::from_extension("py"), CodeLanguage::Python)); assert!(matches!(CodeLanguage::from_extension("unknown"), CodeLanguage::Unknown)); } #[test] fn test_code_reader_creation() { let reader = CodeReader::new(); assert!(reader.is_ok()); } }