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