items.rs

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