1use std::time::Duration;
2
3use editor::Editor;
4use gpui::{
5 Context, Entity, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, Task,
6 WeakEntity, Window,
7};
8use language::Diagnostic;
9use ui::{Button, ButtonLike, Color, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
10use workspace::{StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle};
11
12use crate::{Deploy, IncludeWarnings, ProjectDiagnosticsEditor};
13
14pub struct DiagnosticIndicator {
15 summary: project::DiagnosticSummary,
16 active_editor: Option<WeakEntity<Editor>>,
17 workspace: WeakEntity<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, _: &mut Window, cx: &mut Context<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(|window, cx| {
71 Tooltip::for_action(
72 "Next Diagnostic",
73 &editor::actions::GoToDiagnostic,
74 window,
75 cx,
76 )
77 })
78 .on_click(cx.listener(|this, _, window, cx| {
79 this.go_to_next_diagnostic(window, cx);
80 }))
81 .into_any_element(),
82 )
83 } else {
84 None
85 };
86
87 h_flex()
88 .gap_2()
89 .child(
90 ButtonLike::new("diagnostic-indicator")
91 .child(diagnostic_indicator)
92 .tooltip(|window, cx| {
93 Tooltip::for_action("Project Diagnostics", &Deploy, window, cx)
94 })
95 .on_click(cx.listener(|this, _, window, cx| {
96 if let Some(workspace) = this.workspace.upgrade() {
97 if this.summary.error_count == 0 && this.summary.warning_count > 0 {
98 cx.update_default_global(
99 |show_warnings: &mut IncludeWarnings, _| show_warnings.0 = true,
100 );
101 }
102 workspace.update(cx, |workspace, cx| {
103 ProjectDiagnosticsEditor::deploy(
104 workspace,
105 &Default::default(),
106 window,
107 cx,
108 )
109 })
110 }
111 })),
112 )
113 .children(status)
114 }
115}
116
117impl DiagnosticIndicator {
118 pub fn new(workspace: &Workspace, cx: &mut Context<Self>) -> Self {
119 let project = workspace.project();
120 cx.subscribe(project, |this, project, event, cx| match event {
121 project::Event::DiskBasedDiagnosticsStarted { .. } => {
122 cx.notify();
123 }
124
125 project::Event::DiskBasedDiagnosticsFinished { .. }
126 | project::Event::LanguageServerRemoved(_) => {
127 this.summary = project.read(cx).diagnostic_summary(false, cx);
128 cx.notify();
129 }
130
131 project::Event::DiagnosticsUpdated { .. } => {
132 this.summary = project.read(cx).diagnostic_summary(false, cx);
133 cx.notify();
134 }
135
136 _ => {}
137 })
138 .detach();
139
140 Self {
141 summary: project.read(cx).diagnostic_summary(false, cx),
142 active_editor: None,
143 workspace: workspace.weak_handle(),
144 current_diagnostic: None,
145 _observe_active_editor: None,
146 diagnostics_update: Task::ready(()),
147 }
148 }
149
150 fn go_to_next_diagnostic(&mut self, window: &mut Window, cx: &mut Context<Self>) {
151 if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
152 editor.update(cx, |editor, cx| {
153 editor.go_to_diagnostic_impl(editor::Direction::Next, window, cx);
154 })
155 }
156 }
157
158 fn update(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
159 let (buffer, cursor_position) = editor.update(cx, |editor, cx| {
160 let buffer = editor.buffer().read(cx).snapshot(cx);
161 let cursor_position = editor.selections.newest::<usize>(cx).head();
162 (buffer, cursor_position)
163 });
164 let new_diagnostic = buffer
165 .diagnostics_in_range::<usize>(cursor_position..cursor_position)
166 .filter(|entry| !entry.range.is_empty())
167 .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
168 .map(|entry| entry.diagnostic);
169 if new_diagnostic != self.current_diagnostic {
170 self.diagnostics_update =
171 cx.spawn_in(window, async move |diagnostics_indicator, cx| {
172 cx.background_executor()
173 .timer(Duration::from_millis(50))
174 .await;
175 diagnostics_indicator
176 .update(cx, |diagnostics_indicator, cx| {
177 diagnostics_indicator.current_diagnostic = new_diagnostic;
178 cx.notify();
179 })
180 .ok();
181 });
182 }
183 }
184}
185
186impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
187
188impl StatusItemView for DiagnosticIndicator {
189 fn set_active_pane_item(
190 &mut self,
191 active_pane_item: Option<&dyn ItemHandle>,
192 window: &mut Window,
193 cx: &mut Context<Self>,
194 ) {
195 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
196 self.active_editor = Some(editor.downgrade());
197 self._observe_active_editor = Some(cx.observe_in(&editor, window, Self::update));
198 self.update(editor, window, cx);
199 } else {
200 self.active_editor = None;
201 self.current_diagnostic = None;
202 self._observe_active_editor = None;
203 }
204 cx.notify();
205 }
206}