1use std::time::Duration;
2
3use editor::{Editor, MultiBufferOffset};
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().min_w_0().overflow_x_hidden();
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 .truncate(true)
71 .tooltip(|_window, cx| {
72 Tooltip::for_action(
73 "Next Diagnostic",
74 &editor::actions::GoToDiagnostic::default(),
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(move |_window, cx| {
91 Tooltip::for_action("Project Diagnostics", &Deploy, 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::<MultiBufferOffset>(&editor.display_snapshot(cx))
176 .head();
177 (buffer, cursor_position)
178 });
179 let new_diagnostic = buffer
180 .diagnostics_in_range::<MultiBufferOffset>(cursor_position..cursor_position)
181 .filter(|entry| !entry.range.is_empty())
182 .min_by_key(|entry| {
183 (
184 entry.diagnostic.severity,
185 entry.range.end - entry.range.start,
186 )
187 })
188 .map(|entry| entry.diagnostic);
189 if new_diagnostic != self.current_diagnostic.as_ref() {
190 let new_diagnostic = new_diagnostic.cloned();
191 self.diagnostics_update =
192 cx.spawn_in(window, async move |diagnostics_indicator, cx| {
193 cx.background_executor()
194 .timer(Duration::from_millis(50))
195 .await;
196 diagnostics_indicator
197 .update(cx, |diagnostics_indicator, cx| {
198 diagnostics_indicator.current_diagnostic = new_diagnostic;
199 cx.notify();
200 })
201 .ok();
202 });
203 }
204 }
205}
206
207impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
208
209impl StatusItemView for DiagnosticIndicator {
210 fn set_active_pane_item(
211 &mut self,
212 active_pane_item: Option<&dyn ItemHandle>,
213 window: &mut Window,
214 cx: &mut Context<Self>,
215 ) {
216 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
217 self.active_editor = Some(editor.downgrade());
218 self._observe_active_editor = Some(cx.observe_in(&editor, window, Self::update));
219 self.update(editor, window, cx);
220 } else {
221 self.active_editor = None;
222 self.current_diagnostic = None;
223 self._observe_active_editor = None;
224 }
225 cx.notify();
226 }
227}