items.rs

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