highlighter: Improve Syntax Highlighter performance. (#1230)

Now can support up to 50K lines with code highlight.


https://github.com/user-attachments/assets/3bbba2cd-ea56-4778-b471-6beea323fbaa

----

Ref Links

- https://zed.dev/blog/syntax-aware-editing
This commit is contained in:
Jason Lee 2025-09-10 00:11:53 +08:00 committed by GitHub
parent ab98e53a5a
commit 7998d64501
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 44 additions and 72 deletions

View file

@ -10,7 +10,6 @@ use std::{
ops::Range, ops::Range,
usize, usize,
}; };
use sum_tree::{Bias, SumTree};
use tree_sitter::{ use tree_sitter::{
InputEdit, Node, Parser, Point, Query, QueryCursor, QueryMatch, StreamingIterator, Tree, InputEdit, Node, Parser, Point, Query, QueryCursor, QueryMatch, StreamingIterator, Tree,
}; };
@ -22,9 +21,6 @@ pub struct SyntaxHighlighter {
language: SharedString, language: SharedString,
query: Option<Query>, query: Option<Query>,
injection_queries: HashMap<SharedString, Query>, injection_queries: HashMap<SharedString, Query>,
parser: Parser,
old_tree: Option<Tree>,
text: Rope,
locals_pattern_index: usize, locals_pattern_index: usize,
highlights_pattern_index: usize, highlights_pattern_index: usize,
@ -37,8 +33,11 @@ pub struct SyntaxHighlighter {
local_def_value_capture_index: Option<u32>, local_def_value_capture_index: Option<u32>,
local_ref_capture_index: Option<u32>, local_ref_capture_index: Option<u32>,
/// Cache of highlight, the range is offset of the token in the tree. /// The last parsed source text.
cache: SumTree<HighlightItem>, text: Rope,
parser: Parser,
/// The last parsed tree.
tree: Option<Tree>,
} }
struct TextProvider<'a>(&'a Rope); struct TextProvider<'a>(&'a Rope);
@ -274,10 +273,7 @@ impl SyntaxHighlighter {
language: config.name.clone(), language: config.name.clone(),
query: Some(query), query: Some(query),
injection_queries, injection_queries,
parser,
old_tree: None,
text: Rope::new(),
cache: sum_tree::SumTree::new(&()),
locals_pattern_index, locals_pattern_index,
highlights_pattern_index, highlights_pattern_index,
non_local_variable_patterns, non_local_variable_patterns,
@ -287,6 +283,9 @@ impl SyntaxHighlighter {
local_def_capture_index, local_def_capture_index,
local_def_value_capture_index, local_def_value_capture_index,
local_ref_capture_index, local_ref_capture_index,
text: Rope::new(),
parser,
tree: None,
}) })
} }
@ -295,8 +294,9 @@ impl SyntaxHighlighter {
} }
/// Highlight the given text, returning a map from byte ranges to highlight captures. /// Highlight the given text, returning a map from byte ranges to highlight captures.
/// Uses incremental parsing, detects changed ranges, and caches unchanged results. ///
pub fn update(&mut self, edit: Option<InputEdit>, text: &Rope, cx: &App) { /// Uses incremental parsing by `edit` to efficiently update the highlighter's state.
pub fn update(&mut self, edit: Option<InputEdit>, text: &Rope) {
if self.text.eq(text) { if self.text.eq(text) {
return; return;
} }
@ -311,7 +311,7 @@ impl SyntaxHighlighter {
}); });
let mut old_tree = self let mut old_tree = self
.old_tree .tree
.take() .take()
.unwrap_or(self.parser.parse("", None).unwrap()); .unwrap_or(self.parser.parse("", None).unwrap());
old_tree.edit(&edit); old_tree.edit(&edit);
@ -330,38 +330,26 @@ impl SyntaxHighlighter {
return; return;
}; };
// let changed_ranges = new_tree.changed_ranges(&old_tree); self.tree = Some(new_tree);
// Update state
self.old_tree = Some(new_tree);
self.text = text.clone(); self.text = text.clone();
// let measure = crate::Measure::new("build_styles");
self.build_styles(cx);
// measure.end();
} }
/// NOTE: 10K lines, about 180ms /// Match the visible ranges of nodes in the Tree for highlighting.
/// FIXME: To improve the performance when there more than 5K lines, use partial update. fn match_styles(&self, range: Range<usize>, cx: &App) -> Vec<HighlightItem> {
/// Ref: https://github.com/longbridge/gpui-component/pull/1197 let mut highlights = vec![];
fn build_styles(&mut self, cx: &App) { let Some(tree) = &self.tree else {
let Some(tree) = &self.old_tree else { return highlights;
return;
}; };
let Some(query) = &self.query else { let Some(query) = &self.query else {
return; return highlights;
}; };
let root_node = tree.root_node(); let root_node = tree.root_node();
// Remove the changed items from the cache. let source = &self.text;
let new_cache = sum_tree::SumTree::new(&());
self.cache = new_cache;
let source = self.text.clone();
let mut cursor = QueryCursor::new(); let mut cursor = QueryCursor::new();
cursor.set_byte_range(range);
let mut matches = cursor.matches(&query, root_node, TextProvider(&source)); let mut matches = cursor.matches(&query, root_node, TextProvider(&source));
while let Some(query_match) = matches.next() { while let Some(query_match) = matches.next() {
@ -372,8 +360,7 @@ impl SyntaxHighlighter {
{ {
let styles = self.handle_injection(&language_name, content_node, cx); let styles = self.handle_injection(&language_name, content_node, cx);
for (node_range, highlight_name) in styles { for (node_range, highlight_name) in styles {
self.cache highlights.push(HighlightItem::new(node_range.clone(), highlight_name));
.push(HighlightItem::new(node_range.clone(), highlight_name), &());
} }
continue; continue;
@ -390,42 +377,37 @@ impl SyntaxHighlighter {
let highlight_name = SharedString::from(highlight_name.to_string()); let highlight_name = SharedString::from(highlight_name.to_string());
// Merge near range and same highlight name // Merge near range and same highlight name
let last_item = self.cache.last(); let last_item = highlights.last();
let last_range = last_item.map(|item| &item.range).unwrap_or(&(0..0)); let last_range = last_item.map(|item| &item.range).unwrap_or(&(0..0));
let last_highlight_name = last_item.map(|item| item.name.clone()); let last_highlight_name = last_item.map(|item| item.name.clone());
if last_range.end <= node_range.start if last_range.end <= node_range.start
&& last_highlight_name.as_ref() == Some(&highlight_name) && last_highlight_name.as_ref() == Some(&highlight_name)
{ {
self.cache.push( highlights.push(HighlightItem::new(
HighlightItem::new(
last_range.start..node_range.end, last_range.start..node_range.end,
highlight_name.clone(), highlight_name.clone(),
), ));
&(),
);
} else if last_range == &node_range { } else if last_range == &node_range {
// case: // case:
// last_range: 213..220, last_highlight_name: Some("property") // last_range: 213..220, last_highlight_name: Some("property")
// last_range: 213..220, last_highlight_name: Some("string") // last_range: 213..220, last_highlight_name: Some("string")
self.cache.push( highlights.push(HighlightItem::new(
HighlightItem::new(
node_range, node_range,
last_highlight_name.unwrap_or(highlight_name), last_highlight_name.unwrap_or(highlight_name),
), ));
&(),
);
} else { } else {
self.cache highlights.push(HighlightItem::new(node_range, highlight_name.clone()));
.push(HighlightItem::new(node_range, highlight_name.clone()), &());
} }
} }
} }
// DO NOT REMOVE THIS PRINT, it's useful for debugging // DO NOT REMOVE THIS PRINT, it's useful for debugging
// for item in self.cache.iter() { // for item in highlights {
// println!("item: {:?}", item); // println!("item: {:?}", item);
// } // }
highlights
} }
/// TODO: Use incremental parsing to handle the injection. /// TODO: Use incremental parsing to handle the injection.
@ -570,25 +552,15 @@ impl SyntaxHighlighter {
&self, &self,
range: &Range<usize>, range: &Range<usize>,
theme: &HighlightTheme, theme: &HighlightTheme,
cx: &App,
) -> Vec<(Range<usize>, HighlightStyle)> { ) -> Vec<(Range<usize>, HighlightStyle)> {
let mut styles = vec![]; let mut styles = vec![];
let start_offset = range.start; let start_offset = range.start;
let mut cursor = self.cache.cursor::<usize>(&()); let highlights = self.match_styles(range.clone(), cx);
let bias = if start_offset == 0 {
Bias::Right
} else {
Bias::Left
};
let left_items = cursor.slice(&start_offset, bias);
let mut filter = left_items.filter::<_, Range<usize>>(&(), move |sum| {
range.start <= sum.max_end && range.end >= sum.min_start
});
filter.next();
// let mut iter_count = 0; // let mut iter_count = 0;
while let Some(item) = filter.item() { for item in highlights {
// iter_count += 1; // iter_count += 1;
let node_range = &item.range; let node_range = &item.range;
let name = &item.name; let name = &item.name;
@ -600,7 +572,6 @@ impl SyntaxHighlighter {
} }
styles.push((node_range, theme.style(name.as_ref()).unwrap_or_default())); styles.push((node_range, theme.style(name.as_ref()).unwrap_or_default()));
filter.next();
} }
// dbg!(iter_count); // dbg!(iter_count);

View file

@ -427,7 +427,7 @@ impl TextElement {
// +1 for `\n` // +1 for `\n`
let line_len = line.len() + 1; let line_len = line.len() + 1;
let range = offset..offset + line_len; let range = offset..offset + line_len;
let line_styles = highlighter.styles(&range, &theme); let line_styles = highlighter.styles(&range, &theme, cx);
styles = gpui::combine_highlights(styles, line_styles).collect(); styles = gpui::combine_highlights(styles, line_styles).collect();
offset = range.end; offset = range.end;

View file

@ -217,7 +217,7 @@ impl InputMode {
), ),
}; };
highlighter.update(Some(edit), text, cx); highlighter.update(Some(edit), text);
} }
_ => {} _ => {}
} }

View file

@ -391,6 +391,7 @@ impl InputState {
/// - Syntax Highlighting /// - Syntax Highlighting
/// - Auto Indent /// - Auto Indent
/// - Line Number /// - Line Number
/// - Large Text support, up to 50K lines.
pub fn code_editor(mut self, language: impl Into<SharedString>) -> Self { pub fn code_editor(mut self, language: impl Into<SharedString>) -> Self {
let language: SharedString = language.into(); let language: SharedString = language.into();
self.mode = InputMode::CodeEditor { self.mode = InputMode::CodeEditor {

View file

@ -292,8 +292,8 @@ impl CodeBlock {
let mut styles = vec![]; let mut styles = vec![];
if let Some(lang) = &lang { if let Some(lang) = &lang {
let mut highlighter = SyntaxHighlighter::new(&lang, cx); let mut highlighter = SyntaxHighlighter::new(&lang, cx);
highlighter.update(None, &Rope::from(code.as_str()), cx); highlighter.update(None, &Rope::from(code.as_str()));
styles = highlighter.styles(&(0..code.len()), &theme); styles = highlighter.styles(&(0..code.len()), &theme, cx);
}; };
let state = InlineState::default(); let state = InlineState::default();