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