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