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