items.rs

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