items.rs

  1use collections::HashSet;
  2use editor::{Editor, GoToNextDiagnostic};
  3use gpui::{
  4    elements::*, platform::CursorStyle, serde_json, Entity, ModelHandle, MutableAppContext,
  5    RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
  6};
  7use language::Diagnostic;
  8use project::Project;
  9use settings::Settings;
 10use workspace::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, _: &GoToNextDiagnostic, 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(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 in_progress = !self.in_progress_checks.is_empty();
 90        let mut element = Flex::row().with_child(
 91            MouseEventHandler::new::<Summary, _, _>(0, cx, |state, cx| {
 92                let style = &cx
 93                    .global::<Settings>()
 94                    .theme
 95                    .workspace
 96                    .status_bar
 97                    .diagnostic_summary
 98                    .style_for(state, false);
 99
100                let mut summary_row = Flex::row();
101                if self.summary.error_count > 0 {
102                    summary_row.add_children([
103                        Svg::new("icons/error-solid-14.svg")
104                            .with_color(style.icon_color_error)
105                            .constrained()
106                            .with_width(style.icon_width)
107                            .aligned()
108                            .contained()
109                            .with_margin_right(style.icon_spacing)
110                            .named("error-icon"),
111                        Label::new(self.summary.error_count.to_string(), style.text.clone())
112                            .aligned()
113                            .boxed(),
114                    ]);
115                }
116
117                if self.summary.warning_count > 0 {
118                    summary_row.add_children([
119                        Svg::new("icons/warning-solid-14.svg")
120                            .with_color(style.icon_color_warning)
121                            .constrained()
122                            .with_width(style.icon_width)
123                            .aligned()
124                            .contained()
125                            .with_margin_right(style.icon_spacing)
126                            .with_margin_left(if self.summary.error_count > 0 {
127                                style.summary_spacing
128                            } else {
129                                0.
130                            })
131                            .named("warning-icon"),
132                        Label::new(self.summary.warning_count.to_string(), style.text.clone())
133                            .aligned()
134                            .boxed(),
135                    ]);
136                }
137
138                if self.summary.error_count == 0 && self.summary.warning_count == 0 {
139                    summary_row.add_child(
140                        Svg::new("icons/no-error-solid-14.svg")
141                            .with_color(style.icon_color_ok)
142                            .constrained()
143                            .with_width(style.icon_width)
144                            .aligned()
145                            .named("ok-icon"),
146                    );
147                }
148
149                summary_row
150                    .constrained()
151                    .with_height(style.height)
152                    .contained()
153                    .with_style(if self.summary.error_count > 0 {
154                        style.container_error
155                    } else if self.summary.warning_count > 0 {
156                        style.container_warning
157                    } else {
158                        style.container_ok
159                    })
160                    .boxed()
161            })
162            .with_cursor_style(CursorStyle::PointingHand)
163            .on_click(|_, _, cx| cx.dispatch_action(crate::Deploy))
164            .aligned()
165            .boxed(),
166        );
167
168        let style = &cx.global::<Settings>().theme.workspace.status_bar;
169        let item_spacing = style.item_spacing;
170
171        if in_progress {
172            element.add_child(
173                Label::new(
174                    "checking…".into(),
175                    style.diagnostic_message.default.text.clone(),
176                )
177                .aligned()
178                .contained()
179                .with_margin_left(item_spacing)
180                .boxed(),
181            );
182        } else if let Some(diagnostic) = &self.current_diagnostic {
183            let message_style = style.diagnostic_message.clone();
184            element.add_child(
185                MouseEventHandler::new::<Message, _, _>(1, cx, |state, _| {
186                    Label::new(
187                        diagnostic.message.split('\n').next().unwrap().to_string(),
188                        message_style.style_for(state, false).text.clone(),
189                    )
190                    .aligned()
191                    .contained()
192                    .with_margin_left(item_spacing)
193                    .boxed()
194                })
195                .with_cursor_style(CursorStyle::PointingHand)
196                .on_click(|_, _, cx| cx.dispatch_action(GoToNextDiagnostic))
197                .boxed(),
198            );
199        }
200
201        element.named("diagnostic indicator")
202    }
203
204    fn debug_json(&self, _: &gpui::AppContext) -> serde_json::Value {
205        serde_json::json!({ "summary": self.summary })
206    }
207}
208
209impl StatusItemView for DiagnosticIndicator {
210    fn set_active_pane_item(
211        &mut self,
212        active_pane_item: Option<&dyn workspace::ItemHandle>,
213        cx: &mut ViewContext<Self>,
214    ) {
215        if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
216            self.active_editor = Some(editor.downgrade());
217            self._observe_active_editor = Some(cx.observe(&editor, Self::update));
218            self.update(editor, cx);
219        } else {
220            self.active_editor = None;
221            self.current_diagnostic = None;
222            self._observe_active_editor = None;
223        }
224        cx.notify();
225    }
226}