diagnostics.rs

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