1use std::rc::Rc;
7
8use gpui::{App, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div, px};
9use gpui_kit_semantics::{NodeSpec, Role, Semantic};
10use gpui_kit_theme::{ActiveTheme, ControlSize, Space, TextTone, TypeScale};
11
12use crate::controls::button::Button;
13use crate::controls::filter_bar::{FilterBar, FilterCondition, ResultCount};
14use crate::controls::toggle_button::Toggle;
15use crate::data::list::{List, ListItem};
16use crate::display::badge::{Badge, Tone};
17use crate::display::empty::{EmptyKind, EmptyState};
18use crate::display::loading::PulseLoader;
19use crate::foundation::{Disableable, Ident, Sizable, StyledExt, text};
20use crate::state::Loadable;
21use crate::strings::{ActiveStrings, StringKey};
22
23type FilterHandler = Rc<dyn Fn(DiagnosticFilter, &mut Window, &mut App)>;
24type SelectHandler = Rc<dyn Fn(SharedString, &mut Window, &mut App)>;
25type ActionHandler = Rc<dyn Fn(SharedString, SharedString, &mut Window, &mut App)>;
26type RetryHandler = Rc<dyn Fn(&mut Window, &mut App)>;
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
30pub enum DiagnosticSeverity {
31 Error,
32 Warning,
33 Information,
34 Hint,
35}
36
37impl DiagnosticSeverity {
38 pub const ALL: [Self; 4] = [Self::Error, Self::Warning, Self::Information, Self::Hint];
39
40 pub fn name(self) -> &'static str {
41 match self {
42 Self::Error => "error",
43 Self::Warning => "warning",
44 Self::Information => "information",
45 Self::Hint => "hint",
46 }
47 }
48
49 pub fn tone(self) -> Tone {
50 match self {
51 Self::Error => Tone::Danger,
52 Self::Warning => Tone::Warning,
53 Self::Information => Tone::Info,
54 Self::Hint => Tone::Neutral,
55 }
56 }
57
58 fn label(self, cx: &App) -> SharedString {
59 cx.strings().text(match self {
60 Self::Error => StringKey::DiagnosticsSeverityError,
61 Self::Warning => StringKey::DiagnosticsSeverityWarning,
62 Self::Information => StringKey::DiagnosticsSeverityInformation,
63 Self::Hint => StringKey::DiagnosticsSeverityHint,
64 })
65 }
66
67 const fn bit(self) -> u8 {
68 1 << self as u8
69 }
70}
71
72#[derive(Clone, Debug, PartialEq, Eq)]
74pub struct DiagnosticLocation(SharedString);
75
76impl DiagnosticLocation {
77 pub fn new(label: impl Into<SharedString>) -> Self {
78 Self(label.into())
79 }
80 pub fn label(&self) -> &SharedString {
81 &self.0
82 }
83}
84
85#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct DiagnosticAction {
88 id: SharedString,
89 label: SharedString,
90 disabled: bool,
91}
92
93impl DiagnosticAction {
94 pub fn new(id: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
95 Self {
96 id: id.into(),
97 label: label.into(),
98 disabled: false,
99 }
100 }
101 pub fn disabled(mut self, disabled: bool) -> Self {
102 self.disabled = disabled;
103 self
104 }
105 pub fn id(&self) -> &SharedString {
106 &self.id
107 }
108 pub fn label(&self) -> &SharedString {
109 &self.label
110 }
111 pub fn is_disabled(&self) -> bool {
112 self.disabled
113 }
114}
115
116#[derive(Clone, Debug, PartialEq, Eq)]
118pub struct Diagnostic {
119 id: SharedString,
120 severity: DiagnosticSeverity,
121 location: DiagnosticLocation,
122 message: SharedString,
123 actions: Vec<DiagnosticAction>,
124 disabled: bool,
125}
126
127impl Diagnostic {
128 pub fn new(
129 id: impl Into<SharedString>,
130 severity: DiagnosticSeverity,
131 location: DiagnosticLocation,
132 message: impl Into<SharedString>,
133 ) -> Self {
134 Self {
135 id: id.into(),
136 severity,
137 location,
138 message: message.into(),
139 actions: Vec::new(),
140 disabled: false,
141 }
142 }
143 pub fn action(mut self, action: DiagnosticAction) -> Self {
144 self.actions.push(action);
145 self
146 }
147 pub fn actions(mut self, actions: impl IntoIterator<Item = DiagnosticAction>) -> Self {
148 self.actions.extend(actions);
149 self
150 }
151 pub fn disabled(mut self, disabled: bool) -> Self {
152 self.disabled = disabled;
153 self
154 }
155 pub fn id(&self) -> &SharedString {
156 &self.id
157 }
158 pub fn severity(&self) -> DiagnosticSeverity {
159 self.severity
160 }
161 pub fn location(&self) -> &DiagnosticLocation {
162 &self.location
163 }
164 pub fn message(&self) -> &SharedString {
165 &self.message
166 }
167 pub fn diagnostic_actions(&self) -> &[DiagnosticAction] {
168 &self.actions
169 }
170 pub fn is_disabled(&self) -> bool {
171 self.disabled
172 }
173}
174
175#[derive(Clone, Copy, Debug, PartialEq, Eq)]
177pub struct DiagnosticFilter(u8);
178
179impl Default for DiagnosticFilter {
180 fn default() -> Self {
181 Self::all()
182 }
183}
184
185impl DiagnosticFilter {
186 const ALL_BITS: u8 = 0b1111;
187 pub const fn all() -> Self {
188 Self(Self::ALL_BITS)
189 }
190 pub const fn none() -> Self {
191 Self(0)
192 }
193 pub fn from_severities(values: impl IntoIterator<Item = DiagnosticSeverity>) -> Self {
194 Self(
195 values
196 .into_iter()
197 .fold(0, |bits, severity| bits | severity.bit()),
198 )
199 }
200 pub const fn contains(self, severity: DiagnosticSeverity) -> bool {
201 self.0 & severity.bit() != 0
202 }
203 pub fn includes(self, diagnostic: &Diagnostic) -> bool {
204 self.contains(diagnostic.severity)
205 }
206 pub const fn removing(self, severity: DiagnosticSeverity) -> Self {
207 Self(self.0 & !severity.bit())
208 }
209 pub const fn clearing(self) -> Self {
210 Self::none()
211 }
212 const fn setting(self, severity: DiagnosticSeverity, included: bool) -> Self {
213 if included {
214 Self(self.0 | severity.bit())
215 } else {
216 Self(self.0 & !severity.bit())
217 }
218 }
219 pub fn severities(self) -> impl Iterator<Item = DiagnosticSeverity> {
220 DiagnosticSeverity::ALL
221 .into_iter()
222 .filter(move |severity| self.contains(*severity))
223 }
224}
225
226#[derive(IntoElement)]
228pub struct DiagnosticsList {
229 ident: Ident,
230 diagnostics: Loadable<Vec<Diagnostic>, SharedString>,
231 filter: DiagnosticFilter,
232 selected: Option<SharedString>,
233 visible_rows: Option<usize>,
234 size: ControlSize,
235 disabled: bool,
236 on_filter: Option<FilterHandler>,
237 on_select: Option<SelectHandler>,
238 on_action: Option<ActionHandler>,
239 on_retry: Option<RetryHandler>,
240}
241
242impl std::fmt::Debug for DiagnosticsList {
243 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 formatter
245 .debug_struct("DiagnosticsList")
246 .field("ident", &self.ident)
247 .field("diagnostics", &self.diagnostics)
248 .field("filter", &self.filter)
249 .field("selected", &self.selected)
250 .field("visible_rows", &self.visible_rows)
251 .field("disabled", &self.disabled)
252 .finish()
253 }
254}
255
256impl DiagnosticsList {
257 pub fn new(
258 ident: impl Into<Ident>,
259 diagnostics: Loadable<Vec<Diagnostic>, SharedString>,
260 ) -> Self {
261 Self {
262 ident: ident.into(),
263 diagnostics,
264 filter: DiagnosticFilter::default(),
265 selected: None,
266 visible_rows: None,
267 size: ControlSize::Md,
268 disabled: false,
269 on_filter: None,
270 on_select: None,
271 on_action: None,
272 on_retry: None,
273 }
274 }
275 pub fn filter(mut self, filter: DiagnosticFilter) -> Self {
276 self.filter = filter;
277 self
278 }
279 pub fn selected(mut self, id: impl Into<SharedString>) -> Self {
280 self.selected = Some(id.into());
281 self
282 }
283 pub fn visible_rows(mut self, rows: usize) -> Self {
284 self.visible_rows = Some(rows);
285 self
286 }
287 pub fn on_filter(
288 mut self,
289 handler: impl Fn(DiagnosticFilter, &mut Window, &mut App) + 'static,
290 ) -> Self {
291 self.on_filter = Some(Rc::new(handler));
292 self
293 }
294 pub fn on_select(
295 mut self,
296 handler: impl Fn(SharedString, &mut Window, &mut App) + 'static,
297 ) -> Self {
298 self.on_select = Some(Rc::new(handler));
299 self
300 }
301 pub fn on_action(
302 mut self,
303 handler: impl Fn(SharedString, SharedString, &mut Window, &mut App) + 'static,
304 ) -> Self {
305 self.on_action = Some(Rc::new(handler));
306 self
307 }
308 pub fn on_retry(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
309 self.on_retry = Some(Rc::new(handler));
310 self
311 }
312}
313
314impl Disableable for DiagnosticsList {
315 fn disabled(mut self, disabled: bool) -> Self {
316 self.disabled = disabled;
317 self
318 }
319}
320impl Sizable for DiagnosticsList {
321 fn control_size(mut self, size: ControlSize) -> Self {
322 self.size = size;
323 self
324 }
325}
326
327impl RenderOnce for DiagnosticsList {
328 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
329 let ident = self.ident.clone();
330 let state = match self.diagnostics {
331 Loadable::Idle => EmptyState::new(
332 ident.child("idle"),
333 cx.strings().text(StringKey::DiagnosticsUnstarted),
334 )
335 .kind(EmptyKind::Unstarted)
336 .into_any_element(),
337 Loadable::Loading => PulseLoader::new(ident.child("loading"))
338 .label(cx.strings().text(StringKey::Loading))
339 .into_any_element(),
340 Loadable::Empty => EmptyState::new(
341 ident.child("empty"),
342 cx.strings().text(StringKey::DiagnosticsEmpty),
343 )
344 .kind(EmptyKind::Empty)
345 .into_any_element(),
346 Loadable::Unavailable(reason) => EmptyState::new(
347 ident.child("unavailable"),
348 cx.strings().text(StringKey::DiagnosticsUnavailable),
349 )
350 .kind(EmptyKind::Unavailable)
351 .detail(reason)
352 .into_any_element(),
353 Loadable::Error(reason) => {
354 let mut empty = EmptyState::new(
355 ident.child("error"),
356 cx.strings().text(StringKey::DiagnosticsError),
357 )
358 .kind(EmptyKind::Failed)
359 .detail(reason);
360 if let Some(handler) = self.on_retry.clone().filter(|_| !self.disabled) {
361 empty = empty.action(
362 Button::new(ident.child("retry"))
363 .label(cx.strings().text(StringKey::TryAgain))
364 .semantic_parent(ident.child("error").semantic_id())
365 .control_size(self.size)
366 .on_click(move |window, cx| handler(window, cx)),
367 );
368 }
369 empty.into_any_element()
370 }
371 Loadable::Ready(diagnostics) if diagnostics.is_empty() => EmptyState::new(
372 ident.child("empty"),
373 cx.strings().text(StringKey::DiagnosticsEmpty),
374 )
375 .kind(EmptyKind::Empty)
376 .into_any_element(),
377 Loadable::Ready(diagnostics) => {
378 let rows: Vec<_> = diagnostics
379 .into_iter()
380 .filter(|diagnostic| self.filter.includes(diagnostic))
381 .collect();
382 let filter_handler = self.on_filter.clone().filter(|_| !self.disabled);
383 let conditions = self.filter.severities().map(|severity| {
384 FilterCondition::new(
385 severity.name(),
386 cx.strings().text(StringKey::DiagnosticsFilterField),
387 cx.strings().text(StringKey::DiagnosticsFilterOperator),
388 severity.label(cx),
389 )
390 .tone(severity.tone())
391 });
392 let filter_control = div()
393 .flex()
394 .flex_wrap()
395 .gap(px(cx.theme().space(Space::Xs)))
396 .children(DiagnosticSeverity::ALL.map(|severity| {
397 let mut toggle =
398 Toggle::new(ident.child("filters.severities").child(severity.name()))
399 .label(severity.label(cx))
400 .pressed(self.filter.contains(severity))
401 .control_size(self.size)
402 .disabled(self.disabled)
403 .semantic_parent(ident.child("filters").semantic_id());
404 if let Some(handler) = filter_handler.clone() {
405 let filter = self.filter;
406 toggle = toggle.on_press(move |included, window, cx| {
407 handler(filter.setting(severity, included), window, cx);
408 });
409 }
410 toggle
411 }));
412 let mut bar = FilterBar::new(ident.child("filters"))
413 .conditions(conditions)
414 .add_control(filter_control)
415 .count(ResultCount::Known(rows.len()))
416 .control_size(self.size)
417 .disabled(self.disabled);
418 if let Some(handler) = filter_handler.clone() {
419 let filter = self.filter;
420 bar = bar.on_remove(move |id, window, cx| {
421 if let Some(severity) = DiagnosticSeverity::ALL
422 .into_iter()
423 .find(|severity| severity.name() == id.as_ref())
424 {
425 handler(filter.removing(severity), window, cx);
426 }
427 });
428 }
429 if let Some(handler) = filter_handler {
430 let filter = self.filter;
431 bar = bar.on_clear(move |window, cx| handler(filter.clearing(), window, cx));
432 }
433 if rows.is_empty() {
434 div()
435 .flex()
436 .flex_col()
437 .child(bar)
438 .child(
439 EmptyState::new(
440 ident.child("no-match"),
441 cx.strings().text(StringKey::DiagnosticsNoMatch),
442 )
443 .kind(EmptyKind::Empty),
444 )
445 .into_any_element()
446 } else {
447 let rows = Rc::new(rows);
448 let action_handler = self.on_action.clone().filter(|_| !self.disabled);
449 let row_ident = ident.child("list");
450 let size = self.size;
451 let disabled = self.disabled;
452 let rows_for_render = Rc::clone(&rows);
453 let mut list = List::new(row_ident.clone(), rows.len(), move |index, _, cx| {
454 let diagnostic = &rows_for_render[index];
455 let diagnostic_id = diagnostic.id.clone();
456 let item_ident = row_ident.child(diagnostic.id.as_ref());
457 let actions = diagnostic.actions.iter().map(|action| {
458 let mut button =
459 Button::new(item_ident.child("action").child(action.id.as_ref()))
460 .label(action.label.clone())
461 .semantic_parent(item_ident.semantic_id())
462 .ghost()
463 .control_size(size)
464 .disabled(disabled || diagnostic.disabled || action.disabled);
465 if let Some(handler) = action_handler
466 .clone()
467 .filter(|_| !diagnostic.disabled && !action.disabled)
468 {
469 let diagnostic_id = diagnostic_id.clone();
470 let action_id = action.id.clone();
471 button = button.on_click(move |window, cx| {
472 cx.stop_propagation();
473 handler(diagnostic_id.clone(), action_id.clone(), window, cx);
474 });
475 }
476 button
477 });
478 let content = div()
479 .flex()
480 .items_center()
481 .gap(px(cx.theme().space(Space::Sm)))
482 .child(
483 div()
484 .flex()
485 .flex_col()
486 .flex_1()
487 .child(
488 text(
489 cx.theme(),
490 TypeScale::Caption,
491 diagnostic.location.0.clone(),
492 )
493 .text_tone(cx.theme(), TextTone::Muted),
494 )
495 .child(text(
496 cx.theme(),
497 TypeScale::Body,
498 diagnostic.message.clone(),
499 )),
500 )
501 .child(
502 div()
503 .flex_none()
504 .child(
505 Badge::new(diagnostic.severity.label(cx))
506 .tone(diagnostic.severity.tone()),
507 )
508 .semantic_in(
509 cx,
510 NodeSpec::new(
511 item_ident.child("severity").semantic_id(),
512 Role::Status,
513 )
514 .parent(item_ident.semantic_id())
515 .text(diagnostic.severity.label(cx)),
516 ),
517 )
518 .children(actions);
519 ListItem::new(diagnostic.id.clone(), content)
520 .text(diagnostic.message.clone())
521 .disabled(disabled || diagnostic.disabled)
522 })
523 .row_height(cx.theme().control.get(self.size).height * 2.0)
524 .control_size(self.size)
525 .disabled(self.disabled);
526 if let Some(selected) = self.selected {
527 list = list.selected(selected);
528 }
529 if let Some(rows) = self.visible_rows {
530 list = list.visible_rows(rows);
531 }
532 if let Some(handler) = self.on_select.filter(|_| !self.disabled) {
533 list = list.on_select(move |id, window, cx| handler(id, window, cx));
534 }
535 div()
536 .flex()
537 .flex_col()
538 .child(bar)
539 .child(list)
540 .into_any_element()
541 }
542 }
543 };
544 div()
545 .w_full()
546 .child(state)
547 .semantic_in(cx, NodeSpec::new(ident.semantic_id(), Role::Group))
548 }
549}