items.rs

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