Support diagnostic navigation in multibuffers (#22620)

Cole Miller created

cc @nathansobo 

Release Notes:

- Support diagnostic navigation in multibuffers

Change summary

crates/assistant/src/inline_assistant.rs      |  37 ++++---
crates/assistant2/src/buffer_codegen.rs       |  24 ++-
crates/assistant2/src/inline_assistant.rs     |   5 
crates/diagnostics/src/items.rs               |  10 +
crates/editor/src/editor.rs                   |  84 +++++++++------
crates/editor/src/element.rs                  |  11 +
crates/editor/src/hover_popover.rs            |   9 
crates/editor/src/hunk_diff.rs                |  17 +-
crates/language/src/buffer.rs                 |  14 +
crates/language_tools/src/syntax_tree_view.rs |  23 ++-
crates/multi_buffer/src/multi_buffer.rs       | 108 +++++++++++---------
crates/multi_buffer/src/multi_buffer_tests.rs |  11 -
crates/text/src/text.rs                       |   6 +
13 files changed, 209 insertions(+), 150 deletions(-)

Detailed changes

crates/assistant/src/inline_assistant.rs 🔗

@@ -797,10 +797,11 @@ impl InlineAssistant {
             if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
                 let language_name = assist.editor.upgrade().and_then(|editor| {
                     let multibuffer = editor.read(cx).buffer().read(cx);
-                    let ranges = multibuffer.range_to_buffer_ranges(assist.range.clone(), cx);
+                    let multibuffer_snapshot = multibuffer.snapshot(cx);
+                    let ranges = multibuffer_snapshot.range_to_buffer_ranges(assist.range.clone());
                     ranges
                         .first()
-                        .and_then(|(buffer, _, _)| buffer.read(cx).language())
+                        .and_then(|(excerpt, _)| excerpt.buffer().language())
                         .map(|language| language.name())
                 });
                 report_assistant_event(
@@ -2615,26 +2616,29 @@ impl EventEmitter<CodegenEvent> for CodegenAlternative {}
 
 impl CodegenAlternative {
     pub fn new(
-        buffer: Model<MultiBuffer>,
+        multi_buffer: Model<MultiBuffer>,
         range: Range<Anchor>,
         active: bool,
         telemetry: Option<Arc<Telemetry>>,
         builder: Arc<PromptBuilder>,
         cx: &mut ModelContext<Self>,
     ) -> Self {
-        let snapshot = buffer.read(cx).snapshot(cx);
+        let snapshot = multi_buffer.read(cx).snapshot(cx);
 
-        let (old_buffer, _, _) = buffer
-            .read(cx)
-            .range_to_buffer_ranges(range.clone(), cx)
+        let (old_excerpt, _) = snapshot
+            .range_to_buffer_ranges(range.clone())
             .pop()
             .unwrap();
         let old_buffer = cx.new_model(|cx| {
-            let old_buffer = old_buffer.read(cx);
-            let text = old_buffer.as_rope().clone();
-            let line_ending = old_buffer.line_ending();
-            let language = old_buffer.language().cloned();
-            let language_registry = old_buffer.language_registry();
+            let text = old_excerpt.buffer().as_rope().clone();
+            let line_ending = old_excerpt.buffer().line_ending();
+            let language = old_excerpt.buffer().language().cloned();
+            let language_registry = multi_buffer
+                .read(cx)
+                .buffer(old_excerpt.buffer_id())
+                .unwrap()
+                .read(cx)
+                .language_registry();
 
             let mut buffer = Buffer::local_normalized(text, line_ending, cx);
             buffer.set_language(language, cx);
@@ -2645,7 +2649,7 @@ impl CodegenAlternative {
         });
 
         Self {
-            buffer: buffer.clone(),
+            buffer: multi_buffer.clone(),
             old_buffer,
             edit_position: None,
             message_id: None,
@@ -2656,7 +2660,7 @@ impl CodegenAlternative {
             generation: Task::ready(()),
             diff: Diff::default(),
             telemetry,
-            _subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
+            _subscription: cx.subscribe(&multi_buffer, Self::handle_buffer_event),
             builder,
             active,
             edits: Vec::new(),
@@ -2867,10 +2871,11 @@ impl CodegenAlternative {
         let telemetry = self.telemetry.clone();
         let language_name = {
             let multibuffer = self.buffer.read(cx);
-            let ranges = multibuffer.range_to_buffer_ranges(self.range.clone(), cx);
+            let snapshot = multibuffer.snapshot(cx);
+            let ranges = snapshot.range_to_buffer_ranges(self.range.clone());
             ranges
                 .first()
-                .and_then(|(buffer, _, _)| buffer.read(cx).language())
+                .and_then(|(excerpt, _)| excerpt.buffer().language())
                 .map(|language| language.name())
         };
 

crates/assistant2/src/buffer_codegen.rs 🔗

@@ -257,17 +257,20 @@ impl CodegenAlternative {
     ) -> Self {
         let snapshot = buffer.read(cx).snapshot(cx);
 
-        let (old_buffer, _, _) = buffer
-            .read(cx)
-            .range_to_buffer_ranges(range.clone(), cx)
+        let (old_excerpt, _) = snapshot
+            .range_to_buffer_ranges(range.clone())
             .pop()
             .unwrap();
         let old_buffer = cx.new_model(|cx| {
-            let old_buffer = old_buffer.read(cx);
-            let text = old_buffer.as_rope().clone();
-            let line_ending = old_buffer.line_ending();
-            let language = old_buffer.language().cloned();
-            let language_registry = old_buffer.language_registry();
+            let text = old_excerpt.buffer().as_rope().clone();
+            let line_ending = old_excerpt.buffer().line_ending();
+            let language = old_excerpt.buffer().language().cloned();
+            let language_registry = buffer
+                .read(cx)
+                .buffer(old_excerpt.buffer_id())
+                .unwrap()
+                .read(cx)
+                .language_registry();
 
             let mut buffer = Buffer::local_normalized(text, line_ending, cx);
             buffer.set_language(language, cx);
@@ -471,10 +474,11 @@ impl CodegenAlternative {
         let telemetry = self.telemetry.clone();
         let language_name = {
             let multibuffer = self.buffer.read(cx);
-            let ranges = multibuffer.range_to_buffer_ranges(self.range.clone(), cx);
+            let snapshot = multibuffer.snapshot(cx);
+            let ranges = snapshot.range_to_buffer_ranges(self.range.clone());
             ranges
                 .first()
-                .and_then(|(buffer, _, _)| buffer.read(cx).language())
+                .and_then(|(excerpt, _)| excerpt.buffer().language())
                 .map(|language| language.name())
         };
 

crates/assistant2/src/inline_assistant.rs 🔗

@@ -871,10 +871,11 @@ impl InlineAssistant {
             if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
                 let language_name = assist.editor.upgrade().and_then(|editor| {
                     let multibuffer = editor.read(cx).buffer().read(cx);
-                    let ranges = multibuffer.range_to_buffer_ranges(assist.range.clone(), cx);
+                    let snapshot = multibuffer.snapshot(cx);
+                    let ranges = snapshot.range_to_buffer_ranges(assist.range.clone());
                     ranges
                         .first()
-                        .and_then(|(buffer, _, _)| buffer.read(cx).language())
+                        .and_then(|(excerpt, _)| excerpt.buffer().language())
                         .map(|language| language.name())
                 });
                 report_assistant_event(

crates/diagnostics/src/items.rs 🔗

@@ -1,11 +1,11 @@
 use std::time::Duration;
 
-use editor::Editor;
+use editor::{AnchorRangeExt, Editor};
 use gpui::{
     EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, Task, View,
     ViewContext, WeakView,
 };
-use language::Diagnostic;
+use language::{Diagnostic, DiagnosticEntry};
 use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
 use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
 
@@ -148,7 +148,11 @@ impl DiagnosticIndicator {
             (buffer, cursor_position)
         });
         let new_diagnostic = buffer
-            .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
+            .diagnostics_in_range(cursor_position..cursor_position, false)
+            .map(|DiagnosticEntry { diagnostic, range }| DiagnosticEntry {
+                diagnostic,
+                range: range.to_offset(&buffer),
+            })
             .filter(|entry| !entry.range.is_empty())
             .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
             .map(|entry| entry.diagnostic);

crates/editor/src/editor.rs 🔗

@@ -99,8 +99,8 @@ use itertools::Itertools;
 use language::{
     language_settings::{self, all_language_settings, language_settings, InlayHintSettings},
     markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel,
-    CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
-    Point, Selection, SelectionGoal, TransactionId,
+    CursorShape, Diagnostic, DiagnosticEntry, Documentation, IndentKind, IndentSize, Language,
+    OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
 };
 use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
 use linked_editing_ranges::refresh_linked_ranges;
@@ -3549,13 +3549,12 @@ impl Editor {
             Bias::Left,
         );
         let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end;
-        multi_buffer
-            .range_to_buffer_ranges(multi_buffer_visible_range, cx)
+        multi_buffer_snapshot
+            .range_to_buffer_ranges(multi_buffer_visible_range)
             .into_iter()
-            .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
-            .filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| {
-                let buffer = buffer_handle.read(cx);
-                let buffer_file = project::File::from_dyn(buffer.file())?;
+            .filter(|(_, excerpt_visible_range)| !excerpt_visible_range.is_empty())
+            .filter_map(|(excerpt, excerpt_visible_range)| {
+                let buffer_file = project::File::from_dyn(excerpt.buffer().file())?;
                 let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?;
                 let worktree_entry = buffer_worktree
                     .read(cx)
@@ -3564,17 +3563,17 @@ impl Editor {
                     return None;
                 }
 
-                let language = buffer.language()?;
+                let language = excerpt.buffer().language()?;
                 if let Some(restrict_to_languages) = restrict_to_languages {
                     if !restrict_to_languages.contains(language) {
                         return None;
                     }
                 }
                 Some((
-                    excerpt_id,
+                    excerpt.id(),
                     (
-                        buffer_handle,
-                        buffer.version().clone(),
+                        multi_buffer.buffer(excerpt.buffer_id()).unwrap(),
+                        excerpt.buffer().version().clone(),
                         excerpt_visible_range,
                     ),
                 ))
@@ -9179,10 +9178,23 @@ impl Editor {
         let snapshot = self.snapshot(cx);
         loop {
             let diagnostics = if direction == Direction::Prev {
-                buffer.diagnostics_in_range::<_, usize>(0..search_start, true)
+                buffer
+                    .diagnostics_in_range(0..search_start, true)
+                    .map(|DiagnosticEntry { diagnostic, range }| DiagnosticEntry {
+                        diagnostic,
+                        range: range.to_offset(&buffer),
+                    })
+                    .collect::<Vec<_>>()
             } else {
-                buffer.diagnostics_in_range::<_, usize>(search_start..buffer.len(), false)
+                buffer
+                    .diagnostics_in_range(search_start..buffer.len(), false)
+                    .map(|DiagnosticEntry { diagnostic, range }| DiagnosticEntry {
+                        diagnostic,
+                        range: range.to_offset(&buffer),
+                    })
+                    .collect::<Vec<_>>()
             }
+            .into_iter()
             .filter(|diagnostic| !snapshot.intersects_fold(diagnostic.range.start));
             let group = diagnostics
                 // relies on diagnostics_in_range to return diagnostics with the same starting range to
@@ -10289,11 +10301,12 @@ impl Editor {
             let buffer = self.buffer.read(cx).snapshot(cx);
             let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer);
             let is_valid = buffer
-                .diagnostics_in_range::<_, usize>(active_diagnostics.primary_range.clone(), false)
+                .diagnostics_in_range(active_diagnostics.primary_range.clone(), false)
                 .any(|entry| {
+                    let range = entry.range.to_offset(&buffer);
                     entry.diagnostic.is_primary
-                        && !entry.range.is_empty()
-                        && entry.range.start == primary_range_start
+                        && !range.is_empty()
+                        && range.start == primary_range_start
                         && entry.diagnostic.message == active_diagnostics.primary_message
                 });
 
@@ -11493,21 +11506,23 @@ impl Editor {
             let (buffer, selection) = if let Some(buffer) = self.buffer().read(cx).as_singleton() {
                 (buffer, selection_range.start.row..selection_range.end.row)
             } else {
-                let buffer_ranges = self
-                    .buffer()
-                    .read(cx)
-                    .range_to_buffer_ranges(selection_range, cx);
+                let multi_buffer = self.buffer().read(cx);
+                let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+                let buffer_ranges = multi_buffer_snapshot.range_to_buffer_ranges(selection_range);
 
-                let (buffer, range, _) = if selection.reversed {
+                let (excerpt, range) = if selection.reversed {
                     buffer_ranges.first()
                 } else {
                     buffer_ranges.last()
                 }?;
 
-                let snapshot = buffer.read(cx).snapshot();
+                let snapshot = excerpt.buffer();
                 let selection = text::ToPoint::to_point(&range.start, &snapshot).row
                     ..text::ToPoint::to_point(&range.end, &snapshot).row;
-                (buffer.clone(), selection)
+                (
+                    multi_buffer.buffer(excerpt.buffer_id()).unwrap().clone(),
+                    selection,
+                )
             };
 
             Some((buffer, selection))
@@ -12399,17 +12414,18 @@ impl Editor {
         };
 
         let selections = self.selections.all::<usize>(cx);
-        let buffer = self.buffer.read(cx);
+        let multi_buffer = self.buffer.read(cx);
+        let multi_buffer_snapshot = multi_buffer.snapshot(cx);
         let mut new_selections_by_buffer = HashMap::default();
         for selection in selections {
-            for (buffer, range, _) in
-                buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
+            for (excerpt, range) in
+                multi_buffer_snapshot.range_to_buffer_ranges(selection.start..selection.end)
             {
-                let mut range = range.to_point(buffer.read(cx));
+                let mut range = range.to_point(excerpt.buffer());
                 range.start.column = 0;
-                range.end.column = buffer.read(cx).line_len(range.end.row);
+                range.end.column = excerpt.buffer().line_len(range.end.row);
                 new_selections_by_buffer
-                    .entry(buffer)
+                    .entry(multi_buffer.buffer(excerpt.buffer_id()).unwrap())
                     .or_insert(Vec::new())
                     .push(range)
             }
@@ -12508,13 +12524,15 @@ impl Editor {
             }
             None => {
                 let selections = self.selections.all::<usize>(cx);
-                let buffer = self.buffer.read(cx);
+                let multi_buffer = self.buffer.read(cx);
                 for selection in selections {
-                    for (mut buffer_handle, mut range, _) in
-                        buffer.range_to_buffer_ranges(selection.range(), cx)
+                    for (excerpt, mut range) in multi_buffer
+                        .snapshot(cx)
+                        .range_to_buffer_ranges(selection.range())
                     {
                         // When editing branch buffers, jump to the corresponding location
                         // in their base buffer.
+                        let mut buffer_handle = multi_buffer.buffer(excerpt.buffer_id()).unwrap();
                         let buffer = buffer_handle.read(cx);
                         if let Some(base_buffer) = buffer.base_buffer() {
                             range = buffer.range_to_version(range, &base_buffer.read(cx).version());

crates/editor/src/element.rs 🔗

@@ -45,7 +45,7 @@ use language::{
         IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings,
         ShowWhitespaceSetting,
     },
-    ChunkRendererContext,
+    ChunkRendererContext, DiagnosticEntry,
 };
 use lsp::DiagnosticSeverity;
 use multi_buffer::{
@@ -4741,10 +4741,11 @@ impl EditorElement {
                             if scrollbar_settings.diagnostics != ScrollbarDiagnostics::None {
                                 let diagnostics = snapshot
                                     .buffer_snapshot
-                                    .diagnostics_in_range::<_, Point>(
-                                        Point::zero()..max_point,
-                                        false,
-                                    )
+                                    .diagnostics_in_range(Point::zero()..max_point, false)
+                                    .map(|DiagnosticEntry { diagnostic, range }| DiagnosticEntry {
+                                        diagnostic,
+                                        range: range.to_point(&snapshot.buffer_snapshot),
+                                    })
                                     // Don't show diagnostics the user doesn't care about
                                     .filter(|diagnostic| {
                                         match (

crates/editor/src/hover_popover.rs 🔗

@@ -266,12 +266,11 @@ fn show_hover(
             // If there's a diagnostic, assign it on the hover state and notify
             let mut local_diagnostic = snapshot
                 .buffer_snapshot
-                .diagnostics_in_range::<_, usize>(anchor..anchor, false)
+                .diagnostics_in_range(anchor..anchor, false)
                 // Find the entry with the most specific range
-                .min_by_key(|entry| entry.range.end - entry.range.start)
-                .map(|entry| DiagnosticEntry {
-                    diagnostic: entry.diagnostic,
-                    range: entry.range.to_anchors(&snapshot.buffer_snapshot),
+                .min_by_key(|entry| {
+                    let range = entry.range.to_offset(&snapshot.buffer_snapshot);
+                    range.end - range.start
                 });
 
             // Pull the primary diagnostic out so we can jump to it if the popover is clicked

crates/editor/src/hunk_diff.rs 🔗

@@ -456,16 +456,19 @@ impl Editor {
         range: Range<Anchor>,
         cx: &mut ViewContext<Editor>,
     ) -> Option<()> {
-        let (buffer, range, _) = self
-            .buffer
-            .read(cx)
-            .range_to_buffer_ranges(range, cx)
+        let multi_buffer = self.buffer.read(cx);
+        let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+        let (excerpt, range) = multi_buffer_snapshot
+            .range_to_buffer_ranges(range)
             .into_iter()
             .next()?;
 
-        buffer.update(cx, |branch_buffer, cx| {
-            branch_buffer.merge_into_base(vec![range], cx);
-        });
+        multi_buffer
+            .buffer(excerpt.buffer_id())
+            .unwrap()
+            .update(cx, |branch_buffer, cx| {
+                branch_buffer.merge_into_base(vec![range], cx);
+            });
 
         if let Some(project) = self.project.clone() {
             self.save(true, project, cx).detach_and_log_err(cx);

crates/language/src/buffer.rs 🔗

@@ -3943,14 +3943,14 @@ impl BufferSnapshot {
     ) -> impl 'a + Iterator<Item = DiagnosticEntry<O>>
     where
         T: 'a + Clone + ToOffset,
-        O: 'a + FromAnchor + Ord,
+        O: 'a + FromAnchor,
     {
         let mut iterators: Vec<_> = self
             .diagnostics
             .iter()
             .map(|(_, collection)| {
                 collection
-                    .range::<T, O>(search_range.clone(), self, true, reversed)
+                    .range::<T, text::Anchor>(search_range.clone(), self, true, reversed)
                     .peekable()
             })
             .collect();
@@ -3964,7 +3964,7 @@ impl BufferSnapshot {
                     let cmp = a
                         .range
                         .start
-                        .cmp(&b.range.start)
+                        .cmp(&b.range.start, self)
                         // when range is equal, sort by diagnostic severity
                         .then(a.diagnostic.severity.cmp(&b.diagnostic.severity))
                         // and stabilize order with group_id
@@ -3975,7 +3975,13 @@ impl BufferSnapshot {
                         cmp
                     }
                 })?;
-            iterators[next_ix].next()
+            iterators[next_ix]
+                .next()
+                .map(|DiagnosticEntry { range, diagnostic }| DiagnosticEntry {
+                    diagnostic,
+                    range: FromAnchor::from_anchor(&range.start, self)
+                        ..FromAnchor::from_anchor(&range.end, self),
+                })
         })
     }
 

crates/language_tools/src/syntax_tree_view.rs 🔗

@@ -128,13 +128,18 @@ impl SyntaxTreeView {
     fn editor_updated(&mut self, did_reparse: bool, cx: &mut ViewContext<Self>) -> Option<()> {
         // Find which excerpt the cursor is in, and the position within that excerpted buffer.
         let editor_state = self.editor.as_mut()?;
-        let (buffer, range, excerpt_id) = editor_state.editor.update(cx, |editor, cx| {
+        let snapshot = editor_state
+            .editor
+            .update(cx, |editor, cx| editor.snapshot(cx));
+        let (excerpt, buffer, range) = editor_state.editor.update(cx, |editor, cx| {
             let selection_range = editor.selections.last::<usize>(cx).range();
-            editor
-                .buffer()
-                .read(cx)
-                .range_to_buffer_ranges(selection_range, cx)
-                .pop()
+            let multi_buffer = editor.buffer().read(cx);
+            let (excerpt, range) = snapshot
+                .buffer_snapshot
+                .range_to_buffer_ranges(selection_range)
+                .pop()?;
+            let buffer = multi_buffer.buffer(excerpt.buffer_id()).unwrap().clone();
+            Some((excerpt, buffer, range))
         })?;
 
         // If the cursor has moved into a different excerpt, retrieve a new syntax layer
@@ -143,16 +148,16 @@ impl SyntaxTreeView {
             .active_buffer
             .get_or_insert_with(|| BufferState {
                 buffer: buffer.clone(),
-                excerpt_id,
+                excerpt_id: excerpt.id(),
                 active_layer: None,
             });
         let mut prev_layer = None;
         if did_reparse {
             prev_layer = buffer_state.active_layer.take();
         }
-        if buffer_state.buffer != buffer || buffer_state.excerpt_id != excerpt_id {
+        if buffer_state.buffer != buffer || buffer_state.excerpt_id != excerpt.id() {
             buffer_state.buffer = buffer.clone();
-            buffer_state.excerpt_id = excerpt_id;
+            buffer_state.excerpt_id = excerpt.id();
             buffer_state.active_layer = None;
         }
 

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -1667,42 +1667,6 @@ impl MultiBuffer {
         })
     }
 
-    pub fn range_to_buffer_ranges<T: ToOffset>(
-        &self,
-        range: Range<T>,
-        cx: &AppContext,
-    ) -> Vec<(Model<Buffer>, Range<usize>, ExcerptId)> {
-        let snapshot = self.read(cx);
-        let start = range.start.to_offset(&snapshot);
-        let end = range.end.to_offset(&snapshot);
-
-        let mut result = Vec::new();
-        let mut cursor = snapshot.excerpts.cursor::<usize>(&());
-        cursor.seek(&start, Bias::Right, &());
-        if cursor.item().is_none() {
-            cursor.prev(&());
-        }
-
-        while let Some(excerpt) = cursor.item() {
-            if *cursor.start() > end {
-                break;
-            }
-
-            let mut end_before_newline = cursor.end(&());
-            if excerpt.has_trailing_newline {
-                end_before_newline -= 1;
-            }
-            let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
-            let start = excerpt_start + (cmp::max(start, *cursor.start()) - *cursor.start());
-            let end = excerpt_start + (cmp::min(end, end_before_newline) - *cursor.start());
-            let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone();
-            result.push((buffer, start..end, excerpt.id));
-            cursor.next(&());
-        }
-
-        result
-    }
-
     pub fn remove_excerpts(
         &mut self,
         excerpt_ids: impl IntoIterator<Item = ExcerptId>,
@@ -3914,28 +3878,32 @@ impl MultiBufferSnapshot {
     where
         O: text::FromAnchor + 'a,
     {
-        self.as_singleton()
-            .into_iter()
-            .flat_map(move |(_, _, buffer)| buffer.diagnostic_group(group_id))
+        self.all_excerpts()
+            .flat_map(move |excerpt| excerpt.buffer().diagnostic_group(group_id))
     }
 
-    pub fn diagnostics_in_range<'a, T, O>(
+    pub fn diagnostics_in_range<'a, T>(
         &'a self,
         range: Range<T>,
         reversed: bool,
-    ) -> impl Iterator<Item = DiagnosticEntry<O>> + 'a
+    ) -> impl Iterator<Item = DiagnosticEntry<Anchor>> + 'a
     where
         T: 'a + ToOffset,
-        O: 'a + text::FromAnchor + Ord,
     {
-        self.as_singleton()
-            .into_iter()
-            .flat_map(move |(_, _, buffer)| {
-                buffer.diagnostics_in_range(
-                    range.start.to_offset(self)..range.end.to_offset(self),
-                    reversed,
-                )
-            })
+        let mut ranges = self.range_to_buffer_ranges(range);
+        if reversed {
+            ranges.reverse();
+        }
+        ranges.into_iter().flat_map(move |(excerpt, range)| {
+            let excerpt_id = excerpt.id();
+            excerpt.buffer().diagnostics_in_range(range, reversed).map(
+                move |DiagnosticEntry { diagnostic, range }| DiagnosticEntry {
+                    diagnostic,
+                    range: self.anchor_in_excerpt(excerpt_id, range.start).unwrap()
+                        ..self.anchor_in_excerpt(excerpt_id, range.end).unwrap(),
+                },
+            )
+        })
     }
 
     pub fn syntax_ancestor<T: ToOffset>(
@@ -4185,6 +4153,42 @@ impl MultiBufferSnapshot {
         })
     }
 
+    pub fn range_to_buffer_ranges<T: ToOffset>(
+        &self,
+        range: Range<T>,
+    ) -> Vec<(MultiBufferExcerpt<'_>, Range<usize>)> {
+        let start = range.start.to_offset(self);
+        let end = range.end.to_offset(self);
+
+        let mut result = Vec::new();
+        let mut cursor = self.excerpts.cursor::<(usize, Point)>(&());
+        cursor.seek(&start, Bias::Right, &());
+        if cursor.item().is_none() {
+            cursor.prev(&());
+        }
+
+        while let Some(excerpt) = cursor.item() {
+            if cursor.start().0 > end {
+                break;
+            }
+
+            let mut end_before_newline = cursor.end(&()).0;
+            if excerpt.has_trailing_newline {
+                end_before_newline -= 1;
+            }
+            let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
+            let start = excerpt_start + (cmp::max(start, cursor.start().0) - cursor.start().0);
+            let end = excerpt_start + (cmp::min(end, end_before_newline) - cursor.start().0);
+            result.push((
+                MultiBufferExcerpt::new(&excerpt, *cursor.start()),
+                start..end,
+            ));
+            cursor.next(&());
+        }
+
+        result
+    }
+
     /// Returns excerpts overlapping the given ranges. If range spans multiple excerpts returns one range for each excerpt
     ///
     /// The ranges are specified in the coordinate space of the multibuffer, not the individual excerpted buffers.
@@ -4664,6 +4668,10 @@ impl<'a> MultiBufferExcerpt<'a> {
         self.excerpt.id
     }
 
+    pub fn buffer_id(&self) -> BufferId {
+        self.excerpt.buffer_id
+    }
+
     pub fn start_anchor(&self) -> Anchor {
         Anchor {
             buffer_id: Some(self.excerpt.buffer_id),

crates/multi_buffer/src/multi_buffer_tests.rs 🔗

@@ -1234,14 +1234,13 @@ fn test_random_multibuffer(cx: &mut AppContext, mut rng: StdRng) {
                 start_ix..end_ix
             );
 
-            let excerpted_buffer_ranges = multibuffer
-                .read(cx)
-                .range_to_buffer_ranges(start_ix..end_ix, cx);
+            let snapshot = multibuffer.read(cx).snapshot(cx);
+            let excerpted_buffer_ranges = snapshot.range_to_buffer_ranges(start_ix..end_ix);
             let excerpted_buffers_text = excerpted_buffer_ranges
                 .iter()
-                .map(|(buffer, buffer_range, _)| {
-                    buffer
-                        .read(cx)
+                .map(|(excerpt, buffer_range)| {
+                    excerpt
+                        .buffer()
                         .text_for_range(buffer_range.clone())
                         .collect::<String>()
                 })

crates/text/src/text.rs 🔗

@@ -3047,6 +3047,12 @@ pub trait FromAnchor {
     fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self;
 }
 
+impl FromAnchor for Anchor {
+    fn from_anchor(anchor: &Anchor, _snapshot: &BufferSnapshot) -> Self {
+        *anchor
+    }
+}
+
 impl FromAnchor for Point {
     fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self {
         snapshot.summary_for_anchor(anchor)