items.rs

  1use std::time::Duration;
  2
  3use editor::{AnchorRangeExt, Editor};
  4use gpui::{
  5    EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, Task, View,
  6    ViewContext, WeakView,
  7};
  8use language::{Diagnostic, DiagnosticEntry};
  9use ui::{h_flex, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip};
 10use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
 11
 12use crate::{Deploy, ProjectDiagnosticsEditor};
 13
 14pub struct DiagnosticIndicator {
 15    summary: project::DiagnosticSummary,
 16    active_editor: Option<WeakView<Editor>>,
 17    workspace: WeakView<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, cx: &mut ViewContext<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(|cx| {
 71                        Tooltip::for_action("Next Diagnostic", &editor::actions::GoToDiagnostic, cx)
 72                    })
 73                    .on_click(cx.listener(|this, _, cx| {
 74                        this.go_to_next_diagnostic(cx);
 75                    }))
 76                    .into_any_element(),
 77            )
 78        } else {
 79            None
 80        };
 81
 82        h_flex()
 83            .gap_2()
 84            .pl_1()
 85            .border_l_1()
 86            .border_color(cx.theme().colors().border)
 87            .child(
 88                ButtonLike::new("diagnostic-indicator")
 89                    .child(diagnostic_indicator)
 90                    .tooltip(|cx| Tooltip::for_action("Project Diagnostics", &Deploy, cx))
 91                    .on_click(cx.listener(|this, _, cx| {
 92                        if let Some(workspace) = this.workspace.upgrade() {
 93                            workspace.update(cx, |workspace, cx| {
 94                                ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
 95                            })
 96                        }
 97                    })),
 98            )
 99            .children(status)
100    }
101}
102
103impl DiagnosticIndicator {
104    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
105        let project = workspace.project();
106        cx.subscribe(project, |this, project, event, cx| match event {
107            project::Event::DiskBasedDiagnosticsStarted { .. } => {
108                cx.notify();
109            }
110
111            project::Event::DiskBasedDiagnosticsFinished { .. }
112            | project::Event::LanguageServerRemoved(_) => {
113                this.summary = project.read(cx).diagnostic_summary(false, cx);
114                cx.notify();
115            }
116
117            project::Event::DiagnosticsUpdated { .. } => {
118                this.summary = project.read(cx).diagnostic_summary(false, cx);
119                cx.notify();
120            }
121
122            _ => {}
123        })
124        .detach();
125
126        Self {
127            summary: project.read(cx).diagnostic_summary(false, cx),
128            active_editor: None,
129            workspace: workspace.weak_handle(),
130            current_diagnostic: None,
131            _observe_active_editor: None,
132            diagnostics_update: Task::ready(()),
133        }
134    }
135
136    fn go_to_next_diagnostic(&mut self, cx: &mut ViewContext<Self>) {
137        if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
138            editor.update(cx, |editor, cx| {
139                editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
140            })
141        }
142    }
143
144    fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
145        let (buffer, cursor_position) = editor.update(cx, |editor, cx| {
146            let buffer = editor.buffer().read(cx).snapshot(cx);
147            let cursor_position = editor.selections.newest::<usize>(cx).head();
148            (buffer, cursor_position)
149        });
150        let new_diagnostic = buffer
151            .diagnostics_in_range(cursor_position..cursor_position, false)
152            .map(|DiagnosticEntry { diagnostic, range }| DiagnosticEntry {
153                diagnostic,
154                range: range.to_offset(&buffer),
155            })
156            .filter(|entry| !entry.range.is_empty())
157            .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
158            .map(|entry| entry.diagnostic);
159        if new_diagnostic != self.current_diagnostic {
160            self.diagnostics_update = cx.spawn(|diagnostics_indicator, mut cx| async move {
161                cx.background_executor()
162                    .timer(Duration::from_millis(50))
163                    .await;
164                diagnostics_indicator
165                    .update(&mut cx, |diagnostics_indicator, cx| {
166                        diagnostics_indicator.current_diagnostic = new_diagnostic;
167                        cx.notify();
168                    })
169                    .ok();
170            });
171        }
172    }
173}
174
175impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
176
177impl StatusItemView for DiagnosticIndicator {
178    fn set_active_pane_item(
179        &mut self,
180        active_pane_item: Option<&dyn ItemHandle>,
181        cx: &mut ViewContext<Self>,
182    ) {
183        if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
184            self.active_editor = Some(editor.downgrade());
185            self._observe_active_editor = Some(cx.observe(&editor, Self::update));
186            self.update(editor, cx);
187        } else {
188            self.active_editor = None;
189            self.current_diagnostic = None;
190            self._observe_active_editor = None;
191        }
192        cx.notify();
193    }
194}