diagnostics.rs

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