Render basic diagnostic messages in project diagnostics view

Nathan Sobo and Max Brunsfeld created

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

crates/diagnostics/src/diagnostics.rs | 146 +++++++++++++++-------------
crates/editor/src/display_map.rs      |  19 +++
crates/editor/src/editor.rs           |   5 
crates/editor/src/element.rs          |  77 +++++++++-----
4 files changed, 147 insertions(+), 100 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics.rs 🔗

@@ -1,10 +1,10 @@
 use std::sync::Arc;
 
 use collections::HashMap;
-use editor::{Editor, ExcerptProperties, MultiBuffer};
+use editor::{diagnostic_style, Editor, ExcerptProperties, MultiBuffer};
 use gpui::{
-    action, elements::*, keymap::Binding, AppContext, Entity, ModelContext, ModelHandle,
-    MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
+    action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext,
+    RenderContext, View, ViewContext, ViewHandle,
 };
 use language::Point;
 use postage::watch;
@@ -19,16 +19,63 @@ pub fn init(cx: &mut MutableAppContext) {
 }
 
 struct ProjectDiagnostics {
-    excerpts: ModelHandle<MultiBuffer>,
     project: ModelHandle<Project>,
 }
 
 struct ProjectDiagnosticsEditor {
     editor: ViewHandle<Editor>,
+    excerpts: ModelHandle<MultiBuffer>,
 }
 
 impl ProjectDiagnostics {
-    fn new(project: ModelHandle<Project>, cx: &mut ModelContext<Self>) -> Self {
+    fn new(project: ModelHandle<Project>) -> Self {
+        Self { project }
+    }
+}
+
+impl Entity for ProjectDiagnostics {
+    type Event = ();
+}
+
+impl Entity for ProjectDiagnosticsEditor {
+    type Event = ();
+}
+
+impl View for ProjectDiagnosticsEditor {
+    fn ui_name() -> &'static str {
+        "ProjectDiagnosticsEditor"
+    }
+
+    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+        ChildView::new(self.editor.id()).boxed()
+    }
+}
+
+impl ProjectDiagnosticsEditor {
+    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+        let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
+        workspace.add_item(diagnostics, cx);
+    }
+}
+
+impl workspace::Item for ProjectDiagnostics {
+    type View = ProjectDiagnosticsEditor;
+
+    fn build_view(
+        handle: ModelHandle<Self>,
+        settings: watch::Receiver<workspace::Settings>,
+        cx: &mut ViewContext<Self::View>,
+    ) -> Self::View {
+        let project = handle.read(cx).project.clone();
+        let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id(cx)));
+        let editor = cx.add_view(|cx| {
+            Editor::for_buffer(
+                excerpts.clone(),
+                editor::settings_builder(excerpts.downgrade(), settings.clone()),
+                cx,
+            )
+        });
+
         let project_paths = project
             .read(cx)
             .diagnostic_summaries(cx)
@@ -58,19 +105,35 @@ impl ProjectDiagnostics {
                         grouped_diagnostics.into_values().collect::<Vec<_>>();
                     sorted_diagnostic_groups.sort_by_key(|group| group.0);
 
-                    for diagnostic in snapshot.all_diagnostics::<Point>() {
+                    for entry in snapshot.all_diagnostics::<Point>() {
                         this.update(&mut cx, |this, cx| {
                             this.excerpts.update(cx, |excerpts, cx| {
                                 excerpts.push_excerpt(
                                     ExcerptProperties {
                                         buffer: &buffer,
-                                        range: diagnostic.range,
-                                        header_height: 1,
+                                        range: entry.range,
+                                        header_height: entry
+                                            .diagnostic
+                                            .message
+                                            .matches('\n')
+                                            .count()
+                                            as u8
+                                            + 1,
                                         render_header: Some(Arc::new({
-                                            let message = diagnostic.diagnostic.message.clone();
+                                            let message = entry.diagnostic.message.clone();
+                                            let settings = settings.clone();
+
                                             move |_| {
-                                                Text::new(message.clone(), Default::default())
-                                                    .boxed()
+                                                let editor_style = &settings.borrow().theme.editor;
+                                                let mut text_style = editor_style.text.clone();
+                                                text_style.color = diagnostic_style(
+                                                    entry.diagnostic.severity,
+                                                    true,
+                                                    &editor_style,
+                                                )
+                                                .text;
+
+                                                Text::new(message.clone(), text_style).boxed()
                                             }
                                         })),
                                     },
@@ -86,56 +149,7 @@ impl ProjectDiagnostics {
         })
         .detach();
 
-        Self {
-            excerpts: cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id(cx))),
-            project,
-        }
-    }
-}
-
-impl Entity for ProjectDiagnostics {
-    type Event = ();
-}
-
-impl Entity for ProjectDiagnosticsEditor {
-    type Event = ();
-}
-
-impl View for ProjectDiagnosticsEditor {
-    fn ui_name() -> &'static str {
-        "ProjectDiagnosticsEditor"
-    }
-
-    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
-        ChildView::new(self.editor.id()).boxed()
-    }
-}
-
-impl ProjectDiagnosticsEditor {
-    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
-        let diagnostics =
-            cx.add_model(|cx| ProjectDiagnostics::new(workspace.project().clone(), cx));
-        workspace.add_item(diagnostics, cx);
-    }
-}
-
-impl workspace::Item for ProjectDiagnostics {
-    type View = ProjectDiagnosticsEditor;
-
-    fn build_view(
-        handle: ModelHandle<Self>,
-        settings: watch::Receiver<workspace::Settings>,
-        cx: &mut ViewContext<Self::View>,
-    ) -> Self::View {
-        let excerpts = handle.read(cx).excerpts.clone();
-        let editor = cx.add_view(|cx| {
-            Editor::for_buffer(
-                excerpts.clone(),
-                editor::settings_builder(excerpts.downgrade(), settings),
-                cx,
-            )
-        });
-        ProjectDiagnosticsEditor { editor }
+        ProjectDiagnosticsEditor { editor, excerpts }
     }
 
     fn project_path(&self) -> Option<project::ProjectPath> {
@@ -148,22 +162,22 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
         "Project Diagnostics".to_string()
     }
 
-    fn project_path(&self, cx: &AppContext) -> Option<project::ProjectPath> {
+    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
         None
     }
 
     fn save(
         &mut self,
-        cx: &mut ViewContext<Self>,
+        _: &mut ViewContext<Self>,
     ) -> anyhow::Result<gpui::Task<anyhow::Result<()>>> {
         todo!()
     }
 
     fn save_as(
         &mut self,
-        worktree: ModelHandle<project::Worktree>,
-        path: &std::path::Path,
-        cx: &mut ViewContext<Self>,
+        _: ModelHandle<project::Worktree>,
+        _: &std::path::Path,
+        _: &mut ViewContext<Self>,
     ) -> gpui::Task<anyhow::Result<()>> {
         todo!()
     }

crates/editor/src/display_map.rs 🔗

@@ -3,7 +3,9 @@ mod fold_map;
 mod tab_map;
 mod wrap_map;
 
-use crate::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
+use crate::{
+    multi_buffer::RenderHeaderFn, Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
+};
 use block_map::{BlockMap, BlockPoint};
 use fold_map::{FoldMap, ToFoldPoint as _};
 use gpui::{fonts::FontId, ElementBox, Entity, ModelContext, ModelHandle};
@@ -327,6 +329,21 @@ impl DisplaySnapshot {
         self.blocks_snapshot.blocks_in_range(rows)
     }
 
+    pub fn excerpt_headers_in_range<'a>(
+        &'a self,
+        rows: Range<u32>,
+    ) -> impl 'a + Iterator<Item = (Range<u32>, RenderHeaderFn)> {
+        let start_row = DisplayPoint::new(rows.start, 0).to_point(self).row;
+        let end_row = DisplayPoint::new(rows.end, 0).to_point(self).row;
+        self.buffer_snapshot
+            .excerpt_headers_in_range(start_row..end_row)
+            .map(move |(rows, render)| {
+                let start_row = Point::new(rows.start, 0).to_display_point(self).row();
+                let end_row = Point::new(rows.end, 0).to_display_point(self).row();
+                (start_row..end_row, render)
+            })
+    }
+
     pub fn intersects_fold<T: ToOffset>(&self, offset: T) -> bool {
         self.folds_snapshot.intersects_fold(offset)
     }

