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