perf: speed up large markdown rendering
This commit is contained in:
@@ -3,9 +3,24 @@
|
||||
//! code without re-parsing it.
|
||||
|
||||
use gpui::SharedString;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use tree_sitter::StreamingIterator as _;
|
||||
|
||||
thread_local! {
|
||||
static HIGHLIGHTERS: RefCell<HashMap<Language, Option<Highlighter>>> =
|
||||
RefCell::new(HashMap::new());
|
||||
}
|
||||
|
||||
/// A grammar's parser and compiled highlight query.
|
||||
struct Highlighter {
|
||||
parser: tree_sitter::Parser,
|
||||
query: tree_sitter::Query,
|
||||
roles: Vec<Option<Role>>,
|
||||
}
|
||||
|
||||
/// A language source can highlight as.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Language {
|
||||
Rust,
|
||||
JavaScript,
|
||||
@@ -50,8 +65,6 @@ pub struct HighlightedCode {
|
||||
pub lines: Vec<Line>,
|
||||
}
|
||||
|
||||
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
|
||||
@@ -67,44 +80,62 @@ pub fn highlight_with_tree(
|
||||
return (plain(source), None);
|
||||
};
|
||||
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
if parser.set_language(&grammar).is_err() {
|
||||
return (plain(source), None);
|
||||
}
|
||||
HIGHLIGHTERS.with(|cache| {
|
||||
let mut cache = cache.borrow_mut();
|
||||
let highlighter = cache
|
||||
.entry(language)
|
||||
.or_insert_with(|| {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
if parser.set_language(&grammar).is_err() {
|
||||
return None;
|
||||
}
|
||||
let query = tree_sitter::Query::new(&grammar, query_source).ok()?;
|
||||
let roles = query
|
||||
.capture_names()
|
||||
.iter()
|
||||
.map(|name| role_for_capture(name))
|
||||
.collect();
|
||||
Some(Highlighter {
|
||||
parser,
|
||||
query,
|
||||
roles,
|
||||
})
|
||||
})
|
||||
.as_mut();
|
||||
|
||||
let Some(tree) = parser.parse(source, old_tree) else {
|
||||
return (plain(source), None);
|
||||
};
|
||||
let Some(highlighter) = highlighter else {
|
||||
return (plain(source), None);
|
||||
};
|
||||
|
||||
let Ok(query) = tree_sitter::Query::new(&grammar, query_source) else {
|
||||
return (plain(source), None);
|
||||
};
|
||||
let Some(tree) = highlighter.parser.parse(source, old_tree) 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(&highlighter.query, tree.root_node(), source.as_bytes());
|
||||
|
||||
// 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);
|
||||
while let Some(matched) = matches.next() {
|
||||
for capture in matched.captures {
|
||||
let Some(role) = highlighter
|
||||
.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))
|
||||
(build_lines(source, &role_at), Some(tree))
|
||||
})
|
||||
}
|
||||
|
||||
/// Source rendered without any grammar: plain text roles.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Div, ElementId, FontStyle, FontWeight, HighlightStyle,
|
||||
InteractiveElement as _, InteractiveText, IntoElement, ListAlignment, ListState,
|
||||
InteractiveElement as _, InteractiveText, IntoElement, ListAlignment, ListOffset, ListState,
|
||||
ParentElement as _, Pixels, Render, SharedString, StatefulInteractiveElement as _,
|
||||
StrikethroughStyle, Styled as _, StyledText, UnderlineStyle, Window, div, list, px, relative,
|
||||
};
|
||||
@@ -190,7 +190,7 @@ impl MarkdownViewState {
|
||||
source,
|
||||
blocks,
|
||||
highlight_code: true,
|
||||
handle: ListState::new(count, ListAlignment::Top, px(400.)).measure_all(),
|
||||
handle: ListState::new(count, ListAlignment::Top, px(400.)),
|
||||
a11y_label: None,
|
||||
}
|
||||
}
|
||||
@@ -207,7 +207,11 @@ impl MarkdownViewState {
|
||||
where
|
||||
S: Into<SharedString>,
|
||||
{
|
||||
self.source = source.into();
|
||||
let source = source.into();
|
||||
if self.source == source {
|
||||
return;
|
||||
}
|
||||
self.source = source;
|
||||
self.reparse();
|
||||
cx.notify();
|
||||
}
|
||||
@@ -233,10 +237,13 @@ impl MarkdownViewState {
|
||||
self.highlight_code
|
||||
}
|
||||
|
||||
/// Scrolls `block` into view.
|
||||
/// Scrolls `block` to the top of the view.
|
||||
pub fn scroll_to_block(&self, block: usize) {
|
||||
self.handle
|
||||
.scroll_to_reveal_item(block.min(self.blocks.len().saturating_sub(1)));
|
||||
let item_ix = block.min(self.blocks.len().saturating_sub(1));
|
||||
self.handle.scroll_to(ListOffset {
|
||||
item_ix,
|
||||
offset_in_item: px(0.),
|
||||
});
|
||||
}
|
||||
|
||||
/// Overrides the accessibility label of the view.
|
||||
|
||||
Reference in New Issue
Block a user