Show status bar item for project diagnostic summary

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

crates/diagnostics/src/diagnostics.rs | 10 ++-
crates/diagnostics/src/items.rs       | 71 +++++++++++++++++++++++++++++
crates/project/src/project.rs         | 11 ++++
crates/theme/src/theme.rs             |  1 
crates/zed/assets/themes/_base.toml   |  3 
crates/zed/src/zed.rs                 | 12 ++++
6 files changed, 101 insertions(+), 7 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics.rs 🔗

@@ -1,3 +1,5 @@
+pub mod items;
+
 use anyhow::Result;
 use collections::{HashMap, HashSet};
 use editor::{
@@ -16,13 +18,13 @@ use std::{cmp::Ordering, ops::Range, path::Path, sync::Arc};
 use util::TryFutureExt;
 use workspace::Workspace;
 
-action!(Toggle);
+action!(Deploy);
 
 const CONTEXT_LINE_COUNT: u32 = 1;
 
 pub fn init(cx: &mut MutableAppContext) {
-    cx.add_bindings([Binding::new("alt-shift-D", Toggle, None)]);
-    cx.add_action(ProjectDiagnosticsEditor::toggle);
+    cx.add_bindings([Binding::new("alt-shift-D", Deploy, None)]);
+    cx.add_action(ProjectDiagnosticsEditor::deploy);
 }
 
 type Event = editor::Event;
@@ -148,7 +150,7 @@ impl ProjectDiagnosticsEditor {
         self.editor.read(cx).text(cx)
     }
 
-    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
         let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
         workspace.add_item(diagnostics, cx);
     }

crates/diagnostics/src/items.rs 🔗

@@ -0,0 +1,71 @@
+use gpui::{
+    elements::*, platform::CursorStyle, Entity, ModelHandle, RenderContext, View, ViewContext,
+};
+use postage::watch;
+use project::Project;
+use workspace::{Settings, StatusItemView};
+
+pub struct DiagnosticSummary {
+    settings: watch::Receiver<Settings>,
+    summary: project::DiagnosticSummary,
+}
+
+impl DiagnosticSummary {
+    pub fn new(
+        project: &ModelHandle<Project>,
+        settings: watch::Receiver<Settings>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        cx.subscribe(project, |this, project, event, cx| {
+            if let project::Event::DiskBasedDiagnosticsUpdated { .. } = event {
+                this.summary = project.read(cx).diagnostic_summary(cx);
+                cx.notify();
+            }
+        })
+        .detach();
+        Self {
+            settings,
+            summary: project.read(cx).diagnostic_summary(cx),
+        }
+    }
+}
+
+impl Entity for DiagnosticSummary {
+    type Event = ();
+}
+
+impl View for DiagnosticSummary {
+    fn ui_name() -> &'static str {
+        "DiagnosticSummary"
+    }
+
+    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
+        enum Tag {}
+
+        let theme = &self.settings.borrow().theme.project_diagnostics;
+        MouseEventHandler::new::<Tag, _, _, _>(0, cx, |_, _| {
+            Label::new(
+                format!(
+                    "Errors: {}, Warnings: {}",
+                    self.summary.error_count, self.summary.warning_count
+                ),
+                theme.status_bar_item.text.clone(),
+            )
+            .contained()
+            .with_style(theme.status_bar_item.container)
+            .boxed()
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(|cx| cx.dispatch_action(crate::Deploy))
+        .boxed()
+    }
+}
+
+impl StatusItemView for DiagnosticSummary {
+    fn set_active_pane_item(
+        &mut self,
+        _: Option<&dyn workspace::ItemViewHandle>,
+        _: &mut ViewContext<Self>,
+    ) {
+    }
+}

crates/project/src/project.rs 🔗

@@ -539,6 +539,17 @@ impl Project {
         }
     }
 
+    pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary {
+        let mut summary = DiagnosticSummary::default();
+        for (_, path_summary) in self.diagnostic_summaries(cx) {
+            summary.error_count += path_summary.error_count;
+            summary.warning_count += path_summary.warning_count;
+            summary.info_count += path_summary.info_count;
+            summary.hint_count += path_summary.hint_count;
+        }
+        summary
+    }
+
     pub fn diagnostic_summaries<'a>(
         &'a self,
         cx: &'a AppContext,

crates/theme/src/theme.rs 🔗

@@ -232,6 +232,7 @@ pub struct ProjectDiagnostics {
     #[serde(flatten)]
     pub container: ContainerStyle,
     pub empty_message: TextStyle,
+    pub status_bar_item: ContainedText,
 }
 
 #[derive(Clone, Deserialize, Default)]

crates/zed/assets/themes/_base.toml 🔗

@@ -185,7 +185,7 @@ corner_radius = 6
 
 [project_panel]
 extends = "$panel"
-padding.top = 6    # ($workspace.tab.height - $project_panel.entry.height) / 2
+padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2
 
 [project_panel.entry]
 text = "$text.1"
@@ -273,3 +273,4 @@ header = { padding = { left = 10 }, background = "#ffffff08" }
 [project_diagnostics]
 background = "$surface.1"
 empty_message = "$text.0"
+status_bar_item = { extends = "$text.2", margin.right = 10 }

crates/zed/src/zed.rs 🔗

@@ -88,12 +88,20 @@ pub fn build_workspace(
         .into(),
     );
 
-    let diagnostic =
+    let diagnostic_message =
         cx.add_view(|_| editor::items::DiagnosticMessage::new(app_state.settings.clone()));
+    let diagnostic_summary = cx.add_view(|cx| {
+        diagnostics::items::DiagnosticSummary::new(
+            workspace.project(),
+            app_state.settings.clone(),
+            cx,
+        )
+    });
     let cursor_position =
         cx.add_view(|_| editor::items::CursorPosition::new(app_state.settings.clone()));
     workspace.status_bar().update(cx, |status_bar, cx| {
-        status_bar.add_left_item(diagnostic, cx);
+        status_bar.add_left_item(diagnostic_summary, cx);
+        status_bar.add_left_item(diagnostic_message, cx);
         status_bar.add_right_item(cursor_position, cx);
     });