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