diagnostics: Live update diagnostics view on edits

Lukas Wirth created

Change summary

Cargo.lock                                   |   1 
crates/diagnostics/Cargo.toml                |   1 
crates/diagnostics/src/buffer_diagnostics.rs |   4 
crates/diagnostics/src/diagnostics.rs        | 217 ++++++++++++++-------
crates/diagnostics/src/diagnostics_tests.rs  |  50 +++--
crates/multi_buffer/src/path_key.rs          |  11 +
6 files changed, 185 insertions(+), 99 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4936,6 +4936,7 @@ dependencies = [
  "editor",
  "gpui",
  "indoc",
+ "itertools 0.14.0",
  "language",
  "log",
  "lsp",

crates/diagnostics/Cargo.toml 🔗

@@ -34,6 +34,7 @@ theme.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true
+itertools.workspace = true
 
 [dev-dependencies]
 client = { workspace = true, features = ["test-support"] }

crates/diagnostics/src/buffer_diagnostics.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    DIAGNOSTICS_UPDATE_DELAY, IncludeWarnings, ToggleWarnings, context_range_for_entry,
+    DIAGNOSTICS_UPDATE_DEBOUNCE, IncludeWarnings, ToggleWarnings, context_range_for_entry,
     diagnostic_renderer::{DiagnosticBlock, DiagnosticRenderer},
     toolbar_controls::DiagnosticsToolbarEditor,
 };
