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    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
 164        if let Some(existing) = workspace.item_of_type::<ProjectDiagnostics>(cx) {
 165            workspace.activate_item(&existing, cx);
 166        } else {
 167            let diagnostics =
 168                cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
 169            workspace.open_item(diagnostics, cx);
 170        }
 171    }
 172
 173    fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
 174        if let Some(workspace) = self.workspace.upgrade(cx) {
 175            let editor = self.editor.read(cx);
 176            let excerpts = self.excerpts.read(cx);
 177            let mut new_selections_by_buffer = HashMap::default();
 178
 179            for selection in editor.local_selections::<usize>(cx) {
 180                for (buffer, mut range) in
 181                    excerpts.excerpted_buffers(selection.start..selection.end, cx)
 182                {
 183                    if selection.reversed {
 184                        mem::swap(&mut range.start, &mut range.end);
 185                    }
 186                    new_selections_by_buffer
 187                        .entry(buffer)
 188                        .or_insert(Vec::new())
 189                        .push(range)
 190                }
 191            }
 192
 193            workspace.update(cx, |workspace, cx| {
 194                for (buffer, ranges) in new_selections_by_buffer {
 195                    let buffer = BufferItemHandle(buffer);
 196                    if !workspace.activate_pane_for_item(&buffer, cx) {
 197                        workspace.activate_next_pane(cx);
 198                    }
 199                    let editor = workspace
 200                        .open_item(buffer, cx)
 201                        .to_any()
 202                        .downcast::<Editor>()
 203                        .unwrap();
 204                    editor.update(cx, |editor, cx| {
 205                        editor.select_ranges(ranges, Some(Autoscroll::Center), cx)
 206                    });
 207                }
 208            });
 209        }
 210    }
 211
 212    fn update_excerpts(&self, paths: BTreeSet<ProjectPath>, cx: &mut ViewContext<Self>) {
 213        let project = self.model.read(cx).project.clone();
 214        cx.spawn(|this, mut cx| {
 215            async move {
 216                for path in paths {
 217                    let buffer = project
 218                        .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
 219                        .await?;
 220                    this.update(&mut cx, |view, cx| view.populate_excerpts(path, buffer, cx))
 221                }
 222                Result::<_, anyhow::Error>::Ok(())
 223            }
 224            .log_err()
 225        })
 226        .detach();
 227    }
 228
 229    fn populate_excerpts(
 230        &mut self,
 231        path: ProjectPath,
 232        buffer: ModelHandle<Buffer>,
 233        cx: &mut ViewContext<Self>,
 234    ) {
 235        let was_empty = self.path_states.is_empty();
 236        let snapshot = buffer.read(cx).snapshot();
 237        let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
 238            Ok(ix) => ix,
 239            Err(ix) => {
 240                self.path_states.insert(
 241                    ix,
 242                    PathState {
 243                        path: path.clone(),
 244                        header: None,
 245                        diagnostic_groups: Default::default(),
 246                    },
 247                );
 248                ix
 249            }
 250        };
 251
 252        let mut prev_excerpt_id = if path_ix > 0 {
 253            let prev_path_last_group = &self.path_states[path_ix - 1]
 254                .diagnostic_groups
 255                .last()
 256                .unwrap();
 257            prev_path_last_group.excerpts.last().unwrap().clone()
 258        } else {
 259            ExcerptId::min()
 260        };
 261
 262        let path_state = &mut self.path_states[path_ix];
 263        let mut groups_to_add = Vec::new();
 264        let mut group_ixs_to_remove = Vec::new();
 265        let mut blocks_to_add = Vec::new();
 266        let mut blocks_to_remove = HashSet::default();
 267        let mut first_excerpt_id = None;
 268        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
 269            let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
 270            let mut new_groups = snapshot
 271                .diagnostic_groups()
 272                .into_iter()
 273                .filter(|group| group.entries[group.primary_ix].diagnostic.is_disk_based)
 274                .peekable();
 275
 276            loop {
 277                let mut to_insert = None;
 278                let mut to_remove = None;
 279                let mut to_keep = None;
 280                match (old_groups.peek(), new_groups.peek()) {
 281                    (None, None) => break,
 282                    (None, Some(_)) => to_insert = new_groups.next(),
 283                    (Some(_), None) => to_remove = old_groups.next(),
 284                    (Some((_, old_group)), Some(new_group)) => {
 285                        let old_primary = &old_group.primary_diagnostic;
 286                        let new_primary = &new_group.entries[new_group.primary_ix];
 287                        match compare_diagnostics(old_primary, new_primary, &snapshot) {
 288                            Ordering::Less => to_remove = old_groups.next(),
 289                            Ordering::Equal => {
 290                                to_keep = old_groups.next();
 291                                new_groups.next();
 292                            }
 293                            Ordering::Greater => to_insert = new_groups.next(),
 294                        }
 295                    }
 296                }
 297
 298                if let Some(group) = to_insert {
 299                    let mut group_state = DiagnosticGroupState {
 300                        primary_diagnostic: group.entries[group.primary_ix].clone(),
 301                        primary_excerpt_ix: 0,
 302                        excerpts: Default::default(),
 303                        blocks: Default::default(),
 304                        block_count: 0,
 305                    };
 306                    let mut pending_range: Option<(Range<Point>, usize)> = None;
 307                    let mut is_first_excerpt_for_group = true;
 308                    for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
 309                        let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
 310                        if let Some((range, start_ix)) = &mut pending_range {
 311                            if let Some(entry) = resolved_entry.as_ref() {
 312                                if entry.range.start.row
 313                                    <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
 314                                {
 315                                    range.end = range.end.max(entry.range.end);
 316                                    continue;
 317                                }
 318                            }
 319
 320                            let excerpt_start =
 321                                Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
 322                            let excerpt_end = snapshot.clip_point(
 323                                Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
 324                                Bias::Left,
 325                            );
 326                            let excerpt_id = excerpts.insert_excerpt_after(
 327                                &prev_excerpt_id,
 328                                ExcerptProperties {
 329                                    buffer: &buffer,
 330                                    range: excerpt_start..excerpt_end,
 331                                },
 332                                excerpts_cx,
 333                            );
 334
 335                            prev_excerpt_id = excerpt_id.clone();
 336                            first_excerpt_id.get_or_insert_with(|| prev_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                                blocks_to_add.push(BlockProperties {
 348                                    position: header_position,
 349                                    height: 2,
 350                                    render: diagnostic_header_renderer(
 351                                        header,
 352                                        true,
 353                                        self.build_settings.clone(),
 354                                    ),
 355                                    disposition: BlockDisposition::Above,
 356                                });
 357                            } else {
 358                                group_state.block_count += 1;
 359                                blocks_to_add.push(BlockProperties {
 360                                    position: header_position,
 361                                    height: 1,
 362                                    render: context_header_renderer(self.build_settings.clone()),
 363                                    disposition: BlockDisposition::Above,
 364                                });
 365                            }
 366
 367                            for entry in &group.entries[*start_ix..ix] {
 368                                let mut diagnostic = entry.diagnostic.clone();
 369                                if diagnostic.is_primary {
 370                                    group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
 371                                    diagnostic.message =
 372                                        entry.diagnostic.message.split('\n').skip(1).collect();
 373                                }
 374
 375                                if !diagnostic.message.is_empty() {
 376                                    group_state.block_count += 1;
 377                                    blocks_to_add.push(BlockProperties {
 378                                        position: (excerpt_id.clone(), entry.range.start.clone()),
 379                                        height: diagnostic.message.matches('\n').count() as u8 + 1,
 380                                        render: diagnostic_block_renderer(
 381                                            diagnostic,
 382                                            true,
 383                                            self.build_settings.clone(),
 384                                        ),
 385                                        disposition: BlockDisposition::Below,
 386                                    });
 387                                }
 388                            }
 389
 390                            pending_range.take();
 391                        }
 392
 393                        if let Some(entry) = resolved_entry {
 394                            pending_range = Some((entry.range.clone(), ix));
 395                        }
 396                    }
 397
 398                    groups_to_add.push(group_state);
 399                } else if let Some((group_ix, group_state)) = to_remove {
 400                    excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
 401                    group_ixs_to_remove.push(group_ix);
 402                    blocks_to_remove.extend(group_state.blocks.iter().copied());
 403                } else if let Some((_, group)) = to_keep {
 404                    prev_excerpt_id = group.excerpts.last().unwrap().clone();
 405                    first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
 406                }
 407            }
 408
 409            excerpts.snapshot(excerpts_cx)
 410        });
 411
 412        self.editor.update(cx, |editor, cx| {
 413            blocks_to_remove.extend(path_state.header);
 414            editor.remove_blocks(blocks_to_remove, cx);
 415            let header_block = first_excerpt_id.map(|excerpt_id| BlockProperties {
 416                position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, language::Anchor::min()),
 417                height: 2,
 418                render: path_header_renderer(buffer, self.build_settings.clone()),
 419                disposition: BlockDisposition::Above,
 420            });
 421            let block_ids = editor.insert_blocks(
 422                blocks_to_add
 423                    .into_iter()
 424                    .map(|block| {
 425                        let (excerpt_id, text_anchor) = block.position;
 426                        BlockProperties {
 427                            position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
 428                            height: block.height,
 429                            render: block.render,
 430                            disposition: block.disposition,
 431                        }
 432                    })
 433                    .chain(header_block.into_iter()),
 434                cx,
 435            );
 436
 437            let mut block_ids = block_ids.into_iter();
 438            for group_state in &mut groups_to_add {
 439                group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
 440            }
 441            path_state.header = block_ids.next();
 442        });
 443
 444        for ix in group_ixs_to_remove.into_iter().rev() {
 445            path_state.diagnostic_groups.remove(ix);
 446        }
 447        path_state.diagnostic_groups.extend(groups_to_add);
 448        path_state.diagnostic_groups.sort_unstable_by(|a, b| {
 449            let range_a = &a.primary_diagnostic.range;
 450            let range_b = &b.primary_diagnostic.range;
 451            range_a
 452                .start
 453                .cmp(&range_b.start, &snapshot)
 454                .unwrap()
 455                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
 456        });
 457
 458        if path_state.diagnostic_groups.is_empty() {
 459            self.path_states.remove(path_ix);
 460        }
 461
 462        self.editor.update(cx, |editor, cx| {
 463            let groups;
 464            let mut selections;
 465            let new_excerpt_ids_by_selection_id;
 466            if was_empty {
 467                groups = self.path_states.first()?.diagnostic_groups.as_slice();
 468                new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
 469                selections = vec![Selection {
 470                    id: 0,
 471                    start: 0,
 472                    end: 0,
 473                    reversed: false,
 474                    goal: SelectionGoal::None,
 475                }];
 476            } else {
 477                groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
 478                new_excerpt_ids_by_selection_id = editor.refresh_selections(cx);
 479                selections = editor.local_selections::<usize>(cx);
 480            }
 481
 482            // If any selection has lost its position, move it to start of the next primary diagnostic.
 483            for selection in &mut selections {
 484                if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
 485                    let group_ix = match groups.binary_search_by(|probe| {
 486                        probe.excerpts.last().unwrap().cmp(&new_excerpt_id)
 487                    }) {
 488                        Ok(ix) | Err(ix) => ix,
 489                    };
 490                    if let Some(group) = groups.get(group_ix) {
 491                        let offset = excerpts_snapshot
 492                            .anchor_in_excerpt(
 493                                group.excerpts[group.primary_excerpt_ix].clone(),
 494                                group.primary_diagnostic.range.start.clone(),
 495                            )
 496                            .to_offset(&excerpts_snapshot);
 497                        selection.start = offset;
 498                        selection.end = offset;
 499                    }
 500                }
 501            }
 502            editor.update_selections(selections, None, cx);
 503            Some(())
 504        });
 505
 506        if self.path_states.is_empty() {
 507            if self.editor.is_focused(cx) {
 508                cx.focus_self();
 509            }
 510        } else {
 511            if cx.handle().is_focused(cx) {
 512                cx.focus(&self.editor);
 513            }
 514        }
 515        cx.notify();
 516    }
 517}
 518
 519impl workspace::Item for ProjectDiagnostics {
 520    type View = ProjectDiagnosticsEditor;
 521
 522    fn build_view(
 523        handle: ModelHandle<Self>,
 524        workspace: &Workspace,
 525        cx: &mut ViewContext<Self::View>,
 526    ) -> Self::View {
 527        ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), workspace.settings(), cx)
 528    }
 529
 530    fn project_path(&self) -> Option<project::ProjectPath> {
 531        None
 532    }
 533}
 534
 535impl workspace::ItemView for ProjectDiagnosticsEditor {
 536    type ItemHandle = ModelHandle<ProjectDiagnostics>;
 537
 538    fn item_handle(&self, _: &AppContext) -> Self::ItemHandle {
 539        self.model.clone()
 540    }
 541
 542    fn title(&self, _: &AppContext) -> String {
 543        "Project Diagnostics".to_string()
 544    }
 545
 546    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
 547        None
 548    }
 549
 550    fn is_dirty(&self, cx: &AppContext) -> bool {
 551        self.excerpts.read(cx).read(cx).is_dirty()
 552    }
 553
 554    fn has_conflict(&self, cx: &AppContext) -> bool {
 555        self.excerpts.read(cx).read(cx).has_conflict()
 556    }
 557
 558    fn can_save(&self, _: &AppContext) -> bool {
 559        true
 560    }
 561
 562    fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
 563        self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
 564    }
 565
 566    fn can_save_as(&self, _: &AppContext) -> bool {
 567        false
 568    }
 569
 570    fn save_as(
 571        &mut self,
 572        _: ModelHandle<project::Worktree>,
 573        _: &std::path::Path,
 574        _: &mut ViewContext<Self>,
 575    ) -> Task<Result<()>> {
 576        unreachable!()
 577    }
 578
 579    fn should_activate_item_on_event(event: &Self::Event) -> bool {
 580        Editor::should_activate_item_on_event(event)
 581    }
 582
 583    fn should_update_tab_on_event(event: &Event) -> bool {
 584        matches!(
 585            event,
 586            Event::Saved | Event::Dirtied | Event::FileHandleChanged
 587        )
 588    }
 589
 590    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
 591    where
 592        Self: Sized,
 593    {
 594        Some(ProjectDiagnosticsEditor::new(
 595            self.model.clone(),
 596            self.workspace.clone(),
 597            self.settings.clone(),
 598            cx,
 599        ))
 600    }
 601}
 602
 603fn path_header_renderer(buffer: ModelHandle<Buffer>, build_settings: BuildSettings) -> RenderBlock {
 604    Arc::new(move |cx| {
 605        let settings = build_settings(cx);
 606        let file_path = if let Some(file) = buffer.read(&**cx).file() {
 607            file.path().to_string_lossy().to_string()
 608        } else {
 609            "untitled".to_string()
 610        };
 611        let mut text_style = settings.style.text.clone();
 612        let style = settings.style.diagnostic_path_header;
 613        text_style.color = style.text;
 614        Label::new(file_path, text_style)
 615            .aligned()
 616            .left()
 617            .contained()
 618            .with_style(style.header)
 619            .with_padding_left(cx.line_number_x)
 620            .expanded()
 621            .named("path header block")
 622    })
 623}
 624
 625fn diagnostic_header_renderer(
 626    diagnostic: Diagnostic,
 627    is_valid: bool,
 628    build_settings: BuildSettings,
 629) -> RenderBlock {
 630    Arc::new(move |cx| {
 631        let settings = build_settings(cx);
 632        let mut text_style = settings.style.text.clone();
 633        let diagnostic_style = diagnostic_style(diagnostic.severity, is_valid, &settings.style);
 634        text_style.color = diagnostic_style.text;
 635        Text::new(diagnostic.message.clone(), text_style)
 636            .with_soft_wrap(false)
 637            .aligned()
 638            .left()
 639            .contained()
 640            .with_style(diagnostic_style.header)
 641            .with_padding_left(cx.line_number_x)
 642            .expanded()
 643            .named("diagnostic header")
 644    })
 645}
 646
 647fn context_header_renderer(build_settings: BuildSettings) -> RenderBlock {
 648    Arc::new(move |cx| {
 649        let settings = build_settings(cx);
 650        let text_style = settings.style.text.clone();
 651        Label::new("".to_string(), text_style)
 652            .contained()
 653            .with_padding_left(cx.line_number_x)
 654            .named("collapsed context")
 655    })
 656}
 657
 658fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 659    lhs: &DiagnosticEntry<L>,
 660    rhs: &DiagnosticEntry<R>,
 661    snapshot: &language::BufferSnapshot,
 662) -> Ordering {
 663    lhs.range
 664        .start
 665        .to_offset(&snapshot)
 666        .cmp(&rhs.range.start.to_offset(snapshot))
 667        .then_with(|| {
 668            lhs.range
 669                .end
 670                .to_offset(&snapshot)
 671                .cmp(&rhs.range.end.to_offset(snapshot))
 672        })
 673        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 674}
 675
 676#[cfg(test)]
 677mod tests {
 678    use super::*;
 679    use editor::{display_map::BlockContext, DisplayPoint, EditorSnapshot};
 680    use gpui::TestAppContext;
 681    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
 682    use project::worktree;
 683    use serde_json::json;
 684    use std::sync::Arc;
 685    use unindent::Unindent as _;
 686    use workspace::WorkspaceParams;
 687
 688    #[gpui::test]
 689    async fn test_diagnostics(mut cx: TestAppContext) {
 690        let params = cx.update(WorkspaceParams::test);
 691        let project = params.project.clone();
 692        let workspace = cx.add_view(0, |cx| Workspace::new(&params, cx));
 693
 694        params
 695            .fs
 696            .as_fake()
 697            .insert_tree(
 698                "/test",
 699                json!({
 700                    "consts.rs": "
 701                    const a: i32 = 'a';
 702                    const b: i32 = c;
 703                "
 704                    .unindent(),
 705
 706                    "main.rs": "
 707                    fn main() {
 708                        let x = vec![];
 709                        let y = vec![];
 710                        a(x);
 711                        b(y);
 712                        // comment 1
 713                        // comment 2
 714                        c(y);
 715                        d(x);
 716                    }
 717                "
 718                    .unindent(),
 719                }),
 720            )
 721            .await;
 722
 723        let worktree = project
 724            .update(&mut cx, |project, cx| {
 725                project.add_local_worktree("/test", cx)
 726            })
 727            .await
 728            .unwrap();
 729
 730        // Create some diagnostics
 731        worktree.update(&mut cx, |worktree, cx| {
 732            worktree
 733                .update_diagnostic_entries(
 734                    Arc::from("/test/main.rs".as_ref()),
 735                    None,
 736                    vec![
 737                        DiagnosticEntry {
 738                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
 739                            diagnostic: Diagnostic {
 740                                message:
 741                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 742                                        .to_string(),
 743                                severity: DiagnosticSeverity::INFORMATION,
 744                                is_primary: false,
 745                                is_disk_based: true,
 746                                group_id: 1,
 747                                ..Default::default()
 748                            },
 749                        },
 750                        DiagnosticEntry {
 751                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
 752                            diagnostic: Diagnostic {
 753                                message:
 754                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 755                                        .to_string(),
 756                                severity: DiagnosticSeverity::INFORMATION,
 757                                is_primary: false,
 758                                is_disk_based: true,
 759                                group_id: 0,
 760                                ..Default::default()
 761                            },
 762                        },
 763                        DiagnosticEntry {
 764                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
 765                            diagnostic: Diagnostic {
 766                                message: "value moved here".to_string(),
 767                                severity: DiagnosticSeverity::INFORMATION,
 768                                is_primary: false,
 769                                is_disk_based: true,
 770                                group_id: 1,
 771                                ..Default::default()
 772                            },
 773                        },
 774                        DiagnosticEntry {
 775                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
 776                            diagnostic: Diagnostic {
 777                                message: "value moved here".to_string(),
 778                                severity: DiagnosticSeverity::INFORMATION,
 779                                is_primary: false,
 780                                is_disk_based: true,
 781                                group_id: 0,
 782                                ..Default::default()
 783                            },
 784                        },
 785                        DiagnosticEntry {
 786                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
 787                            diagnostic: Diagnostic {
 788                                message: "use of moved value\nvalue used here after move".to_string(),
 789                                severity: DiagnosticSeverity::ERROR,
 790                                is_primary: true,
 791                                is_disk_based: true,
 792                                group_id: 0,
 793                                ..Default::default()
 794                            },
 795                        },
 796                        DiagnosticEntry {
 797                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
 798                            diagnostic: Diagnostic {
 799                                message: "use of moved value\nvalue used here after move".to_string(),
 800                                severity: DiagnosticSeverity::ERROR,
 801                                is_primary: true,
 802                                is_disk_based: true,
 803                                group_id: 1,
 804                                ..Default::default()
 805                            },
 806                        },
 807                    ],
 808                    cx,
 809                )
 810                .unwrap();
 811        });
 812
 813        // Open the project diagnostics view while there are already diagnostics.
 814        let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
 815        let view = cx.add_view(0, |cx| {
 816            ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
 817        });
 818
 819        view.next_notification(&cx).await;
 820        view.update(&mut cx, |view, cx| {
 821            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
 822
 823            assert_eq!(
 824                editor_blocks(&editor, cx),
 825                [
 826                    (0, "path header block".into()),
 827                    (2, "diagnostic header".into()),
 828                    (15, "diagnostic header".into()),
 829                    (24, "collapsed context".into()),
 830                ]
 831            );
 832            assert_eq!(
 833                editor.text(),
 834                concat!(
 835                    //
 836                    // main.rs
 837                    //
 838                    "\n", // filename
 839                    "\n", // padding
 840                    // diagnostic group 1
 841                    "\n", // primary message
 842                    "\n", // padding
 843                    "    let x = vec![];\n",
 844                    "    let y = vec![];\n",
 845                    "\n", // supporting diagnostic
 846                    "    a(x);\n",
 847                    "    b(y);\n",
 848                    "\n", // supporting diagnostic
 849                    "    // comment 1\n",
 850                    "    // comment 2\n",
 851                    "    c(y);\n",
 852                    "\n", // supporting diagnostic
 853                    "    d(x);\n",
 854                    // diagnostic group 2
 855                    "\n", // primary message
 856                    "\n", // padding
 857                    "fn main() {\n",
 858                    "    let x = vec![];\n",
 859                    "\n", // supporting diagnostic
 860                    "    let y = vec![];\n",
 861                    "    a(x);\n",
 862                    "\n", // supporting diagnostic
 863                    "    b(y);\n",
 864                    "\n", // context ellipsis
 865                    "    c(y);\n",
 866                    "    d(x);\n",
 867                    "\n", // supporting diagnostic
 868                    "}"
 869                )
 870            );
 871
 872            // Cursor is at the first diagnostic
 873            view.editor.update(cx, |editor, cx| {
 874                assert_eq!(
 875                    editor.selected_display_ranges(cx),
 876                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
 877                );
 878            });
 879        });
 880
 881        // Diagnostics are added for another earlier path.
 882        worktree.update(&mut cx, |worktree, cx| {
 883            worktree
 884                .update_diagnostic_entries(
 885                    Arc::from("/test/consts.rs".as_ref()),
 886                    None,
 887                    vec![DiagnosticEntry {
 888                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
 889                        diagnostic: Diagnostic {
 890                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
 891                            severity: DiagnosticSeverity::ERROR,
 892                            is_primary: true,
 893                            is_disk_based: true,
 894                            group_id: 0,
 895                            ..Default::default()
 896                        },
 897                    }],
 898                    cx,
 899                )
 900                .unwrap();
 901            cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated);
 902        });
 903
 904        view.next_notification(&cx).await;
 905        view.update(&mut cx, |view, cx| {
 906            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
 907
 908            assert_eq!(
 909                editor_blocks(&editor, cx),
 910                [
 911                    (0, "path header block".into()),
 912                    (2, "diagnostic header".into()),
 913                    (7, "path header block".into()),
 914                    (9, "diagnostic header".into()),
 915                    (22, "diagnostic header".into()),
 916                    (31, "collapsed context".into()),
 917                ]
 918            );
 919            assert_eq!(
 920                editor.text(),
 921                concat!(
 922                    //
 923                    // consts.rs
 924                    //
 925                    "\n", // filename
 926                    "\n", // padding
 927                    // diagnostic group 1
 928                    "\n", // primary message
 929                    "\n", // padding
 930                    "const a: i32 = 'a';\n",
 931                    "\n", // supporting diagnostic
 932                    "const b: i32 = c;\n",
 933                    //
 934                    // main.rs
 935                    //
 936                    "\n", // filename
 937                    "\n", // padding
 938                    // diagnostic group 1
 939                    "\n", // primary message
 940                    "\n", // padding
 941                    "    let x = vec![];\n",
 942                    "    let y = vec![];\n",
 943                    "\n", // supporting diagnostic
 944                    "    a(x);\n",
 945                    "    b(y);\n",
 946                    "\n", // supporting diagnostic
 947                    "    // comment 1\n",
 948                    "    // comment 2\n",
 949                    "    c(y);\n",
 950                    "\n", // supporting diagnostic
 951                    "    d(x);\n",
 952                    // diagnostic group 2
 953                    "\n", // primary message
 954                    "\n", // filename
 955                    "fn main() {\n",
 956                    "    let x = vec![];\n",
 957                    "\n", // supporting diagnostic
 958                    "    let y = vec![];\n",
 959                    "    a(x);\n",
 960                    "\n", // supporting diagnostic
 961                    "    b(y);\n",
 962                    "\n", // context ellipsis
 963                    "    c(y);\n",
 964                    "    d(x);\n",
 965                    "\n", // supporting diagnostic
 966                    "}"
 967                )
 968            );
 969
 970            // Cursor keeps its position.
 971            view.editor.update(cx, |editor, cx| {
 972                assert_eq!(
 973                    editor.selected_display_ranges(cx),
 974                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
 975                );
 976            });
 977        });
 978
 979        // Diagnostics are added to the first path
 980        worktree.update(&mut cx, |worktree, cx| {
 981            worktree
 982                .update_diagnostic_entries(
 983                    Arc::from("/test/consts.rs".as_ref()),
 984                    None,
 985                    vec![
 986                        DiagnosticEntry {
 987                            range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
 988                            diagnostic: Diagnostic {
 989                                message: "mismatched types\nexpected `usize`, found `char`"
 990                                    .to_string(),
 991                                severity: DiagnosticSeverity::ERROR,
 992                                is_primary: true,
 993                                is_disk_based: true,
 994                                group_id: 0,
 995                                ..Default::default()
 996                            },
 997                        },
 998                        DiagnosticEntry {
 999                            range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1000                            diagnostic: Diagnostic {
1001                                message: "unresolved name `c`".to_string(),
1002                                severity: DiagnosticSeverity::ERROR,
1003                                is_primary: true,
1004                                is_disk_based: true,
1005                                group_id: 1,
1006                                ..Default::default()
1007                            },
1008                        },
1009                    ],
1010                    cx,
1011                )
1012                .unwrap();
1013            cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated);
1014        });
1015
1016        view.next_notification(&cx).await;
1017        view.update(&mut cx, |view, cx| {
1018            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1019
1020            assert_eq!(
1021                editor_blocks(&editor, cx),
1022                [
1023                    (0, "path header block".into()),
1024                    (2, "diagnostic header".into()),
1025                    (7, "diagnostic header".into()),
1026                    (12, "path header block".into()),
1027                    (14, "diagnostic header".into()),
1028                    (27, "diagnostic header".into()),
1029                    (36, "collapsed context".into()),
1030                ]
1031            );
1032            assert_eq!(
1033                editor.text(),
1034                concat!(
1035                    //
1036                    // consts.rs
1037                    //
1038                    "\n", // filename
1039                    "\n", // padding
1040                    // diagnostic group 1
1041                    "\n", // primary message
1042                    "\n", // padding
1043                    "const a: i32 = 'a';\n",
1044                    "\n", // supporting diagnostic
1045                    "const b: i32 = c;\n",
1046                    // diagnostic group 2
1047                    "\n", // primary message
1048                    "\n", // padding
1049                    "const a: i32 = 'a';\n",
1050                    "const b: i32 = c;\n",
1051                    "\n", // supporting diagnostic
1052                    //
1053                    // main.rs
1054                    //
1055                    "\n", // filename
1056                    "\n", // padding
1057                    // diagnostic group 1
1058                    "\n", // primary message
1059                    "\n", // padding
1060                    "    let x = vec![];\n",
1061                    "    let y = vec![];\n",
1062                    "\n", // supporting diagnostic
1063                    "    a(x);\n",
1064                    "    b(y);\n",
1065                    "\n", // supporting diagnostic
1066                    "    // comment 1\n",
1067                    "    // comment 2\n",
1068                    "    c(y);\n",
1069                    "\n", // supporting diagnostic
1070                    "    d(x);\n",
1071                    // diagnostic group 2
1072                    "\n", // primary message
1073                    "\n", // filename
1074                    "fn main() {\n",
1075                    "    let x = vec![];\n",
1076                    "\n", // supporting diagnostic
1077                    "    let y = vec![];\n",
1078                    "    a(x);\n",
1079                    "\n", // supporting diagnostic
1080                    "    b(y);\n",
1081                    "\n", // context ellipsis
1082                    "    c(y);\n",
1083                    "    d(x);\n",
1084                    "\n", // supporting diagnostic
1085                    "}"
1086                )
1087            );
1088        });
1089    }
1090
1091    fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1092        editor
1093            .blocks_in_range(0..editor.max_point().row())
1094            .filter_map(|(row, block)| {
1095                block
1096                    .render(&BlockContext {
1097                        cx,
1098                        anchor_x: 0.,
1099                        line_number_x: 0.,
1100                    })
1101                    .name()
1102                    .map(|s| (row, s.to_string()))
1103            })
1104            .collect()
1105    }
1106}