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