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