diagnostics.rs

   1pub mod items;
   2
   3use anyhow::Result;
   4use collections::{BTreeSet, HashSet};
   5use editor::{
   6    diagnostic_block_renderer,
   7    display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock},
   8    highlight_diagnostic_message, Editor, ExcerptId, MultiBuffer, ToOffset,
   9};
  10use gpui::{
  11    actions, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity,
  12    ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
  13    WeakViewHandle,
  14};
  15use language::{
  16    Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
  17};
  18use project::{DiagnosticSummary, Project, ProjectPath};
  19use settings::Settings;
  20use std::{
  21    any::{Any, TypeId},
  22    cmp::Ordering,
  23    mem,
  24    ops::Range,
  25    path::PathBuf,
  26    sync::Arc,
  27};
  28use util::TryFutureExt;
  29use workspace::{ItemHandle as _, ItemNavHistory, Workspace};
  30
  31actions!(diagnostics, [Deploy]);
  32
  33const CONTEXT_LINE_COUNT: u32 = 1;
  34
  35pub fn init(cx: &mut MutableAppContext) {
  36    cx.add_bindings([Binding::new("alt-shift-D", Deploy, Some("Workspace"))]);
  37    cx.add_action(ProjectDiagnosticsEditor::deploy);
  38}
  39
  40type Event = editor::Event;
  41
  42struct ProjectDiagnosticsEditor {
  43    project: ModelHandle<Project>,
  44    workspace: WeakViewHandle<Workspace>,
  45    editor: ViewHandle<Editor>,
  46    summary: DiagnosticSummary,
  47    excerpts: ModelHandle<MultiBuffer>,
  48    path_states: Vec<PathState>,
  49    paths_to_update: BTreeSet<ProjectPath>,
  50}
  51
  52struct PathState {
  53    path: ProjectPath,
  54    diagnostic_groups: Vec<DiagnosticGroupState>,
  55}
  56
  57struct DiagnosticGroupState {
  58    primary_diagnostic: DiagnosticEntry<language::Anchor>,
  59    primary_excerpt_ix: usize,
  60    excerpts: Vec<ExcerptId>,
  61    blocks: HashSet<BlockId>,
  62    block_count: usize,
  63}
  64
  65impl Entity for ProjectDiagnosticsEditor {
  66    type Event = Event;
  67}
  68
  69impl View for ProjectDiagnosticsEditor {
  70    fn ui_name() -> &'static str {
  71        "ProjectDiagnosticsEditor"
  72    }
  73
  74    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
  75        if self.path_states.is_empty() {
  76            let theme = &cx.global::<Settings>().theme.project_diagnostics;
  77            Label::new(
  78                "No problems in workspace".to_string(),
  79                theme.empty_message.clone(),
  80            )
  81            .aligned()
  82            .contained()
  83            .with_style(theme.container)
  84            .boxed()
  85        } else {
  86            ChildView::new(&self.editor).boxed()
  87        }
  88    }
  89
  90    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
  91        if !self.path_states.is_empty() {
  92            cx.focus(&self.editor);
  93        }
  94    }
  95}
  96
  97impl ProjectDiagnosticsEditor {
  98    fn new(
  99        project_handle: ModelHandle<Project>,
 100        workspace: WeakViewHandle<Workspace>,
 101        cx: &mut ViewContext<Self>,
 102    ) -> Self {
 103        cx.subscribe(&project_handle, |this, _, event, cx| match event {
 104            project::Event::DiskBasedDiagnosticsFinished => {
 105                this.update_excerpts(cx);
 106                this.update_title(cx);
 107            }
 108            project::Event::DiagnosticsUpdated(path) => {
 109                this.paths_to_update.insert(path.clone());
 110            }
 111            _ => {}
 112        })
 113        .detach();
 114
 115        let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
 116        let editor = cx.add_view(|cx| {
 117            let mut editor =
 118                Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
 119            editor.set_vertical_scroll_margin(5, cx);
 120            editor
 121        });
 122        cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
 123            .detach();
 124
 125        let project = project_handle.read(cx);
 126        let paths_to_update = project.diagnostic_summaries(cx).map(|e| e.0).collect();
 127        let summary = project.diagnostic_summary(cx);
 128        let mut this = Self {
 129            project: project_handle,
 130            summary,
 131            workspace,
 132            excerpts,
 133            editor,
 134            path_states: Default::default(),
 135            paths_to_update,
 136        };
 137        this.update_excerpts(cx);
 138        this
 139    }
 140
 141    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
 142        if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
 143            workspace.activate_item(&existing, cx);
 144        } else {
 145            let workspace_handle = cx.weak_handle();
 146            let diagnostics = cx.add_view(|cx| {
 147                ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
 148            });
 149            workspace.add_item(Box::new(diagnostics), cx);
 150        }
 151    }
 152
 153    fn update_excerpts(&mut self, cx: &mut ViewContext<Self>) {
 154        let paths = mem::take(&mut self.paths_to_update);
 155        let project = self.project.clone();
 156        cx.spawn(|this, mut cx| {
 157            async move {
 158                for path in paths {
 159                    let buffer = project
 160                        .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
 161                        .await?;
 162                    this.update(&mut cx, |view, cx| view.populate_excerpts(path, buffer, cx))
 163                }
 164                Result::<_, anyhow::Error>::Ok(())
 165            }
 166            .log_err()
 167        })
 168        .detach();
 169    }
 170
 171    fn populate_excerpts(
 172        &mut self,
 173        path: ProjectPath,
 174        buffer: ModelHandle<Buffer>,
 175        cx: &mut ViewContext<Self>,
 176    ) {
 177        let was_empty = self.path_states.is_empty();
 178        let snapshot = buffer.read(cx).snapshot();
 179        let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
 180            Ok(ix) => ix,
 181            Err(ix) => {
 182                self.path_states.insert(
 183                    ix,
 184                    PathState {
 185                        path: path.clone(),
 186                        diagnostic_groups: Default::default(),
 187                    },
 188                );
 189                ix
 190            }
 191        };
 192
 193        let mut prev_excerpt_id = if path_ix > 0 {
 194            let prev_path_last_group = &self.path_states[path_ix - 1]
 195                .diagnostic_groups
 196                .last()
 197                .unwrap();
 198            prev_path_last_group.excerpts.last().unwrap().clone()
 199        } else {
 200            ExcerptId::min()
 201        };
 202
 203        let path_state = &mut self.path_states[path_ix];
 204        let mut groups_to_add = Vec::new();
 205        let mut group_ixs_to_remove = Vec::new();
 206        let mut blocks_to_add = Vec::new();
 207        let mut blocks_to_remove = HashSet::default();
 208        let mut first_excerpt_id = None;
 209        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
 210            let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
 211            let mut new_groups = snapshot
 212                .diagnostic_groups()
 213                .into_iter()
 214                .filter(|group| {
 215                    group.entries[group.primary_ix].diagnostic.severity
 216                        <= DiagnosticSeverity::WARNING
 217                })
 218                .peekable();
 219            loop {
 220                let mut to_insert = None;
 221                let mut to_remove = None;
 222                let mut to_keep = None;
 223                match (old_groups.peek(), new_groups.peek()) {
 224                    (None, None) => break,
 225                    (None, Some(_)) => to_insert = new_groups.next(),
 226                    (Some(_), None) => to_remove = old_groups.next(),
 227                    (Some((_, old_group)), Some(new_group)) => {
 228                        let old_primary = &old_group.primary_diagnostic;
 229                        let new_primary = &new_group.entries[new_group.primary_ix];
 230                        match compare_diagnostics(old_primary, new_primary, &snapshot) {
 231                            Ordering::Less => to_remove = old_groups.next(),
 232                            Ordering::Equal => {
 233                                to_keep = old_groups.next();
 234                                new_groups.next();
 235                            }
 236                            Ordering::Greater => to_insert = new_groups.next(),
 237                        }
 238                    }
 239                }
 240
 241                if let Some(group) = to_insert {
 242                    let mut group_state = DiagnosticGroupState {
 243                        primary_diagnostic: group.entries[group.primary_ix].clone(),
 244                        primary_excerpt_ix: 0,
 245                        excerpts: Default::default(),
 246                        blocks: Default::default(),
 247                        block_count: 0,
 248                    };
 249                    let mut pending_range: Option<(Range<Point>, usize)> = None;
 250                    let mut is_first_excerpt_for_group = true;
 251                    for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
 252                        let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
 253                        if let Some((range, start_ix)) = &mut pending_range {
 254                            if let Some(entry) = resolved_entry.as_ref() {
 255                                if entry.range.start.row
 256                                    <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
 257                                {
 258                                    range.end = range.end.max(entry.range.end);
 259                                    continue;
 260                                }
 261                            }
 262
 263                            let excerpt_start =
 264                                Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
 265                            let excerpt_end = snapshot.clip_point(
 266                                Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
 267                                Bias::Left,
 268                            );
 269                            let excerpt_id = excerpts
 270                                .insert_excerpts_after(
 271                                    &prev_excerpt_id,
 272                                    buffer.clone(),
 273                                    [excerpt_start..excerpt_end],
 274                                    excerpts_cx,
 275                                )
 276                                .pop()
 277                                .unwrap();
 278
 279                            prev_excerpt_id = excerpt_id.clone();
 280                            first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
 281                            group_state.excerpts.push(excerpt_id.clone());
 282                            let header_position = (excerpt_id.clone(), language::Anchor::MIN);
 283
 284                            if is_first_excerpt_for_group {
 285                                is_first_excerpt_for_group = false;
 286                                let mut primary =
 287                                    group.entries[group.primary_ix].diagnostic.clone();
 288                                primary.message =
 289                                    primary.message.split('\n').next().unwrap().to_string();
 290                                group_state.block_count += 1;
 291                                blocks_to_add.push(BlockProperties {
 292                                    position: header_position,
 293                                    height: 2,
 294                                    render: diagnostic_header_renderer(primary),
 295                                    disposition: BlockDisposition::Above,
 296                                });
 297                            }
 298
 299                            for entry in &group.entries[*start_ix..ix] {
 300                                let mut diagnostic = entry.diagnostic.clone();
 301                                if diagnostic.is_primary {
 302                                    group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
 303                                    diagnostic.message =
 304                                        entry.diagnostic.message.split('\n').skip(1).collect();
 305                                }
 306
 307                                if !diagnostic.message.is_empty() {
 308                                    group_state.block_count += 1;
 309                                    blocks_to_add.push(BlockProperties {
 310                                        position: (excerpt_id.clone(), entry.range.start.clone()),
 311                                        height: diagnostic.message.matches('\n').count() as u8 + 1,
 312                                        render: diagnostic_block_renderer(diagnostic, true),
 313                                        disposition: BlockDisposition::Below,
 314                                    });
 315                                }
 316                            }
 317
 318                            pending_range.take();
 319                        }
 320
 321                        if let Some(entry) = resolved_entry {
 322                            pending_range = Some((entry.range.clone(), ix));
 323                        }
 324                    }
 325
 326                    groups_to_add.push(group_state);
 327                } else if let Some((group_ix, group_state)) = to_remove {
 328                    excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
 329                    group_ixs_to_remove.push(group_ix);
 330                    blocks_to_remove.extend(group_state.blocks.iter().copied());
 331                } else if let Some((_, group)) = to_keep {
 332                    prev_excerpt_id = group.excerpts.last().unwrap().clone();
 333                    first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
 334                }
 335            }
 336
 337            excerpts.snapshot(excerpts_cx)
 338        });
 339
 340        self.editor.update(cx, |editor, cx| {
 341            editor.remove_blocks(blocks_to_remove, cx);
 342            let block_ids = editor.insert_blocks(
 343                blocks_to_add.into_iter().map(|block| {
 344                    let (excerpt_id, text_anchor) = block.position;
 345                    BlockProperties {
 346                        position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
 347                        height: block.height,
 348                        render: block.render,
 349                        disposition: block.disposition,
 350                    }
 351                }),
 352                cx,
 353            );
 354
 355            let mut block_ids = block_ids.into_iter();
 356            for group_state in &mut groups_to_add {
 357                group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
 358            }
 359        });
 360
 361        for ix in group_ixs_to_remove.into_iter().rev() {
 362            path_state.diagnostic_groups.remove(ix);
 363        }
 364        path_state.diagnostic_groups.extend(groups_to_add);
 365        path_state.diagnostic_groups.sort_unstable_by(|a, b| {
 366            let range_a = &a.primary_diagnostic.range;
 367            let range_b = &b.primary_diagnostic.range;
 368            range_a
 369                .start
 370                .cmp(&range_b.start, &snapshot)
 371                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
 372        });
 373
 374        if path_state.diagnostic_groups.is_empty() {
 375            self.path_states.remove(path_ix);
 376        }
 377
 378        self.editor.update(cx, |editor, cx| {
 379            let groups;
 380            let mut selections;
 381            let new_excerpt_ids_by_selection_id;
 382            if was_empty {
 383                groups = self.path_states.first()?.diagnostic_groups.as_slice();
 384                new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
 385                selections = vec![Selection {
 386                    id: 0,
 387                    start: 0,
 388                    end: 0,
 389                    reversed: false,
 390                    goal: SelectionGoal::None,
 391                }];
 392            } else {
 393                groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
 394                new_excerpt_ids_by_selection_id = editor.refresh_selections(cx);
 395                selections = editor.local_selections::<usize>(cx);
 396            }
 397
 398            // If any selection has lost its position, move it to start of the next primary diagnostic.
 399            for selection in &mut selections {
 400                if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
 401                    let group_ix = match groups.binary_search_by(|probe| {
 402                        probe.excerpts.last().unwrap().cmp(&new_excerpt_id)
 403                    }) {
 404                        Ok(ix) | Err(ix) => ix,
 405                    };
 406                    if let Some(group) = groups.get(group_ix) {
 407                        let offset = excerpts_snapshot
 408                            .anchor_in_excerpt(
 409                                group.excerpts[group.primary_excerpt_ix].clone(),
 410                                group.primary_diagnostic.range.start.clone(),
 411                            )
 412                            .to_offset(&excerpts_snapshot);
 413                        selection.start = offset;
 414                        selection.end = offset;
 415                    }
 416                }
 417            }
 418            editor.update_selections(selections, None, cx);
 419            Some(())
 420        });
 421
 422        if self.path_states.is_empty() {
 423            if self.editor.is_focused(cx) {
 424                cx.focus_self();
 425            }
 426        } else {
 427            if cx.handle().is_focused(cx) {
 428                cx.focus(&self.editor);
 429            }
 430        }
 431        cx.notify();
 432    }
 433
 434    fn update_title(&mut self, cx: &mut ViewContext<Self>) {
 435        self.summary = self.project.read(cx).diagnostic_summary(cx);
 436        cx.emit(Event::TitleChanged);
 437    }
 438}
 439
 440impl workspace::Item for ProjectDiagnosticsEditor {
 441    fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
 442        render_summary(
 443            &self.summary,
 444            &style.label.text,
 445            &cx.global::<Settings>().theme.project_diagnostics,
 446        )
 447    }
 448
 449    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
 450        None
 451    }
 452
 453    fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
 454        None
 455    }
 456
 457    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
 458        self.editor
 459            .update(cx, |editor, cx| editor.navigate(data, cx))
 460    }
 461
 462    fn is_dirty(&self, cx: &AppContext) -> bool {
 463        self.excerpts.read(cx).read(cx).is_dirty()
 464    }
 465
 466    fn has_conflict(&self, cx: &AppContext) -> bool {
 467        self.excerpts.read(cx).read(cx).has_conflict()
 468    }
 469
 470    fn can_save(&self, _: &AppContext) -> bool {
 471        true
 472    }
 473
 474    fn save(
 475        &mut self,
 476        project: ModelHandle<Project>,
 477        cx: &mut ViewContext<Self>,
 478    ) -> Task<Result<()>> {
 479        self.editor.save(project, cx)
 480    }
 481
 482    fn reload(
 483        &mut self,
 484        project: ModelHandle<Project>,
 485        cx: &mut ViewContext<Self>,
 486    ) -> Task<Result<()>> {
 487        self.editor.reload(project, cx)
 488    }
 489
 490    fn can_save_as(&self, _: &AppContext) -> bool {
 491        false
 492    }
 493
 494    fn save_as(
 495        &mut self,
 496        _: ModelHandle<Project>,
 497        _: PathBuf,
 498        _: &mut ViewContext<Self>,
 499    ) -> Task<Result<()>> {
 500        unreachable!()
 501    }
 502
 503    fn should_activate_item_on_event(event: &Self::Event) -> bool {
 504        Editor::should_activate_item_on_event(event)
 505    }
 506
 507    fn should_update_tab_on_event(event: &Event) -> bool {
 508        matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
 509    }
 510
 511    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
 512        self.editor.update(cx, |editor, _| {
 513            editor.set_nav_history(Some(nav_history));
 514        });
 515    }
 516
 517    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
 518    where
 519        Self: Sized,
 520    {
 521        Some(ProjectDiagnosticsEditor::new(
 522            self.project.clone(),
 523            self.workspace.clone(),
 524            cx,
 525        ))
 526    }
 527
 528    fn act_as_type(
 529        &self,
 530        type_id: TypeId,
 531        self_handle: &ViewHandle<Self>,
 532        _: &AppContext,
 533    ) -> Option<AnyViewHandle> {
 534        if type_id == TypeId::of::<Self>() {
 535            Some(self_handle.into())
 536        } else if type_id == TypeId::of::<Editor>() {
 537            Some((&self.editor).into())
 538        } else {
 539            None
 540        }
 541    }
 542
 543    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 544        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
 545    }
 546}
 547
 548fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
 549    let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
 550    Arc::new(move |cx| {
 551        let settings = cx.global::<Settings>();
 552        let theme = &settings.theme.editor;
 553        let style = &theme.diagnostic_header;
 554        let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
 555        let icon_width = cx.em_width * style.icon_width_factor;
 556        let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
 557            Svg::new("icons/diagnostic-error-10.svg")
 558                .with_color(theme.error_diagnostic.message.text.color)
 559        } else {
 560            Svg::new("icons/diagnostic-warning-10.svg")
 561                .with_color(theme.warning_diagnostic.message.text.color)
 562        };
 563
 564        Flex::row()
 565            .with_child(
 566                icon.constrained()
 567                    .with_width(icon_width)
 568                    .aligned()
 569                    .contained()
 570                    .boxed(),
 571            )
 572            .with_child(
 573                Label::new(
 574                    message.clone(),
 575                    style.message.label.clone().with_font_size(font_size),
 576                )
 577                .with_highlights(highlights.clone())
 578                .contained()
 579                .with_style(style.message.container)
 580                .with_margin_left(cx.gutter_padding)
 581                .aligned()
 582                .boxed(),
 583            )
 584            .with_children(diagnostic.code.clone().map(|code| {
 585                Label::new(code, style.code.text.clone().with_font_size(font_size))
 586                    .contained()
 587                    .with_style(style.code.container)
 588                    .aligned()
 589                    .boxed()
 590            }))
 591            .contained()
 592            .with_style(style.container)
 593            .with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width)
 594            .expanded()
 595            .named("diagnostic header")
 596    })
 597}
 598
 599pub(crate) fn render_summary(
 600    summary: &DiagnosticSummary,
 601    text_style: &TextStyle,
 602    theme: &theme::ProjectDiagnostics,
 603) -> ElementBox {
 604    if summary.error_count == 0 && summary.warning_count == 0 {
 605        Label::new("No problems".to_string(), text_style.clone()).boxed()
 606    } else {
 607        let icon_width = theme.tab_icon_width;
 608        let icon_spacing = theme.tab_icon_spacing;
 609        let summary_spacing = theme.tab_summary_spacing;
 610        Flex::row()
 611            .with_children([
 612                Svg::new("icons/diagnostic-summary-error.svg")
 613                    .with_color(text_style.color)
 614                    .constrained()
 615                    .with_width(icon_width)
 616                    .aligned()
 617                    .contained()
 618                    .with_margin_right(icon_spacing)
 619                    .named("no-icon"),
 620                Label::new(
 621                    summary.error_count.to_string(),
 622                    LabelStyle {
 623                        text: text_style.clone(),
 624                        highlight_text: None,
 625                    },
 626                )
 627                .aligned()
 628                .boxed(),
 629                Svg::new("icons/diagnostic-summary-warning.svg")
 630                    .with_color(text_style.color)
 631                    .constrained()
 632                    .with_width(icon_width)
 633                    .aligned()
 634                    .contained()
 635                    .with_margin_left(summary_spacing)
 636                    .with_margin_right(icon_spacing)
 637                    .named("warn-icon"),
 638                Label::new(
 639                    summary.warning_count.to_string(),
 640                    LabelStyle {
 641                        text: text_style.clone(),
 642                        highlight_text: None,
 643                    },
 644                )
 645                .aligned()
 646                .boxed(),
 647            ])
 648            .boxed()
 649    }
 650}
 651
 652fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 653    lhs: &DiagnosticEntry<L>,
 654    rhs: &DiagnosticEntry<R>,
 655    snapshot: &language::BufferSnapshot,
 656) -> Ordering {
 657    lhs.range
 658        .start
 659        .to_offset(&snapshot)
 660        .cmp(&rhs.range.start.to_offset(snapshot))
 661        .then_with(|| {
 662            lhs.range
 663                .end
 664                .to_offset(&snapshot)
 665                .cmp(&rhs.range.end.to_offset(snapshot))
 666        })
 667        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 668}
 669
 670#[cfg(test)]
 671mod tests {
 672    use super::*;
 673    use editor::{
 674        display_map::{BlockContext, TransformBlock},
 675        DisplayPoint, EditorSnapshot,
 676    };
 677    use gpui::TestAppContext;
 678    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
 679    use serde_json::json;
 680    use unindent::Unindent as _;
 681    use workspace::WorkspaceParams;
 682
 683    #[gpui::test]
 684    async fn test_diagnostics(cx: &mut TestAppContext) {
 685        let params = cx.update(WorkspaceParams::test);
 686        let project = params.project.clone();
 687        let workspace = cx.add_view(0, |cx| Workspace::new(&params, cx));
 688
 689        params
 690            .fs
 691            .as_fake()
 692            .insert_tree(
 693                "/test",
 694                json!({
 695                    "consts.rs": "
 696                    const a: i32 = 'a';
 697                    const b: i32 = c;
 698                "
 699                    .unindent(),
 700
 701                    "main.rs": "
 702                    fn main() {
 703                        let x = vec![];
 704                        let y = vec![];
 705                        a(x);
 706                        b(y);
 707                        // comment 1
 708                        // comment 2
 709                        c(y);
 710                        d(x);
 711                    }
 712                "
 713                    .unindent(),
 714                }),
 715            )
 716            .await;
 717
 718        project
 719            .update(cx, |project, cx| {
 720                project.find_or_create_local_worktree("/test", true, cx)
 721            })
 722            .await
 723            .unwrap();
 724
 725        // Create some diagnostics
 726        project.update(cx, |project, cx| {
 727            project
 728                .update_diagnostic_entries(
 729                    PathBuf::from("/test/main.rs"),
 730                    None,
 731                    vec![
 732                        DiagnosticEntry {
 733                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
 734                            diagnostic: Diagnostic {
 735                                message:
 736                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 737                                        .to_string(),
 738                                severity: DiagnosticSeverity::INFORMATION,
 739                                is_primary: false,
 740                                is_disk_based: true,
 741                                group_id: 1,
 742                                ..Default::default()
 743                            },
 744                        },
 745                        DiagnosticEntry {
 746                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
 747                            diagnostic: Diagnostic {
 748                                message:
 749                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 750                                        .to_string(),
 751                                severity: DiagnosticSeverity::INFORMATION,
 752                                is_primary: false,
 753                                is_disk_based: true,
 754                                group_id: 0,
 755                                ..Default::default()
 756                            },
 757                        },
 758                        DiagnosticEntry {
 759                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
 760                            diagnostic: Diagnostic {
 761                                message: "value moved here".to_string(),
 762                                severity: DiagnosticSeverity::INFORMATION,
 763                                is_primary: false,
 764                                is_disk_based: true,
 765                                group_id: 1,
 766                                ..Default::default()
 767                            },
 768                        },
 769                        DiagnosticEntry {
 770                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
 771                            diagnostic: Diagnostic {
 772                                message: "value moved here".to_string(),
 773                                severity: DiagnosticSeverity::INFORMATION,
 774                                is_primary: false,
 775                                is_disk_based: true,
 776                                group_id: 0,
 777                                ..Default::default()
 778                            },
 779                        },
 780                        DiagnosticEntry {
 781                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
 782                            diagnostic: Diagnostic {
 783                                message: "use of moved value\nvalue used here after move".to_string(),
 784                                severity: DiagnosticSeverity::ERROR,
 785                                is_primary: true,
 786                                is_disk_based: true,
 787                                group_id: 0,
 788                                ..Default::default()
 789                            },
 790                        },
 791                        DiagnosticEntry {
 792                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
 793                            diagnostic: Diagnostic {
 794                                message: "use of moved value\nvalue used here after move".to_string(),
 795                                severity: DiagnosticSeverity::ERROR,
 796                                is_primary: true,
 797                                is_disk_based: true,
 798                                group_id: 1,
 799                                ..Default::default()
 800                            },
 801                        },
 802                    ],
 803                    cx,
 804                )
 805                .unwrap();
 806        });
 807
 808        // Open the project diagnostics view while there are already diagnostics.
 809        let view = cx.add_view(0, |cx| {
 810            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
 811        });
 812
 813        view.next_notification(&cx).await;
 814        view.update(cx, |view, cx| {
 815            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
 816
 817            assert_eq!(
 818                editor_blocks(&editor, cx),
 819                [
 820                    (0, "path header block".into()),
 821                    (2, "diagnostic header".into()),
 822                    (15, "collapsed context".into()),
 823                    (16, "diagnostic header".into()),
 824                    (25, "collapsed context".into()),
 825                ]
 826            );
 827            assert_eq!(
 828                editor.text(),
 829                concat!(
 830                    //
 831                    // main.rs
 832                    //
 833                    "\n", // filename
 834                    "\n", // padding
 835                    // diagnostic group 1
 836                    "\n", // primary message
 837                    "\n", // padding
 838                    "    let x = vec![];\n",
 839                    "    let y = vec![];\n",
 840                    "\n", // supporting diagnostic
 841                    "    a(x);\n",
 842                    "    b(y);\n",
 843                    "\n", // supporting diagnostic
 844                    "    // comment 1\n",
 845                    "    // comment 2\n",
 846                    "    c(y);\n",
 847                    "\n", // supporting diagnostic
 848                    "    d(x);\n",
 849                    "\n", // context ellipsis
 850                    // diagnostic group 2
 851                    "\n", // primary message
 852                    "\n", // padding
 853                    "fn main() {\n",
 854                    "    let x = vec![];\n",
 855                    "\n", // supporting diagnostic
 856                    "    let y = vec![];\n",
 857                    "    a(x);\n",
 858                    "\n", // supporting diagnostic
 859                    "    b(y);\n",
 860                    "\n", // context ellipsis
 861                    "    c(y);\n",
 862                    "    d(x);\n",
 863                    "\n", // supporting diagnostic
 864                    "}"
 865                )
 866            );
 867
 868            // Cursor is at the first diagnostic
 869            view.editor.update(cx, |editor, cx| {
 870                assert_eq!(
 871                    editor.selected_display_ranges(cx),
 872                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
 873                );
 874            });
 875        });
 876
 877        // Diagnostics are added for another earlier path.
 878        project.update(cx, |project, cx| {
 879            project.disk_based_diagnostics_started(cx);
 880            project
 881                .update_diagnostic_entries(
 882                    PathBuf::from("/test/consts.rs"),
 883                    None,
 884                    vec![DiagnosticEntry {
 885                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
 886                        diagnostic: Diagnostic {
 887                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
 888                            severity: DiagnosticSeverity::ERROR,
 889                            is_primary: true,
 890                            is_disk_based: true,
 891                            group_id: 0,
 892                            ..Default::default()
 893                        },
 894                    }],
 895                    cx,
 896                )
 897                .unwrap();
 898            project.disk_based_diagnostics_finished(cx);
 899        });
 900
 901        view.next_notification(&cx).await;
 902        view.update(cx, |view, cx| {
 903            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
 904
 905            assert_eq!(
 906                editor_blocks(&editor, cx),
 907                [
 908                    (0, "path header block".into()),
 909                    (2, "diagnostic header".into()),
 910                    (7, "path header block".into()),
 911                    (9, "diagnostic header".into()),
 912                    (22, "collapsed context".into()),
 913                    (23, "diagnostic header".into()),
 914                    (32, "collapsed context".into()),
 915                ]
 916            );
 917            assert_eq!(
 918                editor.text(),
 919                concat!(
 920                    //
 921                    // consts.rs
 922                    //
 923                    "\n", // filename
 924                    "\n", // padding
 925                    // diagnostic group 1
 926                    "\n", // primary message
 927                    "\n", // padding
 928                    "const a: i32 = 'a';\n",
 929                    "\n", // supporting diagnostic
 930                    "const b: i32 = c;\n",
 931                    //
 932                    // main.rs
 933                    //
 934                    "\n", // filename
 935                    "\n", // padding
 936                    // diagnostic group 1
 937                    "\n", // primary message
 938                    "\n", // padding
 939                    "    let x = vec![];\n",
 940                    "    let y = vec![];\n",
 941                    "\n", // supporting diagnostic
 942                    "    a(x);\n",
 943                    "    b(y);\n",
 944                    "\n", // supporting diagnostic
 945                    "    // comment 1\n",
 946                    "    // comment 2\n",
 947                    "    c(y);\n",
 948                    "\n", // supporting diagnostic
 949                    "    d(x);\n",
 950                    "\n", // collapsed context
 951                    // diagnostic group 2
 952                    "\n", // primary message
 953                    "\n", // filename
 954                    "fn main() {\n",
 955                    "    let x = vec![];\n",
 956                    "\n", // supporting diagnostic
 957                    "    let y = vec![];\n",
 958                    "    a(x);\n",
 959                    "\n", // supporting diagnostic
 960                    "    b(y);\n",
 961                    "\n", // context ellipsis
 962                    "    c(y);\n",
 963                    "    d(x);\n",
 964                    "\n", // supporting diagnostic
 965                    "}"
 966                )
 967            );
 968
 969            // Cursor keeps its position.
 970            view.editor.update(cx, |editor, cx| {
 971                assert_eq!(
 972                    editor.selected_display_ranges(cx),
 973                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
 974                );
 975            });
 976        });
 977
 978        // Diagnostics are added to the first path
 979        project.update(cx, |project, cx| {
 980            project.disk_based_diagnostics_started(cx);
 981            project
 982                .update_diagnostic_entries(
 983                    PathBuf::from("/test/consts.rs"),
 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            project.disk_based_diagnostics_finished(cx);
1014        });
1015
1016        view.next_notification(&cx).await;
1017        view.update(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, "collapsed context".into()),
1026                    (8, "diagnostic header".into()),
1027                    (13, "path header block".into()),
1028                    (15, "diagnostic header".into()),
1029                    (28, "collapsed context".into()),
1030                    (29, "diagnostic header".into()),
1031                    (38, "collapsed context".into()),
1032                ]
1033            );
1034            assert_eq!(
1035                editor.text(),
1036                concat!(
1037                    //
1038                    // consts.rs
1039                    //
1040                    "\n", // filename
1041                    "\n", // padding
1042                    // diagnostic group 1
1043                    "\n", // primary message
1044                    "\n", // padding
1045                    "const a: i32 = 'a';\n",
1046                    "\n", // supporting diagnostic
1047                    "const b: i32 = c;\n",
1048                    "\n", // context ellipsis
1049                    // diagnostic group 2
1050                    "\n", // primary message
1051                    "\n", // padding
1052                    "const a: i32 = 'a';\n",
1053                    "const b: i32 = c;\n",
1054                    "\n", // supporting diagnostic
1055                    //
1056                    // main.rs
1057                    //
1058                    "\n", // filename
1059                    "\n", // padding
1060                    // diagnostic group 1
1061                    "\n", // primary message
1062                    "\n", // padding
1063                    "    let x = vec![];\n",
1064                    "    let y = vec![];\n",
1065                    "\n", // supporting diagnostic
1066                    "    a(x);\n",
1067                    "    b(y);\n",
1068                    "\n", // supporting diagnostic
1069                    "    // comment 1\n",
1070                    "    // comment 2\n",
1071                    "    c(y);\n",
1072                    "\n", // supporting diagnostic
1073                    "    d(x);\n",
1074                    "\n", // context ellipsis
1075                    // diagnostic group 2
1076                    "\n", // primary message
1077                    "\n", // filename
1078                    "fn main() {\n",
1079                    "    let x = vec![];\n",
1080                    "\n", // supporting diagnostic
1081                    "    let y = vec![];\n",
1082                    "    a(x);\n",
1083                    "\n", // supporting diagnostic
1084                    "    b(y);\n",
1085                    "\n", // context ellipsis
1086                    "    c(y);\n",
1087                    "    d(x);\n",
1088                    "\n", // supporting diagnostic
1089                    "}"
1090                )
1091            );
1092        });
1093    }
1094
1095    fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1096        editor
1097            .blocks_in_range(0..editor.max_point().row())
1098            .filter_map(|(row, block)| {
1099                let name = match block {
1100                    TransformBlock::Custom(block) => block
1101                        .render(&BlockContext {
1102                            cx,
1103                            anchor_x: 0.,
1104                            scroll_x: 0.,
1105                            gutter_padding: 0.,
1106                            gutter_width: 0.,
1107                            line_height: 0.,
1108                            em_width: 0.,
1109                        })
1110                        .name()?
1111                        .to_string(),
1112                    TransformBlock::ExcerptHeader {
1113                        starts_new_buffer, ..
1114                    } => {
1115                        if *starts_new_buffer {
1116                            "path header block".to_string()
1117                        } else {
1118                            "collapsed context".to_string()
1119                        }
1120                    }
1121                };
1122
1123                Some((row, name))
1124            })
1125            .collect()
1126    }
1127}