Skip to content

Commit c0c360a

Browse files
committed
Merge syntax-highlight: revealed mermaid + LaTeX math highlighting
2 parents 7216d06 + b110444 commit c0c360a

5 files changed

Lines changed: 523 additions & 30 deletions

File tree

src/doc_layout.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,7 @@ fn build_ghosts_before(
682682
&[],
683683
None,
684684
&[],
685+
&[],
685686
);
686687
let layout = engine.build_line_hanging(
687688
&lr.text,
@@ -1446,6 +1447,40 @@ impl DocLayout {
14461447
});
14471448

14481449
let extra = github.map(|g| g.extra_regions(i)).unwrap_or_default();
1450+
// Byte ranges on this line to LaTeX-highlight: the content of a revealed inline
1451+
// `$…$` span (caret inside it) or a revealed `$$…$$` block line (caret in the
1452+
// block / a selection overlapping it — the same predicate that stops the block
1453+
// from collapsing to an image). Empty when the `math` feature is off.
1454+
#[cfg(feature = "math")]
1455+
let latex_ranges: Vec<Range<usize>> = {
1456+
let mut lr = Vec::new();
1457+
if let Some(spans) = math_spans.get(&i) {
1458+
for s in spans {
1459+
if cursor_offset >= s.full_range.start && cursor_offset <= s.full_range.end
1460+
{
1461+
let a = s.content_range.start.max(range.start);
1462+
let b = s.content_range.end.min(range.end);
1463+
if a < b {
1464+
lr.push(a..b);
1465+
}
1466+
}
1467+
}
1468+
}
1469+
for m in &math_blocks {
1470+
let revealed = (m.block.start < selection.end && selection.start < m.block.end)
1471+
|| m.block.contains(&cursor_line_start);
1472+
if revealed {
1473+
let a = m.content.start.max(range.start);
1474+
let b = m.content.end.min(range.end);
1475+
if a < b {
1476+
lr.push(a..b);
1477+
}
1478+
}
1479+
}
1480+
lr
1481+
};
1482+
#[cfg(not(feature = "math"))]
1483+
let latex_ranges: Vec<Range<usize>> = Vec::new();
14491484
// Reuse the cached render when nothing about this line changed. Lines with
14501485
// GitHub extra regions bypass the cache (validation can change them without
14511486
// a version bump); all others key on (version, line, cursor-on-line).
@@ -1462,6 +1497,7 @@ impl DocLayout {
14621497
&[],
14631498
tc,
14641499
math_spans.get(&i).map_or(&[][..], |v| v.as_slice()),
1500+
&latex_ranges,
14651501
))
14661502
})
14671503
} else {
@@ -1475,6 +1511,7 @@ impl DocLayout {
14751511
&extra,
14761512
table_ctx.clone(),
14771513
math_spans.get(&i).map_or(&[][..], |v| v.as_slice()),
1514+
&latex_ranges,
14781515
))
14791516
};
14801517
#[cfg_attr(not(feature = "math"), allow(unused_mut))]

src/highlight.rs

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use tree_sitter_highlight::{
66
Highlight, HighlightConfiguration, HighlightEvent, Highlighter as TSHighlighter,
77
};
88

9+
use crate::tokenize;
10+
911
pub const HIGHLIGHT_NAMES: &[&str] = &[
1012
"attribute",
1113
"boolean",
@@ -43,6 +45,18 @@ pub struct HighlightSpan {
4345
pub highlight_id: usize,
4446
}
4547

48+
/// Resolve a capture name (e.g. `"keyword"`, `"operator"`) to its `highlight_id` — the
49+
/// index into [`HIGHLIGHT_NAMES`] that [`EditorTheme::color_for_highlight`] maps to a
50+
/// color. For the hand-written tokenizers (mermaid, latex), which emit categories by name
51+
/// rather than via a tree-sitter query. An unknown name resolves to a sentinel that renders
52+
/// as the plain foreground.
53+
pub fn highlight_id(name: &str) -> usize {
54+
HIGHLIGHT_NAMES
55+
.iter()
56+
.position(|&n| n == name)
57+
.unwrap_or(usize::MAX)
58+
}
59+
4660
/// Language keys are stored lowercase. Only allocate when the input actually has an
4761
/// uppercase byte — the overwhelmingly common case (already-lowercase fence infos)
4862
/// borrows unchanged.
@@ -54,13 +68,21 @@ fn normalized_lang(lang: &str) -> Cow<'_, str> {
5468
}
5569
}
5670