@@ -283,7 +283,7 @@ impl BufferDiagnosticsEditor {
 
         self.update_excerpts_task = Some(cx.spawn_in(window, async move |editor, cx| {
             cx.background_executor()
-                .timer(DIAGNOSTICS_UPDATE_DELAY)
+                .timer(DIAGNOSTICS_UPDATE_DEBOUNCE)
                 .await;
 
             if let Some(buffer) = buffer {

crates/diagnostics/src/diagnostics.rs 🔗

@@ -9,7 +9,7 @@ mod diagnostics_tests;
 
 use anyhow::Result;
 use buffer_diagnostics::BufferDiagnosticsEditor;
-use collections::{BTreeSet, HashMap};
+use collections::{BTreeSet, HashMap, HashSet};
 use diagnostic_renderer::DiagnosticBlock;
 use editor::{
     Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
@@ -17,10 +17,11 @@ use editor::{
     multibuffer_context_lines,
 };
 use gpui::{
-    AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
-    Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
-    Subscription, Task, WeakEntity, Window, actions, div,
+    AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, FocusOutEvent,
+    Focusable, Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
+    Styled, Subscription, Task, WeakEntity, Window, actions, div,
 };
+use itertools::Itertools as _;
 use language::{
     Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, DiagnosticEntryRef, Point,
     ToTreeSitterPoint,
@@ -32,7 +33,7 @@ use project::{
 use settings::Settings;
 use std::{
     any::{Any, TypeId},
-    cmp::{self, Ordering},
+    cmp,
     ops::{Range, RangeInclusive},
     sync::Arc,
     time::Duration,
@@ -89,8 +90,8 @@ pub(crate) struct ProjectDiagnosticsEditor {
 
 impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
 
-const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
-const DIAGNOSTICS_SUMMARY_UPDATE_DELAY: Duration = Duration::from_millis(30);
+const DIAGNOSTICS_UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
+const DIAGNOSTICS_SUMMARY_UPDATE_DEBOUNCE: Duration = Duration::from_millis(30);
 
 impl Render for ProjectDiagnosticsEditor {
     fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
@@ -149,6 +150,12 @@ impl Render for ProjectDiagnosticsEditor {
     }
 }
 
+#[derive(PartialEq, Eq, Copy, Clone, Debug)]
+enum RetainExcerpts {
+    Yes,
+    No,
+}
+
 impl ProjectDiagnosticsEditor {
     pub fn register(
         workspace: &mut Workspace,
@@ -165,14 +172,21 @@ impl ProjectDiagnosticsEditor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
-        let project_event_subscription =
-            cx.subscribe_in(&project_handle, window, |this, _project, event, window, cx| match event {
+        let project_event_subscription = cx.subscribe_in(
+            &project_handle,
+            window,
+            |this, _project, event, window, cx| match event {
                 project::Event::DiskBasedDiagnosticsStarted { .. } => {
                     cx.notify();
                 }
                 project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
                     log::debug!("disk based diagnostics finished for server {language_server_id}");
-                    this.update_stale_excerpts(window, cx);
+                    this.close_diagnosticless_buffers(
+                        window,
+                        cx,
+                        this.editor.focus_handle(cx).contains_focused(window, cx)
+                            || this.focus_handle.contains_focused(window, cx),
+                    );
                 }
                 project::Event::DiagnosticsUpdated {
                     language_server_id,
@@ -181,34 +195,39 @@ impl ProjectDiagnosticsEditor {
                     this.paths_to_update.extend(paths.clone());
                     this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
                         cx.background_executor()
-                            .timer(DIAGNOSTICS_SUMMARY_UPDATE_DELAY)
+                            .timer(DIAGNOSTICS_SUMMARY_UPDATE_DEBOUNCE)
                             .await;
                         this.update(cx, |this, cx| {
                             this.update_diagnostic_summary(cx);
                         })
                         .log_err();
                     });
-                    cx.emit(EditorEvent::TitleChanged);
 
-                    if this.editor.focus_handle(cx).contains_focused(window, cx) || this.focus_handle.contains_focused(window, cx) {
-                        log::debug!("diagnostics updated for server {language_server_id}, paths {paths:?}. recording change");
-                    } else {
-                        log::debug!("diagnostics updated for server {language_server_id}, paths {paths:?}. updating excerpts");
-                        this.update_stale_excerpts(window, cx);
-                    }
+                    log::debug!(
+                        "diagnostics updated for server {language_server_id}, \
+                        paths {paths:?}. updating excerpts"
+                    );
+                    let focused = this.editor.focus_handle(cx).contains_focused(window, cx)
+                        || this.focus_handle.contains_focused(window, cx);
+                    this.update_stale_excerpts(
+                        if focused {
+                            RetainExcerpts::Yes
+                        } else {
+                            RetainExcerpts::No
+                        },
+                        window,
+                        cx,
+                    );
                 }
                 _ => {}
-            });
+            },
+        );
 
         let focus_handle = cx.focus_handle();
-        cx.on_focus_in(&focus_handle, window, |this, window, cx| {
-            this.focus_in(window, cx)
-        })
-        .detach();
-        cx.on_focus_out(&focus_handle, window, |this, _event, window, cx| {
-            this.focus_out(window, cx)
-        })
-        .detach();
+        cx.on_focus_in(&focus_handle, window, Self::focus_in)
+            .detach();
+        cx.on_focus_out(&focus_handle, window, Self::focus_out)
+            .detach();
 
         let excerpts = cx.new(|cx| MultiBuffer::new(project_handle.read(cx).capability()));
         let editor = cx.new(|cx| {
@@ -238,8 +257,11 @@ impl ProjectDiagnosticsEditor {
                             window.focus(&this.focus_handle);
                         }
                     }
-                    EditorEvent::Blurred => this.update_stale_excerpts(window, cx),
-                    EditorEvent::Saved => this.update_stale_excerpts(window, cx),
+                    EditorEvent::Blurred => this.close_diagnosticless_buffers(window, cx, false),
+                    EditorEvent::Saved => this.close_diagnosticless_buffers(window, cx, true),
+                    EditorEvent::SelectionsChanged { .. } => {
+                        this.close_diagnosticless_buffers(window, cx, true)
+                    }
                     _ => {}
                 }
             },
@@ -283,15 +305,58 @@ impl ProjectDiagnosticsEditor {
         this
     }
 
-    fn update_stale_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        if self.update_excerpts_task.is_some() || self.multibuffer.read(cx).is_dirty(cx) {
+    fn close_diagnosticless_buffers(
+        &mut self,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+        retain_selections: bool,
+    ) {
+        // todo: Retain only the dirty buffers
+        if self.multibuffer.read(cx).is_dirty(cx) {
             return;
         }
+        let buffer_ids = self.multibuffer.read(cx).all_buffer_ids();
+        let selected_buffers = self.editor.update(cx, |editor, cx| {
+            editor
+                .selections
+                .all_anchors(cx)
+                .iter()
+                .filter_map(|anchor| anchor.start.buffer_id)
+                .collect::<HashSet<_>>()
+        });
+        for buffer_id in buffer_ids {
+            if retain_selections && selected_buffers.contains(&buffer_id) {
+                continue;
+            }
+            if self
+                .blocks
+                .get(&buffer_id)
+                .is_none_or(|blocks| blocks.is_empty())
+            {
+                self.multibuffer.update(cx, |b, cx| {
+                    b.remove_excerpts_for_buffer(buffer_id, cx);
+                })
+            }
+        }
+    }
+
+    fn update_stale_excerpts(
+        &mut self,
+        mut retain_excerpts: RetainExcerpts,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        if self.update_excerpts_task.is_some() {
+            return;
+        }
+        if self.multibuffer.read(cx).is_dirty(cx) {
+            retain_excerpts = RetainExcerpts::Yes;
+        }
 
         let project_handle = self.project.clone();
         self.update_excerpts_task = Some(cx.spawn_in(window, async move |this, cx| {
             cx.background_executor()
-                .timer(DIAGNOSTICS_UPDATE_DELAY)
+                .timer(DIAGNOSTICS_UPDATE_DEBOUNCE)
                 .await;
             loop {
                 let Some(path) = this.update(cx, |this, cx| {
@@ -312,7 +377,7 @@ impl ProjectDiagnosticsEditor {
                     .log_err()
                 {
                     this.update_in(cx, |this, window, cx| {
-                        this.update_excerpts(buffer, window, cx)
+                        this.update_excerpts(buffer, retain_excerpts, window, cx)
                     })?
                     .await?;
                 }
@@ -378,10 +443,10 @@ impl ProjectDiagnosticsEditor {
         }
     }
 
-    fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+    fn focus_out(&mut self, _: FocusOutEvent, window: &mut Window, cx: &mut Context<Self>) {
         if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)
         {
-            self.update_stale_excerpts(window, cx);
+            self.close_diagnosticless_buffers(window, cx, false);
         }
     }
 
@@ -408,7 +473,7 @@ impl ProjectDiagnosticsEditor {
             self.paths_to_update = project_paths;
         });
 
-        self.update_stale_excerpts(window, cx);
+        self.update_stale_excerpts(RetainExcerpts::No, window, cx);
     }
 
     fn diagnostics_are_unchanged(
@@ -431,6 +496,7 @@ impl ProjectDiagnosticsEditor {
     fn update_excerpts(
         &mut self,
         buffer: Entity<Buffer>,
+        retain_excerpts: RetainExcerpts,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Task<Result<()>> {
@@ -497,24 +563,27 @@ impl ProjectDiagnosticsEditor {
                     )
                 })?;
 
-                for item in more {
-                    let i = blocks
-                        .binary_search_by(|probe| {
-                            probe
-                                .initial_range
-                                .start
-                                .cmp(&item.initial_range.start)
-                                .then(probe.initial_range.end.cmp(&item.initial_range.end))
-                                .then(Ordering::Greater)
-                        })
-                        .unwrap_or_else(|i| i);
-                    blocks.insert(i, item);
-                }
+                blocks.extend(more);
             }
 
-            let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
+            let mut excerpt_ranges: Vec<ExcerptRange<Point>> = match retain_excerpts {
+                RetainExcerpts::No => Vec::new(),
+                RetainExcerpts::Yes => this.update(cx, |this, cx| {
+                    this.multibuffer.update(cx, |multi_buffer, cx| {
+                        multi_buffer
+                            .excerpts_for_buffer(buffer_id, cx)
+                            .into_iter()
+                            .map(|(_, range)| ExcerptRange {
+                                context: range.context.to_point(&buffer_snapshot),
+                                primary: range.primary.to_point(&buffer_snapshot),
+                            })
+                            .collect()
+                    })
+                })?,
+            };
+            let mut result_blocks = vec![None; excerpt_ranges.len()];
             let context_lines = cx.update(|_, cx| multibuffer_context_lines(cx))?;
-            for b in blocks.iter() {
+            for b in blocks {
                 let excerpt_range = context_range_for_entry(
                     b.initial_range.clone(),
                     context_lines,
@@ -541,7 +610,8 @@ impl ProjectDiagnosticsEditor {
                         context: excerpt_range,
                         primary: b.initial_range.clone(),
                     },
-                )
+                );
+                result_blocks.insert(i, Some(b));
             }
 
             this.update_in(cx, |this, window, cx| {
@@ -562,7 +632,7 @@ impl ProjectDiagnosticsEditor {
                     )
                 });
                 #[cfg(test)]
-                let cloned_blocks = blocks.clone();
+                let cloned_blocks = result_blocks.clone();
 
                 if was_empty && let Some(anchor_range) = anchor_ranges.first() {
                     let range_to_select = anchor_range.start..anchor_range.start;
@@ -576,22 +646,20 @@ impl ProjectDiagnosticsEditor {
                     }
                 }
 
-                let editor_blocks =
-                    anchor_ranges
-                        .into_iter()
-                        .zip(blocks.into_iter())
-                        .map(|(anchor, block)| {
-                            let editor = this.editor.downgrade();
-                            BlockProperties {
-                                placement: BlockPlacement::Near(anchor.start),
-                                height: Some(1),
-                                style: BlockStyle::Flex,
-                                render: Arc::new(move |bcx| {
-                                    block.render_block(editor.clone(), bcx)
-                                }),
-                                priority: 1,
-                            }
-                        });
+                let editor_blocks = anchor_ranges
+                    .into_iter()
+                    .zip_eq(result_blocks.into_iter())
+                    .filter_map(|(anchor, block)| {
+                        let block = block?;
+                        let editor = this.editor.downgrade();
+                        Some(BlockProperties {
+                            placement: BlockPlacement::Near(anchor.start),
+                            height: Some(1),
+                            style: BlockStyle::Flex,
+                            render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)),
+                            priority: 1,
+                        })
+                    });
 
                 let block_ids = this.editor.update(cx, |editor, cx| {
                     editor.display_map.update(cx, |display_map, cx| {
@@ -601,7 +669,9 @@ impl ProjectDiagnosticsEditor {
 
                 #[cfg(test)]
                 {
-                    for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) {
+                    for (block_id, block) in
+                        block_ids.iter().zip(cloned_blocks.into_iter().flatten())
+                    {
                         let markdown = block.markdown.clone();
                         editor::test::set_block_content_for_tests(
                             &this.editor,
@@ -626,6 +696,7 @@ impl ProjectDiagnosticsEditor {
 
     fn update_diagnostic_summary(&mut self, cx: &mut Context<Self>) {
         self.summary = self.project.read(cx).diagnostic_summary(false, cx);
+        cx.emit(EditorEvent::TitleChanged);
     }
 }
 
@@ -1010,12 +1081,6 @@ async fn heuristic_syntactic_expand(
                                 return;
                             }
                         }
-
-                        log::info!(
-                            "Expanding to ancestor started on {} node\
-                            exceeding row limit of {max_row_count}.",
-                            node.grammar_name()
-                        );
                         *ancestor_range = Some(None);
                     }
                 })

crates/diagnostics/src/diagnostics_tests.rs 🔗

@@ -119,7 +119,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
     let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
 
     diagnostics
-        .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
+        .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
         .await;
 
     pretty_assertions::assert_eq!(
@@ -190,7 +190,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
     });
 
     diagnostics
-        .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
+        .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
         .await;
 
     pretty_assertions::assert_eq!(
@@ -277,7 +277,7 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
     });
 
     diagnostics
-        .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
+        .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
         .await;
 
     pretty_assertions::assert_eq!(
@@ -391,7 +391,7 @@ async fn test_diagnostics_with_folds(cx: &mut TestAppContext) {
 
     // Only the first language server's diagnostics are shown.
     cx.executor()
-        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
+        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
     cx.executor().run_until_parked();
     editor.update_in(cx, |editor, window, cx| {
         editor.fold_ranges(vec![Point::new(0, 0)..Point::new(3, 0)], false, window, cx);
@@ -490,7 +490,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
 
     // Only the first language server's diagnostics are shown.
     cx.executor()
-        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
+        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
     cx.executor().run_until_parked();
 
     pretty_assertions::assert_eq!(
@@ -530,7 +530,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
 
     // Both language server's diagnostics are shown.
     cx.executor()
-        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
+        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
     cx.executor().run_until_parked();
 
     pretty_assertions::assert_eq!(
@@ -587,7 +587,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
 
     // Only the first language server's diagnostics are updated.
     cx.executor()
-        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
+        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
     cx.executor().run_until_parked();
 
     pretty_assertions::assert_eq!(
@@ -629,7 +629,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
 
     // Both language servers' diagnostics are updated.
     cx.executor()
-        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
+        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
     cx.executor().run_until_parked();
 
     pretty_assertions::assert_eq!(
@@ -760,7 +760,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
                         .unwrap()
                 });
                 cx.executor()
-                    .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
+                    .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
 
                 cx.run_until_parked();
             }
@@ -769,7 +769,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
 
     log::info!("updating mutated diagnostics view");
     mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
-        diagnostics.update_stale_excerpts(window, cx)
+        diagnostics.update_stale_excerpts(RetainExcerpts::No, window, cx)
     });
 
     log::info!("constructing reference diagnostics view");
@@ -777,7 +777,7 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
         ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
     });
     cx.executor()
-        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
+        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
     cx.run_until_parked();
 
     let mutated_excerpts =
@@ -789,7 +789,12 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
 
     // The mutated view may contain more than the reference view as
     // we don't currently shrink excerpts when diagnostics were removed.
-    let mut ref_iter = reference_excerpts.lines().filter(|line| *line != "§ -----");
+    let mut ref_iter = reference_excerpts.lines().filter(|line| {
+        // ignore $ ---- and $ <file>.rs
+        !line.starts_with('§')
+            || line.starts_with("§ diagnostic")
+            || line.starts_with("§ related info")
+    });
     let mut next_ref_line = ref_iter.next();
     let mut skipped_block = false;
 
@@ -797,7 +802,12 @@ async fn test_random_diagnostics_blocks(cx: &mut TestAppContext, mut rng: StdRng
         if let Some(ref_line) = next_ref_line {
             if mut_line == ref_line {
                 next_ref_line = ref_iter.next();
-            } else if mut_line.contains('§') && mut_line != "§ -----" {
+            } else if mut_line.contains('§')
+                // ignore $ ---- and $ <file>.rs
+                && (!mut_line.starts_with('§')
+                    || mut_line.starts_with("§ diagnostic")
+                    || mut_line.starts_with("§ related info"))
+            {
                 skipped_block = true;
             }
         }
@@ -949,7 +959,7 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
                         .unwrap()
                 });
                 cx.executor()
-                    .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
+                    .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
 
                 cx.run_until_parked();
             }
@@ -958,11 +968,11 @@ async fn test_random_diagnostics_with_inlays(cx: &mut TestAppContext, mut rng: S
 
     log::info!("updating mutated diagnostics view");
     mutated_diagnostics.update_in(cx, |diagnostics, window, cx| {
-        diagnostics.update_stale_excerpts(window, cx)
+        diagnostics.update_stale_excerpts(RetainExcerpts::No, window, cx)
     });
 
     cx.executor()
-        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
+        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
     cx.run_until_parked();
 }
 
@@ -1427,7 +1437,7 @@ async fn test_diagnostics_with_code(cx: &mut TestAppContext) {
     let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
 
     diagnostics
-        .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
+        .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
         .await;
 
     // Verify that the diagnostic codes are displayed correctly
@@ -1704,7 +1714,7 @@ async fn test_buffer_diagnostics(cx: &mut TestAppContext) {
     // wait a little bit to ensure that the buffer diagnostic's editor content
     // is rendered.
     cx.executor()
-        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
+        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
 
     pretty_assertions::assert_eq!(
         editor_content_with_blocks(&editor, cx),
@@ -1837,7 +1847,7 @@ async fn test_buffer_diagnostics_without_warnings(cx: &mut TestAppContext) {
     // wait a little bit to ensure that the buffer diagnostic's editor content
     // is rendered.
     cx.executor()
-        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
+        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
 
     pretty_assertions::assert_eq!(
         editor_content_with_blocks(&editor, cx),
@@ -1971,7 +1981,7 @@ async fn test_buffer_diagnostics_multiple_servers(cx: &mut TestAppContext) {
     // wait a little bit to ensure that the buffer diagnostic's editor content
     // is rendered.
     cx.executor()
-        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
+        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
 
     pretty_assertions::assert_eq!(
         editor_content_with_blocks(&editor, cx),

crates/multi_buffer/src/path_key.rs 🔗

@@ -5,7 +5,7 @@ use gpui::{App, AppContext, Context, Entity};
 use itertools::Itertools;
 use language::{Buffer, BufferSnapshot};
 use rope::Point;
-use text::{Bias, OffsetRangeExt, locator::Locator};

+use text::{Bias, BufferId, OffsetRangeExt, locator::Locator};

 use util::{post_inc, rel_path::RelPath};
 
 use crate::{
@@ -152,6 +152,15 @@ impl MultiBuffer {
         }
     }
 
+    pub fn remove_excerpts_for_buffer(&mut self, buffer: BufferId, cx: &mut Context<Self>) {

+        self.remove_excerpts(

+            self.excerpts_for_buffer(buffer, cx)

+                .into_iter()

+                .map(|(excerpt, _)| excerpt),

+            cx,

+        );

+    }

+

     pub(super) fn expand_excerpts_with_paths(
         &mut self,
         ids: impl IntoIterator<Item = ExcerptId>,