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