57-
struct LanguageConfig {
58-
config: HighlightConfiguration,
71+
/// How a registered language produces highlight spans. Most languages use a tree-sitter
72+
/// grammar + query; a few (mermaid, latex) have no publishable grammar crate and are
73+
/// lexed by a pure-Rust tokenizer instead. Both go through the same `highlight()` entry
74+
/// point, so callers (the code-block highlight cache, revealed-math styling) never
75+
/// special-case which backend a language uses.
76+
enum Backend {
77+
/// Boxed — a `HighlightConfiguration` is large, and it lives behind an `Arc` anyway.
78+
TreeSitter(Box<HighlightConfiguration>),
79+
/// `fn(source) -> spans`, each span's `highlight_id` from [`highlight_id`].
80+
Tokenizer(fn(&str) -> Vec<HighlightSpan>),
5981
}
6082

6183
pub struct Highlighter {
6284
inner: TSHighlighter,
63-
languages: HashMap<String, Arc<LanguageConfig>>,
85+
languages: HashMap<String, Arc<Backend>>,
6486
}
6587

6688
impl Default for Highlighter {
@@ -74,25 +96,40 @@ impl Highlighter {
7496
let inner = TSHighlighter::new();
7597
let mut languages = HashMap::new();
7698

77-
// Register Rust
99+
let mut register = |aliases: &[&str], backend: Backend| {
100+
let backend = Arc::new(backend);
101+
for a in aliases {
102+
languages.insert((*a).to_string(), Arc::clone(&backend));
103+
}
104+
};
105+
106+
// Tree-sitter grammars.
78107
if let Some(config) = Self::create_rust_config() {
79-
let config = Arc::new(config);
80-
languages.insert("rust".to_string(), Arc::clone(&config));
81-
languages.insert("rs".to_string(), Arc::clone(&config));
108+
register(&["rust", "rs"], Backend::TreeSitter(Box::new(config)));
82109
}
83-
84-
// Register Bash
85110
if let Some(config) = Self::create_bash_config() {
86-
let config = Arc::new(config);
87-
languages.insert("bash".to_string(), Arc::clone(&config));
88-
languages.insert("sh".to_string(), Arc::clone(&config));
89-
languages.insert("shell".to_string(), Arc::clone(&config));
111+
register(
112+
&["bash", "sh", "shell"],
113+
Backend::TreeSitter(Box::new(config)),
114+
);
90115
}
91116

117+
// Tokenizer-backed languages (no publishable grammar crate). Registered ungated:
118+
// a ```mermaid / ```latex code fence highlights through the normal code-block path,
119+
// and revealed `$…$` math styles via `highlight(content, "latex")`.
120+
register(
121+
&["mermaid"],
122+
Backend::Tokenizer(tokenize::highlight_mermaid),
123+
);
124+
register(
125+
&["latex", "tex"],
126+
Backend::Tokenizer(tokenize::highlight_latex),
127+
);
128+
92129
Self { inner, languages }
93130
}
94131

95-
fn create_rust_config() -> Option<LanguageConfig> {
132+
fn create_rust_config() -> Option<HighlightConfiguration> {
96133
let language = tree_sitter_rust::LANGUAGE.into();
97134

98135
// Use Zed's highlights.scm for better Rust coverage
@@ -116,10 +153,10 @@ impl Highlighter {
116153
// Configure which highlight names we recognize
117154
config.configure(HIGHLIGHT_NAMES);
118155

119-
Some(LanguageConfig { config })
156+
Some(config)
120157
}
121158

122-
fn create_bash_config() -> Option<LanguageConfig> {
159+
fn create_bash_config() -> Option<HighlightConfiguration> {
123160
let language = tree_sitter_bash::LANGUAGE.into();
124161
let highlights_query = tree_sitter_bash::HIGHLIGHT_QUERY;
125162

@@ -134,21 +171,26 @@ impl Highlighter {
134171

135172
config.configure(HIGHLIGHT_NAMES);
136173

137-
Some(LanguageConfig { config })
174+
Some(config)
138175
}
139176

140177
pub fn supports_language(&self, lang: &str) -> bool {
141178
self.languages.contains_key(normalized_lang(lang).as_ref())
142179
}
143180

144181
pub fn highlight(&mut self, code: &str, language: &str) -> Vec<HighlightSpan> {
145-
let Some(lang_config) = self.languages.get(normalized_lang(language).as_ref()) else {
182+
let Some(backend) = self.languages.get(normalized_lang(language).as_ref()) else {
146183
return Vec::new();
147184
};
148185

186+
let config = match backend.as_ref() {
187+
Backend::Tokenizer(f) => return f(code),
188+
Backend::TreeSitter(config) => config.as_ref(),
189+
};
190+
149191
// Run the highlighter
150192
let highlights = match self.inner.highlight(
151-
&lang_config.config,
193+
config,
152194
code.as_bytes(),
153195
None, // cancellation flag
154196
|_| None, // injection callback (not used)

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,5 @@ pub mod status_bar;
6464
pub mod table;
6565
pub mod text_engine;
6666
pub mod text_input;
67+
pub mod tokenize;
6768
pub mod validation;

0 commit comments

Comments
 (0)