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 window,
75 cx,
76 )
77 })
78 .on_click(
79 cx.listener(|this, _, window, cx| this.go_to_next_diagnostic(window, cx)),
80 ),
81 )
82 } else {
83 None
84 };
85
86 indicator
87 .child(
88 ButtonLike::new("diagnostic-indicator")
89 .child(diagnostic_indicator)
90 .tooltip(|window, cx| {
91 Tooltip::for_action("Project Diagnostics", &Deploy, window, cx)
92 })
93 .on_click(cx.listener(|this, _, window, cx| {
94 if let Some(workspace) = this.workspace.upgrade() {
95 if this.summary.error_count == 0 && this.summary.warning_count > 0 {
96 cx.update_default_global(
97 |show_warnings: &mut IncludeWarnings, _| show_warnings.0 = true,
98 );
99 }
100 workspace.update(cx, |workspace, cx| {
101 ProjectDiagnosticsEditor::deploy(
102 workspace,
103 &Default::default(),
104 window,
105 cx,
106 )
107 })
108 }
109 })),
110 )
111 .children(status)
112 }
113}
114
115impl DiagnosticIndicator {
116 pub fn new(workspace: &Workspace, cx: &mut Context<Self>) -> Self {
117 let project = workspace.project();
118 cx.subscribe(project, |this, project, event, cx| match event {
119 project::Event::DiskBasedDiagnosticsStarted { .. } => {
120 cx.notify();
121 }
122
123 project::Event::DiskBasedDiagnosticsFinished { .. }
124 | project::Event::LanguageServerRemoved(_) => {
125 this.summary = project.read(cx).diagnostic_summary(false, cx);
126 cx.notify();
127 }
128
129 project::Event::DiagnosticsUpdated { .. } => {
130 this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
131 cx.background_executor()
132 .timer(Duration::from_millis(30))
133 .await;
134 this.update(cx, |this, cx| {
135 this.summary = project.read(cx).diagnostic_summary(false, cx);
136 cx.notify();
137 })
138 .log_err();
139 });
140 }
141
142 _ => {}
143 })
144 .detach();
145
146 Self {
147 summary: project.read(cx).diagnostic_summary(false, cx),
148 active_editor: None,
149 workspace: workspace.weak_handle(),
150 current_diagnostic: None,
151 _observe_active_editor: None,
152 diagnostics_update: Task::ready(()),
153 diagnostic_summary_update: Task::ready(()),
154 }
155 }
156
157 fn go_to_next_diagnostic(&mut self, window: &mut Window, cx: &mut Context<Self>) {
158 if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
159 editor.update(cx, |editor, cx| {
160 editor.go_to_diagnostic_impl(
161 editor::Direction::Next,
162 GoToDiagnosticSeverityFilter::default(),
163 window,
164 cx,
165 );
166 })
167 }
168 }
169
170 fn update(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
171 let (buffer, cursor_position) = editor.update(cx, |editor, cx| {
172 let buffer = editor.buffer().read(cx).snapshot(cx);
173 let cursor_position = editor
174 .selections
175 .newest::<usize>(&editor.display_snapshot(cx))
176 .head();
177 (buffer, cursor_position)
178 });
179 let new_diagnostic = buffer
180 .diagnostics_in_range::<usize>(cursor_position..cursor_position)
181 .filter(|entry| !entry.range.is_empty())
182 .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
183 .map(|entry| entry.diagnostic);
184 if new_diagnostic != self.current_diagnostic.as_ref() {
185 let new_diagnostic = new_diagnostic.cloned();
186 self.diagnostics_update =
187 cx.spawn_in(window, async move |diagnostics_indicator, cx| {
188 cx.background_executor()
189 .timer(Duration::from_millis(50))
190 .await;
191 diagnostics_indicator
192 .update(cx, |diagnostics_indicator, cx| {
193 diagnostics_indicator.current_diagnostic = new_diagnostic;
194 cx.notify();
195 })
196 .ok();
197 });
198 }
199 }
200}
201
202impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
203
204impl StatusItemView for DiagnosticIndicator {
205 fn set_active_pane_item(
206 &mut self,
207 active_pane_item: Option<&dyn ItemHandle>,
208 window: &mut Window,
209 cx: &mut Context<Self>,
210 ) {
211 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
212 self.active_editor = Some(editor.downgrade());
213 self._observe_active_editor = Some(cx.observe_in(&editor, window, Self::update));
214 self.update(editor, window, cx);
215 } else {
216 self.active_editor = None;
217 self.current_diagnostic = None;
218 self._observe_active_editor = None;
219 }
220 cx.notify();
221 }
222}