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