Do not derive symbol highlights if they do not fit into multi buffer (#50948)

Release Notes:

- N/A

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Kirill BulatovandConrad Irwin authored and GitHub committed 2026-03-09 15:50:44 +00:00
1 parent b06c0e0773
commit d788673f1e
12 files changed
+103 -206

No files matched your search

@@ -297,7 +297,7 @@ impl DiagnosticBlock {
return;
};
for (excerpt_id, range) in multibuffer.excerpts_for_buffer(buffer_id, cx) {
for (excerpt_id, _, range) in multibuffer.excerpts_for_buffer(buffer_id, cx) {
if range.context.overlaps(&diagnostic.range, &snapshot) {
Self::jump_to(
editor,
+1 -1
View File
@@ -583,7 +583,7 @@ impl ProjectDiagnosticsEditor {
RetainExcerpts::All | RetainExcerpts::Dirty => multi_buffer
.excerpts_for_buffer(buffer_id, cx)
.into_iter()
.map(|(_, range)| range)
.map(|(_, _, range)| range)
.sorted_by(|a, b| cmp_excerpts(&buffer_snapshot, a, b))
.collect(),
}
+2 -48
View File
@@ -107,7 +107,7 @@ use project::{InlayId, lsp_store::LspFoldingRange, lsp_store::TokenType};
use serde::Deserialize;
use smallvec::SmallVec;
use sum_tree::{Bias, TreeMap};
use text::{BufferId, LineIndent, Patch, ToOffset as _};
use text::{BufferId, LineIndent, Patch};
use ui::{SharedString, px};
use unicode_segmentation::UnicodeSegmentation;
use ztracing::instrument;
@@ -1977,57 +1977,11 @@ impl DisplaySnapshot {
/// Returned ranges are 0-based relative to `buffer_range.start`.
pub(super) fn combined_highlights(
&self,
buffer_id: BufferId,
buffer_range: Range<usize>,
multibuffer_range: Range<MultiBufferOffset>,
syntax_theme: &theme::SyntaxTheme,
) -> Vec<(Range<usize>, HighlightStyle)> {
let multibuffer = self.buffer_snapshot();
let multibuffer_range = multibuffer
.excerpts()
.find_map(|(excerpt_id, buffer, range)| {
if buffer.remote_id() != buffer_id {
return None;
}
let context_start = range.context.start.to_offset(buffer);
let context_end = range.context.end.to_offset(buffer);
if buffer_range.start < context_start || buffer_range.end > context_end {
return None;
}
let start_anchor = buffer.anchor_before(buffer_range.start);
let end_anchor = buffer.anchor_after(buffer_range.end);
let mb_range =
multibuffer.anchor_range_in_excerpt(excerpt_id, start_anchor..end_anchor)?;
Some(mb_range.start.to_offset(multibuffer)..mb_range.end.to_offset(multibuffer))
});
let Some(multibuffer_range) = multibuffer_range else {
// Range is outside all excerpts (e.g. symbol name not in a
// multi-buffer excerpt). Fall back to buffer-level syntax highlights.
let buffer_snapshot = multibuffer.excerpts().find_map(|(_, buffer, _)| {
(buffer.remote_id() == buffer_id).then(|| buffer.clone())
});
let Some(buffer_snapshot) = buffer_snapshot else {
return Vec::new();
};
let mut highlights = Vec::new();
let mut offset = 0usize;
for chunk in buffer_snapshot.chunks(buffer_range, true) {
let chunk_len = chunk.text.len();
if chunk_len == 0 {
continue;
}
if let Some(style) = chunk
.syntax_highlight_id
.and_then(|id| id.style(syntax_theme))
{
highlights.push((offset..offset + chunk_len, style));
}
offset += chunk_len;
}
return highlights;
};
let chunks = custom_highlights::CustomHighlightsChunks::new(
multibuffer_range,
true,
+74 -139
View File
@@ -1,4 +1,4 @@
use std::{cmp, ops::Range};
use std::ops::Range;
use collections::HashMap;
use futures::FutureExt;
@@ -6,10 +6,15 @@ use futures::future::join_all;
use gpui::{App, Context, HighlightStyle, Task};
use itertools::Itertools as _;
use language::language_settings::language_settings;
use language::{Buffer, BufferSnapshot, OutlineItem};
use multi_buffer::{Anchor, MultiBufferSnapshot};
use text::{Bias, BufferId, OffsetRangeExt as _, ToOffset as _};
use language::{Buffer, OutlineItem};
use multi_buffer::{
Anchor, AnchorRangeExt as _, MultiBufferOffset, MultiBufferRow, MultiBufferSnapshot,
ToOffset as _,
};
use text::BufferId;
use theme::{ActiveTheme as _, SyntaxTheme};
use unicode_segmentation::UnicodeSegmentation as _;
use util::maybe;
use crate::display_map::DisplaySnapshot;
use crate::{Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT};
@@ -215,16 +220,13 @@ impl Editor {
let display_snapshot =
editor.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut highlighted_results = results;
for (buffer_id, items) in &mut highlighted_results {
if let Some(buffer) = editor.buffer.read(cx).buffer(*buffer_id) {
let snapshot = buffer.read(cx).snapshot();
apply_highlights(
items,
*buffer_id,
&snapshot,
&display_snapshot,
&syntax,
);
for items in highlighted_results.values_mut() {
for item in items {
if let Some(highlights) =
highlights_from_buffer(&display_snapshot, &item, &syntax)
{
item.highlight_ranges = highlights;
}
}
}
editor.lsp_document_symbols.extend(highlighted_results);
@@ -242,34 +244,6 @@ fn lsp_symbols_enabled(buffer: &Buffer, cx: &App) -> bool {
.lsp_enabled()
}
/// Applies combined syntax + semantic token highlights to LSP document symbol
/// outline items that were built without highlights by the project layer.
fn apply_highlights(
items: &mut [OutlineItem<text::Anchor>],
buffer_id: BufferId,
buffer_snapshot: &BufferSnapshot,
display_snapshot: &DisplaySnapshot,
syntax_theme: &SyntaxTheme,
) {
for item in items {
let symbol_range = item.range.to_offset(buffer_snapshot);
let selection_start = item.source_range_for_text.start.to_offset(buffer_snapshot);
if let Some(highlights) = highlights_from_buffer(
&item.text,
0,
buffer_id,
buffer_snapshot,
display_snapshot,
symbol_range,
selection_start,
syntax_theme,
) {
item.highlight_ranges = highlights;
}
}
}
/// Finds where the symbol name appears in the buffer and returns combined
/// (tree-sitter + semantic token) highlights for those positions.
///
@@ -278,117 +252,78 @@ fn apply_highlights(
/// to word-by-word matching for cases like `impl<T> Trait<T> for Type`
/// where the LSP name doesn't appear verbatim in the buffer.
fn highlights_from_buffer(
name: &str,
name_offset_in_text: usize,
buffer_id: BufferId,
buffer_snapshot: &BufferSnapshot,
display_snapshot: &DisplaySnapshot,
symbol_range: Range<usize>,
selection_start_offset: usize,
item: &OutlineItem<text::Anchor>,
syntax_theme: &SyntaxTheme,
) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
if name.is_empty() {
let outline_text = &item.text;
if outline_text.is_empty() {
return None;
}
let range_start_offset = symbol_range.start;
let range_end_offset = symbol_range.end;
let multi_buffer_snapshot = display_snapshot.buffer();
let multi_buffer_source_range_anchors =
multi_buffer_snapshot.text_anchors_to_visible_anchors([
item.source_range_for_text.start,
item.source_range_for_text.end,
]);
let Some(anchor_range) = maybe!({
Some(
(*multi_buffer_source_range_anchors.get(0)?)?
..(*multi_buffer_source_range_anchors.get(1)?)?,
)
}) else {
return None;
};
// Try to find the name verbatim in the buffer near the selection range.
let search_start = buffer_snapshot.clip_offset(
selection_start_offset
.saturating_sub(name.len())
.max(range_start_offset),
Bias::Right,
);
let search_end = buffer_snapshot.clip_offset(
cmp::min(selection_start_offset + name.len() * 2, range_end_offset),
Bias::Left,
);
let selection_point_range = anchor_range.to_point(multi_buffer_snapshot);
let mut search_start = selection_point_range.start;
search_start.column = 0;
let search_start_offset = search_start.to_offset(&multi_buffer_snapshot);
let mut search_end = selection_point_range.end;
search_end.column = multi_buffer_snapshot.line_len(MultiBufferRow(search_end.row));
if search_start < search_end {
let buffer_text: String = buffer_snapshot
.text_for_range(search_start..search_end)
.collect();
if let Some(found_at) = buffer_text.find(name) {
let name_start_offset = search_start + found_at;
let name_end_offset = name_start_offset + name.len();
let result = highlights_for_buffer_range(
name_offset_in_text,
name_start_offset..name_end_offset,
buffer_id,
display_snapshot,
syntax_theme,
let search_text = multi_buffer_snapshot
.text_for_range(search_start..search_end)
.collect::<String>();
let mut outline_text_highlights = Vec::new();
match search_text.find(outline_text) {
Some(start_index) => {
let multibuffer_start = search_start_offset + MultiBufferOffset(start_index);
let multibuffer_end = multibuffer_start + MultiBufferOffset(outline_text.len());
outline_text_highlights.extend(
display_snapshot
.combined_highlights(multibuffer_start..multibuffer_end, syntax_theme),
);
if result.is_some() {
return result;
}
None => {
for (outline_text_word_start, outline_word) in outline_text.split_word_bound_indices() {
if let Some(start_index) = search_text.find(outline_word) {
let multibuffer_start = search_start_offset + MultiBufferOffset(start_index);
let multibuffer_end = multibuffer_start + MultiBufferOffset(outline_word.len());
outline_text_highlights.extend(
display_snapshot
.combined_highlights(multibuffer_start..multibuffer_end, syntax_theme)
.into_iter()
.map(|(range_in_word, style)| {
(
outline_text_word_start + range_in_word.start
..outline_text_word_start + range_in_word.end,
style,
)
}),
);
}
}
}
}
// Fallback: match word-by-word. Split the name on whitespace and find
// each word sequentially in the buffer's symbol range.
let range_start_offset = buffer_snapshot.clip_offset(range_start_offset, Bias::Right);
let range_end_offset = buffer_snapshot.clip_offset(range_end_offset, Bias::Left);
let mut highlights = Vec::new();
let mut got_any = false;
let buffer_text: String = buffer_snapshot
.text_for_range(range_start_offset..range_end_offset)
.collect();
let mut buf_search_from = 0usize;
let mut name_search_from = 0usize;
for word in name.split_whitespace() {
let name_word_start = name[name_search_from..]
.find(word)
.map(|pos| name_search_from + pos)
.unwrap_or(name_search_from);
if let Some(found_in_buf) = buffer_text[buf_search_from..].find(word) {
let buf_word_start = range_start_offset + buf_search_from + found_in_buf;
let buf_word_end = buf_word_start + word.len();
let text_cursor = name_offset_in_text + name_word_start;
if let Some(mut word_highlights) = highlights_for_buffer_range(
text_cursor,
buf_word_start..buf_word_end,
buffer_id,
display_snapshot,
syntax_theme,
) {
got_any = true;
highlights.append(&mut word_highlights);
}
buf_search_from = buf_search_from + found_in_buf + word.len();
}
name_search_from = name_word_start + word.len();
if outline_text_highlights.is_empty() {
None
} else {
Some(outline_text_highlights)
}
got_any.then_some(highlights)
}
/// Gets combined (tree-sitter + semantic token) highlights for a buffer byte
/// range via the editor's display snapshot, then shifts the returned ranges
/// so they start at `text_cursor_start` (the position in the outline item text).
fn highlights_for_buffer_range(
text_cursor_start: usize,
buffer_range: Range<usize>,
buffer_id: BufferId,
display_snapshot: &DisplaySnapshot,
syntax_theme: &SyntaxTheme,
) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
let raw = display_snapshot.combined_highlights(buffer_id, buffer_range, syntax_theme);
if raw.is_empty() {
return None;
}
Some(
raw.into_iter()
.map(|(range, style)| {
(
range.start + text_cursor_start..range.end + text_cursor_start,
style,
)
})
.collect(),
)
}
#[cfg(test)]
+5 -4
View File
@@ -7500,7 +7500,8 @@ impl Editor {
let mut read_ranges = Vec::new();
for highlight in highlights {
let buffer_id = cursor_buffer.read(cx).remote_id();
for (excerpt_id, excerpt_range) in buffer.excerpts_for_buffer(buffer_id, cx)
for (excerpt_id, _, excerpt_range) in
buffer.excerpts_for_buffer(buffer_id, cx)
{
let start = highlight
.range
@@ -20539,7 +20540,7 @@ impl Editor {
let mut all_folded_excerpt_ids = Vec::new();
for buffer_id in &ids_to_fold {
let folded_excerpts = self.buffer().read(cx).excerpts_for_buffer(*buffer_id, cx);
all_folded_excerpt_ids.extend(folded_excerpts.into_iter().map(|(id, _)| id));
all_folded_excerpt_ids.extend(folded_excerpts.into_iter().map(|(id, _, _)| id));
}
self.display_map.update(cx, |display_map, cx| {
@@ -20569,7 +20570,7 @@ impl Editor {
display_map.unfold_buffers([buffer_id], cx);
});
cx.emit(EditorEvent::BufferFoldToggled {
ids: unfolded_excerpts.iter().map(|&(id, _)| id).collect(),
ids: unfolded_excerpts.iter().map(|&(id, _, _)| id).collect(),
folded: false,
});
cx.notify();
@@ -22941,7 +22942,7 @@ impl Editor {
.snapshot();
let mut handled = false;
for (id, ExcerptRange { context, .. }) in
for (id, _, ExcerptRange { context, .. }) in
self.buffer.read(cx).excerpts_for_buffer(buffer_id, cx)
{
if context.start.cmp(&position, &snapshot).is_ge()
+2 -2
View File
@@ -1165,8 +1165,8 @@ impl SplittableEditor {
let lhs_ranges: Vec<ExcerptRange<Point>> = rhs_multibuffer
.excerpts_for_buffer(main_buffer_snapshot.remote_id(), cx)
.into_iter()
.filter(|(id, _)| rhs_excerpt_ids.contains(id))
.map(|(_, excerpt_range)| {
.filter(|(id, _, _)| rhs_excerpt_ids.contains(id))
.map(|(_, _, excerpt_range)| {
let to_base_text = |range: Range<Point>| {
let start = diff_snapshot
.buffer_point_to_base_text_range(
+3 -3
View File
@@ -182,7 +182,7 @@ fn conflicts_updated(
let excerpts = multibuffer.excerpts_for_buffer(buffer_id, cx);
let Some(buffer_snapshot) = excerpts
.first()
.and_then(|(excerpt_id, _)| snapshot.buffer_for_excerpt(*excerpt_id))
.and_then(|(excerpt_id, _, _)| snapshot.buffer_for_excerpt(*excerpt_id))
else {
return;
};
@@ -221,7 +221,7 @@ fn conflicts_updated(
let mut removed_highlighted_ranges = Vec::new();
let mut removed_block_ids = HashSet::default();
for (conflict_range, block_id) in old_conflicts {
let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
let Some((excerpt_id, _, _)) = excerpts.iter().find(|(_, _, range)| {
let precedes_start = range
.context
.start
@@ -263,7 +263,7 @@ fn conflicts_updated(
let new_conflicts = &conflict_set.conflicts[event.new_range.clone()];
let mut blocks = Vec::new();
for conflict in new_conflicts {
let Some((excerpt_id, _)) = excerpts.iter().find(|(_, range)| {
let Some((excerpt_id, _, _)) = excerpts.iter().find(|(_, _, range)| {
let precedes_start = range
.context
.start
+3 -1
View File
@@ -94,7 +94,9 @@ impl GoToLine {
.read(cx)
.excerpts_for_buffer(snapshot.remote_id(), cx)
.into_iter()
.map(move |(_, range)| text::ToPoint::to_point(&range.context.end, &snapshot).row)
.map(move |(_, _, range)| {
text::ToPoint::to_point(&range.context.end, &snapshot).row
})
.max()
.unwrap_or(0);
+6 -1
View File
@@ -246,7 +246,12 @@ impl StyledText {
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
let mut text = &**self.text;
for run in &runs {
text = text.get(run.len..).expect("invalid text run");
text = text.get(run.len..).unwrap_or_else(|| {
#[cfg(debug_assertions)]
panic!("invalid text run. Text: '{text}', run: {run:?}");
#[cfg(not(debug_assertions))]
panic!("invalid text run");
});
}
assert!(text.is_empty(), "invalid text run");
self.runs = Some(runs);
+4 -4
View File
@@ -1987,7 +1987,7 @@ impl MultiBuffer {
&self,
buffer_id: BufferId,
cx: &App,
) -> Vec<(ExcerptId, ExcerptRange<text::Anchor>)> {
) -> Vec<(ExcerptId, Arc<BufferSnapshot>, ExcerptRange<text::Anchor>)> {
let mut excerpts = Vec::new();
let snapshot = self.read(cx);
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>(());
@@ -1997,7 +1997,7 @@ impl MultiBuffer {
if let Some(excerpt) = cursor.item()
&& excerpt.locator == *locator
{
excerpts.push((excerpt.id, excerpt.range.clone()));
excerpts.push((excerpt.id, excerpt.buffer.clone(), excerpt.range.clone()));
}
}
}
@@ -2128,7 +2128,7 @@ impl MultiBuffer {
) -> Option<Anchor> {
let mut found = None;
let snapshot = buffer.read(cx).snapshot();
for (excerpt_id, range) in self.excerpts_for_buffer(snapshot.remote_id(), cx) {
for (excerpt_id, _, range) in self.excerpts_for_buffer(snapshot.remote_id(), cx) {
let start = range.context.start.to_point(&snapshot);
let end = range.context.end.to_point(&snapshot);
if start <= point && point < end {
@@ -2157,7 +2157,7 @@ impl MultiBuffer {
cx: &App,
) -> Option<Anchor> {
let snapshot = buffer.read(cx).snapshot();
for (excerpt_id, range) in self.excerpts_for_buffer(snapshot.remote_id(), cx) {
for (excerpt_id, _, range) in self.excerpts_for_buffer(snapshot.remote_id(), cx) {
if range.context.start.cmp(&anchor, &snapshot).is_le()
&& range.context.end.cmp(&anchor, &snapshot).is_ge()
{
@@ -1285,7 +1285,7 @@ fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut App) {
let mut ids = multibuffer
.excerpts_for_buffer(buffer_2.read(cx).remote_id(), cx)
.into_iter()
.map(|(id, _)| id);
.map(|(id, _, _)| id);
(ids.next().unwrap(), ids.next().unwrap())
});
let snapshot_2 = multibuffer.read(cx).snapshot(cx);
+1 -1
View File
@@ -1143,7 +1143,7 @@ impl OutlinePanel {
.excerpts_for_buffer(buffer.read(cx).remote_id(), cx)
})
.and_then(|excerpts| {
let (excerpt_id, excerpt_range) = excerpts.first()?;
let (excerpt_id, _, excerpt_range) = excerpts.first()?;
multi_buffer_snapshot
.anchor_in_excerpt(*excerpt_id, excerpt_range.context.start)
})