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