diagnostics.rs

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