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