items.rs

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