diagnostics.rs

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