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