diagnostics.rs

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