1use collections::HashSet;
2use editor::{Editor, GoToDiagnostic};
3use gpui::{
4 div, Div, EventEmitter, InteractiveComponent, ParentComponent, Render, Stateful,
5 StatefulInteractiveComponent, Styled, Subscription, View, ViewContext, WeakView,
6};
7use language::Diagnostic;
8use lsp::LanguageServerId;
9use theme::ActiveTheme;
10use ui::{h_stack, Icon, IconElement, Label, TextColor, Tooltip};
11use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
12
13use crate::ProjectDiagnosticsEditor;
14
15pub struct DiagnosticIndicator {
16 summary: project::DiagnosticSummary,
17 active_editor: Option<WeakView<Editor>>,
18 workspace: WeakView<Workspace>,
19 current_diagnostic: Option<Diagnostic>,
20 in_progress_checks: HashSet<LanguageServerId>,
21 _observe_active_editor: Option<Subscription>,
22}
23
24impl Render for DiagnosticIndicator {
25 type Element = Stateful<Self, Div<Self>>;
26
27 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
28 let mut summary_row = h_stack()
29 .id(cx.entity_id())
30 .on_action(Self::go_to_next_diagnostic)
31 .rounded_md()
32 .p_1()
33 .cursor_pointer()
34 .bg(gpui::green())
35 .hover(|style| style.bg(cx.theme().colors().element_hover))
36 .active(|style| style.bg(cx.theme().colors().element_active))
37 .tooltip(|_, cx| Tooltip::text("Project Diagnostics", cx))
38 .on_click(|this, _, cx| {
39 if let Some(workspace) = this.workspace.upgrade() {
40 workspace.update(cx, |workspace, cx| {
41 ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
42 })
43 }
44 });
45
46 if self.summary.error_count > 0 {
47 summary_row = summary_row.child(
48 div()
49 .child(IconElement::new(Icon::XCircle).color(TextColor::Error))
50 .bg(gpui::red()),
51 );
52 summary_row = summary_row.child(
53 div()
54 .child(Label::new(self.summary.error_count.to_string()))
55 .bg(gpui::yellow()),
56 );
57 }
58
59 if self.summary.warning_count > 0 {
60 summary_row = summary_row
61 .child(IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning));
62 summary_row = summary_row.child(Label::new(self.summary.warning_count.to_string()));
63 }
64
65 if self.summary.error_count == 0 && self.summary.warning_count == 0 {
66 summary_row =
67 summary_row.child(IconElement::new(Icon::Check).color(TextColor::Success));
68 }
69
70 summary_row
71 }
72}
73
74impl DiagnosticIndicator {
75 pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
76 let project = workspace.project();
77 cx.subscribe(project, |this, project, event, cx| match event {
78 project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
79 this.in_progress_checks.insert(*language_server_id);
80 cx.notify();
81 }
82
83 project::Event::DiskBasedDiagnosticsFinished { language_server_id }
84 | project::Event::LanguageServerRemoved(language_server_id) => {
85 this.summary = project.read(cx).diagnostic_summary(cx);
86 this.in_progress_checks.remove(language_server_id);
87 cx.notify();
88 }
89
90 project::Event::DiagnosticsUpdated { .. } => {
91 this.summary = project.read(cx).diagnostic_summary(cx);
92 cx.notify();
93 }
94
95 _ => {}
96 })
97 .detach();
98
99 Self {
100 summary: project.read(cx).diagnostic_summary(cx),
101 in_progress_checks: project
102 .read(cx)
103 .language_servers_running_disk_based_diagnostics()
104 .collect(),
105 active_editor: None,
106 workspace: workspace.weak_handle(),
107 current_diagnostic: None,
108 _observe_active_editor: None,
109 }
110 }
111
112 fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
113 if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
114 editor.update(cx, |editor, cx| {
115 editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
116 })
117 }
118 }
119
120 fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
121 let editor = editor.read(cx);
122 let buffer = editor.buffer().read(cx);
123 let cursor_position = editor.selections.newest::<usize>(cx).head();
124 let new_diagnostic = buffer
125 .snapshot(cx)
126 .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
127 .filter(|entry| !entry.range.is_empty())
128 .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
129 .map(|entry| entry.diagnostic);
130 if new_diagnostic != self.current_diagnostic {
131 self.current_diagnostic = new_diagnostic;
132 cx.notify();
133 }
134 }
135}
136
137impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
138
139impl StatusItemView for DiagnosticIndicator {
140 fn set_active_pane_item(
141 &mut self,
142 active_pane_item: Option<&dyn ItemHandle>,
143 cx: &mut ViewContext<Self>,
144 ) {
145 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
146 self.active_editor = Some(editor.downgrade());
147 self._observe_active_editor = Some(cx.observe(&editor, Self::update));
148 self.update(editor, cx);
149 } else {
150 self.active_editor = None;
151 self.current_diagnostic = None;
152 self._observe_active_editor = None;
153 }
154 cx.notify();
155 }
156}