items.rs

  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 project::project_settings::{GoToDiagnosticSeverityFilter, ProjectSettings};
 10use settings::Settings;
 11use ui::{Button, ButtonLike, Color, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
 12use util::ResultExt;
 13use workspace::{StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle};
 14
 15use crate::{Deploy, IncludeWarnings, ProjectDiagnosticsEditor};
 16
 17/// The status bar item that displays diagnostic counts.
 18pub struct DiagnosticIndicator {
 19    summary: project::DiagnosticSummary,
 20    workspace: WeakEntity<Workspace>,
 21    current_diagnostic: Option<Diagnostic>,
 22    active_editor: Option<WeakEntity<Editor>>,
 23    _observe_active_editor: Option<Subscription>,
 24
 25    diagnostics_update: Task<()>,
 26    diagnostic_summary_update: Task<()>,
 27}
 28
 29impl Render for DiagnosticIndicator {
 30    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 31        let indicator = h_flex().gap_2();
 32        if !ProjectSettings::get_global(cx).diagnostics.button {
 33            return indicator.hidden();
 34        }
 35
 36        let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
 37            (0, 0) => h_flex().child(
 38                Icon::new(IconName::Check)
 39                    .size(IconSize::Small)
 40                    .color(Color::Default),
 41            ),
 42            (error_count, warning_count) => h_flex()
 43                .gap_1()
 44                .when(error_count > 0, |this| {
 45                    this.child(
 46                        Icon::new(IconName::XCircle)
 47                            .size(IconSize::Small)
 48                            .color(Color::Error),
 49                    )
 50                    .child(Label::new(error_count.to_string()).size(LabelSize::Small))
 51                })
 52                .when(warning_count > 0, |this| {
 53                    this.child(
 54                        Icon::new(IconName::Warning)
 55                            .size(IconSize::Small)
 56                            .color(Color::Warning),
 57                    )
 58                    .child(Label::new(warning_count.to_string()).size(LabelSize::Small))
 59                }),
 60        };
 61
 62        let status = if let Some(diagnostic) = &self.current_diagnostic {
 63            let message = diagnostic
 64                .message
 65                .split_once('\n')
 66                .map_or(&*diagnostic.message, |(first, _)| first);
 67            Some(
 68                Button::new("diagnostic_message", SharedString::new(message))
 69                    .label_size(LabelSize::Small)
 70                    .tooltip(|window, cx| {
 71                        Tooltip::for_action(
 72                            "Next Diagnostic",
 73                            &editor::actions::GoToDiagnostic::default(),
 74                            window,
 75                            cx,
 76                        )
 77                    })
 78                    .on_click(
 79                        cx.listener(|this, _, window, cx| this.go_to_next_diagnostic(window, cx)),
 80                    ),
 81            )
 82        } else {
 83            None
 84        };
 85
 86        indicator
 87            .child(
 88                ButtonLike::new("diagnostic-indicator")
 89                    .child(diagnostic_indicator)
 90                    .tooltip(|window, cx| {
 91                        Tooltip::for_action("Project Diagnostics", &Deploy, window, cx)
 92                    })
 93                    .on_click(cx.listener(|this, _, window, cx| {
 94                        if let Some(workspace) = this.workspace.upgrade() {
 95                            if this.summary.error_count == 0 && this.summary.warning_count > 0 {
 96                                cx.update_default_global(
 97                                    |show_warnings: &mut IncludeWarnings, _| show_warnings.0 = true,
 98                                );
 99                            }
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.diagnostic_summary_update = cx.spawn(async move |this, cx| {
131                    cx.background_executor()
132                        .timer(Duration::from_millis(30))
133                        .await;
134                    this.update(cx, |this, cx| {
135                        this.summary = project.read(cx).diagnostic_summary(false, cx);
136                        cx.notify();
137                    })
138                    .log_err();
139                });
140            }
141
142            _ => {}
143        })
144        .detach();
145
146        Self {
147            summary: project.read(cx).diagnostic_summary(false, cx),
148            active_editor: None,
149            workspace: workspace.weak_handle(),
150            current_diagnostic: None,
151            _observe_active_editor: None,
152            diagnostics_update: Task::ready(()),
153            diagnostic_summary_update: Task::ready(()),
154        }
155    }
156
157    fn go_to_next_diagnostic(&mut self, window: &mut Window, cx: &mut Context<Self>) {
158        if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
159            editor.update(cx, |editor, cx| {
160                editor.go_to_diagnostic_impl(
161                    editor::Direction::Next,
162                    GoToDiagnosticSeverityFilter::default(),
163                    window,
164                    cx,
165                );
166            })
167        }
168    }
169
170    fn update(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
171        let (buffer, cursor_position) = editor.update(cx, |editor, cx| {
172            let buffer = editor.buffer().read(cx).snapshot(cx);
173            let cursor_position = editor
174                .selections
175                .newest::<usize>(&editor.display_snapshot(cx))
176                .head();
177            (buffer, cursor_position)
178        });
179        let new_diagnostic = buffer
180            .diagnostics_in_range::<usize>(cursor_position..cursor_position)
181            .filter(|entry| !entry.range.is_empty())
182            .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
183            .map(|entry| entry.diagnostic);
184        if new_diagnostic != self.current_diagnostic.as_ref() {
185            let new_diagnostic = new_diagnostic.cloned();
186            self.diagnostics_update =
187                cx.spawn_in(window, async move |diagnostics_indicator, cx| {
188                    cx.background_executor()
189                        .timer(Duration::from_millis(50))
190                        .await;
191                    diagnostics_indicator
192                        .update(cx, |diagnostics_indicator, cx| {
193                            diagnostics_indicator.current_diagnostic = new_diagnostic;
194                            cx.notify();
195                        })
196                        .ok();
197                });
198        }
199    }
200}
201
202impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
203
204impl StatusItemView for DiagnosticIndicator {
205    fn set_active_pane_item(
206        &mut self,
207        active_pane_item: Option<&dyn ItemHandle>,
208        window: &mut Window,
209        cx: &mut Context<Self>,
210    ) {
211        if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
212            self.active_editor = Some(editor.downgrade());
213            self._observe_active_editor = Some(cx.observe_in(&editor, window, Self::update));
214            self.update(editor, window, cx);
215        } else {
216            self.active_editor = None;
217            self.current_diagnostic = None;
218            self._observe_active_editor = None;
219        }
220        cx.notify();
221    }
222}