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 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}