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