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 group_ixs_to_remove = Vec::new();
227        let mut blocks_to_add = Vec::new();
228        let mut blocks_to_restyle = HashMap::default();
229        let mut blocks_to_remove = HashSet::default();
230        let selected_excerpts = self
231            .editor
232            .read(cx)
233            .local_anchor_selections()
234            .iter()
235            .flat_map(|s| [s.start.excerpt_id().clone(), s.end.excerpt_id().clone()])
236            .collect::<HashSet<_>>();
237        let mut diagnostic_blocks = Vec::new();
238        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
239            let mut old_groups = groups.iter_mut().enumerate().peekable();
240            let mut new_groups = snapshot
241                .diagnostic_groups()
242                .into_iter()
243                .filter(|group| group.entries[group.primary_ix].diagnostic.is_disk_based)
244                .peekable();
245
246            loop {
247                let mut to_insert = None;
248                let mut to_invalidate = None;
249                let mut to_validate = None;
250                match (old_groups.peek(), new_groups.peek()) {
251                    (None, None) => break,
252                    (None, Some(_)) => to_insert = new_groups.next(),
253                    (Some(_), None) => to_invalidate = old_groups.next(),
254                    (Some((_, old_group)), Some(new_group)) => {
255                        let old_primary = &old_group.primary_diagnostic;
256                        let new_primary = &new_group.entries[new_group.primary_ix];
257                        match compare_diagnostics(old_primary, new_primary, &snapshot) {
258                            Ordering::Less => to_invalidate = old_groups.next(),
259                            Ordering::Equal => {
260                                to_validate = old_groups.next();
261                                new_groups.next();
262                            }
263                            Ordering::Greater => to_insert = new_groups.next(),
264                        }
265                    }
266                }
267
268                if let Some(group) = to_insert {
269                    let mut group_state = DiagnosticGroupState {
270                        primary_diagnostic: group.entries[group.primary_ix].clone(),
271                        excerpts: Default::default(),
272                        blocks: Default::default(),
273                        block_count: 0,
274                        is_valid: true,
275                    };
276                    let mut pending_range: Option<(Range<Point>, usize)> = None;
277                    let mut is_first_excerpt_for_group = true;
278                    for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
279                        let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
280                        if let Some((range, start_ix)) = &mut pending_range {
281                            if let Some(entry) = resolved_entry.as_ref() {
282                                if entry.range.start.row
283                                    <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
284                                {
285                                    range.end = range.end.max(entry.range.end);
286                                    continue;
287                                }
288                            }
289
290                            let excerpt_start =
291                                Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
292                            let excerpt_end = snapshot.clip_point(
293                                Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
294                                Bias::Left,
295                            );
296                            let excerpt_id = excerpts.insert_excerpt_after(
297                                &prev_excerpt_id,
298                                ExcerptProperties {
299                                    buffer: &buffer,
300                                    range: excerpt_start..excerpt_end,
301                                },
302                                excerpts_cx,
303                            );
304
305                            prev_excerpt_id = excerpt_id.clone();
306                            group_state.excerpts.push(excerpt_id.clone());
307                            let header_position = (excerpt_id.clone(), language::Anchor::min());
308
309                            if is_first_excerpt_for_group {
310                                is_first_excerpt_for_group = false;
311                                let primary = &group.entries[group.primary_ix].diagnostic;
312                                let mut header = primary.clone();
313                                header.message =
314                                    primary.message.split('\n').next().unwrap().to_string();
315                                group_state.block_count += 1;
316                                diagnostic_blocks.push(DiagnosticBlock::Header(header.clone()));
317                                blocks_to_add.push(BlockProperties {
318                                    position: header_position,
319                                    height: 2,
320                                    render: diagnostic_header_renderer(
321                                        buffer.clone(),
322                                        header,
323                                        true,
324                                        self.build_settings.clone(),
325                                    ),
326                                    disposition: BlockDisposition::Above,
327                                });
328                            } else {
329                                group_state.block_count += 1;
330                                diagnostic_blocks.push(DiagnosticBlock::Context);
331                                blocks_to_add.push(BlockProperties {
332                                    position: header_position,
333                                    height: 1,
334                                    render: context_header_renderer(self.build_settings.clone()),
335                                    disposition: BlockDisposition::Above,
336                                });
337                            }
338
339                            for entry in &group.entries[*start_ix..ix] {
340                                let mut diagnostic = entry.diagnostic.clone();
341                                if diagnostic.is_primary {
342                                    let mut lines = entry.diagnostic.message.split('\n');
343                                    lines.next();
344                                    diagnostic.message = lines.collect();
345                                }
346
347                                if !diagnostic.message.is_empty() {
348                                    group_state.block_count += 1;
349                                    diagnostic_blocks
350                                        .push(DiagnosticBlock::Inline(diagnostic.clone()));
351                                    blocks_to_add.push(BlockProperties {
352                                        position: (excerpt_id.clone(), entry.range.start.clone()),
353                                        height: diagnostic.message.matches('\n').count() as u8 + 1,
354                                        render: diagnostic_block_renderer(
355                                            diagnostic,
356                                            true,
357                                            self.build_settings.clone(),
358                                        ),
359                                        disposition: BlockDisposition::Below,
360                                    });
361                                }
362                            }
363
364                            pending_range.take();
365                        }
366
367                        if let Some(entry) = resolved_entry {
368                            pending_range = Some((entry.range.clone(), ix));
369                        }
370                    }
371
372                    groups_to_add.push(group_state);
373                } else if let Some((group_ix, group_state)) = to_invalidate {
374                    if group_state
375                        .excerpts
376                        .iter()
377                        .any(|excerpt_id| selected_excerpts.contains(excerpt_id))
378                    {
379                        for (block_id, block) in &group_state.blocks {
380                            match block {
381                                DiagnosticBlock::Header(diagnostic) => {
382                                    blocks_to_restyle.insert(
383                                        *block_id,
384                                        diagnostic_header_renderer(
385                                            buffer.clone(),
386                                            diagnostic.clone(),
387                                            false,
388                                            self.build_settings.clone(),
389                                        ),
390                                    );
391                                }
392                                DiagnosticBlock::Inline(diagnostic) => {
393                                    blocks_to_restyle.insert(
394                                        *block_id,
395                                        diagnostic_block_renderer(
396                                            diagnostic.clone(),
397                                            false,
398                                            self.build_settings.clone(),
399                                        ),
400                                    );
401                                }
402                                DiagnosticBlock::Context => {}
403                            }
404                        }
405
406                        group_state.is_valid = false;
407                        prev_excerpt_id = group_state.excerpts.last().unwrap().clone();
408                    } else {
409                        excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
410                        group_ixs_to_remove.push(group_ix);
411                        blocks_to_remove.extend(group_state.blocks.keys().copied());
412                    }
413                } else if let Some((_, group_state)) = to_validate {
414                    for (block_id, block) in &group_state.blocks {
415                        match block {
416                            DiagnosticBlock::Header(diagnostic) => {
417                                blocks_to_restyle.insert(
418                                    *block_id,
419                                    diagnostic_header_renderer(
420                                        buffer.clone(),
421                                        diagnostic.clone(),
422                                        true,
423                                        self.build_settings.clone(),
424                                    ),
425                                );
426                            }
427                            DiagnosticBlock::Inline(diagnostic) => {
428                                blocks_to_restyle.insert(
429                                    *block_id,
430                                    diagnostic_block_renderer(
431                                        diagnostic.clone(),
432                                        true,
433                                        self.build_settings.clone(),
434                                    ),
435                                );
436                            }
437                            DiagnosticBlock::Context => {}
438                        }
439                    }
440                    group_state.is_valid = true;
441                    prev_excerpt_id = group_state.excerpts.last().unwrap().clone();
442                } else {
443                    unreachable!();
444                }
445            }
446
447            excerpts.snapshot(excerpts_cx)
448        });
449
450        self.editor.update(cx, |editor, cx| {
451            editor.remove_blocks(blocks_to_remove, cx);
452            editor.replace_blocks(blocks_to_restyle, cx);
453            let mut block_ids = editor
454                .insert_blocks(
455                    blocks_to_add.into_iter().map(|block| {
456                        let (excerpt_id, text_anchor) = block.position;
457                        BlockProperties {
458                            position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
459                            height: block.height,
460                            render: block.render,
461                            disposition: block.disposition,
462                        }
463                    }),
464                    cx,
465                )
466                .into_iter()
467                .zip(diagnostic_blocks);
468
469            for group_state in &mut groups_to_add {
470                group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
471            }
472        });
473
474        for ix in group_ixs_to_remove.into_iter().rev() {
475            groups.remove(ix);
476        }
477        groups.extend(groups_to_add);
478        groups.sort_unstable_by(|a, b| {
479            let range_a = &a.primary_diagnostic.range;
480            let range_b = &b.primary_diagnostic.range;
481            range_a
482                .start
483                .cmp(&range_b.start, &snapshot)
484                .unwrap()
485                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
486        });
487
488        if groups.is_empty() {
489            self.path_states.remove(path_ix);
490        }
491
492        cx.notify();
493    }
494}
495
496impl workspace::Item for ProjectDiagnostics {
497    type View = ProjectDiagnosticsEditor;
498
499    fn build_view(
500        handle: ModelHandle<Self>,
501        settings: watch::Receiver<workspace::Settings>,
502        cx: &mut ViewContext<Self::View>,
503    ) -> Self::View {
504        let project = handle.read(cx).project.clone();
505        ProjectDiagnosticsEditor::new(project, settings, cx)
506    }
507
508    fn project_path(&self) -> Option<project::ProjectPath> {
509        None
510    }
511}
512
513impl workspace::ItemView for ProjectDiagnosticsEditor {
514    fn title(&self, _: &AppContext) -> String {
515        "Project Diagnostics".to_string()
516    }
517
518    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
519        None
520    }
521
522    fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
523        self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
524    }
525
526    fn save_as(
527        &mut self,
528        _: ModelHandle<project::Worktree>,
529        _: &std::path::Path,
530        _: &mut ViewContext<Self>,
531    ) -> Task<Result<()>> {
532        unreachable!()
533    }
534
535    fn is_dirty(&self, cx: &AppContext) -> bool {
536        self.excerpts.read(cx).read(cx).is_dirty()
537    }
538
539    fn has_conflict(&self, cx: &AppContext) -> bool {
540        self.excerpts.read(cx).read(cx).has_conflict()
541    }
542
543    fn should_update_tab_on_event(event: &Event) -> bool {
544        matches!(
545            event,
546            Event::Saved | Event::Dirtied | Event::FileHandleChanged
547        )
548    }
549
550    fn can_save(&self, _: &AppContext) -> bool {
551        true
552    }
553
554    fn can_save_as(&self, _: &AppContext) -> bool {
555        false
556    }
557}
558
559fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
560    lhs: &DiagnosticEntry<L>,
561    rhs: &DiagnosticEntry<R>,
562    snapshot: &language::BufferSnapshot,
563) -> Ordering {
564    lhs.range
565        .start
566        .to_offset(&snapshot)
567        .cmp(&rhs.range.start.to_offset(snapshot))
568        .then_with(|| {
569            lhs.range
570                .end
571                .to_offset(&snapshot)
572                .cmp(&rhs.range.end.to_offset(snapshot))
573        })
574        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
575}
576
577#[cfg(test)]
578mod tests {
579    use super::*;
580    use client::{http::ServerResponse, test::FakeHttpClient, Client, UserStore};
581    use gpui::TestAppContext;
582    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry};
583    use project::FakeFs;
584    use serde_json::json;
585    use std::sync::Arc;
586    use unindent::Unindent as _;
587    use workspace::WorkspaceParams;
588
589    #[gpui::test]
590    async fn test_diagnostics(mut cx: TestAppContext) {
591        let settings = cx.update(WorkspaceParams::test).settings;
592        let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
593        let client = Client::new();
594        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
595        let fs = Arc::new(FakeFs::new());
596
597        let project = cx.update(|cx| {
598            Project::local(
599                client.clone(),
600                user_store,
601                Arc::new(LanguageRegistry::new()),
602                fs.clone(),
603                cx,
604            )
605        });
606
607        fs.insert_tree(
608            "/test",
609            json!({
610                "a.rs": "
611                    const a: i32 = 'a';
612                ".unindent(),
613
614                "main.rs": "
615                    fn main() {
616                        let x = vec![];
617                        let y = vec![];
618                        a(x);
619                        b(y);
620                        // comment 1
621                        // comment 2
622                        c(y);
623                        d(x);
624                    }
625                "
626                .unindent(),
627            }),
628        )
629        .await;
630
631        let worktree = project
632            .update(&mut cx, |project, cx| {
633                project.add_local_worktree("/test", cx)
634            })
635            .await
636            .unwrap();
637
638        worktree.update(&mut cx, |worktree, cx| {
639            worktree
640                .update_diagnostics_from_provider(
641                    Arc::from("/test/main.rs".as_ref()),
642                    vec![
643                        DiagnosticEntry {
644                            range: 20..21,
645                            diagnostic: Diagnostic {
646                                message:
647                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
648                                        .to_string(),
649                                severity: DiagnosticSeverity::INFORMATION,
650                                is_primary: false,
651                                group_id: 1,
652                                ..Default::default()
653                            },
654                        },
655                        DiagnosticEntry {
656                            range: 40..41,
657                            diagnostic: Diagnostic {
658                                message:
659                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
660                                        .to_string(),
661                                severity: DiagnosticSeverity::INFORMATION,
662                                is_primary: false,
663                                group_id: 0,
664                                ..Default::default()
665                            },
666                        },
667                        DiagnosticEntry {
668                            range: 58..59,
669                            diagnostic: Diagnostic {
670                                message: "value moved here".to_string(),
671                                severity: DiagnosticSeverity::INFORMATION,
672                                is_primary: false,
673                                group_id: 1,
674                                ..Default::default()
675                            },
676                        },
677                        DiagnosticEntry {
678                            range: 68..69,
679                            diagnostic: Diagnostic {
680                                message: "value moved here".to_string(),
681                                severity: DiagnosticSeverity::INFORMATION,
682                                is_primary: false,
683                                group_id: 0,
684                                ..Default::default()
685                            },
686                        },
687                        DiagnosticEntry {
688                            range: 112..113,
689                            diagnostic: Diagnostic {
690                                message: "use of moved value\nvalue used here after move".to_string(),
691                                severity: DiagnosticSeverity::ERROR,
692                                is_primary: true,
693                                group_id: 0,
694                                ..Default::default()
695                            },
696                        },
697                        DiagnosticEntry {
698                            range: 122..123,
699                            diagnostic: Diagnostic {
700                                message: "use of moved value\nvalue used here after move".to_string(),
701                                severity: DiagnosticSeverity::ERROR,
702                                is_primary: true,
703                                group_id: 1,
704                                ..Default::default()
705                            },
706                        },
707                    ],
708                    cx,
709                )
710                .unwrap();
711        });
712
713        let view = cx.add_view(Default::default(), |cx| {
714            ProjectDiagnosticsEditor::new(project.clone(), settings, cx)
715        });
716
717        view.condition(&mut cx, |view, cx| view.text(cx).contains("fn main()"))
718            .await;
719
720        view.update(&mut cx, |view, cx| {
721            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
722
723            assert_eq!(
724                editor.text(),
725                concat!(
726                    //
727                    // main.rs, diagnostic group 1
728                    //
729                    "\n", // primary message
730                    "\n", // filename
731                    "    let x = vec![];\n",
732                    "    let y = vec![];\n",
733                    "\n", // supporting diagnostic
734                    "    a(x);\n",
735                    "    b(y);\n",
736                    "\n", // supporting diagnostic
737                    "    // comment 1\n",
738                    "    // comment 2\n",
739                    "    c(y);\n",
740                    "\n", // supporting diagnostic
741                    "    d(x);\n",
742                    //
743                    // main.rs, diagnostic group 2
744                    //
745                    "\n", // primary message
746                    "\n", // filename
747                    "fn main() {\n",
748                    "    let x = vec![];\n",
749                    "\n", // supporting diagnostic
750                    "    let y = vec![];\n",
751                    "    a(x);\n",
752                    "\n", // supporting diagnostic
753                    "    b(y);\n",
754                    "\n", // context ellipsis
755                    "    c(y);\n",
756                    "    d(x);\n",
757                    "\n", // supporting diagnostic
758                    "}"
759                )
760            );
761        });
762
763        worktree.update(&mut cx, |worktree, cx| {
764            worktree
765                .update_diagnostics_from_provider(
766                    Arc::from("/test/a.rs".as_ref()),
767                    vec![DiagnosticEntry {
768                        range: 15..15,
769                        diagnostic: Diagnostic {
770                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
771                            severity: DiagnosticSeverity::ERROR,
772                            is_primary: true,
773                            group_id: 0,
774                            ..Default::default()
775                        },
776                    }],
777                    cx,
778                )
779                .unwrap();
780        });
781
782        view.condition(&mut cx, |view, cx| view.text(cx).contains("const a"))
783            .await;
784
785        view.update(&mut cx, |view, cx| {
786            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
787
788            assert_eq!(
789                editor.text(),
790                concat!(
791                    //
792                    // a.rs
793                    //
794                    "\n", // primary message
795                    "\n", // filename
796                    "const a: i32 = 'a';\n",
797                    //
798                    // main.rs, diagnostic group 1
799                    //
800                    "\n", // primary message
801                    "\n", // filename
802                    "    let x = vec![];\n",
803                    "    let y = vec![];\n",
804                    "\n", // supporting diagnostic
805                    "    a(x);\n",
806                    "    b(y);\n",
807                    "\n", // supporting diagnostic
808                    "    // comment 1\n",
809                    "    // comment 2\n",
810                    "    c(y);\n",
811                    "\n", // supporting diagnostic
812                    "    d(x);\n",
813                    //
814                    // main.rs, diagnostic group 2
815                    //
816                    "\n", // primary message
817                    "\n", // filename
818                    "fn main() {\n",
819                    "    let x = vec![];\n",
820                    "\n", // supporting diagnostic
821                    "    let y = vec![];\n",
822                    "    a(x);\n",
823                    "\n", // supporting diagnostic
824                    "    b(y);\n",
825                    "\n", // context ellipsis
826                    "    c(y);\n",
827                    "    d(x);\n",
828                    "\n", // supporting diagnostic
829                    "}"
830                )
831            );
832        });
833    }
834}