diagnostics.rs

  1use anyhow::Result;
  2use editor::{
  3    context_header_renderer, diagnostic_block_renderer, diagnostic_header_renderer,
  4    display_map::{BlockDisposition, BlockProperties},
  5    BuildSettings, Editor, ExcerptProperties, MultiBuffer,
  6};
  7use gpui::{
  8    action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext,
  9    RenderContext, Task, View, ViewContext, ViewHandle,
 10};
 11use language::{Bias, Buffer, Point};
 12use postage::watch;
 13use project::Project;
 14use std::ops::Range;
 15use workspace::Workspace;
 16
 17action!(Toggle);
 18
 19const CONTEXT_LINE_COUNT: u32 = 1;
 20
 21pub fn init(cx: &mut MutableAppContext) {
 22    cx.add_bindings([Binding::new("alt-shift-D", Toggle, None)]);
 23    cx.add_action(ProjectDiagnosticsEditor::toggle);
 24}
 25
 26type Event = editor::Event;
 27
 28struct ProjectDiagnostics {
 29    project: ModelHandle<Project>,
 30}
 31
 32struct ProjectDiagnosticsEditor {
 33    editor: ViewHandle<Editor>,
 34    excerpts: ModelHandle<MultiBuffer>,
 35    build_settings: BuildSettings,
 36}
 37
 38impl ProjectDiagnostics {
 39    fn new(project: ModelHandle<Project>) -> Self {
 40        Self { project }
 41    }
 42}
 43
 44impl Entity for ProjectDiagnostics {
 45    type Event = ();
 46}
 47
 48impl Entity for ProjectDiagnosticsEditor {
 49    type Event = Event;
 50}
 51
 52impl View for ProjectDiagnosticsEditor {
 53    fn ui_name() -> &'static str {
 54        "ProjectDiagnosticsEditor"
 55    }
 56
 57    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
 58        ChildView::new(self.editor.id()).boxed()
 59    }
 60
 61    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
 62        cx.focus(&self.editor);
 63    }
 64}
 65
 66impl ProjectDiagnosticsEditor {
 67    fn new(
 68        replica_id: u16,
 69        settings: watch::Receiver<workspace::Settings>,
 70        cx: &mut ViewContext<Self>,
 71    ) -> Self {
 72        let excerpts = cx.add_model(|_| MultiBuffer::new(replica_id));
 73        let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
 74        let editor =
 75            cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
 76        cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
 77            .detach();
 78        Self {
 79            excerpts,
 80            editor,
 81            build_settings,
 82        }
 83    }
 84
 85    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
 86        let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
 87        workspace.add_item(diagnostics, cx);
 88    }
 89
 90    fn populate_excerpts(&mut self, buffer: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
 91        let mut blocks = Vec::new();
 92        let snapshot = buffer.read(cx).snapshot();
 93
 94        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
 95            for group in snapshot.diagnostic_groups::<Point>() {
 96                let mut pending_range: Option<(Range<Point>, usize)> = None;
 97                let mut is_first_excerpt = true;
 98                for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
 99                    if let Some((range, start_ix)) = &mut pending_range {
100                        if let Some(entry) = entry {
101                            if entry.range.start.row <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2 {
102                                range.end = range.end.max(entry.range.end);
103                                continue;
104                            }
105                        }
106
107                        let excerpt_start =
108                            Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
109                        let excerpt_end = snapshot.clip_point(
110                            Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
111                            Bias::Left,
112                        );
113                        let excerpt_id = excerpts.push_excerpt(
114                            ExcerptProperties {
115                                buffer: &buffer,
116                                range: excerpt_start..excerpt_end,
117                            },
118                            excerpts_cx,
119                        );
120
121                        let header_position = (excerpt_id.clone(), language::Anchor::min());
122                        if is_first_excerpt {
123                            let primary = &group.entries[group.primary_ix].diagnostic;
124                            let mut header = primary.clone();
125                            header.message =
126                                primary.message.split('\n').next().unwrap().to_string();
127                            blocks.push(BlockProperties {
128                                position: header_position,
129                                height: 2,
130                                render: diagnostic_header_renderer(
131                                    buffer.clone(),
132                                    header,
133                                    self.build_settings.clone(),
134                                ),
135                                disposition: BlockDisposition::Above,
136                            });
137                        } else {
138                            blocks.push(BlockProperties {
139                                position: header_position,
140                                height: 1,
141                                render: context_header_renderer(self.build_settings.clone()),
142                                disposition: BlockDisposition::Above,
143                            });
144                        }
145
146                        is_first_excerpt = false;
147                        for entry in &group.entries[*start_ix..ix] {
148                            let mut diagnostic = entry.diagnostic.clone();
149                            if diagnostic.is_primary {
150                                let mut lines = entry.diagnostic.message.split('\n');
151                                lines.next();
152                                diagnostic.message = lines.collect();
153                            }
154
155                            if !diagnostic.message.is_empty() {
156                                let buffer_anchor = snapshot.anchor_before(entry.range.start);
157                                blocks.push(BlockProperties {
158                                    position: (excerpt_id.clone(), buffer_anchor),
159                                    height: diagnostic.message.matches('\n').count() as u8 + 1,
160                                    render: diagnostic_block_renderer(
161                                        diagnostic,
162                                        true,
163                                        self.build_settings.clone(),
164                                    ),
165                                    disposition: BlockDisposition::Below,
166                                });
167                            }
168                        }
169
170                        pending_range.take();
171                    }
172
173                    if let Some(entry) = entry {
174                        pending_range = Some((entry.range.clone(), ix));
175                    }
176                }
177            }
178
179            excerpts.snapshot(excerpts_cx)
180        });
181
182        self.editor.update(cx, |editor, cx| {
183            editor.insert_blocks(
184                blocks.into_iter().map(|block| {
185                    let (excerpt_id, text_anchor) = block.position;
186                    BlockProperties {
187                        position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
188                        height: block.height,
189                        render: block.render,
190                        disposition: block.disposition,
191                    }
192                }),
193                cx,
194            );
195        });
196    }
197}
198
199impl workspace::Item for ProjectDiagnostics {
200    type View = ProjectDiagnosticsEditor;
201
202    fn build_view(
203        handle: ModelHandle<Self>,
204        settings: watch::Receiver<workspace::Settings>,
205        cx: &mut ViewContext<Self::View>,
206    ) -> Self::View {
207        let project = handle.read(cx).project.clone();
208        let project_paths = project
209            .read(cx)
210            .diagnostic_summaries(cx)
211            .map(|e| e.0)
212            .collect::<Vec<_>>();
213
214        cx.spawn(|view, mut cx| {
215            let project = project.clone();
216            async move {
217                for project_path in project_paths {
218                    let buffer = project
219                        .update(&mut cx, |project, cx| project.open_buffer(project_path, cx))
220                        .await?;
221                    view.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx))
222                }
223                Result::<_, anyhow::Error>::Ok(())
224            }
225        })
226        .detach();
227
228        ProjectDiagnosticsEditor::new(project.read(cx).replica_id(), settings, cx)
229    }
230
231    fn project_path(&self) -> Option<project::ProjectPath> {
232        None
233    }
234}
235
236impl workspace::ItemView for ProjectDiagnosticsEditor {
237    fn title(&self, _: &AppContext) -> String {
238        "Project Diagnostics".to_string()
239    }
240
241    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
242        None
243    }
244
245    fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
246        self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
247    }
248
249    fn save_as(
250        &mut self,
251        _: ModelHandle<project::Worktree>,
252        _: &std::path::Path,
253        _: &mut ViewContext<Self>,
254    ) -> Task<Result<()>> {
255        unreachable!()
256    }
257
258    fn is_dirty(&self, cx: &AppContext) -> bool {
259        self.excerpts.read(cx).read(cx).is_dirty()
260    }
261
262    fn has_conflict(&self, cx: &AppContext) -> bool {
263        self.excerpts.read(cx).read(cx).has_conflict()
264    }
265
266    fn should_update_tab_on_event(event: &Event) -> bool {
267        matches!(
268            event,
269            Event::Saved | Event::Dirtied | Event::FileHandleChanged
270        )
271    }
272
273    fn can_save(&self, _: &AppContext) -> bool {
274        true
275    }
276
277    fn can_save_as(&self, _: &AppContext) -> bool {
278        false
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
286    use unindent::Unindent as _;
287    use workspace::WorkspaceParams;
288
289    #[gpui::test]
290    fn test_diagnostics(cx: &mut MutableAppContext) {
291        let settings = WorkspaceParams::test(cx).settings;
292        let view = cx.add_view(Default::default(), |cx| {
293            ProjectDiagnosticsEditor::new(0, settings, cx)
294        });
295
296        let text = "
297        fn main() {
298            let x = vec![];
299            let y = vec![];
300            a(x);
301            b(y);
302            // comment 1
303            // comment 2
304            c(y);
305            d(x);
306        }
307        "
308        .unindent();
309
310        let buffer = cx.add_model(|cx| {
311            let mut buffer = Buffer::new(0, text, cx);
312            buffer
313                .update_diagnostics(
314                    None,
315                    vec![
316                        DiagnosticEntry {
317                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
318                            diagnostic: Diagnostic {
319                                message:
320                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
321                                        .to_string(),
322                                severity: DiagnosticSeverity::INFORMATION,
323                                is_primary: false,
324                                group_id: 1,
325                                ..Default::default()
326                            },
327                        },
328                        DiagnosticEntry {
329                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
330                            diagnostic: Diagnostic {
331                                message:
332                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
333                                        .to_string(),
334                                severity: DiagnosticSeverity::INFORMATION,
335                                is_primary: false,
336                                group_id: 0,
337                                ..Default::default()
338                            },
339                        },
340                        DiagnosticEntry {
341                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
342                            diagnostic: Diagnostic {
343                                message: "value moved here".to_string(),
344                                severity: DiagnosticSeverity::INFORMATION,
345                                is_primary: false,
346                                group_id: 1,
347                                ..Default::default()
348                            },
349                        },
350                        DiagnosticEntry {
351                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
352                            diagnostic: Diagnostic {
353                                message: "value moved here".to_string(),
354                                severity: DiagnosticSeverity::INFORMATION,
355                                is_primary: false,
356                                group_id: 0,
357                                ..Default::default()
358                            },
359                        },
360                        DiagnosticEntry {
361                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
362                            diagnostic: Diagnostic {
363                                message: "use of moved value\nvalue used here after move".to_string(),
364                                severity: DiagnosticSeverity::ERROR,
365                                is_primary: true,
366                                group_id: 0,
367                                ..Default::default()
368                            },
369                        },
370                        DiagnosticEntry {
371                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
372                            diagnostic: Diagnostic {
373                                message: "use of moved value\nvalue used here after move".to_string(),
374                                severity: DiagnosticSeverity::ERROR,
375                                is_primary: true,
376                                group_id: 1,
377                                ..Default::default()
378                            },
379                        },
380                    ],
381                    cx,
382                )
383                .unwrap();
384            buffer
385        });
386
387        view.update(cx, |view, cx| {
388            view.populate_excerpts(buffer, cx);
389            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
390
391            assert_eq!(
392                editor.text(),
393                concat!(
394                    // Diagnostic group 1 (error for `y`)
395                    "\n", // primary message
396                    "\n", // filename
397                    "    let x = vec![];\n",
398                    "    let y = vec![];\n",
399                    "\n", // supporting diagnostic
400                    "    a(x);\n",
401                    "    b(y);\n",
402                    "\n", // supporting diagnostic
403                    "    // comment 1\n",
404                    "    // comment 2\n",
405                    "    c(y);\n",
406                    "\n", // supporting diagnostic
407                    "    d(x);\n",
408                    // Diagnostic group 2 (error for `x`)
409                    "\n", // primary message
410                    "\n", // filename
411                    "fn main() {\n",
412                    "    let x = vec![];\n",
413                    "\n", // supporting diagnostic
414                    "    let y = vec![];\n",
415                    "    a(x);\n",
416                    "\n", // supporting diagnostic
417                    "    b(y);\n",
418                    "\n", // context ellipsis
419                    "    c(y);\n",
420                    "    d(x);\n",
421                    "\n", // supporting diagnostic
422                    "}"
423                )
424            );
425        });
426    }
427}