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