items.rs

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