Update block diagnostics (#28006)

Conrad Irwin , Kirill Bulatov , and Julia Ryan created

Release Notes:

- "Block" diagnostics (that show up in the diagnostics view, or when
using `f8`/`shift-f8`) are rendered more clearly
- `f8`/`shift-f8` now always go to the "next" or "prev" diagnostic,
regardless of the state of the editor

![Screenshot 2025-04-09 at 16 42
09](https://github.com/user-attachments/assets/ae6d2ff6-5183-4b74-89d0-fefee1aa11e3)

---------

Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>

Change summary

Cargo.lock                                    |   5 
crates/auto_update_helper/app-icon.ico        |   0 
crates/diagnostics/Cargo.toml                 |   6 
crates/diagnostics/src/diagnostic_renderer.rs | 302 +++++++
crates/diagnostics/src/diagnostics.rs         | 751 +++++------------
crates/diagnostics/src/diagnostics_tests.rs   | 858 +++++++++-----------
crates/editor/src/display_map/block_map.rs    |  14 
crates/editor/src/editor.rs                   | 560 ++++---------
crates/editor/src/editor_tests.rs             | 270 ------
crates/editor/src/element.rs                  | 133 +-
crates/editor/src/hover_popover.rs            |  30 
crates/editor/src/test.rs                     | 138 +++
crates/gpui/src/elements/text.rs              |  19 
crates/language/src/buffer.rs                 |   1 
crates/language/src/language.rs               |   8 
crates/markdown/src/markdown.rs               |  52 +
crates/multi_buffer/src/multi_buffer.rs       |  36 
17 files changed, 1,407 insertions(+), 1,776 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4315,19 +4315,24 @@ dependencies = [
  "anyhow",
  "client",
  "collections",
+ "component",
  "ctor",
  "editor",
  "env_logger 0.11.8",
  "gpui",
+ "indoc",
  "language",
+ "linkme",
  "log",
  "lsp",
+ "markdown",
  "pretty_assertions",
  "project",
  "rand 0.8.5",
  "serde",
  "serde_json",
  "settings",
+ "text",
  "theme",
  "ui",
  "unindent",

crates/diagnostics/Cargo.toml 🔗

@@ -15,17 +15,22 @@ doctest = false
 [dependencies]
 anyhow.workspace = true
 collections.workspace = true
+component.workspace = true
 ctor.workspace = true
 editor.workspace = true
 env_logger.workspace = true
 gpui.workspace = true
+indoc.workspace = true
 language.workspace = true
+linkme.workspace = true
 log.workspace = true
 lsp.workspace = true
+markdown.workspace = true
 project.workspace = true
 rand.workspace = true
 serde.workspace = true
 settings.workspace = true
+text.workspace = true
 theme.workspace = true
 ui.workspace = true
 util.workspace = true
@@ -37,6 +42,7 @@ client = { workspace = true, features = ["test-support"] }
 editor = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }
 language = { workspace = true, features = ["test-support"] }
+markdown = { workspace = true, features = ["test-support"] }
 lsp = { workspace = true, features = ["test-support"] }
 serde_json.workspace = true
 theme = { workspace = true, features = ["test-support"] }

crates/diagnostics/src/diagnostic_renderer.rs 🔗

@@ -0,0 +1,302 @@
+use std::{ops::Range, sync::Arc};
+
+use editor::{
+    Anchor, Editor, EditorSnapshot, ToOffset,
+    display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle},
+    hover_markdown_style,
+    scroll::Autoscroll,
+};
+use gpui::{AppContext, Entity, Focusable, WeakEntity};
+use language::{BufferId, DiagnosticEntry};
+use lsp::DiagnosticSeverity;
+use markdown::{Markdown, MarkdownElement};
+use settings::Settings;
+use text::{AnchorRangeExt, Point};
+use theme::ThemeSettings;
+use ui::{
+    ActiveTheme, AnyElement, App, Context, IntoElement, ParentElement, SharedString, Styled,
+    Window, div, px,
+};
+use util::maybe;
+
+use crate::ProjectDiagnosticsEditor;
+
+pub struct DiagnosticRenderer;
+
+impl DiagnosticRenderer {
+    pub fn diagnostic_blocks_for_group(
+        diagnostic_group: Vec<DiagnosticEntry<Point>>,
+        buffer_id: BufferId,
+        diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
+        cx: &mut App,
+    ) -> Vec<DiagnosticBlock> {
+        let Some(primary_ix) = diagnostic_group
+            .iter()
+            .position(|d| d.diagnostic.is_primary)
+        else {
+            return Vec::new();
+        };
+        let primary = diagnostic_group[primary_ix].clone();
+        let mut same_row = Vec::new();
+        let mut close = Vec::new();
+        let mut distant = Vec::new();
+        let group_id = primary.diagnostic.group_id;
+        for (ix, entry) in diagnostic_group.into_iter().enumerate() {
+            if entry.diagnostic.is_primary {
+                continue;
+            }
+            if entry.range.start.row == primary.range.start.row {
+                same_row.push(entry)
+            } else if entry.range.start.row.abs_diff(primary.range.start.row) < 5 {
+                close.push(entry)
+            } else {
+                distant.push((ix, entry))
+            }
+        }
+
+        let mut markdown =
+            Markdown::escape(&if let Some(source) = primary.diagnostic.source.as_ref() {
+                format!("{}: {}", source, primary.diagnostic.message)
+            } else {
+                primary.diagnostic.message
+            })
+            .to_string();
+        for entry in same_row {
+            markdown.push_str("\n- hint: ");
+            markdown.push_str(&Markdown::escape(&entry.diagnostic.message))
+        }
+
+        for (ix, entry) in &distant {
+            markdown.push_str("\n- hint: [");
+            markdown.push_str(&Markdown::escape(&entry.diagnostic.message));
+            markdown.push_str(&format!("](file://#diagnostic-{group_id}-{ix})\n",))
+        }
+
+        let mut results = vec![DiagnosticBlock {
+            initial_range: primary.range,
+            severity: primary.diagnostic.severity,
+            buffer_id,
+            diagnostics_editor: diagnostics_editor.clone(),
+            markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
+        }];
+
+        for entry in close {
+            let markdown = if let Some(source) = entry.diagnostic.source.as_ref() {
+                format!("{}: {}", source, entry.diagnostic.message)
+            } else {
+                entry.diagnostic.message
+            };
+            let markdown = Markdown::escape(&markdown).to_string();
+
+            results.push(DiagnosticBlock {
+                initial_range: entry.range,
+                severity: entry.diagnostic.severity,
+                buffer_id,
+                diagnostics_editor: diagnostics_editor.clone(),
+                markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
+            });
+        }
+
+        for (_, entry) in distant {
+            let markdown = if let Some(source) = entry.diagnostic.source.as_ref() {
+                format!("{}: {}", source, entry.diagnostic.message)
+            } else {
+                entry.diagnostic.message
+            };
+            let mut markdown = Markdown::escape(&markdown).to_string();
+            markdown.push_str(&format!(
+                " ([back](file://#diagnostic-{group_id}-{primary_ix}))"
+            ));
+            // problem: group-id changes...
+            //  - only an issue in diagnostics because caching
+
+            results.push(DiagnosticBlock {
+                initial_range: entry.range,
+                severity: entry.diagnostic.severity,
+                buffer_id,
+                diagnostics_editor: diagnostics_editor.clone(),
+                markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
+            });
+        }
+
+        results
+    }
+}
+
+impl editor::DiagnosticRenderer for DiagnosticRenderer {
+    fn render_group(
+        &self,
+        diagnostic_group: Vec<DiagnosticEntry<Point>>,
+        buffer_id: BufferId,
+        snapshot: EditorSnapshot,
+        editor: WeakEntity<Editor>,
+        cx: &mut App,
+    ) -> Vec<BlockProperties<Anchor>> {
+        let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
+        blocks
+            .into_iter()
+            .map(|block| {
+                let editor = editor.clone();
+                BlockProperties {
+                    placement: BlockPlacement::Near(
+                        snapshot
+                            .buffer_snapshot
+                            .anchor_after(block.initial_range.start),
+                    ),
+                    height: Some(1),
+                    style: BlockStyle::Flex,
+                    render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)),
+                    priority: 1,
+                }
+            })
+            .collect()
+    }
+}
+
+#[derive(Clone)]
+pub(crate) struct DiagnosticBlock {
+    pub(crate) initial_range: Range<Point>,
+    pub(crate) severity: DiagnosticSeverity,
+    pub(crate) buffer_id: BufferId,
+    pub(crate) markdown: Entity<Markdown>,
+    pub(crate) diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
+}
+
+impl DiagnosticBlock {
+    pub fn render_block(&self, editor: WeakEntity<Editor>, bcx: &BlockContext) -> AnyElement {
+        let cx = &bcx.app;
+        let status_colors = bcx.app.theme().status();
+        let max_width = px(600.);
+
+        let (background_color, border_color) = match self.severity {
+            DiagnosticSeverity::ERROR => (status_colors.error_background, status_colors.error),
+            DiagnosticSeverity::WARNING => {
+                (status_colors.warning_background, status_colors.warning)
+            }
+            DiagnosticSeverity::INFORMATION => (status_colors.info_background, status_colors.info),
+            DiagnosticSeverity::HINT => (status_colors.hint_background, status_colors.info),
+            _ => (status_colors.ignored_background, status_colors.ignored),
+        };
+        let settings = ThemeSettings::get_global(cx);
+        let editor_line_height = (settings.line_height() * settings.buffer_font_size(cx)).round();
+        let line_height = editor_line_height;
+        let buffer_id = self.buffer_id;
+        let diagnostics_editor = self.diagnostics_editor.clone();
+
+        div()
+            .border_l_2()
+            .px_2()
+            .line_height(line_height)
+            .bg(background_color)
+            .border_color(border_color)
+            .max_w(max_width)
+            .child(
+                MarkdownElement::new(self.markdown.clone(), hover_markdown_style(bcx.window, cx))
+                    .on_url_click({
+                        move |link, window, cx| {
+                            Self::open_link(
+                                editor.clone(),
+                                &diagnostics_editor,
+                                link,
+                                window,
+                                buffer_id,
+                                cx,
+                            )
+                        }
+                    }),
+            )
+            .into_any_element()
+    }
+
+    pub fn open_link(
+        editor: WeakEntity<Editor>,
+        diagnostics_editor: &Option<WeakEntity<ProjectDiagnosticsEditor>>,
+        link: SharedString,
+        window: &mut Window,
+        buffer_id: BufferId,
+        cx: &mut App,
+    ) {
+        editor
+            .update(cx, |editor, cx| {
+                let Some(diagnostic_link) = link.strip_prefix("file://#diagnostic-") else {
+                    editor::hover_popover::open_markdown_url(link, window, cx);
+                    return;
+                };
+                let Some((group_id, ix)) = maybe!({
+                    let (group_id, ix) = diagnostic_link.split_once('-')?;
+                    let group_id: usize = group_id.parse().ok()?;
+                    let ix: usize = ix.parse().ok()?;
+                    Some((group_id, ix))
+                }) else {
+                    return;
+                };
+
+                if let Some(diagnostics_editor) = diagnostics_editor {
+                    if let Some(diagnostic) = diagnostics_editor
+                        .update(cx, |diagnostics, _| {
+                            diagnostics
+                                .diagnostics
+                                .get(&buffer_id)
+                                .cloned()
+                                .unwrap_or_default()
+                                .into_iter()
+                                .filter(|d| d.diagnostic.group_id == group_id)
+                                .nth(ix)
+                        })
+                        .ok()
+                        .flatten()
+                    {
+                        let multibuffer = editor.buffer().read(cx);
+                        let Some(snapshot) = multibuffer
+                            .buffer(buffer_id)
+                            .map(|entity| entity.read(cx).snapshot())
+                        else {
+                            return;
+                        };
+
+                        for (excerpt_id, range) in multibuffer.excerpts_for_buffer(buffer_id, cx) {
+                            if range.context.overlaps(&diagnostic.range, &snapshot) {
+                                Self::jump_to(
+                                    editor,
+                                    Anchor::range_in_buffer(
+                                        excerpt_id,
+                                        buffer_id,
+                                        diagnostic.range,
+                                    ),
+                                    window,
+                                    cx,
+                                );
+                                return;
+                            }
+                        }
+                    }
+                } else {
+                    if let Some(diagnostic) = editor
+                        .snapshot(window, cx)
+                        .buffer_snapshot
+                        .diagnostic_group(buffer_id, group_id)
+                        .nth(ix)
+                    {
+                        Self::jump_to(editor, diagnostic.range, window, cx)
+                    }
+                };
+            })
+            .ok();
+    }
+
+    fn jump_to<T: ToOffset>(
+        editor: &mut Editor,
+        range: Range<T>,
+        window: &mut Window,
+        cx: &mut Context<Editor>,
+    ) {
+        let snapshot = &editor.buffer().read(cx).snapshot(cx);
+        let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);
+
+        editor.unfold_ranges(&[range.start..range.end], true, false, cx);
+        editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+            s.select_ranges([range.start..range.start]);
+        });
+        window.focus(&editor.focus_handle(cx));
+    }
+}

crates/diagnostics/src/diagnostics.rs 🔗

@@ -1,38 +1,39 @@
 pub mod items;
 mod toolbar_controls;
 
+mod diagnostic_renderer;
+
 #[cfg(test)]
 mod diagnostics_tests;
 
 use anyhow::Result;
-use collections::{BTreeSet, HashSet};
+use collections::{BTreeSet, HashMap};
+use diagnostic_renderer::DiagnosticBlock;
 use editor::{
-    Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset, diagnostic_block_renderer,
-    display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock},
-    highlight_diagnostic_message,
+    DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
+    display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
     scroll::Autoscroll,
 };
 use gpui::{
     AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
-    Global, HighlightStyle, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
-    Styled, StyledText, Subscription, Task, WeakEntity, Window, actions, div, svg,
+    Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
+    Subscription, Task, WeakEntity, Window, actions, div,
 };
 use language::{
-    Bias, Buffer, BufferRow, BufferSnapshot, Diagnostic, DiagnosticEntry, DiagnosticSeverity,
-    Point, Selection, SelectionGoal, ToTreeSitterPoint,
+    Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, Point, ToTreeSitterPoint,
 };
-use lsp::LanguageServerId;
+use lsp::DiagnosticSeverity;
 use project::{DiagnosticSummary, Project, ProjectPath, project_settings::ProjectSettings};
 use settings::Settings;
 use std::{
     any::{Any, TypeId},
     cmp,
     cmp::Ordering,
-    mem,
     ops::{Range, RangeInclusive},
     sync::Arc,
     time::Duration,
 };
+use text::{BufferId, OffsetRangeExt};
 use theme::ActiveTheme;
 pub use toolbar_controls::ToolbarControls;
 use ui::{Icon, IconName, Label, h_flex, prelude::*};
@@ -49,41 +50,28 @@ struct IncludeWarnings(bool);
 impl Global for IncludeWarnings {}
 
 pub fn init(cx: &mut App) {
+    editor::set_diagnostic_renderer(diagnostic_renderer::DiagnosticRenderer {}, cx);
     cx.observe_new(ProjectDiagnosticsEditor::register).detach();
 }
 
