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::ProjectSettings;
 10use settings::Settings;
 11use ui::{Button, ButtonLike, Color, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
 12use workspace::{StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle};
 13
 14use crate::{Deploy, IncludeWarnings, ProjectDiagnosticsEditor};
 15
 16pub struct DiagnosticIndicator {
 17    summary: project::DiagnosticSummary,
 18    active_editor: Option<WeakEntity<Editor>>,
 19    workspace: WeakEntity<Workspace>,
 20    current_diagnostic: Option<Diagnostic>,
 21    _observe_active_editor: Option<Subscription>,
 22    diagnostics_update: Task<()>,
 23}
 24
 25impl Render for DiagnosticIndicator {
 26    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 27        let indicator = h_flex().gap_2();
 28        if !ProjectSettings::get_global(cx).diagnostics.button {
 29            return indicator;
 30        }
 31
 32        let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
 33            (0, 0) => h_flex().map(|this| {
 34                this.child(
 35                    Icon::new(IconName::Check)
 36                        .size(IconSize::Small)
 37                        .color(Color::Default),
 38                )
 39            }),
 40            (0, warning_count) => h_flex()
 41                .gap_1()
 42                .child(
 43                    Icon::new(IconName::Warning)
 44                        .size(IconSize::Small)
 45                        .color(Color::Warning),
 46                )
 47                .child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
 48            (error_count, 0) => h_flex()
 49                .gap_1()
 50                .child(
 51                    Icon::new(IconName::XCircle)
 52                        .size(IconSize::Small)
 53                        .color(Color::Error),
 54                )
 55                .child(Label::new(error_count.to_string()).size(LabelSize::Small)),
 56            (error_count, warning_count) => h_flex()
 57                .gap_1()
 58                .child(
 59                    Icon::new(IconName::XCircle)
 60                        .size(IconSize::Small)
 61                        .color(Color::Error),
 62                )
 63                .child(Label::new(error_count.to_string()).size(LabelSize::Small))
 64                .child(
 65                    Icon::new(IconName::Warning)
 66                        .size(IconSize::Small)
 67                        .color(Color::Warning),
 68                )
 69                .child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
 70        };
 71
 72        let status = if let Some(diagnostic) = &self.current_diagnostic {
 73            let message = diagnostic.message.split('\n').next().unwrap().to_string();
 74            Some(
 75                Button::new("diagnostic_message", message)
 76                    .label_size(LabelSize::Small)
 77                    .tooltip(|window, cx| {
 78                        Tooltip::for_action(
 79                            "Next Diagnostic",
 80                            &editor::actions::GoToDiagnostic,
 81                            window,
 82                            cx,
 83                        )
 84                    })
 85                    .on_click(cx.listener(|this, _, window, cx| {
 86                        this.go_to_next_diagnostic(window, cx);
 87                    }))
 88                    .into_any_element(),
 89            )
 90        } else {
 91            None
 92        };
 93
 94        indicator
 95            .child(
 96                ButtonLike::new("diagnostic-indicator")
 97                    .child(diagnostic_indicator)
 98                    .tooltip(|window, cx| {
 99                        Tooltip::for_action("Project Diagnostics", &Deploy, window, cx)
100                    })
101                    .on_click(cx.listener(|this, _, window, cx| {
102                        if let Some(workspace) = this.workspace.upgrade() {
103                            if this.summary.error_count == 0 && this.summary.warning_count > 0 {
104                                cx.update_default_global(
105                                    |show_warnings: &mut IncludeWarnings, _| show_warnings.0 = true,
106                                );
107                            }
108                            workspace.update(cx, |workspace, cx| {
109                                ProjectDiagnosticsEditor::deploy(
110                                    workspace,
111                                    &Default::default(),
112                                    window,
113                                    cx,
114                                )
115                            })
116                        }
117                    })),
118            )
119            .children(status)
120    }
121}
122
123impl DiagnosticIndicator {
124    pub fn new(workspace: &Workspace, cx: &mut Context<Self>) -> Self {
125        let project = workspace.project();
126        cx.subscribe(project, |this, project, event, cx| match event {
127            project::Event::DiskBasedDiagnosticsStarted { .. } => {
128                cx.notify();
129            }
130
131            project::Event::DiskBasedDiagnosticsFinished { .. }
132            | project::Event::LanguageServerRemoved(_) => {
133                this.summary = project.read(cx).diagnostic_summary(false, cx);
134                cx.notify();
135            }
136
137            project::Event::DiagnosticsUpdated { .. } => {
138                this.summary = project.read(cx).diagnostic_summary(false, cx);
139                cx.notify();
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        }
154    }
155
156    fn go_to_next_diagnostic(&mut self, window: &mut Window, cx: &mut Context<Self>) {
157        if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
158            editor.update(cx, |editor, cx| {
159                editor.go_to_diagnostic_impl(editor::Direction::Next, window, cx);
160            })
161        }
162    }
163
164    fn update(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
165        let (buffer, cursor_position) = editor.update(cx, |editor, cx| {
166            let buffer = editor.buffer().read(cx).snapshot(cx);
167            let cursor_position = editor.selections.newest::<usize>(cx).head();
168            (buffer, cursor_position)
169        });
170        let new_diagnostic = buffer
171            .diagnostics_in_range::<usize>(cursor_position..cursor_position)
172            .filter(|entry| !entry.range.is_empty())
173            .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
174            .map(|entry| entry.diagnostic);
175        if new_diagnostic != self.current_diagnostic {
176            self.diagnostics_update =
177                cx.spawn_in(window, async move |diagnostics_indicator, cx| {
178                    cx.background_executor()
179                        .timer(Duration::from_millis(50))
180                        .await;
181                    diagnostics_indicator
182                        .update(cx, |diagnostics_indicator, cx| {
183                            diagnostics_indicator.current_diagnostic = new_diagnostic;
184                            cx.notify();
185                        })
186                        .ok();
187                });
188        }
189    }
190}
191
192impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
193
194impl StatusItemView for DiagnosticIndicator {
195    fn set_active_pane_item(
196        &mut self,
197        active_pane_item: Option<&dyn ItemHandle>,
198        window: &mut Window,
199        cx: &mut Context<Self>,
200    ) {
201        if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
202            self.active_editor = Some(editor.downgrade());
203            self._observe_active_editor = Some(cx.observe_in(&editor, window, Self::update));
204            self.update(editor, window, cx);
205        } else {
206            self.active_editor = None;
207            self.current_diagnostic = None;
208            self._observe_active_editor = None;
209        }
210        cx.notify();
211    }
212}