items.rs

  1use editor::Editor;
  2use gpui::{
  3    elements::*, platform::CursorStyle, serde_json, Entity, ModelHandle, RenderContext,
  4    Subscription, View, ViewContext, ViewHandle,
  5};
  6use language::Diagnostic;
  7use project::Project;
  8use settings::Settings;
  9use workspace::StatusItemView;
 10
 11pub struct DiagnosticIndicator {
 12    summary: project::DiagnosticSummary,
 13    current_diagnostic: Option<Diagnostic>,
 14    check_in_progress: bool,
 15    _observe_active_editor: Option<Subscription>,
 16}
 17
 18impl DiagnosticIndicator {
 19    pub fn new(project: &ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
 20        cx.subscribe(project, |this, project, event, cx| match event {
 21            project::Event::DiskBasedDiagnosticsUpdated => {
 22                cx.notify();
 23            }
 24            project::Event::DiskBasedDiagnosticsStarted => {
 25                this.check_in_progress = true;
 26                cx.notify();
 27            }
 28            project::Event::DiskBasedDiagnosticsFinished => {
 29                this.summary = project.read(cx).diagnostic_summary(cx);
 30                this.check_in_progress = false;
 31                cx.notify();
 32            }
 33            _ => {}
 34        })
 35        .detach();
 36        Self {
 37            summary: project.read(cx).diagnostic_summary(cx),
 38            check_in_progress: project.read(cx).is_running_disk_based_diagnostics(),
 39            current_diagnostic: None,
 40            _observe_active_editor: None,
 41        }
 42    }
 43
 44    fn update(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
 45        let editor = editor.read(cx);
 46        let buffer = editor.buffer().read(cx);
 47        let cursor_position = editor
 48            .newest_selection_with_snapshot::<usize>(&buffer.read(cx))
 49            .head();
 50        let new_diagnostic = buffer
 51            .read(cx)
 52            .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)
 53            .filter(|entry| !entry.range.is_empty())
 54            .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len()))
 55            .map(|entry| entry.diagnostic);
 56        if new_diagnostic != self.current_diagnostic {
 57            self.current_diagnostic = new_diagnostic;
 58            cx.notify();
 59        }
 60    }
 61}
 62
 63impl Entity for DiagnosticIndicator {
 64    type Event = ();
 65}
 66
 67impl View for DiagnosticIndicator {
 68    fn ui_name() -> &'static str {
 69        "DiagnosticIndicator"
 70    }
 71
 72    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
 73        enum Tag {}
 74
 75        let in_progress = self.check_in_progress;
 76        let mut element = Flex::row().with_child(
 77            MouseEventHandler::new::<Tag, _, _>(0, cx, |state, cx| {
 78                let style = &cx
 79                    .global::<Settings>()
 80                    .theme
 81                    .workspace
 82                    .status_bar
 83                    .diagnostics;
 84
 85                let summary_style = if self.summary.error_count > 0 {
 86                    if state.hovered {
 87                        &style.summary_error_hover
 88                    } else {
 89                        &style.summary_error
 90                    }
 91                } else if self.summary.warning_count > 0 {
 92                    if state.hovered {
 93                        &style.summary_warning_hover
 94                    } else {
 95                        &style.summary_warning
 96                    }
 97                } else if state.hovered {
 98                    &style.summary_ok_hover
 99                } else {
100                    &style.summary_ok
101                };
102
103                let mut summary_row = Flex::row();
104                if self.summary.error_count > 0 {
105                    summary_row.add_children([
106                        Svg::new("icons/error-solid-14.svg")
107                            .with_color(style.icon_color_error)
108                            .constrained()
109                            .with_width(style.icon_width)
110                            .aligned()
111                            .contained()
112                            .with_margin_right(style.icon_spacing)
113                            .named("error-icon"),
114                        Label::new(
115                            self.summary.error_count.to_string(),
116                            summary_style.text.clone(),
117                        )
118                        .aligned()
119                        .boxed(),
120                    ]);
121                }
122
123                if self.summary.warning_count > 0 {
124                    summary_row.add_children([
125                        Svg::new("icons/warning-solid-14.svg")
126                            .with_color(style.icon_color_warning)
127                            .constrained()
128                            .with_width(style.icon_width)
129                            .aligned()
130                            .contained()
131                            .with_margin_right(style.icon_spacing)
132                            .with_margin_left(if self.summary.error_count > 0 {
133                                style.summary_spacing
134                            } else {
135                                0.
136                            })
137                            .named("warning-icon"),
138                        Label::new(
139                            self.summary.warning_count.to_string(),
140                            summary_style.text.clone(),
141                        )
142                        .aligned()
143                        .boxed(),
144                    ]);
145                }
146
147                if self.summary.error_count == 0 && self.summary.warning_count == 0 {
148                    summary_row.add_child(
149                        Svg::new("icons/no-error-solid-14.svg")
150                            .with_color(style.icon_color_ok)
151                            .constrained()
152                            .with_width(style.icon_width)
153                            .aligned()
154                            .named("ok-icon"),
155                    );
156                }
157
158                summary_row
159                    .constrained()
160                    .with_height(style.height)
161                    .contained()
162                    .with_style(summary_style.container)
163                    .boxed()
164            })
165            .with_cursor_style(CursorStyle::PointingHand)
166            .on_click(|cx| cx.dispatch_action(crate::Deploy))
167            .aligned()
168            .boxed(),
169        );
170
171        let style = &cx.global::<Settings>().theme.workspace.status_bar;
172
173        if in_progress {
174            element.add_child(
175                Label::new("checking…".into(), style.diagnostics.message.text.clone())
176                    .aligned()
177                    .contained()
178                    .with_margin_left(style.item_spacing)
179                    .boxed(),
180            );
181        } else if let Some(diagnostic) = &self.current_diagnostic {
182            element.add_child(
183                Label::new(
184                    diagnostic.message.split('\n').next().unwrap().to_string(),
185                    style.diagnostics.message.text.clone(),
186                )
187                .aligned()
188                .contained()
189                .with_margin_left(style.item_spacing)
190                .boxed(),
191            );
192        }
193
194        element.named("diagnostic indicator")
195    }
196
197    fn debug_json(&self, _: &gpui::AppContext) -> serde_json::Value {
198        serde_json::json!({ "summary": self.summary })
199    }
200}
201
202impl StatusItemView for DiagnosticIndicator {
203    fn set_active_pane_item(
204        &mut self,
205        active_pane_item: Option<&dyn workspace::ItemHandle>,
206        cx: &mut ViewContext<Self>,
207    ) {
208        if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
209            self._observe_active_editor = Some(cx.observe(&editor, Self::update));
210            self.update(editor, cx);
211        } else {
212            self.current_diagnostic = None;
213            self._observe_active_editor = None;
214        }
215        cx.notify();
216    }
217}