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