In the status bar, show the diagnostic under the cursor

Max Brunsfeld created

Change summary

crates/language/src/lib.rs          |  8 ++
crates/theme/src/lib.rs             |  4 +
crates/workspace/src/items.rs       | 80 ++++++++++++++++++++++++++++++
crates/workspace/src/lib.rs         |  2 
crates/zed/assets/themes/_base.toml |  4 +
5 files changed, 95 insertions(+), 3 deletions(-)

Detailed changes

crates/language/src/lib.rs 🔗

@@ -780,10 +780,14 @@ impl Buffer {
         Ok(Operation::UpdateDiagnostics(self.diagnostics.clone()))
     }
 
-    pub fn diagnostics_in_range<'a, T: 'a + ToOffset>(
+    pub fn diagnostics_in_range<'a, T, O>(
         &'a self,
         range: Range<T>,
-    ) -> impl Iterator<Item = (Range<Point>, &Diagnostic)> + 'a {
+    ) -> impl Iterator<Item = (Range<O>, &Diagnostic)> + 'a
+    where
+        T: 'a + ToOffset,
+        O: 'a + FromAnchor,
+    {
         let content = self.content();
         self.diagnostics
             .intersecting_ranges(range, content, true)

crates/theme/src/lib.rs 🔗

@@ -95,6 +95,10 @@ pub struct StatusBar {
     pub container: ContainerStyle,
     pub height: f32,
     pub cursor_position: TextStyle,
+    pub diagnostic_error: TextStyle,
+    pub diagnostic_warning: TextStyle,
+    pub diagnostic_information: TextStyle,
+    pub diagnostic_hint: TextStyle,
 }
 
 #[derive(Deserialize, Default)]

crates/workspace/src/items.rs 🔗

@@ -7,7 +7,7 @@ use gpui::{
     elements::*, fonts::TextStyle, AppContext, Entity, ModelHandle, RenderContext, Subscription,
     Task, View, ViewContext, ViewHandle,
 };
-use language::{Buffer, File as _};
+use language::{Buffer, Diagnostic, DiagnosticSeverity, File as _};
 use postage::watch;
 use project::{ProjectPath, Worktree};
 use std::fmt::Write;
@@ -240,3 +240,81 @@ impl StatusItemView for CursorPosition {
         cx.notify();
     }
 }
+
+pub struct DiagnosticMessage {
+    settings: watch::Receiver<Settings>,
+    diagnostic: Option<Diagnostic>,
+    _observe_active_editor: Option<Subscription>,
+}
+
+impl DiagnosticMessage {
+    pub fn new(settings: watch::Receiver<Settings>) -> Self {
+        Self {
+            diagnostic: None,
+            settings,
+            _observe_active_editor: None,
+        }
+    }
+
+    fn update(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
+        let editor = editor.read(cx);
+        let cursor_position = editor
+            .selections::<usize>(cx)
+            .max_by_key(|selection| selection.id)
+            .unwrap()
+            .head();
+        let new_diagnostic = editor
+            .buffer()
+            .read(cx)
+            .diagnostics_in_range::<usize, usize>(cursor_position..cursor_position)
+            .min_by_key(|(range, diagnostic)| (diagnostic.severity, range.len()))
+            .map(|(_, diagnostic)| diagnostic.clone());
+        if new_diagnostic != self.diagnostic {
+            self.diagnostic = new_diagnostic;
+            cx.notify();
+        }
+    }
+}
+
+impl Entity for DiagnosticMessage {
+    type Event = ();
+}
+
+impl View for DiagnosticMessage {
+    fn ui_name() -> &'static str {
+        "DiagnosticMessage"
+    }
+
+    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
+        if let Some(diagnostic) = &self.diagnostic {
+            let theme = &self.settings.borrow().theme.workspace.status_bar;
+            let style = match diagnostic.severity {
+                DiagnosticSeverity::ERROR => theme.diagnostic_error.clone(),
+                DiagnosticSeverity::WARNING => theme.diagnostic_warning.clone(),
+                DiagnosticSeverity::INFORMATION => theme.diagnostic_information.clone(),
+                DiagnosticSeverity::HINT => theme.diagnostic_hint.clone(),
+                _ => Default::default(),
+            };
+            Label::new(diagnostic.message.replace('\n', " "), style).boxed()
+        } else {
+            Empty::new().boxed()
+        }
+    }
+}
+
+impl StatusItemView for DiagnosticMessage {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn crate::ItemViewHandle>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some(editor) = active_pane_item.and_then(|item| item.to_any().downcast::<Editor>()) {
+            self._observe_active_editor = Some(cx.observe(&editor, Self::update));
+            self.update(editor, cx);
+        } else {
+            self.diagnostic = Default::default();
+            self._observe_active_editor = None;
+        }
+        cx.notify();
+    }
+}

crates/workspace/src/lib.rs 🔗

@@ -350,8 +350,10 @@ impl Workspace {
         cx.focus(&pane);
 
         let cursor_position = cx.add_view(|_| items::CursorPosition::new(params.settings.clone()));
+        let diagnostic = cx.add_view(|_| items::DiagnosticMessage::new(params.settings.clone()));
         let status_bar = cx.add_view(|cx| {
             let mut status_bar = StatusBar::new(&pane, params.settings.clone(), cx);
+            status_bar.add_left_item(diagnostic, cx);
             status_bar.add_right_item(cursor_position, cx);
             status_bar
         });

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

@@ -64,6 +64,10 @@ border = { width = 1, color = "$border.0", left = true }
 padding = { left = 6, right = 6 }
 height = 24
 cursor_position = "$text.2"
+diagnostic_error = { extends = "$text.2", color = "$status.bad" }
+diagnostic_warning = { extends = "$text.2", color = "$status.warn" }
+diagnostic_information = { extends = "$text.2", color = "$status.info" }
+diagnostic_hint = { extends = "$text.2", color = "$status.info" }
 
 [panel]
 padding = { top = 12, left = 12, bottom = 12, right = 12 }