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().child(
36 Icon::new(IconName::Check)
37 .size(IconSize::Small)
38 .color(Color::Default),
39 ),
40 (error_count, warning_count) => h_flex()
41 .gap_1()
42 .when(error_count > 0, |this| {
43 this.child(
44 Icon::new(IconName::XCircle)
45 .size(IconSize::Small)
46 .color(Color::Error),
47 )
48 .child(Label::new(error_count.to_string()).size(LabelSize::Small))
49 })
50 .when(warning_count > 0, |this| {
51 this.child(
52 Icon::new(IconName::Warning)
53 .size(IconSize::Small)
54 .color(Color::Warning),
55 )
56 .child(Label::new(warning_count.to_string()).size(LabelSize::Small))
57 }),
58 };
59
60 let status = if let Some(diagnostic) = &self.current_diagnostic {
61 let message = diagnostic
62 .message
63 .split_once('\n')
64 .map_or(&*diagnostic.message, |(first, _)| first);
65 Some(
66 Button::new("diagnostic_message", SharedString::new(message))
67 .label_size(LabelSize::Small)
68 .tooltip(|window, cx| {
69 Tooltip::for_action(
70 "Next Diagnostic",
71 &editor::actions::GoToDiagnostic::default(),
72 window,
73 cx,
74 )
75 })
76 .on_click(cx.listener(|this, _, window, cx| {
77 this.go_to_next_diagnostic(window, cx);
78 }))
79 .into_any_element(),
80 )
81 } else {
82 None
83 };
84
85 indicator
86 .child(
87 ButtonLike::new("diagnostic-indicator")
88 .child(diagnostic_indicator)
89 .tooltip(|window, cx| {
90 Tooltip::for_action("Project Diagnostics", &Deploy, window, 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.selections.newest::<usize>(cx).head();
173 (buffer, cursor_position)
174 });
175 let new_diagnostic = buffer
176 .diagnostics_in_range::<usize>(cursor_position..cursor_position)
177 .filter(|entry| !entry.range.is_empty())
178 .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
179 .map(|entry| entry.diagnostic);
180 if new_diagnostic != self.current_diagnostic {
181 self.diagnostics_update =
182 cx.spawn_in(window, async move |diagnostics_indicator, cx| {
183 cx.background_executor()
184 .timer(Duration::from_millis(50))
185 .await;
186 diagnostics_indicator
187 .update(cx, |diagnostics_indicator, cx| {
188 diagnostics_indicator.current_diagnostic = new_diagnostic;
189 cx.notify();
190 })
191 .ok();
192 });
193 }
194 }
195}
196
197impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
198
199impl StatusItemView for DiagnosticIndicator {
200 fn set_active_pane_item(
201 &mut self,
202 active_pane_item: Option<&dyn ItemHandle>,
203 window: &mut Window,
204 cx: &mut Context<Self>,
205 ) {
206 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
207 self.active_editor = Some(editor.downgrade());
208 self._observe_active_editor = Some(cx.observe_in(&editor, window, Self::update));
209 self.update(editor, window, cx);
210 } else {
211 self.active_editor = None;
212 self.current_diagnostic = None;
213 self._observe_active_editor = None;
214 }
215 cx.notify();
216 }
217}