-struct ProjectDiagnosticsEditor {
+pub(crate) struct ProjectDiagnosticsEditor {
     project: Entity<Project>,
     workspace: WeakEntity<Workspace>,
     focus_handle: FocusHandle,
     editor: Entity<Editor>,
+    diagnostics: HashMap<BufferId, Vec<DiagnosticEntry<text::Anchor>>>,
+    blocks: HashMap<BufferId, Vec<CustomBlockId>>,
     summary: DiagnosticSummary,
-    excerpts: Entity<MultiBuffer>,
-    path_states: Vec<PathState>,
-    paths_to_update: BTreeSet<(ProjectPath, Option<LanguageServerId>)>,
+    multibuffer: Entity<MultiBuffer>,
+    paths_to_update: BTreeSet<ProjectPath>,
     include_warnings: bool,
-    context: u32,
     update_excerpts_task: Option<Task<Result<()>>>,
     _subscription: Subscription,
 }
 
-struct PathState {
-    path: ProjectPath,
-    diagnostic_groups: Vec<DiagnosticGroupState>,
-}
-
-struct DiagnosticGroupState {
-    language_server_id: LanguageServerId,
-    primary_diagnostic: DiagnosticEntry<language::Anchor>,
-    primary_excerpt_ix: usize,
-    excerpts: Vec<ExcerptId>,
-    blocks: HashSet<CustomBlockId>,
-    block_count: usize,
-}
-
 impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
 
-const DIAGNOSTICS_UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
+const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
 
 impl Render for ProjectDiagnosticsEditor {
     fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
@@ -149,8 +137,7 @@ impl ProjectDiagnosticsEditor {
         workspace.register_action(Self::deploy);
     }
 
-    fn new_with_context(
-        context: u32,
+    fn new(
         include_warnings: bool,
         project_handle: Entity<Project>,
         workspace: WeakEntity<Workspace>,
@@ -170,8 +157,7 @@ impl ProjectDiagnosticsEditor {
                     language_server_id,
                     path,
                 } => {
-                    this.paths_to_update
-                        .insert((path.clone(), Some(*language_server_id)));
+                    this.paths_to_update.insert(path.clone());
                     this.summary = project.read(cx).diagnostic_summary(false, cx);
                     cx.emit(EditorEvent::TitleChanged);
 
@@ -201,6 +187,7 @@ impl ProjectDiagnosticsEditor {
                 Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), window, cx);
             editor.set_vertical_scroll_margin(5, cx);
             editor.disable_inline_diagnostics();
+            editor.set_all_diagnostics_active(cx);
             editor
         });
         cx.subscribe_in(
@@ -210,7 +197,7 @@ impl ProjectDiagnosticsEditor {
                 cx.emit(event.clone());
                 match event {
                     EditorEvent::Focused => {
-                        if this.path_states.is_empty() {
+                        if this.multibuffer.read(cx).is_empty() {
                             window.focus(&this.focus_handle);
                         }
                     }
@@ -229,14 +216,14 @@ impl ProjectDiagnosticsEditor {
         let project = project_handle.read(cx);
         let mut this = Self {
             project: project_handle.clone(),
-            context,
             summary: project.diagnostic_summary(false, cx),
+            diagnostics: Default::default(),
+            blocks: Default::default(),
             include_warnings,
             workspace,
-            excerpts,
+            multibuffer: excerpts,
             focus_handle,
             editor,
-            path_states: Default::default(),
             paths_to_update: Default::default(),
             update_excerpts_task: None,
             _subscription: project_event_subscription,
@@ -252,15 +239,15 @@ impl ProjectDiagnosticsEditor {
         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_DEBOUNCE)
+                .timer(DIAGNOSTICS_UPDATE_DELAY)
                 .await;
             loop {
-                let Some((path, language_server_id)) = this.update(cx, |this, _| {
-                    let Some((path, language_server_id)) = this.paths_to_update.pop_first() else {
+                let Some(path) = this.update(cx, |this, _| {
+                    let Some(path) = this.paths_to_update.pop_first() else {
                         this.update_excerpts_task.take();
                         return None;
                     };
-                    Some((path, language_server_id))
+                    Some(path)
                 })?
                 else {
                     break;
@@ -272,7 +259,7 @@ impl ProjectDiagnosticsEditor {
                     .log_err()
                 {
                     this.update_in(cx, |this, window, cx| {
-                        this.update_excerpts(path, language_server_id, buffer, window, cx)
+                        this.update_excerpts(buffer, window, cx)
                     })?
                     .await?;
                 }
@@ -281,23 +268,6 @@ impl ProjectDiagnosticsEditor {
         }));
     }
 
-    fn new(
-        project_handle: Entity<Project>,
-        include_warnings: bool,
-        workspace: WeakEntity<Workspace>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        Self::new_with_context(
-            editor::DEFAULT_MULTIBUFFER_CONTEXT,
-            include_warnings,
-            project_handle,
-            workspace,
-            window,
-            cx,
-        )
-    }
-
     fn deploy(
         workspace: &mut Workspace,
         _: &Deploy,
@@ -319,8 +289,8 @@ impl ProjectDiagnosticsEditor {
 
             let diagnostics = cx.new(|cx| {
                 ProjectDiagnosticsEditor::new(
-                    workspace.project().clone(),
                     include_warnings,
+                    workspace.project().clone(),
                     workspace_handle,
                     window,
                     cx,
@@ -338,7 +308,7 @@ impl ProjectDiagnosticsEditor {
     }
 
     fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        if self.focus_handle.is_focused(window) && !self.path_states.is_empty() {
+        if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
             self.editor.focus_handle(cx).focus(window)
         }
     }
@@ -356,396 +326,212 @@ impl ProjectDiagnosticsEditor {
         self.project.update(cx, |project, cx| {
             let mut paths = project
                 .diagnostic_summaries(false, cx)
-                .map(|(path, _, _)| (path, None))
+                .map(|(path, _, _)| path)
                 .collect::<BTreeSet<_>>();
-            paths.extend(
-                self.path_states
-                    .iter()
-                    .map(|state| (state.path.clone(), None)),
-            );
-            let paths_to_update = std::mem::take(&mut self.paths_to_update);
-            paths.extend(paths_to_update.into_iter().map(|(path, _)| (path, None)));
+            self.multibuffer.update(cx, |multibuffer, cx| {
+                for buffer in multibuffer.all_buffers() {
+                    if let Some(file) = buffer.read(cx).file() {
+                        paths.insert(ProjectPath {
+                            path: file.path().clone(),
+                            worktree_id: file.worktree_id(cx),
+                        });
+                    }
+                }
+            });
             self.paths_to_update = paths;
         });
         self.update_stale_excerpts(window, cx);
     }
 
+    fn diagnostics_are_unchanged(
+        &self,
+        existing: &Vec<DiagnosticEntry<text::Anchor>>,
+        new: &Vec<DiagnosticEntry<text::Anchor>>,
+        snapshot: &BufferSnapshot,
+    ) -> bool {
+        if existing.len() != new.len() {
+            return false;
+        }
+        existing.iter().zip(new.iter()).all(|(existing, new)| {
+            existing.diagnostic.message == new.diagnostic.message
+                && existing.diagnostic.severity == new.diagnostic.severity
+                && existing.diagnostic.is_primary == new.diagnostic.is_primary
+                && existing.range.to_offset(snapshot) == new.range.to_offset(snapshot)
+        })
+    }
+
     fn update_excerpts(
         &mut self,
-        path_to_update: ProjectPath,
-        server_to_update: Option<LanguageServerId>,
         buffer: Entity<Buffer>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Task<Result<()>> {
-        let was_empty = self.path_states.is_empty();
-        let snapshot = buffer.read(cx).snapshot();
-        let path_ix = match self
-            .path_states
-            .binary_search_by_key(&&path_to_update, |e| &e.path)
-        {
-            Ok(ix) => ix,
-            Err(ix) => {
-                self.path_states.insert(
-                    ix,
-                    PathState {
-                        path: path_to_update.clone(),
-                        diagnostic_groups: Default::default(),
-                    },
-                );
-                ix
-            }
-        };
-        let mut prev_excerpt_id = if path_ix > 0 {
-            let prev_path_last_group = &self.path_states[path_ix - 1]
-                .diagnostic_groups
-                .last()
-                .unwrap();
-            *prev_path_last_group.excerpts.last().unwrap()
-        } else {
-            ExcerptId::min()
-        };
-
-        let mut new_group_ixs = Vec::new();
-        let mut blocks_to_add = Vec::new();
-        let mut blocks_to_remove = HashSet::default();
-        let mut first_excerpt_id = None;
+        let was_empty = self.multibuffer.read(cx).is_empty();
+        let buffer_snapshot = buffer.read(cx).snapshot();
+        let buffer_id = buffer_snapshot.remote_id();
         let max_severity = if self.include_warnings {
             DiagnosticSeverity::WARNING
         } else {
             DiagnosticSeverity::ERROR
         };
-        let excerpts = self.excerpts.clone().downgrade();
-        let context = self.context;
-        let editor = self.editor.clone().downgrade();
-        cx.spawn_in(window, async move |this, cx| {
-            let mut old_groups = this
-                .update(cx, |this, _| {
-                    mem::take(&mut this.path_states[path_ix].diagnostic_groups)
-                })?
-                .into_iter()
-                .enumerate()
-                .peekable();
-            let mut new_groups = snapshot
-                .diagnostic_groups(server_to_update)
-                .into_iter()
-                .filter(|(_, group)| {
-                    group.entries[group.primary_ix].diagnostic.severity <= max_severity
-                })
-                .peekable();
-            loop {
-                let mut to_insert = None;
-                let mut to_remove = None;
-                let mut to_keep = None;
-                match (old_groups.peek(), new_groups.peek()) {
-                    (None, None) => break,
-                    (None, Some(_)) => to_insert = new_groups.next(),
-                    (Some((_, old_group)), None) => {
-                        if server_to_update.map_or(true, |id| id == old_group.language_server_id) {
-                            to_remove = old_groups.next();
-                        } else {
-                            to_keep = old_groups.next();
-                        }
-                    }
-                    (Some((_, old_group)), Some((new_language_server_id, new_group))) => {
-                        let old_primary = &old_group.primary_diagnostic;
-                        let new_primary = &new_group.entries[new_group.primary_ix];
-                        match compare_diagnostics(old_primary, new_primary, &snapshot)
-                            .then_with(|| old_group.language_server_id.cmp(new_language_server_id))
-                        {
-                            Ordering::Less => {
-                                if server_to_update
-                                    .map_or(true, |id| id == old_group.language_server_id)
-                                {
-                                    to_remove = old_groups.next();
-                                } else {
-                                    to_keep = old_groups.next();
-                                }
-                            }
-                            Ordering::Equal => {
-                                to_keep = old_groups.next();
-                                new_groups.next();
-                            }
-                            Ordering::Greater => to_insert = new_groups.next(),
-                        }
-                    }
-                }
 
-                if let Some((language_server_id, group)) = to_insert {
-                    let mut group_state = DiagnosticGroupState {
-                        language_server_id,
-                        primary_diagnostic: group.entries[group.primary_ix].clone(),
-                        primary_excerpt_ix: 0,
-                        excerpts: Default::default(),
-                        blocks: Default::default(),
-                        block_count: 0,
-                    };
-                    let mut pending_range: Option<(Range<Point>, Range<Point>, usize)> = None;
-                    let mut is_first_excerpt_for_group = true;
-                    for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
-                        let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
-                        let expanded_range = if let Some(entry) = &resolved_entry {
-                            Some(
-                                context_range_for_entry(
-                                    entry.range.clone(),
-                                    context,
-                                    snapshot.clone(),
-                                    (**cx).clone(),
-                                )
-                                .await,
-                            )
-                        } else {
-                            None
-                        };
-                        if let Some((range, context_range, start_ix)) = &mut pending_range {
-                            if let Some(expanded_range) = expanded_range.clone() {
-                                // If the entries are overlapping or next to each-other, merge them into one excerpt.
-                                if context_range.end.row + 1 >= expanded_range.start.row {
-                                    context_range.end = context_range.end.max(expanded_range.end);
-                                    continue;
-                                }
-                            }
+        cx.spawn_in(window, async move |this, mut cx| {
+            let diagnostics = buffer_snapshot
+                .diagnostics_in_range::<_, text::Anchor>(
+                    Point::zero()..buffer_snapshot.max_point(),
+                    false,
+                )
+                .filter(|d| !(d.diagnostic.is_primary && d.diagnostic.is_unnecessary))
+                .collect::<Vec<_>>();
+            let unchanged = this.update(cx, |this, _| {
+                if this.diagnostics.get(&buffer_id).is_some_and(|existing| {
+                    this.diagnostics_are_unchanged(existing, &diagnostics, &buffer_snapshot)
+                }) {
+                    return true;
+                }
+                this.diagnostics.insert(buffer_id, diagnostics.clone());
+                return false;
+            })?;
+            if unchanged {
+                return Ok(());
+            }
 
-                            let excerpt_id = excerpts.update(cx, |excerpts, cx| {
-                                excerpts
-                                    .insert_excerpts_after(
-                                        prev_excerpt_id,
-                                        buffer.clone(),
-                                        [ExcerptRange {
-                                            context: context_range.clone(),
-                                            primary: range.clone(),
-                                        }],
-                                        cx,
-                                    )
-                                    .pop()
-                                    .unwrap()
-                            })?;
-
-                            prev_excerpt_id = excerpt_id;
-                            first_excerpt_id.get_or_insert(prev_excerpt_id);
-                            group_state.excerpts.push(excerpt_id);
-                            let header_position = (excerpt_id, language::Anchor::MIN);
-
-                            if is_first_excerpt_for_group {
-                                is_first_excerpt_for_group = false;
-                                let mut primary =
-                                    group.entries[group.primary_ix].diagnostic.clone();
-                                primary.message =
-                                    primary.message.split('\n').next().unwrap().to_string();
-                                group_state.block_count += 1;
-                                blocks_to_add.push(BlockProperties {
-                                    placement: BlockPlacement::Above(header_position),
-                                    height: Some(2),
-                                    style: BlockStyle::Sticky,
-                                    render: diagnostic_header_renderer(primary),
-                                    priority: 0,
-                                });
-                            }
+            let mut grouped: HashMap<usize, Vec<_>> = HashMap::default();
+            for entry in diagnostics {
+                grouped
+                    .entry(entry.diagnostic.group_id)
+                    .or_default()
+                    .push(DiagnosticEntry {
+                        range: entry.range.to_point(&buffer_snapshot),
+                        diagnostic: entry.diagnostic,
+                    })
+            }
+            let mut blocks: Vec<DiagnosticBlock> = Vec::new();
 
-                            for entry in &group.entries[*start_ix..ix] {
-                                let mut diagnostic = entry.diagnostic.clone();
-                                if diagnostic.is_primary {
-                                    group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
-                                    diagnostic.message =
-                                        entry.diagnostic.message.split('\n').skip(1).collect();
-                                }
+            for (_, group) in grouped {
+                let group_severity = group.iter().map(|d| d.diagnostic.severity).min();
+                if group_severity.is_none_or(|s| s > max_severity) {
+                    continue;
+                }
+                let more = cx.update(|_, cx| {
+                    crate::diagnostic_renderer::DiagnosticRenderer::diagnostic_blocks_for_group(
+                        group,
+                        buffer_snapshot.remote_id(),
+                        Some(this.clone()),
+                        cx,
+                    )
+                })?;
 
-                                if !diagnostic.message.is_empty() {
-                                    group_state.block_count += 1;
-                                    blocks_to_add.push(BlockProperties {
-                                        placement: BlockPlacement::Below((
-                                            excerpt_id,
-                                            entry.range.start,
-                                        )),
-                                        height: Some(
-                                            diagnostic.message.matches('\n').count() as u32 + 1,
-                                        ),
-                                        style: BlockStyle::Fixed,
-                                        render: diagnostic_block_renderer(diagnostic, None, true),
-                                        priority: 0,
-                                    });
-                                }
+                for item in more {
+                    let insert_pos = blocks
+                        .binary_search_by(|existing| {
+                            match existing.initial_range.start.cmp(&item.initial_range.start) {
+                                Ordering::Equal => item
+                                    .initial_range
+                                    .end
+                                    .cmp(&existing.initial_range.end)
+                                    .reverse(),
+                                other => other,
                             }
+                        })
+                        .unwrap_or_else(|pos| pos);
 
-                            pending_range.take();
-                        }
-
-                        if let Some(entry) = resolved_entry.as_ref() {
-                            let range = entry.range.clone();
-                            pending_range = Some((range, expanded_range.unwrap(), ix));
-                        }
-                    }
-
-                    this.update(cx, |this, _| {
-                        new_group_ixs.push(this.path_states[path_ix].diagnostic_groups.len());
-                        this.path_states[path_ix]
-                            .diagnostic_groups
-                            .push(group_state);
-                    })?;
-                } else if let Some((_, group_state)) = to_remove {
-                    excerpts.update(cx, |excerpts, cx| {
-                        excerpts.remove_excerpts(group_state.excerpts.iter().copied(), cx)
-                    })?;
-                    blocks_to_remove.extend(group_state.blocks.iter().copied());
-                } else if let Some((_, group_state)) = to_keep {
-                    prev_excerpt_id = *group_state.excerpts.last().unwrap();
-                    first_excerpt_id.get_or_insert(prev_excerpt_id);
-
-                    this.update(cx, |this, _| {
-                        this.path_states[path_ix]
-                            .diagnostic_groups
-                            .push(group_state)
-                    })?;
+                    blocks.insert(insert_pos, item);
                 }
             }
 
-            let excerpts_snapshot = excerpts.update(cx, |excerpts, cx| excerpts.snapshot(cx))?;
-            editor.update(cx, |editor, cx| {
-                editor.remove_blocks(blocks_to_remove, None, cx);
-                let block_ids = editor.insert_blocks(
-                    blocks_to_add.into_iter().flat_map(|block| {
-                        let placement = match block.placement {
-                            BlockPlacement::Above((excerpt_id, text_anchor)) => {
-                                BlockPlacement::Above(
-                                    excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?,
-                                )
-                            }
-                            BlockPlacement::Below((excerpt_id, text_anchor)) => {
-                                BlockPlacement::Below(
-                                    excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?,
-                                )
-                            }
-                            BlockPlacement::Replace(_) | BlockPlacement::Near(_) => {
-                                unreachable!(
-                                    "no Near/Replace block should have been pushed to blocks_to_add"
-                                )
-                            }
-                        };
-                        Some(BlockProperties {
-                            placement,
-                            height: block.height,
-                            style: block.style,
-                            render: block.render,
-                            priority: 0,
-                        })
-                    }),
-                    Some(Autoscroll::fit()),
-                    cx,
-                );
-
-                let mut block_ids = block_ids.into_iter();
-                this.update(cx, |this, _| {
-                    for ix in new_group_ixs {
-                        let group_state = &mut this.path_states[path_ix].diagnostic_groups[ix];
-                        group_state.blocks =
-                            block_ids.by_ref().take(group_state.block_count).collect();
-                    }
-                })?;
-                Result::<(), anyhow::Error>::Ok(())
-            })??;
+            let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
+            for b in blocks.iter() {
+                let excerpt_range = context_range_for_entry(
+                    b.initial_range.clone(),
+                    DEFAULT_MULTIBUFFER_CONTEXT,
+                    buffer_snapshot.clone(),
+                    &mut cx,
+                )
+                .await;
+                excerpt_ranges.push(ExcerptRange {
+                    context: excerpt_range,
+                    primary: b.initial_range.clone(),
+                })
+            }
 
             this.update_in(cx, |this, window, cx| {
-                if this.path_states[path_ix].diagnostic_groups.is_empty() {
-                    this.path_states.remove(path_ix);
+                if let Some(block_ids) = this.blocks.remove(&buffer_id) {
+                    this.editor.update(cx, |editor, cx| {
+                        editor.display_map.update(cx, |display_map, cx| {
+                            display_map.remove_blocks(block_ids.into_iter().collect(), cx)
+                        });
+                    })
                 }
+                let (anchor_ranges, _) = this.multibuffer.update(cx, |multi_buffer, cx| {
+                    multi_buffer.set_excerpt_ranges_for_path(
+                        PathKey::for_buffer(&buffer, cx),
+                        buffer.clone(),
+                        &buffer_snapshot,
+                        excerpt_ranges,
+                        cx,
+                    )
+                });
+                #[cfg(test)]
+                let cloned_blocks = blocks.clone();
 
-                this.editor.update(cx, |editor, cx| {
-                    let groups;
-                    let mut selections;
-                    let new_excerpt_ids_by_selection_id;
-                    if was_empty {
-                        groups = this.path_states.first()?.diagnostic_groups.as_slice();
-                        new_excerpt_ids_by_selection_id =
-                            [(0, ExcerptId::min())].into_iter().collect();
-                        selections = vec![Selection {
-                            id: 0,
-                            start: 0,
-                            end: 0,
-                            reversed: false,
-                            goal: SelectionGoal::None,
-                        }];
-                    } else {
-                        groups = this.path_states.get(path_ix)?.diagnostic_groups.as_slice();
-                        new_excerpt_ids_by_selection_id =
+                if was_empty {
+                    if let Some(anchor_range) = anchor_ranges.first() {
+                        let range_to_select = anchor_range.start..anchor_range.start;
+                        this.editor.update(cx, |editor, cx| {
                             editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
-                                s.refresh()
-                            });
-                        selections = editor.selections.all::<usize>(cx);
+                                s.select_anchor_ranges([range_to_select]);
+                            })
+                        })
                     }
+                }
 
-                    // If any selection has lost its position, move it to start of the next primary diagnostic.
-                    let snapshot = editor.snapshot(window, cx);
-                    for selection in &mut selections {
-                        if let Some(new_excerpt_id) =
-                            new_excerpt_ids_by_selection_id.get(&selection.id)
-                        {
-                            let group_ix = match groups.binary_search_by(|probe| {
-                                probe
-                                    .excerpts
-                                    .last()
-                                    .unwrap()
-                                    .cmp(new_excerpt_id, &snapshot.buffer_snapshot)
-                            }) {
-                                Ok(ix) | Err(ix) => ix,
-                            };
-                            if let Some(group) = groups.get(group_ix) {
-                                if let Some(offset) = excerpts_snapshot
-                                    .anchor_in_excerpt(
-                                        group.excerpts[group.primary_excerpt_ix],
-                                        group.primary_diagnostic.range.start,
-                                    )
-                                    .map(|anchor| anchor.to_offset(&excerpts_snapshot))
-                                {
-                                    selection.start = offset;
-                                    selection.end = offset;
-                                }
+                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,
                             }
-                        }
-                    }
-                    editor.change_selections(None, window, cx, |s| {
-                        s.select(selections);
-                    });
-                    Some(())
+                        });
+                let block_ids = this.editor.update(cx, |editor, cx| {
+                    editor.display_map.update(cx, |display_map, cx| {
+                        display_map.insert_blocks(editor_blocks, cx)
+                    })
                 });
-            })?;
 
-            this.update_in(cx, |this, window, cx| {
-                if this.path_states.is_empty() {
-                    if this.editor.focus_handle(cx).is_focused(window) {
-                        window.focus(&this.focus_handle);
+                #[cfg(test)]
+                {
+                    for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) {
+                        let markdown = block.markdown.clone();
+                        editor::test::set_block_content_for_tests(
+                            &this.editor,
+                            *block_id,
+                            cx,
+                            move |cx| {
+                                markdown::MarkdownElement::rendered_text(
+                                    markdown.clone(),
+                                    cx,
+                                    editor::hover_markdown_style,
+                                )
+                            },
+                        );
                     }
-                } else if this.focus_handle.is_focused(window) {
-                    let focus_handle = this.editor.focus_handle(cx);
-                    window.focus(&focus_handle);
                 }
 
-                #[cfg(test)]
-                this.check_invariants(cx);
-
-                cx.notify();
+                this.blocks.insert(buffer_id, block_ids);
+                cx.notify()
             })
         })
     }
-
-    #[cfg(test)]
-    fn check_invariants(&self, cx: &mut Context<Self>) {
-        let mut excerpts = Vec::new();
-        for (id, buffer, _) in self.excerpts.read(cx).snapshot(cx).excerpts() {
-            if let Some(file) = buffer.file() {
-                excerpts.push((id, file.path().clone()));
-            }
-        }
-
-        let mut prev_path = None;
-        for (_, path) in &excerpts {
-            if let Some(prev_path) = prev_path {
-                if path < prev_path {
-                    panic!("excerpts are not sorted by path {:?}", excerpts);
-                }
-            }
-            prev_path = Some(path);
-        }
-    }
 }
 
 impl Focusable for ProjectDiagnosticsEditor {
@@ -857,8 +643,8 @@ impl Item for ProjectDiagnosticsEditor {
     {
         Some(cx.new(|cx| {
             ProjectDiagnosticsEditor::new(
-                self.project.clone(),
                 self.include_warnings,
+                self.project.clone(),
                 self.workspace.clone(),
                 window,
                 cx,
@@ -867,15 +653,15 @@ impl Item for ProjectDiagnosticsEditor {
     }
 
     fn is_dirty(&self, cx: &App) -> bool {
-        self.excerpts.read(cx).is_dirty(cx)
+        self.multibuffer.read(cx).is_dirty(cx)
     }
 
     fn has_deleted_file(&self, cx: &App) -> bool {
-        self.excerpts.read(cx).has_deleted_file(cx)
+        self.multibuffer.read(cx).has_deleted_file(cx)
     }
 
     fn has_conflict(&self, cx: &App) -> bool {
-        self.excerpts.read(cx).has_conflict(cx)
+        self.multibuffer.read(cx).has_conflict(cx)
     }
 
     fn can_save(&self, _: &App) -> bool {
@@ -950,128 +736,31 @@ impl Item for ProjectDiagnosticsEditor {
     }
 }
 
-const DIAGNOSTIC_HEADER: &str = "diagnostic header";
-
-fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
-    let (message, code_ranges) = highlight_diagnostic_message(&diagnostic, None);
-    let message: SharedString = message;
-    Arc::new(move |cx| {
-        let color = cx.theme().colors();
-        let highlight_style: HighlightStyle = color.text_accent.into();
-
-        h_flex()
-            .id(DIAGNOSTIC_HEADER)
-            .block_mouse_down()
-            .h(2. * cx.window.line_height())
-            .w_full()
-            .px_9()
-            .justify_between()
-            .gap_2()
-            .child(
-                h_flex()
-                    .gap_2()
-                    .px_1()
-                    .rounded_sm()
-                    .bg(color.surface_background.opacity(0.5))
-                    .map(|stack| {
-                        stack.child(
-                            svg()
-                                .size(cx.window.text_style().font_size)
-                                .flex_none()
-                                .map(|icon| {
-                                    if diagnostic.severity == DiagnosticSeverity::ERROR {
-                                        icon.path(IconName::XCircle.path())
-                                            .text_color(Color::Error.color(cx))
-                                    } else {
-                                        icon.path(IconName::Warning.path())
-                                            .text_color(Color::Warning.color(cx))
-                                    }
-                                }),
-                        )
-                    })
-                    .child(
-                        h_flex()
-                            .gap_1()
-                            .child(
-                                StyledText::new(message.clone()).with_default_highlights(
-                                    &cx.window.text_style(),
-                                    code_ranges
-                                        .iter()
-                                        .map(|range| (range.clone(), highlight_style)),
-                                ),
-                            )
-                            .when_some(diagnostic.code.as_ref(), |stack, code| {
-                                stack.child(
-                                    div()
-                                        .child(SharedString::from(format!("({code:?})")))
-                                        .text_color(color.text_muted),
-                                )
-                            }),
-                    ),
-            )
-            .when_some(diagnostic.source.as_ref(), |stack, source| {
-                stack.child(
-                    div()
-                        .child(SharedString::from(source.clone()))
-                        .text_color(color.text_muted),
-                )
-            })
-            .into_any_element()
-    })
-}
-
-fn compare_diagnostics(
-    old: &DiagnosticEntry<language::Anchor>,
-    new: &DiagnosticEntry<language::Anchor>,
-    snapshot: &language::BufferSnapshot,
-) -> Ordering {
-    use language::ToOffset;
-
-    // The diagnostics may point to a previously open Buffer for this file.
-    if !old.range.start.is_valid(snapshot) || !new.range.start.is_valid(snapshot) {
-        return Ordering::Greater;
-    }
-
-    old.range
-        .start
-        .to_offset(snapshot)
-        .cmp(&new.range.start.to_offset(snapshot))
-        .then_with(|| {
-            old.range
-                .end
-                .to_offset(snapshot)
-                .cmp(&new.range.end.to_offset(snapshot))
-        })
-        .then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message))
-}
-
 const DIAGNOSTIC_EXPANSION_ROW_LIMIT: u32 = 32;
 
-fn context_range_for_entry(
+async fn context_range_for_entry(
     range: Range<Point>,
     context: u32,
     snapshot: BufferSnapshot,
-    cx: AsyncApp,
-) -> Task<Range<Point>> {
-    cx.spawn(async move |cx| {
-        if let Some(rows) = heuristic_syntactic_expand(
-            range.clone(),
-            DIAGNOSTIC_EXPANSION_ROW_LIMIT,
-            snapshot.clone(),
-            cx,
-        )
-        .await
-        {
-            return Range {
-                start: Point::new(*rows.start(), 0),
-                end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
-            };
-        }
-        Range {
-            start: Point::new(range.start.row.saturating_sub(context), 0),
-            end: snapshot.clip_point(Point::new(range.end.row + context, u32::MAX), Bias::Left),
-        }
-    })
+    cx: &mut AsyncApp,
+) -> Range<Point> {
+    if let Some(rows) = heuristic_syntactic_expand(
+        range.clone(),
+        DIAGNOSTIC_EXPANSION_ROW_LIMIT,
+        snapshot.clone(),
+        cx,
+    )
+    .await
+    {
+        return Range {
+            start: Point::new(*rows.start(), 0),
+            end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
+        };
+    }
+    Range {
+        start: Point::new(range.start.row.saturating_sub(context), 0),
+        end: snapshot.clip_point(Point::new(range.end.row + context, u32::MAX), Bias::Left),
+    }
 }
 
 /// Expands the input range using syntax information from TreeSitter. This expansion will be limited

crates/diagnostics/src/diagnostics_tests.rs 🔗

@@ -1,13 +1,15 @@
 use super::*;
-use collections::HashMap;
+use collections::{HashMap, HashSet};
 use editor::{
-    DisplayPoint, GutterDimensions,
-    display_map::{Block, BlockContext, DisplayRow},
-};
-use gpui::{AvailableSpace, Stateful, TestAppContext, VisualTestContext, px};
-use language::{
-    Diagnostic, DiagnosticEntry, DiagnosticSeverity, OffsetRangeExt, PointUtf16, Rope, Unclipped,
+    DisplayPoint,
+    actions::{GoToDiagnostic, GoToPreviousDiagnostic, MoveToBeginning},
+    display_map::DisplayRow,
+    test::{editor_content_with_blocks, editor_test_context::EditorTestContext},
 };
+use gpui::{TestAppContext, VisualTestContext};
+use indoc::indoc;
+use language::Rope;
+use lsp::LanguageServerId;
 use pretty_assertions::assert_eq;
 use project::FakeFs;
 use rand::{Rng, rngs::StdRng, seq::IteratorRandom as _};
@@ -64,163 +66,91 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
     let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
     let cx = &mut VisualTestContext::from_window(*window, cx);
     let workspace = window.root(cx).unwrap();
+    let uri = lsp::Url::from_file_path(path!("/test/main.rs")).unwrap();
 
     // Create some diagnostics
     lsp_store.update(cx, |lsp_store, cx| {
-        lsp_store
-            .update_diagnostic_entries(
-                language_server_id,
-                PathBuf::from(path!("/test/main.rs")),
-                None,
-                vec![
-                    DiagnosticEntry {
-                        range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
-                        diagnostic: Diagnostic {
-                            message:
-                                "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
-                                    .to_string(),
-                            severity: DiagnosticSeverity::INFORMATION,
-                            is_primary: false,
-                            is_disk_based: true,
-                            group_id: 1,
-                            ..Default::default()
-                        },
-                    },
-                    DiagnosticEntry {
-                        range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
-                        diagnostic: Diagnostic {
-                            message:
-                                "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
-                                    .to_string(),
-                            severity: DiagnosticSeverity::INFORMATION,
-                            is_primary: false,
-                            is_disk_based: true,
-                            group_id: 0,
-                            ..Default::default()
-                        },
-                    },
-                    DiagnosticEntry {
-                        range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
-                        diagnostic: Diagnostic {
-                            message: "value moved here".to_string(),
-                            severity: DiagnosticSeverity::INFORMATION,
-                            is_primary: false,
-                            is_disk_based: true,
-                            group_id: 1,
-                            ..Default::default()
-                        },
-                    },
-                    DiagnosticEntry {
-                        range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
-                        diagnostic: Diagnostic {
-                            message: "value moved here".to_string(),
-                            severity: DiagnosticSeverity::INFORMATION,
-                            is_primary: false,
-                            is_disk_based: true,
-                            group_id: 0,
-                            ..Default::default()
-                        },
-                    },
-                    DiagnosticEntry {
-                        range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
-                        diagnostic: Diagnostic {
-                            message: "use of moved value\nvalue used here after move".to_string(),
-                            severity: DiagnosticSeverity::ERROR,
-                            is_primary: true,
-                            is_disk_based: true,
-                            group_id: 0,
-                            ..Default::default()
-                        },
-                    },
-                    DiagnosticEntry {
-                        range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
-                        diagnostic: Diagnostic {
-                            message: "use of moved value\nvalue used here after move".to_string(),
-                            severity: DiagnosticSeverity::ERROR,
-                            is_primary: true,
-                            is_disk_based: true,
-                            group_id: 1,
-                            ..Default::default()
-                        },
-                    },
-                ],
-                cx,
-            )
-            .unwrap();
+        lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
+            uri: uri.clone(),
+            diagnostics: vec![lsp::Diagnostic{
+                range: lsp::Range::new(lsp::Position::new(7, 6),lsp::Position::new(7, 7)),
+                severity:Some(lsp::DiagnosticSeverity::ERROR),
+                message: "use of moved value\nvalue used here after move".to_string(),
+                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
+                    location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2,8),lsp::Position::new(2,9))),
+                    message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
+                },
+                lsp::DiagnosticRelatedInformation {
+                    location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4,6),lsp::Position::new(4,7))),
+                    message: "value moved here".to_string()
+                },
+                ]),
+                ..Default::default()
+            },
+            lsp::Diagnostic{
+                range: lsp::Range::new(lsp::Position::new(8, 6),lsp::Position::new(8, 7)),
+                severity:Some(lsp::DiagnosticSeverity::ERROR),
+                message: "use of moved value\nvalue used here after move".to_string(),
+                related_information: Some(vec![lsp::DiagnosticRelatedInformation {
+                    location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1,8),lsp::Position::new(1,9))),
+                    message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
+                },
+                lsp::DiagnosticRelatedInformation {
+                    location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3,6),lsp::Position::new(3,7))),
+                    message: "value moved here".to_string()
+                },
+                ]),
+                ..Default::default()
+            }
+            ],
+            version: None
+        }, &[], cx).unwrap();
     });
 
     // Open the project diagnostics view while there are already diagnostics.
     let diagnostics = window.build_entity(cx, |window, cx| {
-        ProjectDiagnosticsEditor::new_with_context(
-            1,
-            true,
-            project.clone(),
-            workspace.downgrade(),
-            window,
-            cx,
-        )
+        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
     });
     let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
 
     diagnostics
-        .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
+        .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
         .await;
-    assert_eq!(
-        editor_blocks(&editor, cx),
-        [
-            (DisplayRow(0), FILE_HEADER.into()),
-            (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
-            (DisplayRow(15), EXCERPT_HEADER.into()),
-            (DisplayRow(16), DIAGNOSTIC_HEADER.into()),
-            (DisplayRow(25), EXCERPT_HEADER.into()),
-        ]
-    );
-    assert_eq!(
-        editor.update(cx, |editor, cx| editor.display_text(cx)),
-        concat!(
-            //
-            // main.rs
-            //
-            "\n", // filename
-            "\n", // padding
-            // diagnostic group 1
-            "\n", // primary message
-            "\n", // padding
-            "    let x = vec![];\n",
-            "    let y = vec![];\n",
-            "\n", // supporting diagnostic
-            "    a(x);\n",
-            "    b(y);\n",
-            "\n", // supporting diagnostic
-            "    // comment 1\n",
-            "    // comment 2\n",
-            "    c(y);\n",
-            "\n", // supporting diagnostic
-            "    d(x);\n",
-            "\n", // context ellipsis
-            // diagnostic group 2
-            "\n", // primary message
-            "\n", // padding
-            "fn main() {\n",
-            "    let x = vec![];\n",
-            "\n", // supporting diagnostic
-            "    let y = vec![];\n",
-            "    a(x);\n",
-            "\n", // supporting diagnostic
-            "    b(y);\n",
-            "\n", // context ellipsis
-            "    c(y);\n",
-            "    d(x);\n",
-            "\n", // supporting diagnostic
-            "}",
-        )
+
+    pretty_assertions::assert_eq!(
+        editor_content_with_blocks(&editor, cx),
+        indoc::indoc! {
+            "§ main.rs
+             § -----
+             fn main() {
+                 let x = vec![];
+             § move occurs because `x` has type `Vec<char>`, which does not implement
+             § the `Copy` trait (back)
+                 let y = vec![];
+             § move occurs because `y` has type `Vec<char>`, which does not implement
+             § the `Copy` trait (back)
+                 a(x); § value moved here (back)
+                 b(y); § value moved here
+                 // comment 1
+                 // comment 2
+                 c(y);
+             § use of moved value value used here after move
+             § hint: move occurs because `y` has type `Vec<char>`, which does not
+             § implement the `Copy` trait
+                 d(x);
+             § use of moved value value used here after move
+             § hint: move occurs because `x` has type `Vec<char>`, which does not
+             § implement the `Copy` trait
+             § hint: value moved here
+             }"
+        }
     );
 
     // Cursor is at the first diagnostic
     editor.update(cx, |editor, cx| {
         assert_eq!(
             editor.selections.display_ranges(cx),
-            [DisplayPoint::new(DisplayRow(12), 6)..DisplayPoint::new(DisplayRow(12), 6)]
+            [DisplayPoint::new(DisplayRow(3), 8)..DisplayPoint::new(DisplayRow(3), 8)]
         );
     });
 
@@ -228,21 +158,22 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
     lsp_store.update(cx, |lsp_store, cx| {
         lsp_store.disk_based_diagnostics_started(language_server_id, cx);
         lsp_store
-            .update_diagnostic_entries(
+            .update_diagnostics(
                 language_server_id,
-                PathBuf::from(path!("/test/consts.rs")),
-                None,
-                vec![DiagnosticEntry {
-                    range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
-                    diagnostic: Diagnostic {
+                lsp::PublishDiagnosticsParams {
+                    uri: lsp::Url::from_file_path(path!("/test/consts.rs")).unwrap(),
+                    diagnostics: vec![lsp::Diagnostic {
+                        range: lsp::Range::new(
+                            lsp::Position::new(0, 15),
+                            lsp::Position::new(0, 15),
+                        ),
+                        severity: Some(lsp::DiagnosticSeverity::ERROR),
                         message: "mismatched types\nexpected `usize`, found `char`".to_string(),
-                        severity: DiagnosticSeverity::ERROR,
-                        is_primary: true,
-                        is_disk_based: true,
-                        group_id: 0,
                         ..Default::default()
-                    },
-                }],
+                    }],
+                    version: None,
+                },
+                &[],
                 cx,
             )
             .unwrap();
@@ -250,78 +181,48 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
     });
 
     diagnostics
-        .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
+        .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
         .await;
-    assert_eq!(
-        editor_blocks(&editor, cx),
-        [
-            (DisplayRow(0), FILE_HEADER.into()),
-            (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
-            (DisplayRow(7), FILE_HEADER.into()),
-            (DisplayRow(9), DIAGNOSTIC_HEADER.into()),
-            (DisplayRow(22), EXCERPT_HEADER.into()),
-            (DisplayRow(23), DIAGNOSTIC_HEADER.into()),
-            (DisplayRow(32), EXCERPT_HEADER.into()),
-        ]
-    );
 
-    assert_eq!(
-        editor.update(cx, |editor, cx| editor.display_text(cx)),
-        concat!(
-            //
-            // consts.rs
-            //
-            "\n", // filename
-            "\n", // padding
-            // diagnostic group 1
-            "\n", // primary message
-            "\n", // padding
-            "const a: i32 = 'a';\n",
-            "\n", // supporting diagnostic
-            "const b: i32 = c;\n",
-            //
-            // main.rs
-            //
-            "\n", // filename
-            "\n", // padding
-            // diagnostic group 1
-            "\n", // primary message
-            "\n", // padding
-            "    let x = vec![];\n",
-            "    let y = vec![];\n",
-            "\n", // supporting diagnostic
-            "    a(x);\n",
-            "    b(y);\n",
-            "\n", // supporting diagnostic
-            "    // comment 1\n",
-            "    // comment 2\n",
-            "    c(y);\n",
-            "\n", // supporting diagnostic
-            "    d(x);\n",
-            "\n", // collapsed context
-            // diagnostic group 2
-            "\n", // primary message
-            "\n", // filename
-            "fn main() {\n",
-            "    let x = vec![];\n",
-            "\n", // supporting diagnostic
-            "    let y = vec![];\n",
-            "    a(x);\n",
-            "\n", // supporting diagnostic
-            "    b(y);\n",
-            "\n", // context ellipsis
-            "    c(y);\n",
-            "    d(x);\n",
-            "\n", // supporting diagnostic
-            "}",
-        )
+    pretty_assertions::assert_eq!(
+        editor_content_with_blocks(&editor, cx),
+        indoc::indoc! {
+            "§ consts.rs
+             § -----
+             const a: i32 = 'a'; § mismatched types expected `usize`, found `char`
+             const b: i32 = c;
+
+             § main.rs
+             § -----
+             fn main() {
+                 let x = vec![];
+             § move occurs because `x` has type `Vec<char>`, which does not implement
+             § the `Copy` trait (back)
+                 let y = vec![];
+             § move occurs because `y` has type `Vec<char>`, which does not implement
+             § the `Copy` trait (back)
+                 a(x); § value moved here (back)
+                 b(y); § value moved here
+                 // comment 1
+                 // comment 2
+                 c(y);
+             § use of moved value value used here after move
+             § hint: move occurs because `y` has type `Vec<char>`, which does not
+             § implement the `Copy` trait
+                 d(x);
+             § use of moved value value used here after move
+             § hint: move occurs because `x` has type `Vec<char>`, which does not
+             § implement the `Copy` trait
+             § hint: value moved here
+             }"
+        }
     );
 
     // Cursor keeps its position.
     editor.update(cx, |editor, cx| {
         assert_eq!(
             editor.selections.display_ranges(cx),
-            [DisplayPoint::new(DisplayRow(19), 6)..DisplayPoint::new(DisplayRow(19), 6)]
+            [DisplayPoint::new(DisplayRow(8), 8)..DisplayPoint::new(DisplayRow(8), 8)]
         );
     });
 
@@ -329,34 +230,33 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
     lsp_store.update(cx, |lsp_store, cx| {
         lsp_store.disk_based_diagnostics_started(language_server_id, cx);
         lsp_store
-            .update_diagnostic_entries(
+            .update_diagnostics(
                 language_server_id,
-                PathBuf::from(path!("/test/consts.rs")),
-                None,
-                vec![
-                    DiagnosticEntry {
-                        range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
-                        diagnostic: Diagnostic {
+                lsp::PublishDiagnosticsParams {
+                    uri: lsp::Url::from_file_path(path!("/test/consts.rs")).unwrap(),
+                    diagnostics: vec![
+                        lsp::Diagnostic {
+                            range: lsp::Range::new(
+                                lsp::Position::new(0, 15),
+                                lsp::Position::new(0, 15),
+                            ),
+                            severity: Some(lsp::DiagnosticSeverity::ERROR),
                             message: "mismatched types\nexpected `usize`, found `char`".to_string(),
-                            severity: DiagnosticSeverity::ERROR,
-                            is_primary: true,
-                            is_disk_based: true,
-                            group_id: 0,
                             ..Default::default()
                         },
-                    },
-                    DiagnosticEntry {
-                        range: Unclipped(PointUtf16::new(1, 15))..Unclipped(PointUtf16::new(1, 15)),
-                        diagnostic: Diagnostic {
+                        lsp::Diagnostic {
+                            range: lsp::Range::new(
+                                lsp::Position::new(1, 15),
+                                lsp::Position::new(1, 15),
+                            ),
+                            severity: Some(lsp::DiagnosticSeverity::ERROR),
                             message: "unresolved name `c`".to_string(),
-                            severity: DiagnosticSeverity::ERROR,
-                            is_primary: true,
-                            is_disk_based: true,
-                            group_id: 1,
                             ..Default::default()
                         },
-                    },
-                ],
+                    ],
+                    version: None,
+                },
+                &[],
                 cx,
             )
             .unwrap();
@@ -364,80 +264,148 @@ async fn test_diagnostics(cx: &mut TestAppContext) {
     });
 
     diagnostics
-        .next_notification(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10), cx)
+        .next_notification(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10), cx)
         .await;
-    assert_eq!(
-        editor_blocks(&editor, cx),
-        [
-            (DisplayRow(0), FILE_HEADER.into()),
-            (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
-            (DisplayRow(7), EXCERPT_HEADER.into()),
-            (DisplayRow(8), DIAGNOSTIC_HEADER.into()),
-            (DisplayRow(13), FILE_HEADER.into()),
-            (DisplayRow(15), DIAGNOSTIC_HEADER.into()),
-            (DisplayRow(28), EXCERPT_HEADER.into()),
-            (DisplayRow(29), DIAGNOSTIC_HEADER.into()),
-            (DisplayRow(38), EXCERPT_HEADER.into()),
-        ]
+
+    pretty_assertions::assert_eq!(
+        editor_content_with_blocks(&editor, cx),
+        indoc::indoc! {
+            "§ consts.rs
+             § -----
+             const a: i32 = 'a'; § mismatched types expected `usize`, found `char`
+             const b: i32 = c; § unresolved name `c`
+
+             § main.rs
+             § -----
+             fn main() {
+                 let x = vec![];
+             § move occurs because `x` has type `Vec<char>`, which does not implement
+             § the `Copy` trait (back)
+                 let y = vec![];
+             § move occurs because `y` has type `Vec<char>`, which does not implement
+             § the `Copy` trait (back)
+                 a(x); § value moved here (back)
+                 b(y); § value moved here
+                 // comment 1
+                 // comment 2
+                 c(y);
+             § use of moved value value used here after move
+             § hint: move occurs because `y` has type `Vec<char>`, which does not
+             § implement the `Copy` trait
+                 d(x);
+             § use of moved value value used here after move
+             § hint: move occurs because `x` has type `Vec<char>`, which does not
+             § implement the `Copy` trait
+             § hint: value moved here
+             }"
+        }
+    );
+}
+
+#[gpui::test]
+async fn test_diagnostics_with_folds(cx: &mut TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        path!("/test"),
+        json!({
+            "main.js": "
+            function test() {
+                return 1
+            };
+
+            tset();
+            ".unindent()
+        }),
+    )
+    .await;
+
+    let server_id_1 = LanguageServerId(100);
+    let server_id_2 = LanguageServerId(101);
+    let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
+    let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
+    let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
+    let cx = &mut VisualTestContext::from_window(*window, cx);
+    let workspace = window.root(cx).unwrap();
+
+    let diagnostics = window.build_entity(cx, |window, cx| {
+        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
+    });
+    let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
+
+    // Two language servers start updating diagnostics
+    lsp_store.update(cx, |lsp_store, cx| {
+        lsp_store.disk_based_diagnostics_started(server_id_1, cx);
+        lsp_store.disk_based_diagnostics_started(server_id_2, cx);
+        lsp_store
+            .update_diagnostics(
+                server_id_1,
+                lsp::PublishDiagnosticsParams {
+                    uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
+                    diagnostics: vec![lsp::Diagnostic {
+                        range: lsp::Range::new(lsp::Position::new(4, 0), lsp::Position::new(4, 4)),
+                        severity: Some(lsp::DiagnosticSeverity::WARNING),
+                        message: "no method `tset`".to_string(),
+                        related_information: Some(vec![lsp::DiagnosticRelatedInformation {
+                            location: lsp::Location::new(
+                                lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
+                                lsp::Range::new(
+                                    lsp::Position::new(0, 9),
+                                    lsp::Position::new(0, 13),
+                                ),
+                            ),
+                            message: "method `test` defined here".to_string(),
+                        }]),
+                        ..Default::default()
+                    }],
+                    version: None,
+                },
+                &[],
+                cx,
+            )
+            .unwrap();
+    });
+
+    // The first language server finishes
+    lsp_store.update(cx, |lsp_store, cx| {
+        lsp_store.disk_based_diagnostics_finished(server_id_1, cx);
+    });
+
+    // Only the first language server's diagnostics are shown.
+    cx.executor()
+        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + 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);
+    });
+
+    pretty_assertions::assert_eq!(
+        editor_content_with_blocks(&editor, cx),
+        indoc::indoc! {
+            "§ main.js
+             § -----
+             ⋯
+
+             tset(); § no method `tset`"
+        }
     );
 
-    assert_eq!(
-        editor.update(cx, |editor, cx| editor.display_text(cx)),
-        concat!(
-            //
-            // consts.rs
-            //
-            "\n", // filename
-            "\n", // padding
-            // diagnostic group 1
-            "\n", // primary message
-            "\n", // padding
-            "const a: i32 = 'a';\n",
-            "\n", // supporting diagnostic
-            "const b: i32 = c;\n",
-            "\n", // context ellipsis
-            // diagnostic group 2
-            "\n", // primary message
-            "\n", // padding
-            "const a: i32 = 'a';\n",
-            "const b: i32 = c;\n",
-            "\n", // supporting diagnostic
-            //
-            // main.rs
-            //
-            "\n", // filename
-            "\n", // padding
-            // diagnostic group 1
-            "\n", // primary message
-            "\n", // padding
-            "    let x = vec![];\n",
-            "    let y = vec![];\n",
-            "\n", // supporting diagnostic
-            "    a(x);\n",
-            "    b(y);\n",
-            "\n", // supporting diagnostic
-            "    // comment 1\n",
-            "    // comment 2\n",
-            "    c(y);\n",
-            "\n", // supporting diagnostic
-            "    d(x);\n",
-            "\n", // context ellipsis
-            // diagnostic group 2
-            "\n", // primary message
-            "\n", // filename
-            "fn main() {\n",
-            "    let x = vec![];\n",
-            "\n", // supporting diagnostic
-            "    let y = vec![];\n",
-            "    a(x);\n",
-            "\n", // supporting diagnostic
-            "    b(y);\n",
-            "\n", // context ellipsis
-            "    c(y);\n",
-            "    d(x);\n",
-            "\n", // supporting diagnostic
-            "}",
-        )
+    editor.update(cx, |editor, cx| {
+        editor.unfold_ranges(&[Point::new(0, 0)..Point::new(3, 0)], false, false, cx);
+    });
+
+    pretty_assertions::assert_eq!(
+        editor_content_with_blocks(&editor, cx),
+        indoc::indoc! {
+            "§ main.js
+             § -----
+             function test() { § method `test` defined here
+                 return 1
+             };
+
+             tset(); § no method `tset`"
+        }
     );
 }
 
@@ -469,14 +437,7 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
     let workspace = window.root(cx).unwrap();
 
     let diagnostics = window.build_entity(cx, |window, cx| {
-        ProjectDiagnosticsEditor::new_with_context(
-            1,
-            true,
-            project.clone(),
-            workspace.downgrade(),
-            window,
-            cx,
-        )
+        ProjectDiagnosticsEditor::new(true, project.clone(), workspace.downgrade(), window, cx)
     });
     let editor = diagnostics.update(cx, |diagnostics, _| diagnostics.editor.clone());
 
@@ -485,21 +446,19 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
         lsp_store.disk_based_diagnostics_started(server_id_1, cx);
         lsp_store.disk_based_diagnostics_started(server_id_2, cx);
         lsp_store
-            .update_diagnostic_entries(
+            .update_diagnostics(
                 server_id_1,
-                PathBuf::from(path!("/test/main.js")),
-                None,
-                vec![DiagnosticEntry {
-                    range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
-                    diagnostic: Diagnostic {
+                lsp::PublishDiagnosticsParams {
+                    uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
+                    diagnostics: vec![lsp::Diagnostic {
+                        range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 1)),
+                        severity: Some(lsp::DiagnosticSeverity::WARNING),
                         message: "error 1".to_string(),
-                        severity: DiagnosticSeverity::WARNING,
-                        is_primary: true,
-                        is_disk_based: true,
-                        group_id: 1,
                         ..Default::default()
-                    },
-                }],
+                    }],
+                    version: None,
+                },
+                &[],
                 cx,
             )
             .unwrap();
@@ -512,46 +471,36 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
 
     // Only the first language server's diagnostics are shown.
     cx.executor()
-        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
+        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
     cx.executor().run_until_parked();
-    assert_eq!(
-        editor_blocks(&editor, cx),
-        [
-            (DisplayRow(0), FILE_HEADER.into()),
-            (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
-        ]
-    );
-    assert_eq!(
-        editor.update(cx, |editor, cx| editor.display_text(cx)),
-        concat!(
-            "\n", // filename
-            "\n", // padding
-            // diagnostic group 1
-            "\n",     // primary message
-            "\n",     // padding
-            "a();\n", //
-            "b();",
-        )
+
+    pretty_assertions::assert_eq!(
+        editor_content_with_blocks(&editor, cx),
+        indoc::indoc! {
+            "§ main.js
+             § -----
+             a(); § error 1
+             b();
+             c();"
+        }
     );
 
     // The second language server finishes
     lsp_store.update(cx, |lsp_store, cx| {
         lsp_store
-            .update_diagnostic_entries(
+            .update_diagnostics(
                 server_id_2,
-                PathBuf::from(path!("/test/main.js")),
-                None,
-                vec![DiagnosticEntry {
-                    range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
-                    diagnostic: Diagnostic {
+                lsp::PublishDiagnosticsParams {
+                    uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
+                    diagnostics: vec![lsp::Diagnostic {
+                        range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 1)),
+                        severity: Some(lsp::DiagnosticSeverity::ERROR),
                         message: "warning 1".to_string(),
-                        severity: DiagnosticSeverity::ERROR,
-                        is_primary: true,
-                        is_disk_based: true,
-                        group_id: 2,
                         ..Default::default()
-                    },
-                }],
+                    }],
+                    version: None,
+                },
+                &[],
                 cx,
             )
             .unwrap();
@@ -560,35 +509,19 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
 
     // Both language server's diagnostics are shown.
     cx.executor()
-        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
+        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
     cx.executor().run_until_parked();
-    assert_eq!(
-        editor_blocks(&editor, cx),
-        [
-            (DisplayRow(0), FILE_HEADER.into()),
-            (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
-            (DisplayRow(6), EXCERPT_HEADER.into()),
-            (DisplayRow(7), DIAGNOSTIC_HEADER.into()),
-        ]
-    );
-    assert_eq!(
-        editor.update(cx, |editor, cx| editor.display_text(cx)),
-        concat!(
-            "\n", // filename
-            "\n", // padding
-            // diagnostic group 1
-            "\n",     // primary message
-            "\n",     // padding
-            "a();\n", // location
-            "b();\n", //
-            "\n",     // collapsed context
-            // diagnostic group 2
-            "\n",     // primary message
-            "\n",     // padding
-            "a();\n", // context
-            "b();\n", //
-            "c();",   // context
-        )
+
+    pretty_assertions::assert_eq!(
+        editor_content_with_blocks(&editor, cx),
+        indoc::indoc! {
+            "§ main.js
+             § -----
+             a(); § error 1
+             b(); § warning 1
+             c();
+             d();"
+        }
     );
 
     // Both language servers start updating diagnostics, and the first server finishes.
@@ -596,30 +529,31 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
         lsp_store.disk_based_diagnostics_started(server_id_1, cx);
         lsp_store.disk_based_diagnostics_started(server_id_2, cx);
         lsp_store
-            .update_diagnostic_entries(
+            .update_diagnostics(
                 server_id_1,
-                PathBuf::from(path!("/test/main.js")),
-                None,
-                vec![DiagnosticEntry {
-                    range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
-                    diagnostic: Diagnostic {
+                lsp::PublishDiagnosticsParams {
+                    uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
+                    diagnostics: vec![lsp::Diagnostic {
+                        range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 1)),
+                        severity: Some(lsp::DiagnosticSeverity::WARNING),
                         message: "warning 2".to_string(),
-                        severity: DiagnosticSeverity::WARNING,
-                        is_primary: true,
-                        is_disk_based: true,
-                        group_id: 1,
                         ..Default::default()
-                    },
-                }],
+                    }],
+                    version: None,
+                },
+                &[],
                 cx,
             )
             .unwrap();
         lsp_store
-            .update_diagnostic_entries(
+            .update_diagnostics(
                 server_id_2,
-                PathBuf::from(path!("/test/main.rs")),
-                None,
-                vec![],
+                lsp::PublishDiagnosticsParams {
+                    uri: lsp::Url::from_file_path(path!("/test/main.rs")).unwrap(),
+                    diagnostics: vec![],
+                    version: None,
+                },
+                &[],
                 cx,
             )
             .unwrap();
@@ -628,56 +562,38 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
 
     // Only the first language server's diagnostics are updated.
     cx.executor()
-        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
+        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
     cx.executor().run_until_parked();
-    assert_eq!(
-        editor_blocks(&editor, cx),
-        [
-            (DisplayRow(0), FILE_HEADER.into()),
-            (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
-            (DisplayRow(7), EXCERPT_HEADER.into()),
-            (DisplayRow(8), DIAGNOSTIC_HEADER.into()),
-        ]
-    );
-    assert_eq!(
-        editor.update(cx, |editor, cx| editor.display_text(cx)),
-        concat!(
-            "\n", // filename
-            "\n", // padding
-            // diagnostic group 1
-            "\n",     // primary message
-            "\n",     // padding
-            "a();\n", // location
-            "b();\n", //
-            "c();\n", // context
-            "\n",     // collapsed context
-            // diagnostic group 2
-            "\n",     // primary message
-            "\n",     // padding
-            "b();\n", // context
-            "c();\n", //
-            "d();",   // context
-        )
+
+    pretty_assertions::assert_eq!(
+        editor_content_with_blocks(&editor, cx),
+        indoc::indoc! {
+            "§ main.js
+             § -----
+             a();
+             b(); § warning 1
+             c(); § warning 2
+             d();
+             e();"
+        }
     );
 
     // The second language server finishes.
     lsp_store.update(cx, |lsp_store, cx| {
         lsp_store
-            .update_diagnostic_entries(
+            .update_diagnostics(
                 server_id_2,
-                PathBuf::from(path!("/test/main.js")),
-                None,
-                vec![DiagnosticEntry {
-                    range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
-                    diagnostic: Diagnostic {
+                lsp::PublishDiagnosticsParams {
+                    uri: lsp::Url::from_file_path(path!("/test/main.js")).unwrap(),
+                    diagnostics: vec![lsp::Diagnostic {
+                        range: lsp::Range::new(lsp::Position::new(3, 0), lsp::Position::new(3, 1)),
+                        severity: Some(lsp::DiagnosticSeverity::WARNING),
                         message: "warning 2".to_string(),
-                        severity: DiagnosticSeverity::WARNING,
-                        is_primary: true,
-                        is_disk_based: true,
-                        group_id: 1,
                         ..Default::default()
-                    },
-                }],
+                    }],
+                    version: None,
+                },
+                &[],
                 cx,
             )
             .unwrap();
@@ -686,36 +602,20 @@ async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
 
     // Both language servers' diagnostics are updated.
     cx.executor()
-        .advance_clock(DIAGNOSTICS_UPDATE_DEBOUNCE + Duration::from_millis(10));
+        .advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
     cx.executor().run_until_parked();
-    assert_eq!(
-        editor_blocks(&editor, cx),
-        [
-            (DisplayRow(0), FILE_HEADER.into()),
-            (DisplayRow(2), DIAGNOSTIC_HEADER.into()),
-            (DisplayRow(7), EXCERPT_HEADER.into()),
-            (DisplayRow(8), DIAGNOSTIC_HEADER.into()),
-        ]
-    );
-    assert_eq!(
-        editor.update(cx, |editor, cx| editor.display_text(cx)),
-        concat!(
-            "\n", // filename
-            "\n", // padding
-            // diagnostic group 1
-            "\n",     // primary message
-            "\n",     // padding
-            "b();\n", // location
-            "c();\n", //
-            "d();\n", // context
-            "\n",     // collapsed context
-            // diagnostic group 2
-            "\n",     // primary message
-            "\n",     // padding
-            "c();\n", // context
-            "d();\n", //
-            "e();",   // context
-        )
+
+    pretty_assertions::assert_eq!(
+        editor_content_with_blocks(&editor, cx),
+        indoc::indoc! {
+            "§ main.js
+                 § -----
+                 a();
+                 b();
+                 c(); § warning 2
+                 d(); § warning 2
+                 e();"
+        }
     );
 }
 

crates/editor/src/display_map/block_map.rs 🔗

@@ -61,7 +61,7 @@ pub struct BlockSnapshot {
 }
 
 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
-pub struct CustomBlockId(usize);
+pub struct CustomBlockId(pub usize);
 
 impl From<CustomBlockId> for ElementId {
     fn from(val: CustomBlockId) -> Self {
@@ -89,7 +89,7 @@ pub enum BlockPlacement<T> {
 }
 
 impl<T> BlockPlacement<T> {
-    fn start(&self) -> &T {
+    pub fn start(&self) -> &T {
         match self {
             BlockPlacement::Above(position) => position,
             BlockPlacement::Below(position) => position,
@@ -187,14 +187,15 @@ impl BlockPlacement<Anchor> {
 }
 
 pub struct CustomBlock {
-    id: CustomBlockId,
-    placement: BlockPlacement<Anchor>,
-    height: Option<u32>,
+    pub id: CustomBlockId,
+    pub placement: BlockPlacement<Anchor>,
+    pub height: Option<u32>,
     style: BlockStyle,
     render: Arc<Mutex<RenderBlock>>,
     priority: usize,
 }
 
+#[derive(Clone)]
 pub struct BlockProperties<P> {
     pub placement: BlockPlacement<P>,
     // None if the block takes up no space
@@ -686,6 +687,9 @@ impl BlockMap {
                         rows_before_block = position.0 - new_transforms.summary().input_rows;
                     }
                     BlockPlacement::Near(position) | BlockPlacement::Below(position) => {
+                        if position.0 + 1 < new_transforms.summary().input_rows {
+                            continue;
+                        }
                         rows_before_block = (position.0 + 1) - new_transforms.summary().input_rows;
                     }
                     BlockPlacement::Replace(range) => {

crates/editor/src/editor.rs 🔗

@@ -23,7 +23,7 @@ mod element;
 mod git;
 mod highlight_matching_bracket;
 mod hover_links;
-mod hover_popover;
+pub mod hover_popover;
 mod indent_guides;
 mod inlay_hint_cache;
 pub mod items;
@@ -88,10 +88,9 @@ use gpui::{
     ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter,
     FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla,
     KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render,
-    SharedString, Size, Stateful, Styled, StyledText, Subscription, Task, TextStyle,
-    TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity,
-    WeakFocusHandle, Window, div, impl_actions, point, prelude::*, pulsating_between, px, relative,
-    size,
+    SharedString, Size, Stateful, Styled, Subscription, Task, TextStyle, TextStyleRefinement,
+    UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window,
+    div, impl_actions, point, prelude::*, pulsating_between, px, relative, size,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file};
@@ -105,7 +104,7 @@ pub use items::MAX_TAB_TITLE_LEN;
 use itertools::Itertools;
 use language::{
     AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel,
-    CursorShape, Diagnostic, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText,
+    CursorShape, DiagnosticEntry, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText,
     IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject,
     TransactionId, TreeSitterOptions, WordsQuery,
     language_settings::{
@@ -143,12 +142,12 @@ use language::BufferSnapshot;
 pub use lsp_ext::lsp_tasks;
 use movement::TextLayoutDetails;
 pub use multi_buffer::{
-    Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, RowInfo,
-    ToOffset, ToPoint,
+    Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, PathKey,
+    RowInfo, ToOffset, ToPoint,
 };
 use multi_buffer::{
     ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow,
-    MultiOrSingleBufferOffsetRange, PathKey, ToOffsetUtf16,
+    MultiOrSingleBufferOffsetRange, ToOffsetUtf16,
 };
 use parking_lot::Mutex;
 use project::{
@@ -356,6 +355,24 @@ pub fn set_blame_renderer(renderer: impl BlameRenderer + 'static, cx: &mut App)
     cx.set_global(GlobalBlameRenderer(Arc::new(renderer)));
 }
 
+pub trait DiagnosticRenderer {
+    fn render_group(
+        &self,
+        diagnostic_group: Vec<DiagnosticEntry<Point>>,
+        buffer_id: BufferId,
+        snapshot: EditorSnapshot,
+        editor: WeakEntity<Editor>,
+        cx: &mut App,
+    ) -> Vec<BlockProperties<Anchor>>;
+}
+
+pub(crate) struct GlobalDiagnosticRenderer(pub Arc<dyn DiagnosticRenderer>);
+
+impl gpui::Global for GlobalDiagnosticRenderer {}
+pub fn set_diagnostic_renderer(renderer: impl DiagnosticRenderer + 'static, cx: &mut App) {
+    cx.set_global(GlobalDiagnosticRenderer(Arc::new(renderer)));
+}
+
 pub struct SearchWithinRange;
 
 trait InvalidationRegion {
@@ -701,7 +718,7 @@ pub struct Editor {
     snippet_stack: InvalidationStack<SnippetState>,
     select_syntax_node_history: SelectSyntaxNodeHistory,
     ime_transaction: Option<TransactionId>,
-    active_diagnostics: Option<ActiveDiagnosticGroup>,
+    active_diagnostics: ActiveDiagnostic,
     show_inline_diagnostics: bool,
     inline_diagnostics_update: Task<()>,
     inline_diagnostics_enabled: bool,
@@ -1074,12 +1091,19 @@ struct RegisteredInlineCompletionProvider {
 }
 
 #[derive(Debug, PartialEq, Eq)]
-struct ActiveDiagnosticGroup {
-    primary_range: Range<Anchor>,
-    primary_message: String,
-    group_id: usize,
-    blocks: HashMap<CustomBlockId, Diagnostic>,
-    is_valid: bool,
+pub struct ActiveDiagnosticGroup {
+    pub active_range: Range<Anchor>,
+    pub active_message: String,
+    pub group_id: usize,
+    pub blocks: HashSet<CustomBlockId>,
+}
+
+#[derive(Debug, PartialEq, Eq)]
+#[allow(clippy::large_enum_variant)]
+pub(crate) enum ActiveDiagnostic {
+    None,
+    All,
+    Group(ActiveDiagnosticGroup),
 }
 
 #[derive(Serialize, Deserialize, Clone, Debug)]
@@ -1475,7 +1499,7 @@ impl Editor {
             snippet_stack: Default::default(),
             select_syntax_node_history: SelectSyntaxNodeHistory::default(),
             ime_transaction: Default::default(),
-            active_diagnostics: None,
+            active_diagnostics: ActiveDiagnostic::None,
             show_inline_diagnostics: ProjectSettings::get_global(cx).diagnostics.inline.enabled,
             inline_diagnostics_update: Task::ready(()),
             inline_diagnostics: Vec::new(),
@@ -3076,7 +3100,7 @@ impl Editor {
             return true;
         }
 
-        if self.mode.is_full() && self.active_diagnostics.is_some() {
+        if self.mode.is_full() && matches!(self.active_diagnostics, ActiveDiagnostic::Group(_)) {
             self.dismiss_diagnostics(cx);
             return true;
         }
@@ -13052,7 +13076,7 @@ impl Editor {
         });
     }
 
-    fn go_to_diagnostic(
+    pub fn go_to_diagnostic(
         &mut self,
         _: &GoToDiagnostic,
         window: &mut Window,
@@ -13062,7 +13086,7 @@ impl Editor {
         self.go_to_diagnostic_impl(Direction::Next, window, cx)
     }
 
-    fn go_to_prev_diagnostic(
+    pub fn go_to_prev_diagnostic(
         &mut self,
         _: &GoToPreviousDiagnostic,
         window: &mut Window,
@@ -13080,137 +13104,76 @@ impl Editor {
     ) {
         let buffer = self.buffer.read(cx).snapshot(cx);
         let selection = self.selections.newest::<usize>(cx);
-        // If there is an active Diagnostic Popover jump to its diagnostic instead.
-        if direction == Direction::Next {
-            if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() {
-                let Some(buffer_id) = popover.local_diagnostic.range.start.buffer_id else {
-                    return;
-                };
-                self.activate_diagnostics(
-                    buffer_id,
-                    popover.local_diagnostic.diagnostic.group_id,
-                    window,
-                    cx,
-                );
-                if let Some(active_diagnostics) = self.active_diagnostics.as_ref() {
-                    let primary_range_start = active_diagnostics.primary_range.start;
-                    self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
-                        let mut new_selection = s.newest_anchor().clone();
-                        new_selection.collapse_to(primary_range_start, SelectionGoal::None);
-                        s.select_anchors(vec![new_selection.clone()]);
-                    });
-                    self.refresh_inline_completion(false, true, window, cx);
-                }
-                return;
+
+        let mut active_group_id = None;
+        if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics {
+            if active_group.active_range.start.to_offset(&buffer) == selection.start {
+                active_group_id = Some(active_group.group_id);
             }
         }
 
-        let active_group_id = self
-            .active_diagnostics
-            .as_ref()
-            .map(|active_group| active_group.group_id);
-        let active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| {
-            active_diagnostics
-                .primary_range
-                .to_offset(&buffer)
-                .to_inclusive()
-        });
-        let search_start = if let Some(active_primary_range) = active_primary_range.as_ref() {
-            if active_primary_range.contains(&selection.head()) {
-                *active_primary_range.start()
-            } else {
-                selection.head()
-            }
-        } else {
-            selection.head()
-        };
+        fn filtered(
+            snapshot: EditorSnapshot,
+            diagnostics: impl Iterator<Item = DiagnosticEntry<usize>>,
+        ) -> impl Iterator<Item = DiagnosticEntry<usize>> {
+            diagnostics
+                .filter(|entry| entry.range.start != entry.range.end)
+                .filter(|entry| !entry.diagnostic.is_unnecessary)
+                .filter(move |entry| !snapshot.intersects_fold(entry.range.start))
+        }
 
         let snapshot = self.snapshot(window, cx);
-        let primary_diagnostics_before = buffer
-            .diagnostics_in_range::<usize>(0..search_start)
-            .filter(|entry| entry.diagnostic.is_primary)
-            .filter(|entry| entry.range.start != entry.range.end)
-            .filter(|entry| entry.diagnostic.severity <= DiagnosticSeverity::WARNING)
-            .filter(|entry| !snapshot.intersects_fold(entry.range.start))
-            .collect::<Vec<_>>();
-        let last_same_group_diagnostic_before = active_group_id.and_then(|active_group_id| {
-            primary_diagnostics_before
-                .iter()
-                .position(|entry| entry.diagnostic.group_id == active_group_id)
-        });
+        let before = filtered(
+            snapshot.clone(),
+            buffer
+                .diagnostics_in_range(0..selection.start)
+                .filter(|entry| entry.range.start <= selection.start),
+        );
+        let after = filtered(
+            snapshot,
+            buffer
+                .diagnostics_in_range(selection.start..buffer.len())
+                .filter(|entry| entry.range.start >= selection.start),
+        );
 
-        let primary_diagnostics_after = buffer
-            .diagnostics_in_range::<usize>(search_start..buffer.len())
-            .filter(|entry| entry.diagnostic.is_primary)
-            .filter(|entry| entry.range.start != entry.range.end)
-            .filter(|entry| entry.diagnostic.severity <= DiagnosticSeverity::WARNING)
-            .filter(|diagnostic| !snapshot.intersects_fold(diagnostic.range.start))
-            .collect::<Vec<_>>();
-        let last_same_group_diagnostic_after = active_group_id.and_then(|active_group_id| {
-            primary_diagnostics_after
-                .iter()
-                .enumerate()
-                .rev()
-                .find_map(|(i, entry)| {
-                    if entry.diagnostic.group_id == active_group_id {
-                        Some(i)
-                    } else {
-                        None
+        let mut found: Option<DiagnosticEntry<usize>> = None;
+        if direction == Direction::Prev {
+            'outer: for prev_diagnostics in [before.collect::<Vec<_>>(), after.collect::<Vec<_>>()]
+            {
+                for diagnostic in prev_diagnostics.into_iter().rev() {
+                    if diagnostic.range.start != selection.start
+                        || active_group_id
+                            .is_some_and(|active| diagnostic.diagnostic.group_id < active)
+                    {
+                        found = Some(diagnostic);
+                        break 'outer;
                     }
-                })
-        });
-
-        let next_primary_diagnostic = match direction {
-            Direction::Prev => primary_diagnostics_before
-                .iter()
-                .take(last_same_group_diagnostic_before.unwrap_or(usize::MAX))
-                .rev()
-                .next(),
-            Direction::Next => primary_diagnostics_after
-                .iter()
-                .skip(
-                    last_same_group_diagnostic_after
-                        .map(|index| index + 1)
-                        .unwrap_or(0),
-                )
-                .next(),
+                }
+            }
+        } else {
+            for diagnostic in after.chain(before) {
+                if diagnostic.range.start != selection.start
+                    || active_group_id.is_some_and(|active| diagnostic.diagnostic.group_id > active)
+                {
+                    found = Some(diagnostic);
+                    break;
+                }
+            }
+        }
+        let Some(next_diagnostic) = found else {
+            return;
         };
 
-        // Cycle around to the start of the buffer, potentially moving back to the start of
-        // the currently active diagnostic.
-        let cycle_around = || match direction {
-            Direction::Prev => primary_diagnostics_after
-                .iter()
-                .rev()
-                .chain(primary_diagnostics_before.iter().rev())
-                .next(),
-            Direction::Next => primary_diagnostics_before
-                .iter()
-                .chain(primary_diagnostics_after.iter())
-                .next(),
+        let Some(buffer_id) = buffer.anchor_after(next_diagnostic.range.start).buffer_id else {
+            return;
         };
-
-        if let Some((primary_range, group_id)) = next_primary_diagnostic
-            .or_else(cycle_around)
-            .map(|entry| (&entry.range, entry.diagnostic.group_id))
-        {
-            let Some(buffer_id) = buffer.anchor_after(primary_range.start).buffer_id else {
-                return;
-            };
-            self.activate_diagnostics(buffer_id, group_id, window, cx);
-            if self.active_diagnostics.is_some() {
-                self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
-                    s.select(vec![Selection {
-                        id: selection.id,
-                        start: primary_range.start,
-                        end: primary_range.start,
-                        reversed: false,
-                        goal: SelectionGoal::None,
-                    }]);
-                });
-                self.refresh_inline_completion(false, true, window, cx);
-            }
-        }
+        self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+            s.select_ranges(vec![
+                next_diagnostic.range.start..next_diagnostic.range.start,
+            ])
+        });
+        self.activate_diagnostics(buffer_id, next_diagnostic, window, cx);
+        self.refresh_inline_completion(false, true, window, cx);
     }
 
     fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context<Self>) {
@@ -14502,110 +14465,91 @@ impl Editor {
     }
 
     fn refresh_active_diagnostics(&mut self, cx: &mut Context<Editor>) {
-        if let Some(active_diagnostics) = self.active_diagnostics.as_mut() {
+        if let ActiveDiagnostic::Group(active_diagnostics) = &mut self.active_diagnostics {
             let buffer = self.buffer.read(cx).snapshot(cx);
-            let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer);
-            let primary_range_end = active_diagnostics.primary_range.end.to_offset(&buffer);
+            let primary_range_start = active_diagnostics.active_range.start.to_offset(&buffer);
+            let primary_range_end = active_diagnostics.active_range.end.to_offset(&buffer);
             let is_valid = buffer
                 .diagnostics_in_range::<usize>(primary_range_start..primary_range_end)
                 .any(|entry| {
                     entry.diagnostic.is_primary
                         && !entry.range.is_empty()
                         && entry.range.start == primary_range_start
-                        && entry.diagnostic.message == active_diagnostics.primary_message
+                        && entry.diagnostic.message == active_diagnostics.active_message
                 });
 
-            if is_valid != active_diagnostics.is_valid {
-                active_diagnostics.is_valid = is_valid;
-                if is_valid {
-                    let mut new_styles = HashMap::default();
-                    for (block_id, diagnostic) in &active_diagnostics.blocks {
-                        new_styles.insert(
-                            *block_id,
-                            diagnostic_block_renderer(diagnostic.clone(), None, true),
-                        );
-                    }
-                    self.display_map.update(cx, |display_map, _cx| {
-                        display_map.replace_blocks(new_styles);
-                    });
-                } else {
-                    self.dismiss_diagnostics(cx);
-                }
+            if !is_valid {
+                self.dismiss_diagnostics(cx);
             }
         }
     }
 
+    pub fn active_diagnostic_group(&self) -> Option<&ActiveDiagnosticGroup> {
+        match &self.active_diagnostics {
+            ActiveDiagnostic::Group(group) => Some(group),
+            _ => None,
+        }
+    }
+
+    pub fn set_all_diagnostics_active(&mut self, cx: &mut Context<Self>) {
+        self.dismiss_diagnostics(cx);
+        self.active_diagnostics = ActiveDiagnostic::All;
+    }
+
     fn activate_diagnostics(
         &mut self,
         buffer_id: BufferId,
-        group_id: usize,
+        diagnostic: DiagnosticEntry<usize>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        if matches!(self.active_diagnostics, ActiveDiagnostic::All) {
+            return;
+        }
         self.dismiss_diagnostics(cx);
         let snapshot = self.snapshot(window, cx);
-        self.active_diagnostics = self.display_map.update(cx, |display_map, cx| {
-            let buffer = self.buffer.read(cx).snapshot(cx);
+        let Some(diagnostic_renderer) = cx
+            .try_global::<GlobalDiagnosticRenderer>()
+            .map(|g| g.0.clone())
+        else {
+            return;
+        };
+        let buffer = self.buffer.read(cx).snapshot(cx);
 
-            let mut primary_range = None;
-            let mut primary_message = None;
-            let diagnostic_group = buffer
-                .diagnostic_group(buffer_id, group_id)
-                .filter_map(|entry| {
-                    let start = entry.range.start;
-                    let end = entry.range.end;
-                    if snapshot.is_line_folded(MultiBufferRow(start.row))
-                        && (start.row == end.row
-                            || snapshot.is_line_folded(MultiBufferRow(end.row)))
-                    {
-                        return None;
-                    }
-                    if entry.diagnostic.is_primary {
-                        primary_range = Some(entry.range.clone());
-                        primary_message = Some(entry.diagnostic.message.clone());
-                    }
-                    Some(entry)
-                })
-                .collect::<Vec<_>>();
-            let primary_range = primary_range?;
-            let primary_message = primary_message?;
-
-            let blocks = display_map
-                .insert_blocks(
-                    diagnostic_group.iter().map(|entry| {
-                        let diagnostic = entry.diagnostic.clone();
-                        let message_height = diagnostic.message.matches('\n').count() as u32 + 1;
-                        BlockProperties {
-                            style: BlockStyle::Fixed,
-                            placement: BlockPlacement::Below(
-                                buffer.anchor_after(entry.range.start),
-                            ),
-                            height: Some(message_height),
-                            render: diagnostic_block_renderer(diagnostic, None, true),
-                            priority: 0,
-                        }
-                    }),
-                    cx,
-                )
-                .into_iter()
-                .zip(diagnostic_group.into_iter().map(|entry| entry.diagnostic))
-                .collect();
+        let diagnostic_group = buffer
+            .diagnostic_group(buffer_id, diagnostic.diagnostic.group_id)
+            .collect::<Vec<_>>();
 
-            Some(ActiveDiagnosticGroup {
-                primary_range: buffer.anchor_before(primary_range.start)
-                    ..buffer.anchor_after(primary_range.end),
-                primary_message,
-                group_id,
-                blocks,
-                is_valid: true,
-            })
+        let blocks = diagnostic_renderer.render_group(
+            diagnostic_group,
+            buffer_id,
+            snapshot,
+            cx.weak_entity(),
+            cx,
+        );
+
+        let blocks = self.display_map.update(cx, |display_map, cx| {
+            display_map.insert_blocks(blocks, cx).into_iter().collect()
+        });
+        self.active_diagnostics = ActiveDiagnostic::Group(ActiveDiagnosticGroup {
+            active_range: buffer.anchor_before(diagnostic.range.start)
+                ..buffer.anchor_after(diagnostic.range.end),
+            active_message: diagnostic.diagnostic.message.clone(),
+            group_id: diagnostic.diagnostic.group_id,
+            blocks,
         });
+        cx.notify();
     }
 
     fn dismiss_diagnostics(&mut self, cx: &mut Context<Self>) {
-        if let Some(active_diagnostic_group) = self.active_diagnostics.take() {
+        if matches!(self.active_diagnostics, ActiveDiagnostic::All) {
+            return;
+        };
+
+        let prev = mem::replace(&mut self.active_diagnostics, ActiveDiagnostic::None);
+        if let ActiveDiagnostic::Group(group) = prev {
             self.display_map.update(cx, |display_map, cx| {
-                display_map.remove_blocks(active_diagnostic_group.blocks.into_keys().collect(), cx);
+                display_map.remove_blocks(group.blocks, cx);
             });
             cx.notify();
         }
@@ -14658,6 +14602,8 @@ impl Editor {
             None
         };
         self.inline_diagnostics_update = cx.spawn_in(window, async move |editor, cx| {
+            let editor = editor.upgrade().unwrap();
+
             if let Some(debounce) = debounce {
                 cx.background_executor().timer(debounce).await;
             }
@@ -15230,7 +15176,7 @@ impl Editor {
         &mut self,
         creases: Vec<Crease<T>>,
         auto_scroll: bool,
-        window: &mut Window,
+        _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
         if creases.is_empty() {
@@ -15255,18 +15201,6 @@ impl Editor {
 
         cx.notify();
 
-        if let Some(active_diagnostics) = self.active_diagnostics.take() {
-            // Clear diagnostics block when folding a range that contains it.
-            let snapshot = self.snapshot(window, cx);
-            if snapshot.intersects_fold(active_diagnostics.primary_range.start) {
-                drop(snapshot);
-                self.active_diagnostics = Some(active_diagnostics);
-                self.dismiss_diagnostics(cx);
-            } else {
-                self.active_diagnostics = Some(active_diagnostics);
-            }
-        }
-
         self.scrollbar_marker_state.dirty = true;
         self.folds_did_change(cx);
     }
@@ -20120,103 +20054,6 @@ impl InvalidationRegion for SnippetState {
     }
 }
 
-pub fn diagnostic_block_renderer(
-    diagnostic: Diagnostic,
-    max_message_rows: Option<u8>,
-    allow_closing: bool,
-) -> RenderBlock {
-    let (text_without_backticks, code_ranges) =
-        highlight_diagnostic_message(&diagnostic, max_message_rows);
-
-    Arc::new(move |cx: &mut BlockContext| {
-        let group_id: SharedString = cx.block_id.to_string().into();
-
-        let mut text_style = cx.window.text_style().clone();
-        text_style.color = diagnostic_style(diagnostic.severity, cx.theme().status());
-        let theme_settings = ThemeSettings::get_global(cx);
-        text_style.font_family = theme_settings.buffer_font.family.clone();
-        text_style.font_style = theme_settings.buffer_font.style;
-        text_style.font_features = theme_settings.buffer_font.features.clone();
-        text_style.font_weight = theme_settings.buffer_font.weight;
-
-        let multi_line_diagnostic = diagnostic.message.contains('\n');
-
-        let buttons = |diagnostic: &Diagnostic| {
-            if multi_line_diagnostic {
-                v_flex()
-            } else {
-                h_flex()
-            }
-            .when(allow_closing, |div| {
-                div.children(diagnostic.is_primary.then(|| {
-                    IconButton::new("close-block", IconName::XCircle)
-                        .icon_color(Color::Muted)
-                        .size(ButtonSize::Compact)
-                        .style(ButtonStyle::Transparent)
-                        .visible_on_hover(group_id.clone())
-                        .on_click(move |_click, window, cx| {
-                            window.dispatch_action(Box::new(Cancel), cx)
-                        })
-                        .tooltip(|window, cx| {
-                            Tooltip::for_action("Close Diagnostics", &Cancel, window, cx)
-                        })
-                }))
-            })
-            .child(
-                IconButton::new("copy-block", IconName::Copy)
-                    .icon_color(Color::Muted)
-                    .size(ButtonSize::Compact)
-                    .style(ButtonStyle::Transparent)
-                    .visible_on_hover(group_id.clone())
-                    .on_click({
-                        let message = diagnostic.message.clone();
-                        move |_click, _, cx| {
-                            cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
-                        }
-                    })
-                    .tooltip(Tooltip::text("Copy diagnostic message")),
-            )
-        };
-
-        let icon_size = buttons(&diagnostic).into_any_element().layout_as_root(
-            AvailableSpace::min_size(),
-            cx.window,
-            cx.app,
-        );
-
-        h_flex()
-            .id(cx.block_id)
-            .group(group_id.clone())
-            .relative()
-            .size_full()
-            .block_mouse_down()
-            .pl(cx.gutter_dimensions.width)
-            .w(cx.max_width - cx.gutter_dimensions.full_width())
-            .child(
-                div()
-                    .flex()
-                    .w(cx.anchor_x - cx.gutter_dimensions.width - icon_size.width)
-                    .flex_shrink(),
-            )
-            .child(buttons(&diagnostic))
-            .child(div().flex().flex_shrink_0().child(
-                StyledText::new(text_without_backticks.clone()).with_default_highlights(
-                    &text_style,
-                    code_ranges.iter().map(|range| {
-                        (
-                            range.clone(),
-                            HighlightStyle {
-                                font_weight: Some(FontWeight::BOLD),
-                                ..Default::default()
-                            },
-                        )
-                    }),
-                ),
-            ))
-            .into_any_element()
-    })
-}
-
 fn inline_completion_edit_text(
     current_snapshot: &BufferSnapshot,
     edits: &[(Range<Anchor>, String)],
@@ -20237,74 +20074,7 @@ fn inline_completion_edit_text(
     edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx)
 }
 
-pub fn highlight_diagnostic_message(
-    diagnostic: &Diagnostic,
-    mut max_message_rows: Option<u8>,
-) -> (SharedString, Vec<Range<usize>>) {
-    let mut text_without_backticks = String::new();
-    let mut code_ranges = Vec::new();
-
-    if let Some(source) = &diagnostic.source {
-        text_without_backticks.push_str(source);
-        code_ranges.push(0..source.len());
-        text_without_backticks.push_str(": ");
-    }
-
-    let mut prev_offset = 0;
-    let mut in_code_block = false;
-    let has_row_limit = max_message_rows.is_some();
-    let mut newline_indices = diagnostic
-        .message
-        .match_indices('\n')
-        .filter(|_| has_row_limit)
-        .map(|(ix, _)| ix)
-        .fuse()
-        .peekable();
-
-    for (quote_ix, _) in diagnostic
-        .message
-        .match_indices('`')
-        .chain([(diagnostic.message.len(), "")])
-    {
-        let mut first_newline_ix = None;
-        let mut last_newline_ix = None;
-        while let Some(newline_ix) = newline_indices.peek() {
-            if *newline_ix < quote_ix {
-                if first_newline_ix.is_none() {
-                    first_newline_ix = Some(*newline_ix);
-                }
-                last_newline_ix = Some(*newline_ix);
-
-                if let Some(rows_left) = &mut max_message_rows {
-                    if *rows_left == 0 {
-                        break;
-                    } else {
-                        *rows_left -= 1;
-                    }
-                }
-                let _ = newline_indices.next();
-            } else {
-                break;
-            }
-        }
-        let prev_len = text_without_backticks.len();
-        let new_text = &diagnostic.message[prev_offset..first_newline_ix.unwrap_or(quote_ix)];
-        text_without_backticks.push_str(new_text);
-        if in_code_block {
-            code_ranges.push(prev_len..text_without_backticks.len());
-        }
-        prev_offset = last_newline_ix.unwrap_or(quote_ix) + 1;
-        in_code_block = !in_code_block;
-        if first_newline_ix.map_or(false, |newline_ix| newline_ix < quote_ix) {
-            text_without_backticks.push_str("...");
-            break;
-        }
-    }
-
-    (text_without_backticks.into(), code_ranges)
-}
-
-fn diagnostic_style(severity: DiagnosticSeverity, colors: &StatusColors) -> Hsla {
+pub fn diagnostic_style(severity: DiagnosticSeverity, colors: &StatusColors) -> Hsla {
     match severity {
         DiagnosticSeverity::ERROR => colors.error,
         DiagnosticSeverity::WARNING => colors.warning,

crates/editor/src/editor_tests.rs 🔗

@@ -12585,276 +12585,6 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
     "});
 }
 
-#[gpui::test]
-async fn cycle_through_same_place_diagnostics(
-    executor: BackgroundExecutor,
-    cx: &mut TestAppContext,
-) {
-    init_test(cx, |_| {});
-
-    let mut cx = EditorTestContext::new(cx).await;
-    let lsp_store =
-        cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
-
-    cx.set_state(indoc! {"
-        ˇfn func(abc def: i32) -> u32 {
-        }
-    "});
-
-    cx.update(|_, cx| {
-        lsp_store.update(cx, |lsp_store, cx| {
-            lsp_store
-                .update_diagnostics(
-                    LanguageServerId(0),
-                    lsp::PublishDiagnosticsParams {
-                        uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
-                        version: None,
-                        diagnostics: vec![
-                            lsp::Diagnostic {
-                                range: lsp::Range::new(
-                                    lsp::Position::new(0, 11),
-                                    lsp::Position::new(0, 12),
-                                ),
-                                severity: Some(lsp::DiagnosticSeverity::ERROR),
-                                ..Default::default()
-                            },
-                            lsp::Diagnostic {
-                                range: lsp::Range::new(
-                                    lsp::Position::new(0, 12),
-                                    lsp::Position::new(0, 15),
-                                ),
-                                severity: Some(lsp::DiagnosticSeverity::ERROR),
-                                ..Default::default()
-                            },
-                            lsp::Diagnostic {
-                                range: lsp::Range::new(
-                                    lsp::Position::new(0, 12),
-                                    lsp::Position::new(0, 15),
-                                ),
-                                severity: Some(lsp::DiagnosticSeverity::ERROR),
-                                ..Default::default()
-                            },
-                            lsp::Diagnostic {
-                                range: lsp::Range::new(
-                                    lsp::Position::new(0, 25),
-                                    lsp::Position::new(0, 28),
-                                ),
-                                severity: Some(lsp::DiagnosticSeverity::ERROR),
-                                ..Default::default()
-                            },
-                        ],
-                    },
-                    &[],
-                    cx,
-                )
-                .unwrap()
-        });
-    });
-    executor.run_until_parked();
-
-    //// Backward
-
-    // Fourth diagnostic
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abc def: i32) -> ˇu32 {
-        }
-    "});
-
-    // Third diagnostic
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abc ˇdef: i32) -> u32 {
-        }
-    "});
-
-    // Second diagnostic, same place
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abc ˇdef: i32) -> u32 {
-        }
-    "});
-
-    // First diagnostic
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abcˇ def: i32) -> u32 {
-        }
-    "});
-
-    // Wrapped over, fourth diagnostic
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abc def: i32) -> ˇu32 {
-        }
-    "});
-
-    cx.update_editor(|editor, window, cx| {
-        editor.move_to_beginning(&MoveToBeginning, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        ˇfn func(abc def: i32) -> u32 {
-        }
-    "});
-
-    //// Forward
-
-    // First diagnostic
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abcˇ def: i32) -> u32 {
-        }
-    "});
-
-    // Second diagnostic
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abc ˇdef: i32) -> u32 {
-        }
-    "});
-
-    // Third diagnostic, same place
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abc ˇdef: i32) -> u32 {
-        }
-    "});
-
-    // Fourth diagnostic
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abc def: i32) -> ˇu32 {
-        }
-    "});
-
-    // Wrapped around, first diagnostic
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abcˇ def: i32) -> u32 {
-        }
-    "});
-}
-
-#[gpui::test]
-async fn active_diagnostics_dismiss_after_invalidation(
-    executor: BackgroundExecutor,
-    cx: &mut TestAppContext,
-) {
-    init_test(cx, |_| {});
-
-    let mut cx = EditorTestContext::new(cx).await;
-    let lsp_store =
-        cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
-
-    cx.set_state(indoc! {"
-        ˇfn func(abc def: i32) -> u32 {
-        }
-    "});
-
-    let message = "Something's wrong!";
-    cx.update(|_, cx| {
-        lsp_store.update(cx, |lsp_store, cx| {
-            lsp_store
-                .update_diagnostics(
-                    LanguageServerId(0),
-                    lsp::PublishDiagnosticsParams {
-                        uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
-                        version: None,
-                        diagnostics: vec![lsp::Diagnostic {
-                            range: lsp::Range::new(
-                                lsp::Position::new(0, 11),
-                                lsp::Position::new(0, 12),
-                            ),
-                            severity: Some(lsp::DiagnosticSeverity::ERROR),
-                            message: message.to_string(),
-                            ..Default::default()
-                        }],
-                    },
-                    &[],
-                    cx,
-                )
-                .unwrap()
-        });
-    });
-    executor.run_until_parked();
-
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
-        assert_eq!(
-            editor
-                .active_diagnostics
-                .as_ref()
-                .map(|diagnostics_group| diagnostics_group.primary_message.as_str()),
-            Some(message),
-            "Should have a diagnostics group activated"
-        );
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abcˇ def: i32) -> u32 {
-        }
-    "});
-
-    cx.update(|_, cx| {
-        lsp_store.update(cx, |lsp_store, cx| {
-            lsp_store
-                .update_diagnostics(
-                    LanguageServerId(0),
-                    lsp::PublishDiagnosticsParams {
-                        uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
-                        version: None,
-                        diagnostics: Vec::new(),
-                    },
-                    &[],
-                    cx,
-                )
-                .unwrap()
-        });
-    });
-    executor.run_until_parked();
-    cx.update_editor(|editor, _, _| {
-        assert_eq!(
-            editor.active_diagnostics, None,
-            "After no diagnostics set to the editor, no diagnostics should be active"
-        );
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abcˇ def: i32) -> u32 {
-        }
-    "});
-
-    cx.update_editor(|editor, window, cx| {
-        editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
-        assert_eq!(
-            editor.active_diagnostics, None,
-            "Should be no diagnostics to go to and activate"
-        );
-    });
-    cx.assert_editor_state(indoc! {"
-        fn func(abcˇ def: i32) -> u32 {
-        }
-    "});
-}
-
 #[gpui::test]
 async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
     init_test(cx, |_| {});

crates/editor/src/element.rs 🔗

@@ -1,11 +1,11 @@
 use crate::{
-    BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR, ChunkRendererContext,
-    ChunkReplacement, ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk,
-    DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode,
-    Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT,
-    FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
-    InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
-    MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts,
+    ActiveDiagnostic, BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
+    ChunkRendererContext, ChunkReplacement, ContextMenuPlacement, CursorShape, CustomBlockId,
+    DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite,
+    EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
+    FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput,
+    HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight,
+    LineUp, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts,
     PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection,
     SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold,
     code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
@@ -1614,12 +1614,12 @@ impl EditorElement {
                 project_settings::DiagnosticSeverity::Hint => DiagnosticSeverity::HINT,
             });
 
-        let active_diagnostics_group = self
-            .editor
-            .read(cx)
-            .active_diagnostics
-            .as_ref()
-            .map(|active_diagnostics| active_diagnostics.group_id);
+        let active_diagnostics_group =
+            if let ActiveDiagnostic::Group(group) = &self.editor.read(cx).active_diagnostics {
+                Some(group.group_id)
+            } else {
+                None
+            };
 
         let diagnostics_by_rows = self.editor.update(cx, |editor, cx| {
             let snapshot = editor.snapshot(window, cx);
@@ -2643,12 +2643,15 @@ impl EditorElement {
         sticky_header_excerpt_id: Option<ExcerptId>,
         window: &mut Window,
         cx: &mut App,
-    ) -> (AnyElement, Size<Pixels>, DisplayRow, Pixels) {
+    ) -> Option<(AnyElement, Size<Pixels>, DisplayRow, Pixels)> {
         let mut x_position = None;
         let mut element = match block {
-            Block::Custom(block) => {
-                let block_start = block.start().to_point(&snapshot.buffer_snapshot);
-                let block_end = block.end().to_point(&snapshot.buffer_snapshot);
+            Block::Custom(custom) => {
+                let block_start = custom.start().to_point(&snapshot.buffer_snapshot);
+                let block_end = custom.end().to_point(&snapshot.buffer_snapshot);
+                if block.place_near() && snapshot.is_line_folded(MultiBufferRow(block_start.row)) {
+                    return None;
+                }
                 let align_to = block_start.to_display_point(snapshot);
                 let x_and_width = |layout: &LineWithInvisibles| {
                     Some((
@@ -2686,7 +2689,7 @@ impl EditorElement {
 
                 div()
                     .size_full()
-                    .child(block.render(&mut BlockContext {
+                    .child(custom.render(&mut BlockContext {
                         window,
                         app: cx,
                         anchor_x,
@@ -2774,6 +2777,7 @@ impl EditorElement {
         } else {
             element.layout_as_root(size(available_width, quantized_height.into()), window, cx)
         };
+        let mut element_height_in_lines = ((final_size.height / line_height).ceil() as u32).max(1);
 
         let mut row = block_row_start;
         let mut x_offset = px(0.);
@@ -2781,20 +2785,19 @@ impl EditorElement {
 
         if let BlockId::Custom(custom_block_id) = block_id {
             if block.has_height() {
-                let mut element_height_in_lines =
-                    ((final_size.height / line_height).ceil() as u32).max(1);
-
-                if block.place_near() && element_height_in_lines == 1 {
+                if block.place_near() {
                     if let Some((x_target, line_width)) = x_position {
                         let margin = em_width * 2;
                         if line_width + final_size.width + margin
                             < editor_width + gutter_dimensions.full_width()
                             && !row_block_types.contains_key(&(row - 1))
+                            && element_height_in_lines == 1
                         {
                             x_offset = line_width + margin;
                             row = row - 1;
                             is_block = false;
                             element_height_in_lines = 0;
+                            row_block_types.insert(row, is_block);
                         } else {
                             let max_offset =
                                 editor_width + gutter_dimensions.full_width() - final_size.width;
@@ -2809,9 +2812,11 @@ impl EditorElement {
                 }
             }
         }
-        row_block_types.insert(row, is_block);
+        for i in 0..element_height_in_lines {
+            row_block_types.insert(row + i, is_block);
+        }
 
-        (element, final_size, row, x_offset)
+        Some((element, final_size, row, x_offset))
     }
 
     fn render_buffer_header(
@@ -3044,7 +3049,7 @@ impl EditorElement {
                 focused_block = None;
             }
 
-            let (element, element_size, row, x_offset) = self.render_block(
+            if let Some((element, element_size, row, x_offset)) = self.render_block(
                 block,
                 AvailableSpace::MinContent,
                 block_id,
@@ -3067,19 +3072,19 @@ impl EditorElement {
                 sticky_header_excerpt_id,
                 window,
                 cx,
-            );
-
-            fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
-            blocks.push(BlockLayout {
-                id: block_id,
-                x_offset,
-                row: Some(row),
-                element,
-                available_space: size(AvailableSpace::MinContent, element_size.height.into()),
-                style: BlockStyle::Fixed,
-                overlaps_gutter: true,
-                is_buffer_header: block.is_buffer_header(),
-            });
+            ) {
+                fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
+                blocks.push(BlockLayout {
+                    id: block_id,
+                    x_offset,
+                    row: Some(row),
+                    element,
+                    available_space: size(AvailableSpace::MinContent, element_size.height.into()),
+                    style: BlockStyle::Fixed,
+                    overlaps_gutter: true,
+                    is_buffer_header: block.is_buffer_header(),
+                });
+            }
         }
 
         for (row, block) in non_fixed_blocks {
@@ -3101,7 +3106,7 @@ impl EditorElement {
                 focused_block = None;
             }
 
-            let (element, element_size, row, x_offset) = self.render_block(
+            if let Some((element, element_size, row, x_offset)) = self.render_block(
                 block,
                 width,
                 block_id,
@@ -3124,18 +3129,18 @@ impl EditorElement {
                 sticky_header_excerpt_id,
                 window,
                 cx,
-            );
-
-            blocks.push(BlockLayout {
-                id: block_id,
-                x_offset,
-                row: Some(row),
-                element,
-                available_space: size(width, element_size.height.into()),
-                style,
-                overlaps_gutter: !block.place_near(),
-                is_buffer_header: block.is_buffer_header(),
-            });
+            ) {
+                blocks.push(BlockLayout {
+                    id: block_id,
+                    x_offset,
+                    row: Some(row),
+                    element,
+                    available_space: size(width, element_size.height.into()),
+                    style,
+                    overlaps_gutter: !block.place_near(),
+                    is_buffer_header: block.is_buffer_header(),
+                });
+            }
         }
 
         if let Some(focused_block) = focused_block {
@@ -3155,7 +3160,7 @@ impl EditorElement {
                             BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width),
                         };
 
-                        let (element, element_size, _, x_offset) = self.render_block(
+                        if let Some((element, element_size, _, x_offset)) = self.render_block(
                             &block,
                             width,
                             focused_block.id,
@@ -3178,18 +3183,18 @@ impl EditorElement {
                             sticky_header_excerpt_id,
                             window,
                             cx,
-                        );
-
-                        blocks.push(BlockLayout {
-                            id: block.id(),
-                            x_offset,
-                            row: None,
-                            element,
-                            available_space: size(width, element_size.height.into()),
-                            style,
-                            overlaps_gutter: true,
-                            is_buffer_header: block.is_buffer_header(),
-                        });
+                        ) {
+                            blocks.push(BlockLayout {
+                                id: block.id(),
+                                x_offset,
+                                row: None,
+                                element,
+                                available_space: size(width, element_size.height.into()),
+                                style,
+                                overlaps_gutter: true,
+                                is_buffer_header: block.is_buffer_header(),
+                            });
+                        }
                     }
                 }
             }

crates/editor/src/hover_popover.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
-    Hover,
+    ActiveDiagnostic, Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings,
+    EditorSnapshot, Hover,
     display_map::{InlayOffset, ToDisplayPoint, invisibles::is_invisible},
     hover_links::{InlayHighlight, RangeInEditor},
     scroll::{Autoscroll, ScrollAmount},
@@ -95,7 +95,7 @@ pub fn show_keyboard_hover(
 }
 
 pub struct InlayHover {
-    pub range: InlayHighlight,
+    pub(crate) range: InlayHighlight,
     pub tooltip: HoverBlock,
 }
 
@@ -276,6 +276,12 @@ fn show_hover(
     }
 
     let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
+    let all_diagnostics_active = editor.active_diagnostics == ActiveDiagnostic::All;
+    let active_group_id = if let ActiveDiagnostic::Group(group) = &editor.active_diagnostics {
+        Some(group.group_id)
+    } else {
+        None
+    };
 
     let task = cx.spawn_in(window, async move |this, cx| {
         async move {
@@ -302,11 +308,16 @@ fn show_hover(
             }
 
             let offset = anchor.to_offset(&snapshot.buffer_snapshot);
-            let local_diagnostic = snapshot
-                .buffer_snapshot
-                .diagnostics_in_range::<usize>(offset..offset)
-                // Find the entry with the most specific range
-                .min_by_key(|entry| entry.range.len());
+            let local_diagnostic = if all_diagnostics_active {
+                None
+            } else {
+                snapshot
+                    .buffer_snapshot
+                    .diagnostics_in_range::<usize>(offset..offset)
+                    .filter(|diagnostic| Some(diagnostic.diagnostic.group_id) != active_group_id)
+                    // Find the entry with the most specific range
+                    .min_by_key(|entry| entry.range.len())
+            };
 
             let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic {
                 let text = match local_diagnostic.diagnostic.source {
@@ -638,6 +649,7 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
         },
         syntax: cx.theme().syntax().clone(),
         selection_background_color: { cx.theme().players().local().selection },
+        height_is_multiple_of_line_height: true,
         heading: StyleRefinement::default()
             .font_weight(FontWeight::BOLD)
             .text_base()
@@ -707,7 +719,7 @@ pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App)
 
 #[derive(Default)]
 pub struct HoverState {
-    pub info_popovers: Vec<InfoPopover>,
+    pub(crate) info_popovers: Vec<InfoPopover>,
     pub diagnostic_popover: Option<DiagnosticPopover>,
     pub triggered_from: Option<Anchor>,
     pub info_task: Option<Task<Option<()>>>,

crates/editor/src/test.rs 🔗

@@ -1,18 +1,25 @@
 pub mod editor_lsp_test_context;
 pub mod editor_test_context;
 
+use std::{rc::Rc, sync::LazyLock};
+
 pub use crate::rust_analyzer_ext::expand_macro_recursively;
 use crate::{
     DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer,
-    display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
+    display_map::{
+        Block, BlockPlacement, CustomBlockId, DisplayMap, DisplayRow, DisplaySnapshot,
+        ToDisplayPoint,
+    },
 };
+use collections::HashMap;
 use gpui::{
-    AppContext as _, Context, Entity, Font, FontFeatures, FontStyle, FontWeight, Pixels, Window,
-    font,
+    AppContext as _, Context, Entity, EntityId, Font, FontFeatures, FontStyle, FontWeight, Pixels,
+    VisualTestContext, Window, font, size,
 };
+use multi_buffer::ToPoint;
 use pretty_assertions::assert_eq;
 use project::Project;
-use std::sync::LazyLock;
+use ui::{App, BorrowAppContext, px};
 use util::test::{marked_text_offsets, marked_text_ranges};
 
 #[cfg(test)]
@@ -122,3 +129,126 @@ pub(crate) fn build_editor_with_project(
 ) -> Editor {
     Editor::new(EditorMode::full(), buffer, Some(project), window, cx)
 }
+
+#[derive(Default)]
+struct TestBlockContent(
+    HashMap<(EntityId, CustomBlockId), Rc<dyn Fn(&mut VisualTestContext) -> String>>,
+);
+
+impl gpui::Global for TestBlockContent {}
+
+pub fn set_block_content_for_tests(
+    editor: &Entity<Editor>,
+    id: CustomBlockId,
+    cx: &mut App,
+    f: impl Fn(&mut VisualTestContext) -> String + 'static,
+) {
+    cx.update_default_global::<TestBlockContent, _>(|bc, _| {
+        bc.0.insert((editor.entity_id(), id), Rc::new(f))
+    });
+}
+
+pub fn block_content_for_tests(
+    editor: &Entity<Editor>,
+    id: CustomBlockId,
+    cx: &mut VisualTestContext,
+) -> Option<String> {
+    let f = cx.update(|_, cx| {
+        cx.default_global::<TestBlockContent>()
+            .0
+            .get(&(editor.entity_id(), id))
+            .cloned()
+    })?;
+    Some(f(cx))
+}
+
+pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestContext) -> String {
+    cx.draw(
+        gpui::Point::default(),
+        size(px(3000.0), px(3000.0)),
+        |_, _| editor.clone(),
+    );
+    let (snapshot, mut lines, blocks) = editor.update_in(cx, |editor, window, cx| {
+        let snapshot = editor.snapshot(window, cx);
+        let text = editor.display_text(cx);
+        let lines = text.lines().map(|s| s.to_string()).collect::<Vec<String>>();
+        let blocks = snapshot
+            .blocks_in_range(DisplayRow(0)..snapshot.max_point().row())
+            .map(|(row, block)| (row, block.clone()))
+            .collect::<Vec<_>>();
+        (snapshot, lines, blocks)
+    });
+    for (row, block) in blocks {
+        match block {
+            Block::Custom(custom_block) => {
+                if let BlockPlacement::Near(x) = &custom_block.placement {
+                    if snapshot.intersects_fold(x.to_point(&snapshot.buffer_snapshot)) {
+                        continue;
+                    }
+                };
+                let content = block_content_for_tests(&editor, custom_block.id, cx)
+                    .expect("block content not found");
+                // 2: "related info 1 for diagnostic 0"
+                if let Some(height) = custom_block.height {
+                    if height == 0 {
+                        lines[row.0 as usize - 1].push_str(" § ");
+                        lines[row.0 as usize - 1].push_str(&content);
+                    } else {
+                        let block_lines = content.lines().collect::<Vec<_>>();
+                        assert_eq!(block_lines.len(), height as usize);
+                        lines[row.0 as usize].push_str("§ ");
+                        lines[row.0 as usize].push_str(block_lines[0].trim_end());
+                        for i in 1..height as usize {
+                            lines[row.0 as usize + i].push_str("§ ");
+                            lines[row.0 as usize + i].push_str(block_lines[i].trim_end());
+                        }
+                    }
+                }
+            }
+            Block::FoldedBuffer {
+                first_excerpt,
+                height,
+            } => {
+                lines[row.0 as usize].push_str(&cx.update(|_, cx| {
+                    format!(
+                        "§ {}",
+                        first_excerpt
+                            .buffer
+                            .file()
+                            .unwrap()
+                            .file_name(cx)
+                            .to_string_lossy()
+                    )
+                }));
+                for row in row.0 + 1..row.0 + height {
+                    lines[row as usize].push_str("§ -----");
+                }
+            }
+            Block::ExcerptBoundary {
+                excerpt,
+                height,
+                starts_new_buffer,
+            } => {
+                if starts_new_buffer {
+                    lines[row.0 as usize].push_str(&cx.update(|_, cx| {
+                        format!(
+                            "§ {}",
+                            excerpt
+                                .buffer
+                                .file()
+                                .unwrap()
+                                .file_name(cx)
+                                .to_string_lossy()
+                        )
+                    }));
+                } else {
+                    lines[row.0 as usize].push_str("§ -----")
+                }
+                for row in row.0 + 1..row.0 + height {
+                    lines[row as usize].push_str("§ -----");
+                }
+            }
+        }
+    }
+    lines.join("\n")
+}

crates/gpui/src/elements/text.rs 🔗

@@ -556,6 +556,25 @@ impl TextLayout {
             .collect::<Vec<_>>()
             .join("\n")
     }
+
+    /// The text for this layout (with soft-wraps as newlines)
+    pub fn wrapped_text(&self) -> String {
+        let mut lines = Vec::new();
+        for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() {
+            let mut seen = 0;
+            for boundary in wrapped.layout.wrap_boundaries.iter() {
+                let index = wrapped.layout.unwrapped_layout.runs[boundary.run_ix].glyphs
+                    [boundary.glyph_ix]
+                    .index;
+
+                lines.push(wrapped.text[seen..index].to_string());
+                seen = index;
+            }
+            lines.push(wrapped.text[seen..].to_string());
+        }
+
+        lines.join("\n")
+    }
 }
 
 /// A text element that can be interacted with.

crates/language/src/buffer.rs 🔗

@@ -1265,6 +1265,7 @@ impl Buffer {
         self.reload_task = Some(cx.spawn(async move |this, cx| {
             let Some((new_mtime, new_text)) = this.update(cx, |this, cx| {
                 let file = this.file.as_ref()?.as_local()?;
+
                 Some((file.disk_state().mtime(), file.load(cx)))
             })?
             else {

crates/language/src/language.rs 🔗

@@ -550,13 +550,7 @@ pub trait LspAdapter: 'static + Send + Sync {
 
     /// Returns a list of code actions supported by a given LspAdapter
     fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
-        Some(vec![
-            CodeActionKind::EMPTY,
-            CodeActionKind::QUICKFIX,
-            CodeActionKind::REFACTOR,
-            CodeActionKind::REFACTOR_EXTRACT,
-            CodeActionKind::SOURCE,
-        ])
+        None
     }
 
     fn disk_based_diagnostic_sources(&self) -> Vec<String> {

crates/markdown/src/markdown.rs 🔗

@@ -1,6 +1,7 @@
 pub mod parser;
 mod path_range;
 
+use std::borrow::Cow;
 use std::collections::HashSet;
 use std::iter;
 use std::mem;
@@ -59,6 +60,7 @@ pub struct MarkdownStyle {
     pub heading: StyleRefinement,
     pub heading_level_styles: Option<HeadingLevelStyles>,
     pub table_overflow_x_scroll: bool,
+    pub height_is_multiple_of_line_height: bool,
 }
 
 impl Default for MarkdownStyle {
@@ -78,6 +80,7 @@ impl Default for MarkdownStyle {
             heading: Default::default(),
             heading_level_styles: None,
             table_overflow_x_scroll: false,
+            height_is_multiple_of_line_height: false,
         }
     }
 }
@@ -205,6 +208,22 @@ impl Markdown {
         &self.parsed_markdown
     }
 
+    pub fn escape(s: &str) -> Cow<str> {
+        let count = s.bytes().filter(|c| c.is_ascii_punctuation()).count();
+        if count > 0 {
+            let mut output = String::with_capacity(s.len() + count);
+            for c in s.chars() {
+                if c.is_ascii_punctuation() {
+                    output.push('\\')
+                }
+                output.push(c)
+            }
+            output.into()
+        } else {
+            s.into()
+        }
+    }
+
     fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) {
         if self.selection.end <= self.selection.start {
             return;
@@ -367,6 +386,27 @@ impl MarkdownElement {
         }
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn rendered_text(
+        markdown: Entity<Markdown>,
+        cx: &mut gpui::VisualTestContext,
+        style: impl FnOnce(&Window, &App) -> MarkdownStyle,
+    ) -> String {
+        use gpui::size;
+
+        let (text, _) = cx.draw(
+            Default::default(),
+            size(px(600.0), px(600.0)),
+            |window, cx| Self::new(markdown, style(window, cx)),
+        );
+        text.text
+            .lines
+            .iter()
+            .map(|line| line.layout.wrapped_text())
+            .collect::<Vec<_>>()
+            .join("\n")
+    }
+
     pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self {
         self.code_block_renderer = variant;
         self
@@ -496,9 +536,9 @@ impl MarkdownElement {
                                 pending: true,
                             };
                             window.focus(&markdown.focus_handle);
-                            window.prevent_default();
                         }
 
+                        window.prevent_default();
                         cx.notify();
                     }
                 } else if phase.capture() {
@@ -634,7 +674,9 @@ impl Element for MarkdownElement {
                     match tag {
                         MarkdownTag::Paragraph => {
                             builder.push_div(
-                                div().mb_2().line_height(rems(1.3)),
+                                div().when(!self.style.height_is_multiple_of_line_height, |el| {
+                                    el.mb_2().line_height(rems(1.3))
+                                }),
                                 range,
                                 markdown_end,
                             );
@@ -767,11 +809,11 @@ impl Element for MarkdownElement {
                             };
                             builder.push_div(
                                 div()
-                                    .mb_1()
+                                    .when(!self.style.height_is_multiple_of_line_height, |el| {
+                                        el.mb_1().gap_1().line_height(rems(1.3))
+                                    })
                                     .h_flex()
                                     .items_start()
-                                    .gap_1()
-                                    .line_height(rems(1.3))
                                     .child(bullet),
                                 range,
                                 markdown_end,

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -1578,7 +1578,27 @@ impl MultiBuffer {
         let excerpt_ranges = build_excerpt_ranges(ranges, context_line_count, &buffer_snapshot);
 
         let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges);
-        self.set_excerpt_ranges_for_path(
+        self.set_merged_excerpt_ranges_for_path(
+            path,
+            buffer,
+            excerpt_ranges,
+            &buffer_snapshot,
+            new,
+            counts,
+            cx,
+        )
+    }
+
+    pub fn set_excerpt_ranges_for_path(
+        &mut self,
+        path: PathKey,
+        buffer: Entity<Buffer>,
+        buffer_snapshot: &BufferSnapshot,
+        excerpt_ranges: Vec<ExcerptRange<Point>>,
+        cx: &mut Context<Self>,
+    ) -> (Vec<Range<Anchor>>, bool) {
+        let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges);
+        self.set_merged_excerpt_ranges_for_path(
             path,
             buffer,
             excerpt_ranges,
@@ -1612,11 +1632,11 @@ impl MultiBuffer {
 
             multi_buffer
                 .update(cx, move |multi_buffer, cx| {
-                    let (ranges, _) = multi_buffer.set_excerpt_ranges_for_path(
+                    let (ranges, _) = multi_buffer.set_merged_excerpt_ranges_for_path(
                         path_key,
                         buffer,
                         excerpt_ranges,
-                        buffer_snapshot,
+                        &buffer_snapshot,
                         new,
                         counts,
                         cx,
@@ -1629,12 +1649,12 @@ impl MultiBuffer {
     }
 
     /// Sets excerpts, returns `true` if at least one new excerpt was added.
-    fn set_excerpt_ranges_for_path(
+    fn set_merged_excerpt_ranges_for_path(
         &mut self,
         path: PathKey,
         buffer: Entity<Buffer>,
         ranges: Vec<ExcerptRange<Point>>,
-        buffer_snapshot: BufferSnapshot,
+        buffer_snapshot: &BufferSnapshot,
         new: Vec<ExcerptRange<Point>>,
         counts: Vec<usize>,
         cx: &mut Context<Self>,
@@ -1665,6 +1685,7 @@ impl MultiBuffer {
         let mut counts: Vec<usize> = Vec::new();
         for range in expanded_ranges {
             if let Some(last_range) = merged_ranges.last_mut() {
+                debug_assert!(last_range.context.start <= range.context.start);
                 if last_range.context.end >= range.context.start {
                     last_range.context.end = range.context.end;
                     *counts.last_mut().unwrap() += 1;
@@ -5878,13 +5899,14 @@ impl MultiBufferSnapshot {
         buffer_id: BufferId,
         group_id: usize,
     ) -> impl Iterator<Item = DiagnosticEntry<Point>> + '_ {
-        self.lift_buffer_metadata(Point::zero()..self.max_point(), move |buffer, _| {
+        self.lift_buffer_metadata(Point::zero()..self.max_point(), move |buffer, range| {
             if buffer.remote_id() != buffer_id {
                 return None;
             };
             Some(
                 buffer
-                    .diagnostic_group(group_id)
+                    .diagnostics_in_range(range, false)
+                    .filter(move |diagnostic| diagnostic.diagnostic.group_id == group_id)
                     .map(move |DiagnosticEntry { diagnostic, range }| (range, diagnostic)),
             )
         })