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