diagnostics.rs

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