diagnostics.rs

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