feat: add selection, active line, and incremental highlighting to code

view
This commit is contained in:
2026-11-05 16:42:15 +07:00
parent 503b105bad
commit f272f80d09
6 changed files with 640 additions and 92 deletions
+54 -39
View File
@@ -54,10 +54,62 @@ use tree_sitter::StreamingIterator as _;
/// Highlights `source` with `language`'s tree-sitter grammar.
pub fn highlight(source: &str, language: Language) -> HighlightedCode {
highlight_with_tree(source, language, None).0
}
/// Highlights `source`, reusing `old_tree` so only the changed region is re-lexed.
pub fn highlight_with_tree(
source: &str,
language: Language,
old_tree: Option<&tree_sitter::Tree>,
) -> (HighlightedCode, Option<tree_sitter::Tree>) {
let Some((grammar, query_source)) = grammar_for(language) else {
return build_lines(source, &[]);
return (plain(source), None);
};
highlight_with(grammar, query_source, source).unwrap_or_else(|| build_lines(source, &[]))
let mut parser = tree_sitter::Parser::new();
if parser.set_language(&grammar).is_err() {
return (plain(source), None);
}
let Some(tree) = parser.parse(source, old_tree) else {
return (plain(source), None);
};
let Ok(query) = tree_sitter::Query::new(&grammar, query_source) else {
return (plain(source), None);
};
let roles: Vec<Option<Role>> = query
.capture_names()
.iter()
.map(|name| role_for_capture(name))
.collect();
// One role per byte; matches arrive in query order
let mut role_at = vec![None; source.len()];
let mut cursor = tree_sitter::QueryCursor::new();
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
while let Some(matched) = matches.next() {
for capture in matched.captures {
let Some(role) = roles.get(capture.index as usize).copied().flatten() else {
continue;
};
for byte in capture.node.byte_range() {
if let Some(slot) = role_at.get_mut(byte) {
*slot = Some(role);
}
}
}
}
(build_lines(source, &role_at), Some(tree))
}
/// Source rendered without any grammar: plain text roles.
pub(crate) fn plain(source: &str) -> HighlightedCode {
build_lines(source, &[])
}
/// The grammar and its highlight query for a language, when compiled in.
@@ -90,43 +142,6 @@ fn grammar_for(language: Language) -> Option<(tree_sitter::Language, &'static st
}
}
fn highlight_with(
grammar: tree_sitter::Language,
query_source: &str,
source: &str,
) -> Option<HighlightedCode> {
let mut parser = tree_sitter::Parser::new();
parser.set_language(&grammar).ok()?;
let tree = parser.parse(source, None)?;
let query = tree_sitter::Query::new(&grammar, query_source).ok()?;
let roles: Vec<Option<Role>> = query
.capture_names()
.iter()
.map(|name| role_for_capture(name))
.collect();
let mut role_at = vec![None; source.len()];
let mut cursor = tree_sitter::QueryCursor::new();
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
while let Some(matched) = matches.next() {
for capture in matched.captures {
let Some(role) = roles.get(capture.index as usize).copied().flatten() else {
continue;
};
for byte in capture.node.byte_range() {
if let Some(slot) = role_at.get_mut(byte) {
*slot = Some(role);
}
}
}
}
Some(build_lines(source, &role_at))
}
/// Maps a query capture name to a role; unmapped captures render as text.
fn role_for_capture(name: &str) -> Option<Role> {
match name.split('.').next()? {