items.rs

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