1use collections::HashSet;
2use editor::Editor;
3use gpui::{
4 rems, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View,
5 ViewContext, WeakView,
6};
7use language::Diagnostic;
8use lsp::LanguageServerId;
9use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconElement, Label, Tooltip};
10use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
11
12use crate::{Deploy, ProjectDiagnosticsEditor};
13
14pub struct DiagnosticIndicator {
15 summary: project::DiagnosticSummary,
16 active_editor: Option<WeakView<Editor>>,
17 workspace: WeakView<Workspace>,
18 current_diagnostic: Option<Diagnostic>,
19 in_progress_checks: HashSet<LanguageServerId>,
20 _observe_active_editor: Option<Subscription>,
21}
22
23impl Render for DiagnosticIndicator {
24 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
25 let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
26 (0, 0) => h_stack().child(
27 IconElement::new(Icon::Check)
28 .size(IconSize::Small)
29 .color(Color::Success),
30 ),
31 (0, warning_count) => h_stack()
32 .gap_1()
33 .child(
34 IconElement::new(Icon::ExclamationTriangle)
35 .size(IconSize::Small)
36 .color(Color::Warning),
37 )
38 .child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
39 (error_count, 0) => h_stack()
40 .gap_1()
41 .child(
42 IconElement::new(Icon::XCircle)
43 .size(IconSize::Small)
44 .color(Color::Error),
45 )
46 .child(Label::new(error_count.to_string()).size(LabelSize::Small)),
47 (error_count, warning_count) => h_stack()
48 .gap_1()
49 .child(
50 IconElement::new(Icon::XCircle)
51 .size(IconSize::Small)
52 .color(Color::Error),
53 )
54 .child(Label::new(error_count.to_string()).size(LabelSize::Small))
55 .child(
56 IconElement::new(Icon::ExclamationTriangle)
57 .size(IconSize::Small)
58 .color(Color::Warning),
59 )
60 .child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
61 };
62
63 let status = if !self.in_progress_checks.is_empty() {
64 Some(
65 Label::new("Checking…")
66 .size(LabelSize::Small)
67 .into_any_element(),
68 )
69 } else if let Some(diagnostic) = &self.current_diagnostic {
70 let message = diagnostic.message.split('\n').next().unwrap().to_string();
71 Some(
72 Button::new("diagnostic_message", message)
73 .label_size(LabelSize::Small)
74 .tooltip(|cx| {
75 Tooltip::for_action("Next Diagnostic", &editor::GoToDiagnostic, cx)
76 })
77 .on_click(cx.listener(|this, _, cx| {
78 this.go_to_next_diagnostic(cx);
79 }))
80 .into_any_element(),
81 )
82 } else {
83 None
84 };
85
86 h_stack()
87 .h(rems(1.375))
88 .gap_2()
89 .child(
90 ButtonLike::new("diagnostic-indicator")
91 .child(diagnostic_indicator)
92 .tooltip(|cx| Tooltip::for_action("Project Diagnostics", &Deploy, cx))
93 .on_click(cx.listener(|this, _, cx| {
94 if let Some(workspace) = this.workspace.upgrade() {
95 workspace.update(cx, |workspace, cx| {
96 ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
97 })
98 }
99 })),
100 )
101 .children(status)
102 }
103}
104
105impl DiagnosticIndicator {
106 pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
107 let project = workspace.project();
108 cx.subscribe(project, |this, project, event, cx| match event {
109 project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
110 this.in_progress_checks.insert(*language_server_id);
111 cx.notify();
112 }
113
114 project::Event::DiskBasedDiagnosticsFinished { language_server_id }
115 | project::Event::LanguageServerRemoved(language_server_id) => {
116 this.summary = project.read(cx).diagnostic_summary(false, cx);
117 this.in_progress_checks.remove(language_server_id);
118 cx.notify();
119 }
120
121 project::Event::DiagnosticsUpdated { .. } => {
122 this.summary = project.read(cx).diagnostic_summary(false, cx);
123 cx.notify();
124 }
125
126 _ => {}
127 })
128 .detach();
129
130 Self {
131 summary: project.read(cx).diagnostic_summary(false, cx),
132 in_progress_checks: project
133 .read(cx)
134 .language_servers_running_disk_based_diagnostics()
135 .collect(),
136 active_editor: None,
137 workspace: workspace.weak_handle(),
138 current_diagnostic: None,
139 _observe_active_editor: None,
140 }
141 }
142
143 fn go_to_next_diagnostic(&mut self, cx: &mut ViewContext<Self>) {
144 if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
145 editor.update(cx, |editor, cx| {
146 editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
147 })
148 }
149 }
150
151 fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
152 let editor = editor.read(cx);
153 let buffer = editor.buffer().read(cx);
154 let cursor_position = editor.selections.newest::<usize>(cx).head();
155 let new_diagnostic = buffer
156 .snapshot(cx)
157 .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
158 .filter(|entry| !entry.range.is_empty())
159 .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
160 .map(|entry| entry.diagnostic);
161 if new_diagnostic != self.current_diagnostic {
162 self.current_diagnostic = new_diagnostic;
163 cx.notify();
164 }
165 }
166}
167
168impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
169
170impl StatusItemView for DiagnosticIndicator {
171 fn set_active_pane_item(
172 &mut self,
173 active_pane_item: Option<&dyn ItemHandle>,
174 cx: &mut ViewContext<Self>,
175 ) {
176 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
177 self.active_editor = Some(editor.downgrade());
178 self._observe_active_editor = Some(cx.observe(&editor, Self::update));
179 self.update(editor, cx);
180 } else {
181 self.active_editor = None;
182 self.current_diagnostic = None;
183 self._observe_active_editor = None;
184 }
185 cx.notify();
186 }
187}