items.rs

  1use collections::HashSet;
  2use editor::{Editor, GoToDiagnostic};
  3use gpui::{
  4    rems, Div, EventEmitter, InteractiveElement, IntoElement, ParentElement, Render, Stateful,
  5    StatefulInteractiveElement, Styled, Subscription, View, ViewContext, WeakView,
  6};
  7use language::Diagnostic;
  8use lsp::LanguageServerId;
  9use theme::ActiveTheme;
 10use ui::{h_stack, Button, Clickable, Color, Icon, IconElement, Label, Tooltip};
 11use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
 12
 13use crate::ProjectDiagnosticsEditor;
 14
 15pub struct DiagnosticIndicator {
 16    summary: project::DiagnosticSummary,
 17    active_editor: Option<WeakView<Editor>>,
 18    workspace: WeakView<Workspace>,
 19    current_diagnostic: Option<Diagnostic>,
 20    in_progress_checks: HashSet<LanguageServerId>,
 21    _observe_active_editor: Option<Subscription>,
 22}
 23
 24impl Render for DiagnosticIndicator {
 25    type Element = Stateful<Div>;
 26
 27    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
 28        let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
 29            (0, 0) => h_stack().child(IconElement::new(Icon::Check).color(Color::Success)),
 30            (0, warning_count) => h_stack()
 31                .gap_1()
 32                .child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
 33                .child(Label::new(warning_count.to_string())),
 34            (error_count, 0) => h_stack()
 35                .gap_1()
 36                .child(IconElement::new(Icon::XCircle).color(Color::Error))
 37                .child(Label::new(error_count.to_string())),
 38            (error_count, warning_count) => h_stack()
 39                .gap_1()
 40                .child(IconElement::new(Icon::XCircle).color(Color::Error))
 41                .child(Label::new(error_count.to_string()))
 42                .child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
 43                .child(Label::new(warning_count.to_string())),
 44        };
 45
 46        let status = if !self.in_progress_checks.is_empty() {
 47            Some(Label::new("Checking…").into_any_element())
 48        } else if let Some(diagnostic) = &self.current_diagnostic {
 49            let message = diagnostic.message.split('\n').next().unwrap().to_string();
 50            Some(
 51                Button::new("diagnostic_message", message)
 52                    .on_click(cx.listener(|this, _, cx| {
 53                        this.go_to_next_diagnostic(&GoToDiagnostic, cx);
 54                    }))
 55                    .into_any_element(),
 56            )
 57        } else {
 58            None
 59        };
 60
 61        h_stack()
 62            .id("diagnostic-indicator")
 63            .on_action(cx.listener(Self::go_to_next_diagnostic))
 64            .rounded_md()
 65            .flex_none()
 66            .h(rems(1.375))
 67            .px_6()
 68            .cursor_pointer()
 69            .bg(cx.theme().colors().ghost_element_background)
 70            .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
 71            .active(|style| style.bg(cx.theme().colors().ghost_element_active))
 72            .tooltip(|cx| Tooltip::text("Project Diagnostics", cx))
 73            .on_click(cx.listener(|this, _, cx| {
 74                if let Some(workspace) = this.workspace.upgrade() {
 75                    workspace.update(cx, |workspace, cx| {
 76                        ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
 77                    })
 78                }
 79            }))
 80            .child(diagnostic_indicator)
 81            .children(status)
 82    }
 83}
 84
 85impl DiagnosticIndicator {
 86    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
 87        let project = workspace.project();
 88        cx.subscribe(project, |this, project, event, cx| match event {
 89            project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
 90                this.in_progress_checks.insert(*language_server_id);
 91                cx.notify();
 92            }
 93
 94            project::Event::DiskBasedDiagnosticsFinished { language_server_id }
 95            | project::Event::LanguageServerRemoved(language_server_id) => {
 96                this.summary = project.read(cx).diagnostic_summary(false, cx);
 97                this.in_progress_checks.remove(language_server_id);
 98                cx.notify();
 99            }
100
101            project::Event::DiagnosticsUpdated { .. } => {
102                this.summary = project.read(cx).diagnostic_summary(false, cx);
103                cx.notify();
104            }
105
106            _ => {}
107        })
108        .detach();
109
110        Self {
111            summary: project.read(cx).diagnostic_summary(false, cx),
112            in_progress_checks: project
113                .read(cx)
114                .language_servers_running_disk_based_diagnostics()
115                .collect(),
116            active_editor: None,
117            workspace: workspace.weak_handle(),
118            current_diagnostic: None,
119            _observe_active_editor: None,
120        }
121    }
122
123    fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
124        if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
125            editor.update(cx, |editor, cx| {
126                editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
127            })
128        }
129    }
130
131    fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
132        let editor = editor.read(cx);
133        let buffer = editor.buffer().read(cx);
134        let cursor_position = editor.selections.newest::<usize>(cx).head();
135        let new_diagnostic = buffer
136            .snapshot(cx)
137            .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
138            .filter(|entry| !entry.range.is_empty())
139            .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
140            .map(|entry| entry.diagnostic);
141        if new_diagnostic != self.current_diagnostic {
142            self.current_diagnostic = new_diagnostic;
143            cx.notify();
144        }
145    }
146}
147
148impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
149
150impl StatusItemView for DiagnosticIndicator {
151    fn set_active_pane_item(
152        &mut self,
153        active_pane_item: Option<&dyn ItemHandle>,
154        cx: &mut ViewContext<Self>,
155    ) {
156        if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
157            self.active_editor = Some(editor.downgrade());
158            self._observe_active_editor = Some(cx.observe(&editor, Self::update));
159            self.update(editor, cx);
160        } else {
161            self.active_editor = None;
162            self.current_diagnostic = None;
163            self._observe_active_editor = None;
164        }
165        cx.notify();
166    }
167}