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