diagnostics.rs

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