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