items.rs

  1use collections::HashSet;
  2use editor::Editor;
  3use gpui::{
  4    rems, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View,
  5    ViewContext, WeakView,
  6};
  7use language::Diagnostic;
  8use lsp::LanguageServerId;
  9use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconElement, 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    in_progress_checks: HashSet<LanguageServerId>,
 20    _observe_active_editor: Option<Subscription>,
 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_stack().map(|this| {
 27                if !self.in_progress_checks.is_empty() {
 28                    this.child(IconElement::new(Icon::ArrowCircle).size(IconSize::Small))
 29                } else {
 30                    this.child(
 31                        IconElement::new(Icon::Check)
 32                            .size(IconSize::Small)
 33                            .color(Color::Default),
 34                    )
 35                }
 36            }),
 37            (0, warning_count) => h_stack()
 38                .gap_1()
 39                .child(
 40                    IconElement::new(Icon::ExclamationTriangle)
 41                        .size(IconSize::Small)
 42                        .color(Color::Warning),
 43                )
 44                .child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
 45            (error_count, 0) => h_stack()
 46                .gap_1()
 47                .child(
 48                    IconElement::new(Icon::XCircle)
 49                        .size(IconSize::Small)
 50                        .color(Color::Error),
 51                )
 52                .child(Label::new(error_count.to_string()).size(LabelSize::Small)),
 53            (error_count, warning_count) => h_stack()
 54                .gap_1()
 55                .child(
 56                    IconElement::new(Icon::XCircle)
 57                        .size(IconSize::Small)
 58                        .color(Color::Error),
 59                )
 60                .child(Label::new(error_count.to_string()).size(LabelSize::Small))
 61                .child(
 62                    IconElement::new(Icon::ExclamationTriangle)
 63                        .size(IconSize::Small)
 64                        .color(Color::Warning),
 65                )
 66                .child(Label::new(warning_count.to_string()).size(LabelSize::Small)),
 67        };
 68
 69        let status = if !self.in_progress_checks.is_empty() {
 70            Some(
 71                Label::new("Checking…")
 72                    .size(LabelSize::Small)
 73                    .into_any_element(),
 74            )
 75        } else if let Some(diagnostic) = &self.current_diagnostic {
 76            let message = diagnostic.message.split('\n').next().unwrap().to_string();
 77            Some(
 78                Button::new("diagnostic_message", message)
 79                    .label_size(LabelSize::Small)
 80                    .tooltip(|cx| {
 81                        Tooltip::for_action("Next Diagnostic", &editor::GoToDiagnostic, cx)
 82                    })
 83                    .on_click(cx.listener(|this, _, cx| {
 84                        this.go_to_next_diagnostic(cx);
 85                    }))
 86                    .into_any_element(),
 87            )
 88        } else {
 89            None
 90        };
 91
 92        h_stack()
 93            .h(rems(1.375))
 94            .gap_2()
 95            .child(
 96                ButtonLike::new("diagnostic-indicator")
 97                    .child(diagnostic_indicator)
 98                    .tooltip(|cx| Tooltip::for_action("Project Diagnostics", &Deploy, cx))
 99                    .on_click(cx.listener(|this, _, cx| {
100                        if let Some(workspace) = this.workspace.upgrade() {
101                            workspace.update(cx, |workspace, cx| {
102                                ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
103                            })
104                        }
105                    })),
106            )
107            .children(status)
108    }
109}
110
111impl DiagnosticIndicator {
112    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
113        let project = workspace.project();
114        cx.subscribe(project, |this, project, event, cx| match event {
115            project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
116                this.in_progress_checks.insert(*language_server_id);
117                cx.notify();
118            }
119
120            project::Event::DiskBasedDiagnosticsFinished { language_server_id }
121            | project::Event::LanguageServerRemoved(language_server_id) => {
122                this.summary = project.read(cx).diagnostic_summary(false, cx);
123                this.in_progress_checks.remove(language_server_id);
124                cx.notify();
125            }
126
127            project::Event::DiagnosticsUpdated { .. } => {
128                this.summary = project.read(cx).diagnostic_summary(false, cx);
129                cx.notify();
130            }
131
132            _ => {}
133        })
134        .detach();
135
136        Self {
137            summary: project.read(cx).diagnostic_summary(false, cx),
138            in_progress_checks: project
139                .read(cx)
140                .language_servers_running_disk_based_diagnostics()
141                .collect(),
142            active_editor: None,
143            workspace: workspace.weak_handle(),
144            current_diagnostic: None,
145            _observe_active_editor: None,
146        }
147    }
148
149    fn go_to_next_diagnostic(&mut self, cx: &mut ViewContext<Self>) {
150        if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
151            editor.update(cx, |editor, cx| {
152                editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
153            })
154        }
155    }
156
157    fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
158        let editor = editor.read(cx);
159        let buffer = editor.buffer().read(cx);
160        let cursor_position = editor.selections.newest::<usize>(cx).head();
161        let new_diagnostic = buffer
162            .snapshot(cx)
163            .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
164            .filter(|entry| !entry.range.is_empty())
165            .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
166            .map(|entry| entry.diagnostic);
167        if new_diagnostic != self.current_diagnostic {
168            self.current_diagnostic = new_diagnostic;
169            cx.notify();
170        }
171    }
172}
173
174impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
175
176impl StatusItemView for DiagnosticIndicator {
177    fn set_active_pane_item(
178        &mut self,
179        active_pane_item: Option<&dyn ItemHandle>,
180        cx: &mut ViewContext<Self>,
181    ) {
182        if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
183            self.active_editor = Some(editor.downgrade());
184            self._observe_active_editor = Some(cx.observe(&editor, Self::update));
185            self.update(editor, cx);
186        } else {
187            self.active_editor = None;
188            self.current_diagnostic = None;
189            self._observe_active_editor = None;
190        }
191        cx.notify();
192    }
193}