items.rs

  1use collections::HashSet;
  2use editor::{Editor, GoToDiagnostic};
  3use gpui::{
  4    rems, Div, EventEmitter, InteractiveElement, ParentElement, Render, Stateful,
  5    StatefulInteractiveElement, Styled, Subscription, View, ViewContext, WeakView,
  6};
  7use language::Diagnostic;
  8use lsp::LanguageServerId;
  9use theme::ActiveTheme;
 10use ui::{h_stack, 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        h_stack()
 47            .id("diagnostic-indicator")
 48            .on_action(cx.listener(Self::go_to_next_diagnostic))
 49            .rounded_md()
 50            .flex_none()
 51            .h(rems(1.375))
 52            .px_1()
 53            .cursor_pointer()
 54            .bg(cx.theme().colors().ghost_element_background)
 55            .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
 56            .active(|style| style.bg(cx.theme().colors().ghost_element_active))
 57            .tooltip(|cx| Tooltip::text("Project Diagnostics", cx))
 58            .on_click(cx.listener(|this, _, cx| {
 59                if let Some(workspace) = this.workspace.upgrade() {
 60                    workspace.update(cx, |workspace, cx| {
 61                        ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
 62                    })
 63                }
 64            }))
 65            .child(diagnostic_indicator)
 66    }
 67}
 68
 69impl DiagnosticIndicator {
 70    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
 71        let project = workspace.project();
 72        cx.subscribe(project, |this, project, event, cx| match event {
 73            project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
 74                this.in_progress_checks.insert(*language_server_id);
 75                cx.notify();
 76            }
 77
 78            project::Event::DiskBasedDiagnosticsFinished { language_server_id }
 79            | project::Event::LanguageServerRemoved(language_server_id) => {
 80                this.summary = project.read(cx).diagnostic_summary(false, cx);
 81                this.in_progress_checks.remove(language_server_id);
 82                cx.notify();
 83            }
 84
 85            project::Event::DiagnosticsUpdated { .. } => {
 86                this.summary = project.read(cx).diagnostic_summary(false, cx);
 87                cx.notify();
 88            }
 89
 90            _ => {}
 91        })
 92        .detach();
 93
 94        Self {
 95            summary: project.read(cx).diagnostic_summary(false, cx),
 96            in_progress_checks: project
 97                .read(cx)
 98                .language_servers_running_disk_based_diagnostics()
 99                .collect(),
100            active_editor: None,
101            workspace: workspace.weak_handle(),
102            current_diagnostic: None,
103            _observe_active_editor: None,
104        }
105    }
106
107    fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
108        if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
109            editor.update(cx, |editor, cx| {
110                editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
111            })
112        }
113    }
114
115    fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
116        let editor = editor.read(cx);
117        let buffer = editor.buffer().read(cx);
118        let cursor_position = editor.selections.newest::<usize>(cx).head();
119        let new_diagnostic = buffer
120            .snapshot(cx)
121            .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
122            .filter(|entry| !entry.range.is_empty())
123            .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
124            .map(|entry| entry.diagnostic);
125        if new_diagnostic != self.current_diagnostic {
126            self.current_diagnostic = new_diagnostic;
127            cx.notify();
128        }
129    }
130}
131
132impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
133
134impl StatusItemView for DiagnosticIndicator {
135    fn set_active_pane_item(
136        &mut self,
137        active_pane_item: Option<&dyn ItemHandle>,
138        cx: &mut ViewContext<Self>,
139    ) {
140        if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
141            self.active_editor = Some(editor.downgrade());
142            self._observe_active_editor = Some(cx.observe(&editor, Self::update));
143            self.update(editor, cx);
144        } else {
145            self.active_editor = None;
146            self.current_diagnostic = None;
147            self._observe_active_editor = None;
148        }
149        cx.notify();
150    }
151}