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