1use collections::HashSet;
2use editor::{Editor, GoToDiagnostic};
3use gpui::{
4 rems, Div, EventEmitter, InteractiveElement, IntoElement, ParentElement, Render, Stateful,
5 StatefulInteractiveElement, Styled, Subscription, View, ViewContext, WeakView,
6};
7use language::Diagnostic;
8use lsp::LanguageServerId;
9use theme::ActiveTheme;
10use ui::{h_stack, Button, Clickable, 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 let status = if !self.in_progress_checks.is_empty() {
47 Some(Label::new("Checking…").into_any_element())
48 } else if let Some(diagnostic) = &self.current_diagnostic {
49 let message = diagnostic.message.split('\n').next().unwrap().to_string();
50 Some(
51 Button::new("diagnostic_message", message)
52 .on_click(cx.listener(|this, _, cx| {
53 this.go_to_next_diagnostic(&GoToDiagnostic, cx);
54 }))
55 .into_any_element(),
56 )
57 } else {
58 None
59 };
60
61 h_stack()
62 .id("diagnostic-indicator")
63 .on_action(cx.listener(Self::go_to_next_diagnostic))
64 .rounded_md()
65 .flex_none()
66 .h(rems(1.375))
67 .px_6()
68 .cursor_pointer()
69 .bg(cx.theme().colors().ghost_element_background)
70 .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
71 .active(|style| style.bg(cx.theme().colors().ghost_element_active))
72 .tooltip(|cx| Tooltip::text("Project Diagnostics", cx))
73 .on_click(cx.listener(|this, _, cx| {
74 if let Some(workspace) = this.workspace.upgrade() {
75 workspace.update(cx, |workspace, cx| {
76 ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
77 })
78 }
79 }))
80 .child(diagnostic_indicator)
81 .children(status)
82 }
83}
84
85impl DiagnosticIndicator {
86 pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
87 let project = workspace.project();
88 cx.subscribe(project, |this, project, event, cx| match event {
89 project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
90 this.in_progress_checks.insert(*language_server_id);
91 cx.notify();
92 }
93
94 project::Event::DiskBasedDiagnosticsFinished { language_server_id }
95 | project::Event::LanguageServerRemoved(language_server_id) => {
96 this.summary = project.read(cx).diagnostic_summary(false, cx);
97 this.in_progress_checks.remove(language_server_id);
98 cx.notify();
99 }
100
101 project::Event::DiagnosticsUpdated { .. } => {
102 this.summary = project.read(cx).diagnostic_summary(false, cx);
103 cx.notify();
104 }
105
106 _ => {}
107 })
108 .detach();
109
110 Self {
111 summary: project.read(cx).diagnostic_summary(false, cx),
112 in_progress_checks: project
113 .read(cx)
114 .language_servers_running_disk_based_diagnostics()
115 .collect(),
116 active_editor: None,
117 workspace: workspace.weak_handle(),
118 current_diagnostic: None,
119 _observe_active_editor: None,
120 }
121 }
122
123 fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
124 if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
125 editor.update(cx, |editor, cx| {
126 editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
127 })
128 }
129 }
130
131 fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
132 let editor = editor.read(cx);
133 let buffer = editor.buffer().read(cx);
134 let cursor_position = editor.selections.newest::<usize>(cx).head();
135 let new_diagnostic = buffer
136 .snapshot(cx)
137 .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
138 .filter(|entry| !entry.range.is_empty())
139 .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
140 .map(|entry| entry.diagnostic);
141 if new_diagnostic != self.current_diagnostic {
142 self.current_diagnostic = new_diagnostic;
143 cx.notify();
144 }
145 }
146}
147
148impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
149
150impl StatusItemView for DiagnosticIndicator {
151 fn set_active_pane_item(
152 &mut self,
153 active_pane_item: Option<&dyn ItemHandle>,
154 cx: &mut ViewContext<Self>,
155 ) {
156 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
157 self.active_editor = Some(editor.downgrade());
158 self._observe_active_editor = Some(cx.observe(&editor, Self::update));
159 self.update(editor, cx);
160 } else {
161 self.active_editor = None;
162 self.current_diagnostic = None;
163 self._observe_active_editor = None;
164 }
165 cx.notify();
166 }
167}