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