Highlight strings without building ropes

This commit is contained in:
Kirill Bulatov 2026-10-09 19:56:25 +03:00
parent d5be3d8bec
commit eb70b3fe85
14 changed files with 148 additions and 97 deletions

View file

@ -33,7 +33,7 @@ use gpui::{
TextStyle, WeakEntity, Window, WindowHandle, div, ease_in_out, img, linear_color_stop,
linear_gradient, list, pulsating_between,
};
use language::{Buffer, Language, Rope};
use language::{Buffer, Language};
use language_model::{
LanguageModelCompletionError, ProviderErrorCategory, ZED_CLOUD_PROVIDER_NAME,
};

View file

@ -454,7 +454,7 @@ fn highlight_code_runs(
let mut runs = Vec::new();
let mut offset = 0;
let highlights = language.highlight_text_resolved(&Rope::from(code), 0..code.len());
let highlights = language.highlight_text_resolved(code, 0..code.len());
for (range, highlight_id) in highlights.runs.iter() {
if range.start > offset {
runs.push(code_text_style.to_run(range.start - offset));

View file

@ -81,7 +81,7 @@ impl RenderedPreview {
let highlights = match (preview, language) {
(InlineInputPreview::Text(_), Some(language)) => {
language
.highlight_text_resolved(&Rope::from(text.as_str()), 0..text.len())
.highlight_text_resolved(text.as_str(), 0..text.len())
.runs
}
_ => Arc::default(),

View file

@ -12,7 +12,7 @@ use multi_buffer::{Anchor, MultiBufferOffset, ToOffset};
use settings::Settings;
use std::ops::Range;
use std::time::Duration;
use text::Rope;
use theme_settings::ThemeSettings;
use ui::{
ActiveTheme, AnyElement, ButtonCommon, ButtonStyle, Clickable, FluentBuilder, IconButton,
@ -232,9 +232,10 @@ impl Editor {
if let Some(language) = language {
for signature in &mut signature_help.signatures {
let text = Rope::from(signature.label.as_ref());
let resolved = language
.highlight_text_resolved(&text, 0..signature.label.len());
let resolved = language.highlight_text_resolved(
signature.label.as_str(),
0..signature.label.len(),
);
let highlights =
resolved.runs.iter().flat_map(|(range, highlight_id)| {
Some((

View file

@ -2427,7 +2427,7 @@ impl RenderOnce for SyntaxHighlightedText {
let highlights = self
.language
.highlight_text_resolved(&text.as_ref().into(), 0..text.len());
.highlight_text_resolved(text.as_str(), 0..text.len());
let mut runs = Vec::with_capacity(highlights.runs.len());
let mut offset = 0;

View file

@ -49,6 +49,7 @@ pub use language_core::{
use futures::future::FutureExt as _;
use language_core::highlight_cache::{MAX_TEXT_HIGHLIGHT_ENTRY_BYTES, TextHighlightKey};
use language_core::highlight_map::HighlightCaptureRef;
pub use language_core::{
BlockCommentConfig, BracketPair, BracketPairConfig, BracketPairContent, BracketsConfig,
BracketsPatternConfig, CodeLabel, CodeLabelBuilder, DebugVariablesConfig, DebuggerTextObject,
@ -78,13 +79,14 @@ use std::{
ffi::OsStr,
fmt::Debug,
hash::Hash,
mem,
iter, mem,
ops::{DerefMut, Range},
path::{Path, PathBuf},
str,
sync::{Arc, LazyLock},
};
use syntax_map::{QueryCursorHandle, SyntaxSnapshot, flattened_highlight_regions};
use streaming_iterator::StreamingIterator as _;
use syntax_map::{QueryCursorHandle, TextProvider, flatten_capture_regions};
use task::RunnableTag;
pub use task_context::{ContextLocation, ContextProvider};
pub use text_diff::{
@ -1130,18 +1132,18 @@ impl Language {
self.toolchain.clone()
}
pub fn highlight_text<'a>(
self: &'a Arc<Self>,
text: &'a Rope,
pub fn highlight_text(
self: &Arc<Self>,
text: &(impl HighlightSource + ?Sized),
range: Range<usize>,
) -> Vec<(Range<usize>, HighlightId)> {
let highlights = self.highlight_text_resolved(text, 0..text.len());
let highlights = self.highlight_text_resolved(text, 0..text.source_len());
highlight_runs_in_range(&highlights.runs, range).collect()
}
pub fn highlight_text_resolved(
self: &Arc<Self>,
text: &Rope,
text: &(impl HighlightSource + ?Sized),
range: Range<usize>,
) -> ResolvedHighlights {
let Some(grammar) = &self.grammar else {
@ -1150,27 +1152,28 @@ impl Language {
let Some(highlights_config) = &grammar.highlights_config else {
return ResolvedHighlights::default();
};
let highlights = if text.len() > MAX_TEXT_HIGHLIGHT_ENTRY_BYTES {
self.compute_resolved_highlights(grammar, text)
let text_len = text.source_len();
let highlights = if text_len > MAX_TEXT_HIGHLIGHT_ENTRY_BYTES {
Self::compute_resolved_highlights(grammar, &highlights_config.query, text)
} else {
let key = TextHighlightKey::new(text.chunks(), text.len());
let key = TextHighlightKey::new(text.source_chunks(), text_len);
match highlights_config
.text_highlight_cache
.get(&key, text.chunks())
.get(&key, text.source_chunks())
{
Some(highlights) => highlights,
None => {
let mut cached_text = String::with_capacity(text.len());
cached_text.extend(text.chunks());
let mut cached_text = String::with_capacity(text_len);
cached_text.extend(text.source_chunks());
highlights_config.text_highlight_cache.insert(
key,
cached_text,
self.compute_resolved_highlights(grammar, text),
Self::compute_resolved_highlights(grammar, &highlights_config.query, text),
)
}
}
};
if range.start == 0 && range.end >= text.len() {
if range.start == 0 && range.end >= text_len {
return highlights;
}
ResolvedHighlights {
@ -1180,21 +1183,31 @@ impl Language {
}
fn compute_resolved_highlights(
self: &Arc<Self>,
grammar: &Arc<Grammar>,
text: &Rope,
query: &tree_sitter::Query,
text: &(impl HighlightSource + ?Sized),
) -> ResolvedHighlights {
let highlight_map = grammar.highlight_map();
let tree = parse_text(grammar, text, None);
let captures =
SyntaxSnapshot::single_tree_captures(0..text.len(), text, &tree, self, |grammar| {
grammar
.highlights_config
.as_ref()
.map(|config| &config.query)
let text_len = text.source_len();
let tree = text.parse(grammar);
let regions = with_query_cursor(|cursor| {
cursor.set_byte_range(0..text_len);
let mut captures = cursor.captures(query, tree.root_node(), text.text_provider());
let capture_refs = iter::from_fn(move || {
let (query_match, capture_index) = captures.next()?;
let capture = query_match.captures[*capture_index];
Some((
capture.node.byte_range(),
HighlightCaptureRef {
grammar_index: 0,
capture_id: CaptureId(capture.index),
},
))
});
flatten_capture_regions(0..text_len, capture_refs)
});
let mut runs = Vec::<(Range<usize>, HighlightId)>::new();
for region in flattened_highlight_regions(captures, 0..text.len()) {
for region in regions {
let highlight_id = region
.stack
.iter()
@ -1454,6 +1467,54 @@ impl Debug for Language {
}
}
pub trait HighlightSource {
fn source_len(&self) -> usize;
fn source_chunks(&self) -> impl Iterator<Item = &str>;
fn parse(&self, grammar: &Grammar) -> Tree;
fn text_provider<'a>(&'a self) -> impl tree_sitter::TextProvider<&'a [u8]> + 'a;
}
impl HighlightSource for str {
fn source_len(&self) -> usize {
self.len()
}
fn source_chunks(&self) -> impl Iterator<Item = &str> {
iter::once(self)
}
fn parse(&self, grammar: &Grammar) -> Tree {
with_parser(|parser| {
parser
.set_language(&grammar.ts_language)
.expect("incompatible grammar");
parser.parse(self, None).unwrap()
})
}
fn text_provider<'a>(&'a self) -> impl tree_sitter::TextProvider<&'a [u8]> + 'a {
self.as_bytes()
}
}
impl HighlightSource for Rope {
fn source_len(&self) -> usize {
self.len()
}
fn source_chunks(&self) -> impl Iterator<Item = &str> {
self.chunks()
}
fn parse(&self, grammar: &Grammar) -> Tree {
parse_text(grammar, self, None)
}
fn text_provider<'a>(&'a self) -> impl tree_sitter::TextProvider<&'a [u8]> + 'a {
TextProvider(self)
}
}
pub(crate) fn parse_text(grammar: &Grammar, text: &Rope, old_tree: Option<Tree>) -> Tree {
with_parser(|parser| {
parser
@ -1847,6 +1908,11 @@ mod tests {
Arc::ptr_eq(&highlights.runs, &memoized.runs),
"repeated highlighting of the same text must be memoized"
);
let memoized_str = language.highlight_text_resolved(code, 0..code.len());
assert!(
Arc::ptr_eq(&highlights.runs, &memoized_str.runs),
"str and rope sources of the same text must share memoized highlights"
);
let partial = language.highlight_text_resolved(&Rope::from(code), 0..2);
assert!(
!Arc::ptr_eq(&highlights.runs, &partial.runs),
@ -1925,6 +1991,14 @@ mod tests {
small_highlights.runs.as_ref(),
"texts over the cache entry cap must still be highlighted"
);
assert_eq!(
language
.highlight_text_resolved(oversized_code.as_str(), 0..oversized_code.len())
.runs
.as_ref(),
first_highlights.runs.as_ref(),
"str and rope sources must be highlighted identically"
);
let second_highlights =
language.highlight_text_resolved(&oversized_rope, 0..oversized_code.len());
assert_eq!(

View file

@ -2,7 +2,7 @@ use crate::{BufferSnapshot, Language, Point, ToPoint, ToTreeSitterPoint};
use fuzzy_nucleo::{Case, LengthPenalty, StringMatch, StringMatchCandidate};
use gpui::{BackgroundExecutor, HighlightStyle, SharedString};
use std::{ops::Range, sync::Arc};
use text::Rope;
use theme::SyntaxTheme;
/// An outline of all the symbols contained in a buffer.
@ -269,8 +269,7 @@ pub fn highlight_ranges_from_text(
language: &Arc<Language>,
syntax_theme: &SyntaxTheme,
) -> Vec<(Range<usize>, HighlightStyle)> {
let rope = Rope::from(text);
let highlights = language.highlight_text_resolved(&rope, 0..text.len());
let highlights = language.highlight_text_resolved(text, 0..text.len());
syntax_theme.resolve_runs(highlights.runs.iter()).collect()
}

View file

@ -115,7 +115,7 @@ pub(crate) fn flattened_highlight_regions(
flatten_capture_regions(range, capture_refs)
}
fn flatten_capture_regions(
pub(crate) fn flatten_capture_regions(
range: Range<usize>,
mut captures: impl Iterator<Item = (Range<usize>, HighlightCaptureRef)>,
) -> Vec<HighlightCaptureRegion> {
@ -347,7 +347,7 @@ struct ChangedRegion {
#[derive(Default)]
struct ChangeRegionSet(Vec<ChangedRegion>);
struct TextProvider<'a>(&'a Rope);
pub(crate) struct TextProvider<'a>(pub(crate) &'a Rope);
pub(crate) struct QueryCursorHandle(Option<QueryCursor>);
@ -1044,28 +1044,6 @@ impl SyntaxSnapshot {
}
}
pub fn single_tree_captures<'a>(
range: Range<usize>,
text: &'a Rope,
tree: &'a tree_sitter::Tree,
language: &'a Arc<Language>,
query: fn(&Grammar) -> Option<&Query>,
) -> SyntaxMapCaptures<'a> {
SyntaxMapCaptures::new(
range,
text,
[SyntaxLayer {
language,
tree,
included_sub_ranges: None,
depth: 0,
offset: (0, tree_sitter::Point::new(0, 0)),
}]
.into_iter(),
query,
)
}
pub fn captures<'a>(
&'a self,
range: Range<usize>,

View file

@ -514,7 +514,7 @@ fn labels_from_extension(
Arc::default()
} else {
language
.highlight_text_resolved(&label.code.as_str().into(), 0..label.code.len())
.highlight_text_resolved(label.code.as_str(), 0..label.code.len())
.runs
};
build_code_label(&label, &runs, language)

View file

@ -211,8 +211,8 @@ impl super::LspAdapter for CLspAdapter {
Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
let detail = completion.detail.as_ref().unwrap();
let text = format!("{} {}", detail, label);
let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
let runs = language.highlight_text(&source, 11..11 + text.len());
let source = format!("struct S {{ {} }}", text);
let runs = language.highlight_text(source.as_str(), 11..11 + text.len());
let filter_range = completion
.filter_text
.as_deref()
@ -228,7 +228,7 @@ impl super::LspAdapter for CLspAdapter {
{
let detail = completion.detail.as_ref().unwrap();
let text = format!("{} {}", detail, label);
let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
let runs = language.highlight_text(text.as_str(), 0..text.len());
let filter_range = completion
.filter_text
.as_deref()
@ -244,7 +244,7 @@ impl super::LspAdapter for CLspAdapter {
{
let detail = completion.detail.as_ref().unwrap();
let text = format!("{} {}", detail, label);
let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
let runs = language.highlight_text(text.as_str(), 0..text.len());
let filter_range = completion
.filter_text
.as_deref()
@ -348,7 +348,7 @@ impl super::LspAdapter for CLspAdapter {
Some(CodeLabel::new(
text[display_range.clone()].to_string(),
filter_range,
language.highlight_text(&text.as_str().into(), display_range),
language.highlight_text(text.as_str(), display_range),
))
}

View file

@ -278,8 +278,9 @@ impl LspAdapter for GoLspAdapter {
match completion.kind.zip(completion.detail.as_ref()) {
Some((lsp::CompletionItemKind::MODULE, detail)) => {
let text = format!("{label} {detail}");
let source = Rope::from(format!("import {text}").as_str());
let runs = language.highlight_text(&source, 7..7 + text[name_offset..].len());
let source = format!("import {text}");
let runs =
language.highlight_text(source.as_str(), 7..7 + text[name_offset..].len());
let filter_range = completion
.filter_text
.as_deref()
@ -295,11 +296,10 @@ impl LspAdapter for GoLspAdapter {
detail,
)) => {
let text = format!("{label} {detail}");
let source =
Rope::from(format!("var {} {}", &text[name_offset..], detail).as_str());
let source = format!("var {} {}", &text[name_offset..], detail);
let runs = adjust_runs(
name_offset,
language.highlight_text(&source, 4..4 + text[name_offset..].len()),
language.highlight_text(source.as_str(), 4..4 + text[name_offset..].len()),
);
let filter_range = completion
.filter_text
@ -313,10 +313,10 @@ impl LspAdapter for GoLspAdapter {
}
Some((lsp::CompletionItemKind::STRUCT, _)) => {
let text = format!("{label} struct {{}}");
let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
let source = format!("type {}", &text[name_offset..]);
let runs = adjust_runs(
name_offset,
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
language.highlight_text(source.as_str(), 5..5 + text[name_offset..].len()),
);
let filter_range = completion
.filter_text
@ -330,10 +330,10 @@ impl LspAdapter for GoLspAdapter {
}
Some((lsp::CompletionItemKind::INTERFACE, _)) => {
let text = format!("{label} interface {{}}");
let source = Rope::from(format!("type {}", &text[name_offset..]).as_str());
let source = format!("type {}", &text[name_offset..]);
let runs = adjust_runs(
name_offset,
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
language.highlight_text(source.as_str(), 5..5 + text[name_offset..].len()),
);
let filter_range = completion
.filter_text
@ -347,11 +347,10 @@ impl LspAdapter for GoLspAdapter {
}
Some((lsp::CompletionItemKind::FIELD, detail)) => {
let text = format!("{label} {detail}");
let source =
Rope::from(format!("type T struct {{ {} }}", &text[name_offset..]).as_str());
let source = format!("type T struct {{ {} }}", &text[name_offset..]);
let runs = adjust_runs(
name_offset,
language.highlight_text(&source, 16..16 + text[name_offset..].len()),
language.highlight_text(source.as_str(), 16..16 + text[name_offset..].len()),
);
let filter_range = completion
.filter_text
@ -366,10 +365,10 @@ impl LspAdapter for GoLspAdapter {
Some((lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD, detail)) => {
if let Some(signature) = detail.strip_prefix("func") {
let text = format!("{label}{signature}");
let source = Rope::from(format!("func {} {{}}", &text[name_offset..]).as_str());
let source = format!("func {} {{}}", &text[name_offset..]);
let runs = adjust_runs(
name_offset,
language.highlight_text(&source, 5..5 + text[name_offset..].len()),
language.highlight_text(source.as_str(), 5..5 + text[name_offset..].len()),
);
let filter_range = completion
.filter_text
@ -442,7 +441,7 @@ impl LspAdapter for GoLspAdapter {
Some(CodeLabel::new(
text[display_range.clone()].to_string(),
filter_range,
language.highlight_text(&text.as_str().into(), display_range),
language.highlight_text(text.as_str(), display_range),
))
}

View file

@ -243,7 +243,7 @@ fn label_for_python_symbol(
Some(language::CodeLabel::new(
text[display_range.clone()].to_string(),
filter_range,
language.highlight_text(&text.as_str().into(), display_range),
language.highlight_text(text.as_str(), display_range),
))
}

View file

@ -430,8 +430,8 @@ impl LspAdapter for RustLspAdapter {
.filter(|it| it.contains(&label))
.and_then(|it| Some((it, FULL_SIGNATURE_REGEX.find(it)?)))
{
let source = Rope::from(function_signature);
let runs = language.highlight_text(&source, 0..function_signature.len());
let runs =
language.highlight_text(function_signature, 0..function_signature.len());
mk_label(
function_signature.to_owned(),
&|| match_.range().start + 3..match_.range().end - 1,
@ -446,8 +446,7 @@ impl LspAdapter for RustLspAdapter {
} else if detail_right.is_some_and(|detail| detail.starts_with("macro_rules! ")) {
let text = completion.label.clone();
let len = text.len();
let source = Rope::from(text.as_str());
let runs = language.highlight_text(&source, 0..len);
let runs = language.highlight_text(text.as_str(), 0..len);
mk_label(text, &|| 0..completion.label.len(), runs)
} else if detail_left.is_none() {
return None;
@ -544,7 +543,7 @@ impl LspAdapter for RustLspAdapter {
detail_left.take();
}
runs.extend(language.highlight_text(&Rope::from(&label), 0..label.len()));
runs.extend(language.highlight_text(label.as_str(), 0..label.len()));
} else {
let highlight_name = kind.and_then(|kind| match kind {
lsp::CompletionItemKind::STRUCT

View file

@ -1674,22 +1674,25 @@ fn compute_code_block_highlights(parsed: &ParsedMarkdown) -> CodeBlockHighlights
}
fn highlight_code_block(block: PendingCodeBlock, code_block_highlights: &mut CodeBlockHighlights) {
let mut combined = Rope::new();
if let [(event_index, text)] = block.texts.as_slice() {
let resolved = block.language.highlight_text_resolved(*text, 0..text.len());
if !resolved.runs.is_empty() {
code_block_highlights.insert(*event_index, resolved);
}
return;
}
let mut combined = String::new();
let mut text_offsets = Vec::with_capacity(block.texts.len());
for (_, text) in &block.texts {
text_offsets.push(combined.len());
combined.push(text);
combined.push_str(text);
}
let resolved = block
.language
.highlight_text_resolved(&combined, 0..combined.len());
.highlight_text_resolved(combined.as_str(), 0..combined.len());
if resolved.runs.is_empty() {
return;
}
if let [(event_index, _)] = block.texts.as_slice() {
code_block_highlights.insert(*event_index, resolved);
return;
}
let mut runs = resolved.runs.iter().peekable();
for ((event_index, text), text_offset) in block.texts.iter().zip(text_offsets) {
let text_end = text_offset + text.len();
@ -4169,9 +4172,7 @@ impl MarkdownElementBuilder {
let runs = if resolved.is_current() {
resolved.runs.clone()
} else {
language
.highlight_text_resolved(&Rope::from(text), 0..text.len())
.runs
language.highlight_text_resolved(text, 0..text.len()).runs
};
let mut offset = 0;
for (run_range, highlight_id) in runs.iter() {