crates/editor/src/editor.rs 🔗

@@ -590,11 +590,6 @@ impl Editor {
             scroll_position.y() - self.scroll_top_anchor.to_display_point(&map).row() as f32,
         );
 
-        debug_assert_eq!(
-            compute_scroll_position(&map, self.scroll_position, &self.scroll_top_anchor),
-            scroll_position
-        );
-
         cx.notify();
     }
 

crates/editor/src/element.rs 🔗

@@ -631,34 +631,55 @@ impl EditorElement {
         line_layouts: &[text_layout::Line],
         cx: &mut LayoutContext,
     ) -> Vec<(u32, ElementBox)> {
-        snapshot
-            .blocks_in_range(rows.clone())
-            .map(|(start_row, block)| {
-                let anchor_row = block
-                    .position()
-                    .to_point(&snapshot.buffer_snapshot)
-                    .to_display_point(snapshot)
-                    .row();
-
-                let anchor_x = if rows.contains(&anchor_row) {
-                    line_layouts[(anchor_row - rows.start) as usize]
-                        .x_for_index(block.column() as usize)
-                } else {
-                    layout_line(anchor_row, snapshot, style, cx.text_layout_cache)
-                        .x_for_index(block.column() as usize)
-                };
-
-                let mut element = block.render(&BlockContext { cx, anchor_x });
-                element.layout(
-                    SizeConstraint {
-                        min: Vector2F::zero(),
-                        max: vec2f(text_width, block.height() as f32 * line_height),
-                    },
-                    cx,
-                );
-                (start_row, element)
-            })
-            .collect()
+        let mut blocks = Vec::new();
+
+        blocks.extend(
+            snapshot
+                .blocks_in_range(rows.clone())
+                .map(|(start_row, block)| {
+                    let anchor_row = block
+                        .position()
+                        .to_point(&snapshot.buffer_snapshot)
+                        .to_display_point(snapshot)
+                        .row();
+
+                    let anchor_x = if rows.contains(&anchor_row) {
+                        line_layouts[(anchor_row - rows.start) as usize]
+                            .x_for_index(block.column() as usize)
+                    } else {
+                        layout_line(anchor_row, snapshot, style, cx.text_layout_cache)
+                            .x_for_index(block.column() as usize)
+                    };
+
+                    let mut element = block.render(&BlockContext { cx, anchor_x });
+                    element.layout(
+                        SizeConstraint {
+                            min: Vector2F::zero(),
+                            max: vec2f(text_width, block.height() as f32 * line_height),
+                        },
+                        cx,
+                    );
+                    (start_row, element)
+                }),
+        );
+
+        blocks.extend(
+            snapshot
+                .excerpt_headers_in_range(rows.clone())
+                .map(|(rows, render)| {
+                    let mut element = render(cx);
+                    element.layout(
+                        SizeConstraint {
+                            min: Vector2F::zero(),
+                            max: vec2f(text_width, rows.len() as f32 * line_height),
+                        },
+                        cx,
+                    );
+                    (rows.start, element)
+                }),
+        );
+
+        blocks
     }
 }