1use std::time::Duration;
2
3use editor::Editor;
4use gpui::{
5 percentage, rems, Animation, AnimationExt, EventEmitter, IntoElement, ParentElement, Render,
6 Styled, Subscription, Transformation, View, ViewContext, WeakView,
7};
8use language::Diagnostic;
9use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
10use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
11
12use crate::{Deploy, ProjectDiagnosticsEditor};
13
14pub struct DiagnosticIndicator {
15 summary: project::DiagnosticSummary,
16 active_editor: Option<WeakView<Editor>>,
17 workspace: WeakView<Workspace>,
18 current_diagnostic: Option<Diagnostic>,
19 _observe_active_editor: Option<Subscription>,
20}
21
22impl Render for DiagnosticIndicator {
23 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
24 let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
25 (0, 0) => h_flex().map(|this| {
26 this.child(
27 Icon::new(IconName::Check)
28 .size(IconSize::Small)
29 .color(Color::Default),
30 )
31 }),
32 (0, warning_count) => h_flex()
33 .gap_1()
34 .child(
35 Icon::new(IconName::ExclamationTriangle)
36 .size(IconSize::Small)
37 .color(Color::Warning),
38 )
39 .child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
40 (error_count, 0) => h_flex()
41 .gap_1()
42 .child(
43 Icon::new(IconName::XCircle)
44 .size(IconSize::Small)
45 .color(Color::Error),
46 )
47 .child(Label::new(error_count.to_string()).size(LabelSize::Small)),
48 (error_count, warning_count) => 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 .child(
57 Icon::new(IconName::ExclamationTriangle)
58 .size(IconSize::Small)
59 .color(Color::Warning),
60 )
61 .child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
62 };
63
64 let has_in_progress_checks = self
65 .workspace
66 .upgrade()
67 .and_then(|workspace| {
68 workspace
69 .read(cx)
70 .project()
71 .read(cx)
72 .language_servers_running_disk_based_diagnostics()
73 .next()
74 })
75 .is_some();
76
77 let status = if has_in_progress_checks {
78 Some(
79 h_flex()
80 .gap_2()
81 .child(
82 Icon::new(IconName::ArrowCircle)
83 .size(IconSize::Small)
84 .with_animation(
85 "arrow-circle",
86 Animation::new(Duration::from_secs(2)).repeat(),
87 |icon, delta| {
88 icon.transform(Transformation::rotate(percentage(delta)))
89 },
90 ),
91 )
92 .child(
93 Label::new("Checking…")
94 .size(LabelSize::Small)
95 .into_any_element(),
96 )
97 .into_any_element(),
98 )
99 } else if let Some(diagnostic) = &self.current_diagnostic {
100 let message = diagnostic.message.split('\n').next().unwrap().to_string();
101 Some(
102 Button::new("diagnostic_message", message)
103 .label_size(LabelSize::Small)
104 .tooltip(|cx| {
105 Tooltip::for_action("Next Diagnostic", &editor::actions::GoToDiagnostic, cx)
106 })
107 .on_click(cx.listener(|this, _, cx| {
108 this.go_to_next_diagnostic(cx);
109 }))
110 .into_any_element(),
111 )
112 } else {
113 None
114 };
115
116 h_flex()
117 .h(rems(1.375))
118 .gap_2()
119 .child(
120 ButtonLike::new("diagnostic-indicator")
121 .child(diagnostic_indicator)
122 .tooltip(|cx| Tooltip::for_action("Project Diagnostics", &Deploy, cx))
123 .on_click(cx.listener(|this, _, cx| {
124 if let Some(workspace) = this.workspace.upgrade() {
125 workspace.update(cx, |workspace, cx| {
126 ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
127 })
128 }
129 })),
130 )
131 .children(status)
132 }
133}
134
135impl DiagnosticIndicator {
136 pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
137 let project = workspace.project();
138 cx.subscribe(project, |this, project, event, cx| match event {
139 project::Event::DiskBasedDiagnosticsStarted { .. } => {
140 cx.notify();
141 }
142
143 project::Event::DiskBasedDiagnosticsFinished { .. }
144 | project::Event::LanguageServerRemoved(_) => {
145 this.summary = project.read(cx).diagnostic_summary(false, cx);
146 cx.notify();
147 }
148
149 project::Event::DiagnosticsUpdated { .. } => {
150 this.summary = project.read(cx).diagnostic_summary(false, cx);
151 cx.notify();
152 }
153
154 _ => {}
155 })
156 .detach();
157
158 Self {
159 summary: project.read(cx).diagnostic_summary(false, cx),
160 active_editor: None,
161 workspace: workspace.weak_handle(),
162 current_diagnostic: None,
163 _observe_active_editor: None,
164 }
165 }
166
167 fn go_to_next_diagnostic(&mut self, cx: &mut ViewContext<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(editor::Direction::Next, cx);
171 })
172 }
173 }
174
175 fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
176 let editor = editor.read(cx);
177 let buffer = editor.buffer().read(cx);
178 let cursor_position = editor.selections.newest::<usize>(cx).head();
179 let new_diagnostic = buffer
180 .snapshot(cx)
181 .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
182 .filter(|entry| !entry.range.is_empty())
183 .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
184 .map(|entry| entry.diagnostic);
185 if new_diagnostic != self.current_diagnostic {
186 self.current_diagnostic = new_diagnostic;
187 cx.notify();
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 cx: &mut ViewContext<Self>,
199 ) {
200 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
201 self.active_editor = Some(editor.downgrade());
202 self._observe_active_editor = Some(cx.observe(&editor, Self::update));
203 self.update(editor, cx);
204 } else {
205 self.active_editor = None;
206 self.current_diagnostic = None;
207 self._observe_active_editor = None;
208 }
209 cx.notify();
210 }
211}