use std::ops::Range;
use ropey::Rope;
use crate::{App, HighlightStyle, SharedString, theme::ActiveTheme};
#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))]
pub mod tree_sitter;
#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter-json"))]
pub use tree_sitter::json_highlighter;
#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter-toml"))]
pub use tree_sitter::toml_highlighter;
#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))]
pub use tree_sitter::{TreeSitterHighlighter, rust_highlighter};
pub trait Highlighter {
fn language(&self) -> SharedString;
fn update(
&mut self,
edit: Option<TextEdit>,
text: &Rope,
folding: bool,
window: &mut crate::Window,
cx: &mut App,
);
fn styles(
&self,
range: &Range<usize>,
resolver: &dyn HighlightStyleResolver,
) -> Vec<(Range<usize>, HighlightStyle)>;
fn fold_ranges(&self, text: &Rope) -> Vec<FoldRange>;
fn fold_ranges_for_edit(&self, range: Range<usize>, text: &Rope) -> Vec<FoldRange> {
let _ = range;
self.fold_ranges(text)
}
fn document_symbols(&self, _text: &Rope) -> Vec<DocumentSymbol> {
Vec::new()
}
}
#[derive(Debug, Clone)]
pub struct TextEdit {
pub old_range: Range<usize>,
pub new_len: usize,
}
#[derive(Debug, Clone)]
pub struct FoldRange {
pub start: usize,
pub end: usize,
pub default_folded: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SymbolKind {
Function,
Struct,
Enum,
Trait,
Impl,
Module,
Const,
Static,
}
#[derive(Debug, Clone)]
pub struct DocumentSymbol {
pub kind: SymbolKind,
pub name: SharedString,
pub range: Range<usize>,
pub start_row: usize,
}
pub trait HighlightStyleResolver: Send + Sync {
fn style(&self, name: &str) -> Option<HighlightStyle>;
}
#[derive(Default)]
pub struct NoHighlightStyles;
impl HighlightStyleResolver for NoHighlightStyles {
fn style(&self, _: &str) -> Option<HighlightStyle> {
None
}
}
#[derive(Debug, Clone, Default)]
pub struct ThemeHighlightResolver {
colors: crate::theme::SyntaxColors,
}
impl ThemeHighlightResolver {
pub fn new(colors: crate::theme::SyntaxColors) -> Self {
Self { colors }
}
pub fn from_app(cx: &App) -> Self {
Self::new(cx.theme().highlight_theme.style.syntax.clone())
}
}
impl HighlightStyleResolver for ThemeHighlightResolver {
fn style(&self, name: &str) -> Option<HighlightStyle> {
self.colors.style(name)
}
}
pub type HighlighterFactory = Box<dyn Fn(&str) -> Option<Box<dyn Highlighter>> + Send + Sync>;
static HIGHLIGHTER_REGISTRY: std::sync::LazyLock<
std::sync::RwLock<std::collections::HashMap<&'static str, HighlighterFactory>>,
> = std::sync::LazyLock::new(|| std::sync::RwLock::new(std::collections::HashMap::new()));
pub fn register_highlighter(language: &'static str, factory: HighlighterFactory) {
if let Ok(mut registry) = HIGHLIGHTER_REGISTRY.write() {
registry.insert(language, factory);
}
}
pub fn highlighter_for(language: &str) -> Option<Box<dyn Highlighter>> {
if let Ok(registry) = HIGHLIGHTER_REGISTRY.read() {
if let Some(factory) = registry.get(language) {
return factory(language);
}
}
#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))]
if language == "rust" {
return Some(rust_highlighter());
}
#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter-json"))]
if language == "json" {
return Some(json_highlighter());
}
#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter-toml"))]
if language == "toml" {
return Some(toml_highlighter());
}
None
}
pub fn supported_languages() -> Vec<&'static str> {
vec![
"rust",
"javascript",
"typescript",
"python",
"go",
"java",
"c",
"cpp",
"c_sharp",
"ruby",
"php",
"swift",
"kotlin",
"scala",
"html",
"css",
"json",
"yaml",
"toml",
"markdown",
"sql",
"bash",
"dockerfile",
]
}
#[cfg(test)]
mod tests {
use super::*;
struct StubHighlighter;
impl Highlighter for StubHighlighter {
fn language(&self) -> SharedString {
"test-only-x".into()
}
fn update(
&mut self,
_edit: Option<TextEdit>,
_text: &Rope,
_folding: bool,
_window: &mut crate::Window,
_cx: &mut App,
) {
}
fn styles(
&self,
range: &Range<usize>,
_resolver: &dyn HighlightStyleResolver,
) -> Vec<(Range<usize>, HighlightStyle)> {
vec![(range.clone(), HighlightStyle::default())]
}
fn fold_ranges(&self, _text: &Rope) -> Vec<FoldRange> {
Vec::new()
}
}
#[test]
fn unknown_language_returns_none() {
assert!(highlighter_for("cobol-xyz-not-registered").is_none());
}
#[test]
fn registry_override_hits() {
register_highlighter(
"test-only-x",
Box::new(|_| Some(Box::new(StubHighlighter) as Box<dyn Highlighter>)),
);
let highlighter = highlighter_for("test-only-x").expect("刚注册必须命中");
assert_eq!(highlighter.language().to_string(), "test-only-x");
}
#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter"))]
#[test]
fn rust_builtin_available() {
let highlighter = highlighter_for("rust").expect("tree-sitter 下 Rust 内置可用");
assert_eq!(highlighter.language().to_string(), "rust");
}
#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter-json"))]
#[test]
fn json_builtin_available() {
let highlighter = highlighter_for("json").expect("json feature 下 JSON 内置可用");
assert_eq!(highlighter.language().to_string(), "json");
}
#[cfg(all(not(target_family = "wasm"), feature = "tree-sitter-toml"))]
#[test]
fn toml_builtin_available() {
let highlighter = highlighter_for("toml").expect("toml feature 下 TOML 内置可用");
assert_eq!(highlighter.language().to_string(), "toml");
}
}