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