1use std::time::Duration;
2
3use editor::Editor;
4use gpui::{
5 Context, Entity, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, Task,
6 WeakEntity, Window,
7};
8use language::Diagnostic;
9use project::project_settings::{GoToDiagnosticSeverityFilter, ProjectSettings};
10use settings::Settings;
11use ui::{Button, ButtonLike, Color, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
12use util::ResultExt;
13use workspace::{StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle};
14
15use crate::{Deploy, IncludeWarnings, ProjectDiagnosticsEditor};
16
17/// The status bar item that displays diagnostic counts.
18pub struct DiagnosticIndicator {
19 summary: project::DiagnosticSummary,
20 workspace: WeakEntity<Workspace>,
21 current_diagnostic: Option<Diagnostic>,
22 active_editor: Option<WeakEntity<Editor>>,
23 _observe_active_editor: Option<Subscription>,
24
25 diagnostics_update: Task<()>,
26 diagnostic_summary_update: Task<()>,
27}
28
29impl Render for DiagnosticIndicator {
30 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
31 let indicator = h_flex().gap_2();
32 if !ProjectSettings::get_global(cx).diagnostics.button {
33 return indicator.hidden();
34 }
35
36 let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
37 (0, 0) => h_flex().child(
38 Icon::new(IconName::Check)
39 .size(IconSize::Small)
40 .color(Color::Default),
41 ),
42 (error_count, warning_count) => h_flex()
43 .gap_1()
44 .when(error_count > 0, |this| {
45 this.child(
46 Icon::new(IconName::XCircle)
47 .size(IconSize::Small)
48 .color(Color::Error),
49 )
50 .child(Label::new(error_count.to_string()).size(LabelSize::Small))
51 })
52 .when(warning_count > 0, |this| {
53 this.child(
54 Icon::new(IconName::Warning)
55 .size(IconSize::Small)
56 .color(Color::Warning),
57 )
58 .child(Label::new(warning_count.to_string()).size(LabelSize::Small))
59 }),
60 };
61
62 let status = if let Some(diagnostic) = &self.current_diagnostic {
63 let message = diagnostic
64 .message
65 .split_once('\n')
66 .map_or(&*diagnostic.message, |(first, _)| first);
67 Some(
68 Button::new("diagnostic_message", SharedString::new(message))
69 .label_size(LabelSize::Small)
70 .tooltip(|_window, cx| {
71 Tooltip::for_action(
72 "Next Diagnostic",
73 &editor::actions::GoToDiagnostic::default(),
74 cx,
75 )
76 })
77 .on_click(
78 cx.listener(|this, _, window, cx| this.go_to_next_diagnostic(window, cx)),
79 ),
80 )
81 } else {
82 None
83 };
84
85 indicator
86 .child(
87 ButtonLike::new("diagnostic-indicator")
88 .child(diagnostic_indicator)
89 .tooltip(move |_window, cx| {
90 Tooltip::for_action("Project Diagnostics", &Deploy, cx)
91 })
92 .on_click(cx.listener(|this, _, window, cx| {
93 if let Some(workspace) = this.workspace.upgrade() {
94 if this.summary.error_count == 0 && this.summary.warning_count > 0 {
95 cx.update_default_global(
96 |show_warnings: &mut IncludeWarnings, _| show_warnings.0 = true,
97 );
98 }
99 workspace.update(cx, |workspace, cx| {
100 ProjectDiagnosticsEditor::deploy(
101 workspace,
102 &Default::default(),
103 window,
104 cx,
105 )
106 })
107 }
108 })),
109 )
110 .children(status)
111 }
112}
113
114impl DiagnosticIndicator {
115 pub fn new(workspace: &Workspace, cx: &mut Context<Self>) -> Self {
116 let project = workspace.project();
117 cx.subscribe(project, |this, project, event, cx| match event {
118 project::Event::DiskBasedDiagnosticsStarted { .. } => {
119 cx.notify();
120 }
121
122 project::Event::DiskBasedDiagnosticsFinished { .. }
123 | project::Event::LanguageServerRemoved(_) => {
124 this.summary = project.read(cx).diagnostic_summary(false, cx);
125 cx.notify();
126 }
127
128 project::Event::DiagnosticsUpdated { .. } => {
129 this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
130 cx.background_executor()
131 .timer(Duration::from_millis(30))
132 .await;
133 this.update(cx, |this, cx| {
134 this.summary = project.read(cx).diagnostic_summary(false, cx);
135 cx.notify();
136 })
137 .log_err();
138 });
139 }
140
141 _ => {}
142 })
143 .detach();
144
145 Self {
146 summary: project.read(cx).diagnostic_summary(false, cx),
147 active_editor: None,
148 workspace: workspace.weak_handle(),
149 current_diagnostic: None,
150 _observe_active_editor: None,
151 diagnostics_update: Task::ready(()),
152 diagnostic_summary_update: Task::ready(()),
153 }
154 }
155
156 fn go_to_next_diagnostic(&mut self, window: &mut Window, cx: &mut Context<Self>) {
157 if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
158 editor.update(cx, |editor, cx| {
159 editor.go_to_diagnostic_impl(
160 editor::Direction::Next,
161 GoToDiagnosticSeverityFilter::default(),
162 window,
163 cx,
164 );
165 })
166 }
167 }
168
169 fn update(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
170 let (buffer, cursor_position) = editor.update(cx, |editor, cx| {
171 let buffer = editor.buffer().read(cx).snapshot(cx);
172 let cursor_position = editor
173 .selections
174 .newest::<usize>(&editor.display_snapshot(cx))
175 .head();
176 (buffer, cursor_position)
177 });
178 let new_diagnostic = buffer
179 .diagnostics_in_range::<usize>(cursor_position..cursor_position)
180 .filter(|entry| !entry.range.is_empty())
181 .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
182 .map(|entry| entry.diagnostic);
183 if new_diagnostic != self.current_diagnostic.as_ref() {
184 let new_diagnostic = new_diagnostic.cloned();
185 self.diagnostics_update =
186 cx.spawn_in(window, async move |diagnostics_indicator, cx| {
187 cx.background_executor()
188 .timer(Duration::from_millis(50))
189 .await;
190 diagnostics_indicator
191 .update(cx, |diagnostics_indicator, cx| {
192 diagnostics_indicator.current_diagnostic = new_diagnostic;
193 cx.notify();
194 })
195 .ok();
196 });
197 }
198 }
199}
200
201impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
202
203impl StatusItemView for DiagnosticIndicator {
204 fn set_active_pane_item(
205 &mut self,
206 active_pane_item: Option<&dyn ItemHandle>,
207 window: &mut Window,
208 cx: &mut Context<Self>,
209 ) {
210 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
211 self.active_editor = Some(editor.downgrade());
212 self._observe_active_editor = Some(cx.observe_in(&editor, window, Self::update));
213 self.update(editor, window, cx);
214 } else {
215 self.active_editor = None;
216 self.current_diagnostic = None;
217 self._observe_active_editor = None;
218 }
219 cx.notify();
220 }
221}