use collections::HashSet; use editor::{Editor, GoToDiagnostic}; use gpui::{ elements::*, platform::CursorStyle, serde_json, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; use language::Diagnostic; use project::Project; use settings::Settings; use workspace::{item::ItemHandle, StatusItemView}; pub struct DiagnosticIndicator { summary: project::DiagnosticSummary, active_editor: Option>, current_diagnostic: Option, in_progress_checks: HashSet, _observe_active_editor: Option, } pub fn init(cx: &mut MutableAppContext) { cx.add_action(DiagnosticIndicator::go_to_next_diagnostic); } impl DiagnosticIndicator { pub fn new(project: &ModelHandle, cx: &mut ViewContext) -> Self { cx.subscribe(project, |this, project, event, cx| match event { project::Event::DiskBasedDiagnosticsStarted { language_server_id } => { this.in_progress_checks.insert(*language_server_id); cx.notify(); } project::Event::DiskBasedDiagnosticsFinished { language_server_id } => { this.summary = project.read(cx).diagnostic_summary(cx); this.in_progress_checks.remove(language_server_id); cx.notify(); } _ => {} }) .detach(); Self { summary: project.read(cx).diagnostic_summary(cx), in_progress_checks: project .read(cx) .language_servers_running_disk_based_diagnostics() .collect(), active_editor: None, current_diagnostic: None, _observe_active_editor: None, } } fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext) { if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade(cx)) { editor.update(cx, |editor, cx| { editor.go_to_diagnostic_impl(editor::Direction::Next, cx); }) } } fn update(&mut self, editor: ViewHandle, cx: &mut ViewContext) { let editor = editor.read(cx); let buffer = editor.buffer().read(cx); let cursor_position = editor.selections.newest::(cx).head(); let new_diagnostic = buffer .snapshot(cx) .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false) .filter(|entry| !entry.range.is_empty()) .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len())) .map(|entry| entry.diagnostic); if new_diagnostic != self.current_diagnostic { self.current_diagnostic = new_diagnostic; cx.notify(); } } } impl Entity for DiagnosticIndicator { type Event = (); } impl View for DiagnosticIndicator { fn ui_name() -> &'static str { "DiagnosticIndicator" } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { enum Summary {} enum Message {} let tooltip_style = cx.global::().theme.tooltip.clone(); let in_progress = !self.in_progress_checks.is_empty(); let mut element = Flex::row().with_child( MouseEventHandler::::new(0, cx, |state, cx| { let style = cx .global::() .theme .workspace .status_bar .diagnostic_summary .style_for(state, false); let mut summary_row = Flex::row(); if self.summary.error_count > 0 { summary_row.add_children([ Svg::new("icons/circle_x_mark_16.svg") .with_color(style.icon_color_error) .constrained() .with_width(style.icon_width) .aligned() .contained() .with_margin_right(style.icon_spacing) .named("error-icon"), Label::new(self.summary.error_count.to_string(), style.text.clone()) .aligned() .boxed(), ]); } if self.summary.warning_count > 0 { summary_row.add_children([ Svg::new("icons/triangle_exclamation_16.svg") .with_color(style.icon_color_warning) .constrained() .with_width(style.icon_width) .aligned() .contained() .with_margin_right(style.icon_spacing) .with_margin_left(if self.summary.error_count > 0 { style.summary_spacing } else { 0. }) .named("warning-icon"), Label::new(self.summary.warning_count.to_string(), style.text.clone()) .aligned() .boxed(), ]); } if self.summary.error_count == 0 && self.summary.warning_count == 0 { summary_row.add_child( Svg::new("icons/circle_check_16.svg") .with_color(style.icon_color_ok) .constrained() .with_width(style.icon_width) .aligned() .named("ok-icon"), ); } summary_row .constrained() .with_height(style.height) .contained() .with_style(if self.summary.error_count > 0 { style.container_error } else if self.summary.warning_count > 0 { style.container_warning } else { style.container_ok }) .boxed() }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, |_, cx| cx.dispatch_action(crate::Deploy)) .with_tooltip::( 0, "Project Diagnostics".to_string(), Some(Box::new(crate::Deploy)), tooltip_style, cx, ) .aligned() .boxed(), ); let style = &cx.global::().theme.workspace.status_bar; let item_spacing = style.item_spacing; if in_progress { element.add_child( Label::new( "Checking…".into(), style.diagnostic_message.default.text.clone(), ) .aligned() .contained() .with_margin_left(item_spacing) .boxed(), ); } else if let Some(diagnostic) = &self.current_diagnostic { let message_style = style.diagnostic_message.clone(); element.add_child( MouseEventHandler::::new(1, cx, |state, _| { Label::new( diagnostic.message.split('\n').next().unwrap().to_string(), message_style.style_for(state, false).text.clone(), ) .aligned() .contained() .with_margin_left(item_spacing) .boxed() }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, |_, cx| { cx.dispatch_action(GoToDiagnostic) }) .boxed(), ); } element.named("diagnostic indicator") } fn debug_json(&self, _: &gpui::AppContext) -> serde_json::Value { serde_json::json!({ "summary": self.summary }) } } impl StatusItemView for DiagnosticIndicator { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn ItemHandle>, cx: &mut ViewContext, ) { if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) { self.active_editor = Some(editor.downgrade()); self._observe_active_editor = Some(cx.observe(&editor, Self::update)); self.update(editor, cx); } else { self.active_editor = None; self.current_diagnostic = None; self._observe_active_editor = None; } cx.notify(); } }