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