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