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 util::TryFutureExt;
 16use workspace::Workspace;
 17
 18action!(Toggle);
 19
 20const CONTEXT_LINE_COUNT: u32 = 1;
 21
 22pub fn init(cx: &mut MutableAppContext) {
 23    cx.add_bindings([Binding::new("alt-shift-D", Toggle, None)]);
 24    cx.add_action(ProjectDiagnosticsEditor::toggle);
 25}
 26
 27type Event = editor::Event;
 28
 29struct ProjectDiagnostics {
 30    project: ModelHandle<Project>,
 31}
 32
 33struct ProjectDiagnosticsEditor {
 34    editor: ViewHandle<Editor>,
 35    excerpts: ModelHandle<MultiBuffer>,
 36    build_settings: BuildSettings,
 37}
 38
 39impl ProjectDiagnostics {
 40    fn new(project: ModelHandle<Project>) -> Self {
 41        Self { project }
 42    }
 43}
 44
 45impl Entity for ProjectDiagnostics {
 46    type Event = ();
 47}
 48
 49impl Entity for ProjectDiagnosticsEditor {
 50    type Event = Event;
 51}
 52
 53impl View for ProjectDiagnosticsEditor {
 54    fn ui_name() -> &'static str {
 55        "ProjectDiagnosticsEditor"
 56    }
 57
 58    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
 59        ChildView::new(self.editor.id()).boxed()
 60    }
 61
 62    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
 63        cx.focus(&self.editor);
 64    }
 65}
 66
 67impl ProjectDiagnosticsEditor {
 68    fn new(
 69        project: ModelHandle<Project>,
 70        settings: watch::Receiver<workspace::Settings>,
 71        cx: &mut ViewContext<Self>,
 72    ) -> Self {
 73        let project_paths = project
 74            .read(cx)
 75            .diagnostic_summaries(cx)
 76            .map(|e| e.0)
 77            .collect::<Vec<_>>();
 78
 79        cx.spawn(|this, mut cx| {
 80            let project = project.clone();
 81            async move {
 82                for project_path in project_paths {
 83                    let buffer = project
 84                        .update(&mut cx, |project, cx| project.open_buffer(project_path, cx))
 85                        .await?;
 86                    this.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx))
 87                }
 88                Result::<_, anyhow::Error>::Ok(())
 89            }
 90        })
 91        .detach();
 92
 93        cx.subscribe(&project, |_, project, event, cx| {
 94            if let project::Event::DiagnosticsUpdated(project_path) = event {
 95                let project_path = project_path.clone();
 96                cx.spawn(|this, mut cx| {
 97                    async move {
 98                        let buffer = project
 99                            .update(&mut cx, |project, cx| project.open_buffer(project_path, cx))
100                            .await?;
101                        this.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx));
102                        Ok(())
103                    }
104                    .log_err()
105                })
106                .detach();
107            }
108        })
109        .detach();
110
111        let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
112        let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
113        let editor =
114            cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
115        cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
116            .detach();
117        Self {
118            excerpts,
119            editor,
120            build_settings,
121        }
122    }
123
124    #[cfg(test)]
125    fn text(&self, cx: &AppContext) -> String {
126        self.editor.read(cx).text(cx)
127    }
128
129    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
130        let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
131        workspace.add_item(diagnostics, cx);
132    }
133
134    fn populate_excerpts(&mut self, buffer: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
135        let mut blocks = Vec::new();
136        let snapshot = buffer.read(cx).snapshot();
137
138        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
139            for group in snapshot.diagnostic_groups::<Point>() {
140                let mut pending_range: Option<(Range<Point>, usize)> = None;
141                let mut is_first_excerpt = true;
142                for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
143                    if let Some((range, start_ix)) = &mut pending_range {
144                        if let Some(entry) = entry {
145                            if entry.range.start.row <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2 {
146                                range.end = range.end.max(entry.range.end);
147                                continue;
148                            }
149                        }
150
151                        let excerpt_start =
152                            Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
153                        let excerpt_end = snapshot.clip_point(
154                            Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
155                            Bias::Left,
156                        );
157                        let excerpt_id = excerpts.push_excerpt(
158                            ExcerptProperties {
159                                buffer: &buffer,
160                                range: excerpt_start..excerpt_end,
161                            },
162                            excerpts_cx,
163                        );
164
165                        let header_position = (excerpt_id.clone(), language::Anchor::min());
166                        if is_first_excerpt {
167                            let primary = &group.entries[group.primary_ix].diagnostic;
168                            let mut header = primary.clone();
169                            header.message =
170                                primary.message.split('\n').next().unwrap().to_string();
171                            blocks.push(BlockProperties {
172                                position: header_position,
173                                height: 2,
174                                render: diagnostic_header_renderer(
175                                    buffer.clone(),
176                                    header,
177                                    self.build_settings.clone(),
178                                ),
179                                disposition: BlockDisposition::Above,
180                            });
181                        } else {
182                            blocks.push(BlockProperties {
183                                position: header_position,
184                                height: 1,
185                                render: context_header_renderer(self.build_settings.clone()),
186                                disposition: BlockDisposition::Above,
187                            });
188                        }
189
190                        is_first_excerpt = false;
191                        for entry in &group.entries[*start_ix..ix] {
192                            let mut diagnostic = entry.diagnostic.clone();
193                            if diagnostic.is_primary {
194                                let mut lines = entry.diagnostic.message.split('\n');
195                                lines.next();
196                                diagnostic.message = lines.collect();
197                            }
198
199                            if !diagnostic.message.is_empty() {
200                                let buffer_anchor = snapshot.anchor_before(entry.range.start);
201                                blocks.push(BlockProperties {
202                                    position: (excerpt_id.clone(), buffer_anchor),
203                                    height: diagnostic.message.matches('\n').count() as u8 + 1,
204                                    render: diagnostic_block_renderer(
205                                        diagnostic,
206                                        true,
207                                        self.build_settings.clone(),
208                                    ),
209                                    disposition: BlockDisposition::Below,
210                                });
211                            }
212                        }
213
214                        pending_range.take();
215                    }
216
217                    if let Some(entry) = entry {
218                        pending_range = Some((entry.range.clone(), ix));
219                    }
220                }
221            }
222
223            excerpts.snapshot(excerpts_cx)
224        });
225
226        self.editor.update(cx, |editor, cx| {
227            editor.insert_blocks(
228                blocks.into_iter().map(|block| {
229                    let (excerpt_id, text_anchor) = block.position;
230                    BlockProperties {
231                        position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
232                        height: block.height,
233                        render: block.render,
234                        disposition: block.disposition,
235                    }
236                }),
237                cx,
238            );
239        });
240        cx.notify();
241    }
242}
243
244impl workspace::Item for ProjectDiagnostics {
245    type View = ProjectDiagnosticsEditor;
246
247    fn build_view(
248        handle: ModelHandle<Self>,
249        settings: watch::Receiver<workspace::Settings>,
250        cx: &mut ViewContext<Self::View>,
251    ) -> Self::View {
252        let project = handle.read(cx).project.clone();
253        ProjectDiagnosticsEditor::new(project, settings, cx)
254    }
255
256    fn project_path(&self) -> Option<project::ProjectPath> {
257        None
258    }
259}
260
261impl workspace::ItemView for ProjectDiagnosticsEditor {
262    fn title(&self, _: &AppContext) -> String {
263        "Project Diagnostics".to_string()
264    }
265
266    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
267        None
268    }
269
270    fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
271        self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
272    }
273
274    fn save_as(
275        &mut self,
276        _: ModelHandle<project::Worktree>,
277        _: &std::path::Path,
278        _: &mut ViewContext<Self>,
279    ) -> Task<Result<()>> {
280        unreachable!()
281    }
282
283    fn is_dirty(&self, cx: &AppContext) -> bool {
284        self.excerpts.read(cx).read(cx).is_dirty()
285    }
286
287    fn has_conflict(&self, cx: &AppContext) -> bool {
288        self.excerpts.read(cx).read(cx).has_conflict()
289    }
290
291    fn should_update_tab_on_event(event: &Event) -> bool {
292        matches!(
293            event,
294            Event::Saved | Event::Dirtied | Event::FileHandleChanged
295        )
296    }
297
298    fn can_save(&self, _: &AppContext) -> bool {
299        true
300    }
301
302    fn can_save_as(&self, _: &AppContext) -> bool {
303        false
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use client::{http::ServerResponse, test::FakeHttpClient, Client, UserStore};
311    use gpui::TestAppContext;
312    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry, PointUtf16};
313    use project::FakeFs;
314    use serde_json::json;
315    use std::sync::Arc;
316    use unindent::Unindent as _;
317    use workspace::WorkspaceParams;
318
319    #[gpui::test]
320    async fn test_diagnostics(mut cx: TestAppContext) {
321        let settings = cx.update(WorkspaceParams::test).settings;
322        let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
323        let client = Client::new();
324        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
325        let fs = Arc::new(FakeFs::new());
326
327        let project = cx.update(|cx| {
328            Project::local(
329                client.clone(),
330                user_store,
331                Arc::new(LanguageRegistry::new()),
332                fs.clone(),
333                cx,
334            )
335        });
336
337        fs.insert_tree(
338            "/test",
339            json!({
340                "a.rs": "
341                    const a: i32 = 'a';
342                ".unindent(),
343
344                "main.rs": "
345                    fn main() {
346                        let x = vec![];
347                        let y = vec![];
348                        a(x);
349                        b(y);
350                        // comment 1
351                        // comment 2
352                        c(y);
353                        d(x);
354                    }
355                "
356                .unindent(),
357            }),
358        )
359        .await;
360
361        let worktree = project
362            .update(&mut cx, |project, cx| {
363                project.add_local_worktree("/test", cx)
364            })
365            .await
366            .unwrap();
367
368        worktree.update(&mut cx, |worktree, cx| {
369            worktree
370                .update_diagnostic_entries(
371                    Arc::from("/test/main.rs".as_ref()),
372                    None,
373                    vec![
374                        DiagnosticEntry {
375                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
376                            diagnostic: Diagnostic {
377                                message:
378                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
379                                        .to_string(),
380                                severity: DiagnosticSeverity::INFORMATION,
381                                is_primary: false,
382                                group_id: 1,
383                                ..Default::default()
384                            },
385                        },
386                        DiagnosticEntry {
387                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
388                            diagnostic: Diagnostic {
389                                message:
390                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
391                                        .to_string(),
392                                severity: DiagnosticSeverity::INFORMATION,
393                                is_primary: false,
394                                group_id: 0,
395                                ..Default::default()
396                            },
397                        },
398                        DiagnosticEntry {
399                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
400                            diagnostic: Diagnostic {
401                                message: "value moved here".to_string(),
402                                severity: DiagnosticSeverity::INFORMATION,
403                                is_primary: false,
404                                group_id: 1,
405                                ..Default::default()
406                            },
407                        },
408                        DiagnosticEntry {
409                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
410                            diagnostic: Diagnostic {
411                                message: "value moved here".to_string(),
412                                severity: DiagnosticSeverity::INFORMATION,
413                                is_primary: false,
414                                group_id: 0,
415                                ..Default::default()
416                            },
417                        },
418                        DiagnosticEntry {
419                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
420                            diagnostic: Diagnostic {
421                                message: "use of moved value\nvalue used here after move".to_string(),
422                                severity: DiagnosticSeverity::ERROR,
423                                is_primary: true,
424                                group_id: 0,
425                                ..Default::default()
426                            },
427                        },
428                        DiagnosticEntry {
429                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
430                            diagnostic: Diagnostic {
431                                message: "use of moved value\nvalue used here after move".to_string(),
432                                severity: DiagnosticSeverity::ERROR,
433                                is_primary: true,
434                                group_id: 1,
435                                ..Default::default()
436                            },
437                        },
438                    ],
439                    cx,
440                )
441                .unwrap();
442        });
443
444        let view = cx.add_view(Default::default(), |cx| {
445            ProjectDiagnosticsEditor::new(project.clone(), settings, cx)
446        });
447
448        view.condition(&mut cx, |view, cx| view.text(cx).contains("fn main()"))
449            .await;
450
451        view.update(&mut cx, |view, cx| {
452            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
453
454            assert_eq!(
455                editor.text(),
456                concat!(
457                    // Diagnostic group 1 (error for `y`)
458                    "\n", // primary message
459                    "\n", // filename
460                    "    let x = vec![];\n",
461                    "    let y = vec![];\n",
462                    "\n", // supporting diagnostic
463                    "    a(x);\n",
464                    "    b(y);\n",
465                    "\n", // supporting diagnostic
466                    "    // comment 1\n",
467                    "    // comment 2\n",
468                    "    c(y);\n",
469                    "\n", // supporting diagnostic
470                    "    d(x);\n",
471                    // Diagnostic group 2 (error for `x`)
472                    "\n", // primary message
473                    "\n", // filename
474                    "fn main() {\n",
475                    "    let x = vec![];\n",
476                    "\n", // supporting diagnostic
477                    "    let y = vec![];\n",
478                    "    a(x);\n",
479                    "\n", // supporting diagnostic
480                    "    b(y);\n",
481                    "\n", // context ellipsis
482                    "    c(y);\n",
483                    "    d(x);\n",
484                    "\n", // supporting diagnostic
485                    "}"
486                )
487            );
488        });
489
490        worktree.update(&mut cx, |worktree, cx| {
491            worktree
492                .update_diagnostic_entries(
493                    Arc::from("/test/a.rs".as_ref()),
494                    None,
495                    vec![DiagnosticEntry {
496                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
497                        diagnostic: Diagnostic {
498                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
499                            severity: DiagnosticSeverity::ERROR,
500                            is_primary: true,
501                            group_id: 0,
502                            ..Default::default()
503                        },
504                    }],
505                    cx,
506                )
507                .unwrap();
508        });
509
510        view.condition(&mut cx, |view, cx| view.text(cx).contains("const a"))
511            .await;
512
513        view.update(&mut cx, |view, cx| {
514            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
515
516            assert_eq!(
517                editor.text(),
518                concat!(
519                    // a.rs
520                    "\n", // primary message
521                    "\n", // filename
522                    "const a: i32 = 'a';\n",
523                    // main.rs, diagnostic group 1
524                    "\n", // primary message
525                    "\n", // filename
526                    "    let x = vec![];\n",
527                    "    let y = vec![];\n",
528                    "\n", // supporting diagnostic
529                    "    a(x);\n",
530                    "    b(y);\n",
531                    "\n", // supporting diagnostic
532                    "    // comment 1\n",
533                    "    // comment 2\n",
534                    "    c(y);\n",
535                    "\n", // supporting diagnostic
536                    "    d(x);\n",
537                    // main.rs, diagnostic group 2
538                    "\n", // primary message
539                    "\n", // filename
540                    "fn main() {\n",
541                    "    let x = vec![];\n",
542                    "\n", // supporting diagnostic
543                    "    let y = vec![];\n",
544                    "    a(x);\n",
545                    "\n", // supporting diagnostic
546                    "    b(y);\n",
547                    "\n", // context ellipsis
548                    "    c(y);\n",
549                    "    d(x);\n",
550                    "\n", // supporting diagnostic
551                    "}"
552                )
553            );
554        });
555    }
556}