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