diagnostics.rs

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