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