1use collections::HashSet;
2use editor::{Editor, GoToDiagnostic};
3use gpui::{
4 elements::*,
5 platform::{CursorStyle, MouseButton},
6 serde_json, AppContext, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
7};
8use language::Diagnostic;
9use lsp::LanguageServerId;
10use workspace::{item::ItemHandle, StatusItemView, Workspace};
11
12use crate::ProjectDiagnosticsEditor;
13
14pub struct DiagnosticIndicator {
15 summary: project::DiagnosticSummary,
16 active_editor: Option<WeakViewHandle<Editor>>,
17 workspace: WeakViewHandle<Workspace>,
18 current_diagnostic: Option<Diagnostic>,
19 in_progress_checks: HashSet<LanguageServerId>,
20 _observe_active_editor: Option<Subscription>,
21}
22
23pub fn init(cx: &mut AppContext) {
24 cx.add_action(DiagnosticIndicator::go_to_next_diagnostic);
25}
26
27impl DiagnosticIndicator {
28 pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
29 let project = workspace.project();
30 cx.subscribe(project, |this, project, event, cx| match event {
31 project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
32 this.in_progress_checks.insert(*language_server_id);
33 cx.notify();
34 }
35 project::Event::DiskBasedDiagnosticsFinished { language_server_id }
36 | project::Event::LanguageServerRemoved(language_server_id) => {
37 this.summary = project.read(cx).diagnostic_summary(cx);
38 this.in_progress_checks.remove(language_server_id);
39 cx.notify();
40 }
41 project::Event::DiagnosticsUpdated { .. } => {
42 this.summary = project.read(cx).diagnostic_summary(cx);
43 cx.notify();
44 }
45 _ => {}
46 })
47 .detach();
48 Self {
49 summary: project.read(cx).diagnostic_summary(cx),
50 in_progress_checks: project
51 .read(cx)
52 .language_servers_running_disk_based_diagnostics()
53 .collect(),
54 active_editor: None,
55 workspace: workspace.weak_handle(),
56 current_diagnostic: None,
57 _observe_active_editor: None,
58 }
59 }
60
61 fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
62 if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade(cx)) {
63 editor.update(cx, |editor, cx| {
64 editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
65 })
66 }
67 }
68
69 fn update(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
70 let editor = editor.read(cx);
71 let buffer = editor.buffer().read(cx);
72 let cursor_position = editor.selections.newest::<usize>(cx).head();
73 let new_diagnostic = buffer
74 .snapshot(cx)
75 .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
76 .filter(|entry| !entry.range.is_empty())
77 .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
78 .map(|entry| entry.diagnostic);
79 if new_diagnostic != self.current_diagnostic {
80 self.current_diagnostic = new_diagnostic;
81 cx.notify();
82 }
83 }
84}
85
86impl Entity for DiagnosticIndicator {
87 type Event = ();
88}
89
90impl View for DiagnosticIndicator {
91 fn ui_name() -> &'static str {
92 "DiagnosticIndicator"
93 }
94
95 fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
96 enum Summary {}
97 enum Message {}
98
99 let tooltip_style = theme::current(cx).tooltip.clone();
100 let in_progress = !self.in_progress_checks.is_empty();
101 let mut element = Flex::row().with_child(
102 MouseEventHandler::new::<Summary, _>(0, cx, |state, cx| {
103 let theme = theme::current(cx);
104 let style = theme
105 .workspace
106 .status_bar
107 .diagnostic_summary
108 .style_for(state);
109
110 let mut summary_row = Flex::row();
111 if self.summary.error_count > 0 {
112 summary_row.add_child(
113 Svg::new("icons/error.svg")
114 .with_color(style.icon_color_error)
115 .constrained()
116 .with_width(style.icon_width)
117 .aligned()
118 .contained()
119 .with_margin_right(style.icon_spacing),
120 );
121 summary_row.add_child(
122 Label::new(self.summary.error_count.to_string(), style.text.clone())
123 .aligned(),
124 );
125 }
126
127 if self.summary.warning_count > 0 {
128 summary_row.add_child(
129 Svg::new("icons/warning.svg")
130 .with_color(style.icon_color_warning)
131 .constrained()
132 .with_width(style.icon_width)
133 .aligned()
134 .contained()
135 .with_margin_right(style.icon_spacing)
136 .with_margin_left(if self.summary.error_count > 0 {
137 style.summary_spacing
138 } else {
139 0.
140 }),
141 );
142 summary_row.add_child(
143 Label::new(self.summary.warning_count.to_string(), style.text.clone())
144 .aligned(),
145 );
146 }
147
148 if self.summary.error_count == 0 && self.summary.warning_count == 0 {
149 summary_row.add_child(
150 Svg::new("icons/check_circle.svg")
151 .with_color(style.icon_color_ok)
152 .constrained()
153 .with_width(style.icon_width)
154 .aligned()
155 .into_any_named("ok-icon"),
156 );
157 }
158
159 summary_row
160 .constrained()
161 .with_height(style.height)
162 .contained()
163 .with_style(if self.summary.error_count > 0 {
164 style.container_error
165 } else if self.summary.warning_count > 0 {
166 style.container_warning
167 } else {
168 style.container_ok
169 })
170 })
171 .with_cursor_style(CursorStyle::PointingHand)
172 .on_click(MouseButton::Left, |_, this, cx| {
173 if let Some(workspace) = this.workspace.upgrade(cx) {
174 workspace.update(cx, |workspace, cx| {
175 ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
176 })
177 }
178 })
179 .with_tooltip::<Summary>(
180 0,
181 "Project Diagnostics",
182 Some(Box::new(crate::Deploy)),
183 tooltip_style,
184 cx,
185 )
186 .aligned()
187 .into_any(),
188 );
189
190 let style = &theme::current(cx).workspace.status_bar;
191 let item_spacing = style.item_spacing;
192
193 if in_progress {
194 element.add_child(
195 Label::new("Checking…", style.diagnostic_message.default.text.clone())
196 .aligned()
197 .contained()
198 .with_margin_left(item_spacing),
199 );
200 } else if let Some(diagnostic) = &self.current_diagnostic {
201 let message_style = style.diagnostic_message.clone();
202 element.add_child(
203 MouseEventHandler::new::<Message, _>(1, cx, |state, _| {
204 Label::new(
205 diagnostic.message.split('\n').next().unwrap().to_string(),
206 message_style.style_for(state).text.clone(),
207 )
208 .aligned()
209 .contained()
210 .with_margin_left(item_spacing)
211 })
212 .with_cursor_style(CursorStyle::PointingHand)
213 .on_click(MouseButton::Left, |_, this, cx| {
214 this.go_to_next_diagnostic(&Default::default(), cx)
215 }),
216 );
217 }
218
219 element.into_any_named("diagnostic indicator")
220 }
221
222 fn debug_json(&self, _: &gpui::AppContext) -> serde_json::Value {
223 serde_json::json!({ "summary": self.summary })
224 }
225}
226
227impl StatusItemView for DiagnosticIndicator {
228 fn set_active_pane_item(
229 &mut self,
230 active_pane_item: Option<&dyn ItemHandle>,
231 cx: &mut ViewContext<Self>,
232 ) {
233 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
234 self.active_editor = Some(editor.downgrade());
235 self._observe_active_editor = Some(cx.observe(&editor, Self::update));
236 self.update(editor, cx);
237 } else {
238 self.active_editor = None;
239 self.current_diagnostic = None;
240 self._observe_active_editor = None;
241 }
242 cx.notify();
243 }
244}