items.rs

  1use collections::HashSet;
  2use editor::{Editor, GoToDiagnostic};
  3use gpui::{
  4    div, Div, EventEmitter, InteractiveComponent, ParentComponent, Render, Stateful,
  5    StatefulInteractiveComponent, Styled, Subscription, View, ViewContext, WeakView,
  6};
  7use language::Diagnostic;
  8use lsp::LanguageServerId;
  9use theme::ActiveTheme;
 10use ui::{h_stack, Icon, IconElement, Label, TextColor, 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<Self, Div<Self>>;
 26
 27    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
 28        let mut summary_row = h_stack()
 29            .id(cx.entity_id())
 30            .on_action(Self::go_to_next_diagnostic)
 31            .rounded_md()
 32            .p_1()
 33            .cursor_pointer()
 34            .bg(gpui::green())
 35            .hover(|style| style.bg(cx.theme().colors().element_hover))
 36            .active(|style| style.bg(cx.theme().colors().element_active))
 37            .tooltip(|_, cx| Tooltip::text("Project Diagnostics", cx))
 38            .on_click(|this, _, cx| {
 39                if let Some(workspace) = this.workspace.upgrade() {
 40                    workspace.update(cx, |workspace, cx| {
 41                        ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
 42                    })
 43                }
 44            });
 45
 46        if self.summary.error_count > 0 {
 47            summary_row = summary_row.child(
 48                div()
 49                    .child(IconElement::new(Icon::XCircle).color(TextColor::Error))
 50                    .bg(gpui::red()),
 51            );
 52            summary_row = summary_row.child(
 53                div()
 54                    .child(Label::new(self.summary.error_count.to_string()))
 55                    .bg(gpui::yellow()),
 56            );
 57        }
 58
 59        if self.summary.warning_count > 0 {
 60            summary_row = summary_row
 61                .child(IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning));
 62            summary_row = summary_row.child(Label::new(self.summary.warning_count.to_string()));
 63        }
 64
 65        if self.summary.error_count == 0 && self.summary.warning_count == 0 {
 66            summary_row =
 67                summary_row.child(IconElement::new(Icon::Check).color(TextColor::Success));
 68        }
 69
 70        summary_row
 71    }
 72}
 73
 74impl DiagnosticIndicator {
 75    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
 76        let project = workspace.project();
 77        cx.subscribe(project, |this, project, event, cx| match event {
 78            project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
 79                this.in_progress_checks.insert(*language_server_id);
 80                cx.notify();
 81            }
 82
 83            project::Event::DiskBasedDiagnosticsFinished { language_server_id }
 84            | project::Event::LanguageServerRemoved(language_server_id) => {
 85                this.summary = project.read(cx).diagnostic_summary(cx);
 86                this.in_progress_checks.remove(language_server_id);
 87                cx.notify();
 88            }
 89
 90            project::Event::DiagnosticsUpdated { .. } => {
 91                this.summary = project.read(cx).diagnostic_summary(cx);
 92                cx.notify();
 93            }
 94
 95            _ => {}
 96        })
 97        .detach();
 98
 99        Self {
100            summary: project.read(cx).diagnostic_summary(cx),
101            in_progress_checks: project
102                .read(cx)
103                .language_servers_running_disk_based_diagnostics()
104                .collect(),
105            active_editor: None,
106            workspace: workspace.weak_handle(),
107            current_diagnostic: None,
108            _observe_active_editor: None,
109        }
110    }
111
112    fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
113        if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) {
114            editor.update(cx, |editor, cx| {
115                editor.go_to_diagnostic_impl(editor::Direction::Next, cx);
116            })
117        }
118    }
119
120    fn update(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
121        let editor = editor.read(cx);
122        let buffer = editor.buffer().read(cx);
123        let cursor_position = editor.selections.newest::<usize>(cx).head();
124        let new_diagnostic = buffer
125            .snapshot(cx)
126            .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
127            .filter(|entry| !entry.range.is_empty())
128            .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
129            .map(|entry| entry.diagnostic);
130        if new_diagnostic != self.current_diagnostic {
131            self.current_diagnostic = new_diagnostic;
132            cx.notify();
133        }
134    }
135}
136
137impl EventEmitter<ToolbarItemEvent> for DiagnosticIndicator {}
138
139impl StatusItemView for DiagnosticIndicator {
140    fn set_active_pane_item(
141        &mut self,
142        active_pane_item: Option<&dyn ItemHandle>,
143        cx: &mut ViewContext<Self>,
144    ) {
145        if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
146            self.active_editor = Some(editor.downgrade());
147            self._observe_active_editor = Some(cx.observe(&editor, Self::update));
148            self.update(editor, cx);
149        } else {
150            self.active_editor = None;
151            self.current_diagnostic = None;
152            self._observe_active_editor = None;
153        }
154        cx.notify();
155    }
156}