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