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::{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<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 .pl_1()
90 .border_l_1()
91 .border_color(cx.theme().colors().border)
92 .child(
93 ButtonLike::new("diagnostic-indicator")
94 .child(diagnostic_indicator)
95 .tooltip(|window, cx| {
96 Tooltip::for_action("Project Diagnostics", &Deploy, window, cx)
97 })
98 .on_click(cx.listener(|this, _, window, cx| {
99 if let Some(workspace) = this.workspace.upgrade() {
100 workspace.update(cx, |workspace, cx| {
101 ProjectDiagnosticsEditor::deploy(
102 workspace,
103 &Default::default(),
104 window,
105 cx,
106 )
107 })
108 }
109 })),
110 )
111 .children(status)
112 }
113}
114
115impl DiagnosticIndicator {
116 pub fn new(workspace: &Workspace, cx: &mut Context<Self>) -> Self {
117 let project = workspace.project();
118 cx.subscribe(project, |this, project, event, cx| match event {
119 project::Event::DiskBasedDiagnosticsStarted { .. } => {
120 cx.notify();
121 }
122
123 project::Event::DiskBasedDiagnosticsFinished { .. }
124 | project::Event::LanguageServerRemoved(_) => {
125 this.summary = project.read(cx).diagnostic_summary(false, cx);
126 cx.notify();
127 }
128
129 project::Event::DiagnosticsUpdated { .. } => {
130 this.summary = project.read(cx).diagnostic_summary(false, cx);
131 cx.notify();
132 }
133
134 _ => {}
135 })
136 .detach();
137
138 Self {
139 summary: project.read(cx).diagnostic_summary(false, cx),
140 active_editor: None,
141 workspace: workspace.weak_handle(),
142 current_diagnostic: None,
143 _observe_active_editor: None,
144 diagnostics_update: Task::ready(()),
145 }
146 }
147
148 fn go_to_next_diagnostic(&mut self, window: &mut Window, cx: &mut Context<Self>) {
149 if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
150 editor.update(cx, |editor, cx| {
151 editor.go_to_diagnostic_impl(editor::Direction::Next, window, cx);
152 })
153 }
154 }
155
156 fn update(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
157 let (buffer, cursor_position) = editor.update(cx, |editor, cx| {
158 let buffer = editor.buffer().read(cx).snapshot(cx);
159 let cursor_position = editor.selections.newest::<usize>(cx).head();
160 (buffer, cursor_position)
161 });
162 let new_diagnostic = buffer
163 .diagnostics_in_range::<_, usize>(cursor_position..cursor_position)
164 .filter(|entry| !entry.range.is_empty())
165 .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
166 .map(|entry| entry.diagnostic);
167 if new_diagnostic != self.current_diagnostic {
168 self.diagnostics_update =
169 cx.spawn_in(window, |diagnostics_indicator, mut cx| async move {
170 cx.background_executor()
171 .timer(Duration::from_millis(50))
172 .await;
173 diagnostics_indicator
174 .update(&mut cx, |diagnostics_indicator, cx| {
175 diagnostics_indicator.current_diagnostic = new_diagnostic;
176 cx.notify();
177 })
178 .ok();
179 });
180 }
181 }
182}
183
184impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
185
186impl StatusItemView for DiagnosticIndicator {
187 fn set_active_pane_item(
188 &mut self,
189 active_pane_item: Option<&dyn ItemHandle>,
190 window: &mut Window,
191 cx: &mut Context<Self>,
192 ) {
193 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
194 self.active_editor = Some(editor.downgrade());
195 self._observe_active_editor = Some(cx.observe_in(&editor, window, Self::update));
196 self.update(editor, window, cx);
197 } else {
198 self.active_editor = None;
199 self.current_diagnostic = None;
200 self._observe_active_editor = None;
201 }
202 cx.notify();
203 }
204}