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                    Flex::row()
 690                        .with_child(
 691                            Label::new(
 692                                "Jump to diagnostic (".to_string(),
 693                                tooltip_style.text.clone(),
 694                            )
 695                            .boxed(),
 696                        )
 697                        .with_child(
 698                            KeystrokeLabel::new(
 699                                Box::new(editor::OpenExcerpts),
 700                                Default::default(),
 701                                tooltip_style.text.clone(),
 702                            )
 703                            .boxed(),
 704                        )
 705                        .with_child(Label::new(")".to_string(), tooltip_style.text).boxed())
 706                        .contained()
 707                        .with_style(tooltip_style.container)
 708                        .boxed(),
 709                    cx,
 710                )
 711                .aligned()
 712                .flex_float()
 713                .boxed(),
 714            )
 715            .contained()
 716            .with_style(style.container)
 717            .with_padding_left(x_padding)
 718            .with_padding_right(x_padding)
 719            .expanded()
 720            .named("diagnostic header")
 721    })
 722}
 723
 724pub(crate) fn render_summary(
 725    summary: &DiagnosticSummary,
 726    text_style: &TextStyle,
 727    theme: &theme::ProjectDiagnostics,
 728) -> ElementBox {
 729    if summary.error_count == 0 && summary.warning_count == 0 {
 730        Label::new("No problems".to_string(), text_style.clone()).boxed()
 731    } else {
 732        let icon_width = theme.tab_icon_width;
 733        let icon_spacing = theme.tab_icon_spacing;
 734        let summary_spacing = theme.tab_summary_spacing;
 735        Flex::row()
 736            .with_children([
 737                Svg::new("icons/diagnostic-summary-error.svg")
 738                    .with_color(text_style.color)
 739                    .constrained()
 740                    .with_width(icon_width)
 741                    .aligned()
 742                    .contained()
 743                    .with_margin_right(icon_spacing)
 744                    .named("no-icon"),
 745                Label::new(
 746                    summary.error_count.to_string(),
 747                    LabelStyle {
 748                        text: text_style.clone(),
 749                        highlight_text: None,
 750                    },
 751                )
 752                .aligned()
 753                .boxed(),
 754                Svg::new("icons/diagnostic-summary-warning.svg")
 755                    .with_color(text_style.color)
 756                    .constrained()
 757                    .with_width(icon_width)
 758                    .aligned()
 759                    .contained()
 760                    .with_margin_left(summary_spacing)
 761                    .with_margin_right(icon_spacing)
 762                    .named("warn-icon"),
 763                Label::new(
 764                    summary.warning_count.to_string(),
 765                    LabelStyle {
 766                        text: text_style.clone(),
 767                        highlight_text: None,
 768                    },
 769                )
 770                .aligned()
 771                .boxed(),
 772            ])
 773            .boxed()
 774    }
 775}
 776
 777fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 778    lhs: &DiagnosticEntry<L>,
 779    rhs: &DiagnosticEntry<R>,
 780    snapshot: &language::BufferSnapshot,
 781) -> Ordering {
 782    lhs.range
 783        .start
 784        .to_offset(&snapshot)
 785        .cmp(&rhs.range.start.to_offset(snapshot))
 786        .then_with(|| {
 787            lhs.range
 788                .end
 789                .to_offset(&snapshot)
 790                .cmp(&rhs.range.end.to_offset(snapshot))
 791        })
 792        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 793}
 794
 795#[cfg(test)]
 796mod tests {
 797    use super::*;
 798    use editor::{
 799        display_map::{BlockContext, TransformBlock},
 800        DisplayPoint,
 801    };
 802    use gpui::TestAppContext;
 803    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
 804    use serde_json::json;
 805    use unindent::Unindent as _;
 806    use workspace::AppState;
 807
 808    #[gpui::test]
 809    async fn test_diagnostics(cx: &mut TestAppContext) {
 810        let app_state = cx.update(AppState::test);
 811        app_state
 812            .fs
 813            .as_fake()
 814            .insert_tree(
 815                "/test",
 816                json!({
 817                    "consts.rs": "
 818                        const a: i32 = 'a';
 819                        const b: i32 = c;
 820                    "
 821                    .unindent(),
 822
 823                    "main.rs": "
 824                        fn main() {
 825                            let x = vec![];
 826                            let y = vec![];
 827                            a(x);
 828                            b(y);
 829                            // comment 1
 830                            // comment 2
 831                            c(y);
 832                            d(x);
 833                        }
 834                    "
 835                    .unindent(),
 836                }),
 837            )
 838            .await;
 839
 840        let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
 841        let workspace = cx.add_view(0, |cx| Workspace::new(project.clone(), cx));
 842
 843        // Create some diagnostics
 844        project.update(cx, |project, cx| {
 845            project
 846                .update_diagnostic_entries(
 847                    PathBuf::from("/test/main.rs"),
 848                    None,
 849                    vec![
 850                        DiagnosticEntry {
 851                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
 852                            diagnostic: Diagnostic {
 853                                message:
 854                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 855                                        .to_string(),
 856                                severity: DiagnosticSeverity::INFORMATION,
 857                                is_primary: false,
 858                                is_disk_based: true,
 859                                group_id: 1,
 860                                ..Default::default()
 861                            },
 862                        },
 863                        DiagnosticEntry {
 864                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
 865                            diagnostic: Diagnostic {
 866                                message:
 867                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 868                                        .to_string(),
 869                                severity: DiagnosticSeverity::INFORMATION,
 870                                is_primary: false,
 871                                is_disk_based: true,
 872                                group_id: 0,
 873                                ..Default::default()
 874                            },
 875                        },
 876                        DiagnosticEntry {
 877                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
 878                            diagnostic: Diagnostic {
 879                                message: "value moved here".to_string(),
 880                                severity: DiagnosticSeverity::INFORMATION,
 881                                is_primary: false,
 882                                is_disk_based: true,
 883                                group_id: 1,
 884                                ..Default::default()
 885                            },
 886                        },
 887                        DiagnosticEntry {
 888                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
 889                            diagnostic: Diagnostic {
 890                                message: "value moved here".to_string(),
 891                                severity: DiagnosticSeverity::INFORMATION,
 892                                is_primary: false,
 893                                is_disk_based: true,
 894                                group_id: 0,
 895                                ..Default::default()
 896                            },
 897                        },
 898                        DiagnosticEntry {
 899                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
 900                            diagnostic: Diagnostic {
 901                                message: "use of moved value\nvalue used here after move".to_string(),
 902                                severity: DiagnosticSeverity::ERROR,
 903                                is_primary: true,
 904                                is_disk_based: true,
 905                                group_id: 0,
 906                                ..Default::default()
 907                            },
 908                        },
 909                        DiagnosticEntry {
 910                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
 911                            diagnostic: Diagnostic {
 912                                message: "use of moved value\nvalue used here after move".to_string(),
 913                                severity: DiagnosticSeverity::ERROR,
 914                                is_primary: true,
 915                                is_disk_based: true,
 916                                group_id: 1,
 917                                ..Default::default()
 918                            },
 919                        },
 920                    ],
 921                    cx,
 922                )
 923                .unwrap();
 924        });
 925
 926        // Open the project diagnostics view while there are already diagnostics.
 927        let view = cx.add_view(0, |cx| {
 928            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
 929        });
 930
 931        view.next_notification(&cx).await;
 932        view.update(cx, |view, cx| {
 933            assert_eq!(
 934                editor_blocks(&view.editor, cx),
 935                [
 936                    (0, "path header block".into()),
 937                    (2, "diagnostic header".into()),
 938                    (15, "collapsed context".into()),
 939                    (16, "diagnostic header".into()),
 940                    (25, "collapsed context".into()),
 941                ]
 942            );
 943            assert_eq!(
 944                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
 945                concat!(
 946                    //
 947                    // main.rs
 948                    //
 949                    "\n", // filename
 950                    "\n", // padding
 951                    // diagnostic group 1
 952                    "\n", // primary message
 953                    "\n", // padding
 954                    "    let x = vec![];\n",
 955                    "    let y = vec![];\n",
 956                    "\n", // supporting diagnostic
 957                    "    a(x);\n",
 958                    "    b(y);\n",
 959                    "\n", // supporting diagnostic
 960                    "    // comment 1\n",
 961                    "    // comment 2\n",
 962                    "    c(y);\n",
 963                    "\n", // supporting diagnostic
 964                    "    d(x);\n",
 965                    "\n", // context ellipsis
 966                    // diagnostic group 2
 967                    "\n", // primary message
 968                    "\n", // padding
 969                    "fn main() {\n",
 970                    "    let x = vec![];\n",
 971                    "\n", // supporting diagnostic
 972                    "    let y = vec![];\n",
 973                    "    a(x);\n",
 974                    "\n", // supporting diagnostic
 975                    "    b(y);\n",
 976                    "\n", // context ellipsis
 977                    "    c(y);\n",
 978                    "    d(x);\n",
 979                    "\n", // supporting diagnostic
 980                    "}"
 981                )
 982            );
 983
 984            // Cursor is at the first diagnostic
 985            view.editor.update(cx, |editor, cx| {
 986                assert_eq!(
 987                    editor.selections.display_ranges(cx),
 988                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
 989                );
 990            });
 991        });
 992
 993        // Diagnostics are added for another earlier path.
 994        project.update(cx, |project, cx| {
 995            project.disk_based_diagnostics_started(cx);
 996            project
 997                .update_diagnostic_entries(
 998                    PathBuf::from("/test/consts.rs"),
 999                    None,
1000                    vec![DiagnosticEntry {
1001                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1002                        diagnostic: Diagnostic {
1003                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1004                            severity: DiagnosticSeverity::ERROR,
1005                            is_primary: true,
1006                            is_disk_based: true,
1007                            group_id: 0,
1008                            ..Default::default()
1009                        },
1010                    }],
1011                    cx,
1012                )
1013                .unwrap();
1014            project.disk_based_diagnostics_finished(cx);
1015        });
1016
1017        view.next_notification(&cx).await;
1018        view.update(cx, |view, cx| {
1019            assert_eq!(
1020                editor_blocks(&view.editor, cx),
1021                [
1022                    (0, "path header block".into()),
1023                    (2, "diagnostic header".into()),
1024                    (7, "path header block".into()),
1025                    (9, "diagnostic header".into()),
1026                    (22, "collapsed context".into()),
1027                    (23, "diagnostic header".into()),
1028                    (32, "collapsed context".into()),
1029                ]
1030            );
1031            assert_eq!(
1032                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1033                concat!(
1034                    //
1035                    // consts.rs
1036                    //
1037                    "\n", // filename
1038                    "\n", // padding
1039                    // diagnostic group 1
1040                    "\n", // primary message
1041                    "\n", // padding
1042                    "const a: i32 = 'a';\n",
1043                    "\n", // supporting diagnostic
1044                    "const b: i32 = c;\n",
1045                    //
1046                    // main.rs
1047                    //
1048                    "\n", // filename
1049                    "\n", // padding
1050                    // diagnostic group 1
1051                    "\n", // primary message
1052                    "\n", // padding
1053                    "    let x = vec![];\n",
1054                    "    let y = vec![];\n",
1055                    "\n", // supporting diagnostic
1056                    "    a(x);\n",
1057                    "    b(y);\n",
1058                    "\n", // supporting diagnostic
1059                    "    // comment 1\n",
1060                    "    // comment 2\n",
1061                    "    c(y);\n",
1062                    "\n", // supporting diagnostic
1063                    "    d(x);\n",
1064                    "\n", // collapsed context
1065                    // diagnostic group 2
1066                    "\n", // primary message
1067                    "\n", // filename
1068                    "fn main() {\n",
1069                    "    let x = vec![];\n",
1070                    "\n", // supporting diagnostic
1071                    "    let y = vec![];\n",
1072                    "    a(x);\n",
1073                    "\n", // supporting diagnostic
1074                    "    b(y);\n",
1075                    "\n", // context ellipsis
1076                    "    c(y);\n",
1077                    "    d(x);\n",
1078                    "\n", // supporting diagnostic
1079                    "}"
1080                )
1081            );
1082
1083            // Cursor keeps its position.
1084            view.editor.update(cx, |editor, cx| {
1085                assert_eq!(
1086                    editor.selections.display_ranges(cx),
1087                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1088                );
1089            });
1090        });
1091
1092        // Diagnostics are added to the first path
1093        project.update(cx, |project, cx| {
1094            project.disk_based_diagnostics_started(cx);
1095            project
1096                .update_diagnostic_entries(
1097                    PathBuf::from("/test/consts.rs"),
1098                    None,
1099                    vec![
1100                        DiagnosticEntry {
1101                            range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1102                            diagnostic: Diagnostic {
1103                                message: "mismatched types\nexpected `usize`, found `char`"
1104                                    .to_string(),
1105                                severity: DiagnosticSeverity::ERROR,
1106                                is_primary: true,
1107                                is_disk_based: true,
1108                                group_id: 0,
1109                                ..Default::default()
1110                            },
1111                        },
1112                        DiagnosticEntry {
1113                            range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1114                            diagnostic: Diagnostic {
1115                                message: "unresolved name `c`".to_string(),
1116                                severity: DiagnosticSeverity::ERROR,
1117                                is_primary: true,
1118                                is_disk_based: true,
1119                                group_id: 1,
1120                                ..Default::default()
1121                            },
1122                        },
1123                    ],
1124                    cx,
1125                )
1126                .unwrap();
1127            project.disk_based_diagnostics_finished(cx);
1128        });
1129
1130        view.next_notification(&cx).await;
1131        view.update(cx, |view, cx| {
1132            assert_eq!(
1133                editor_blocks(&view.editor, cx),
1134                [
1135                    (0, "path header block".into()),
1136                    (2, "diagnostic header".into()),
1137                    (7, "collapsed context".into()),
1138                    (8, "diagnostic header".into()),
1139                    (13, "path header block".into()),
1140                    (15, "diagnostic header".into()),
1141                    (28, "collapsed context".into()),
1142                    (29, "diagnostic header".into()),
1143                    (38, "collapsed context".into()),
1144                ]
1145            );
1146            assert_eq!(
1147                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1148                concat!(
1149                    //
1150                    // consts.rs
1151                    //
1152                    "\n", // filename
1153                    "\n", // padding
1154                    // diagnostic group 1
1155                    "\n", // primary message
1156                    "\n", // padding
1157                    "const a: i32 = 'a';\n",
1158                    "\n", // supporting diagnostic
1159                    "const b: i32 = c;\n",
1160                    "\n", // context ellipsis
1161                    // diagnostic group 2
1162                    "\n", // primary message
1163                    "\n", // padding
1164                    "const a: i32 = 'a';\n",
1165                    "const b: i32 = c;\n",
1166                    "\n", // supporting diagnostic
1167                    //
1168                    // main.rs
1169                    //
1170                    "\n", // filename
1171                    "\n", // padding
1172                    // diagnostic group 1
1173                    "\n", // primary message
1174                    "\n", // padding
1175                    "    let x = vec![];\n",
1176                    "    let y = vec![];\n",
1177                    "\n", // supporting diagnostic
1178                    "    a(x);\n",
1179                    "    b(y);\n",
1180                    "\n", // supporting diagnostic
1181                    "    // comment 1\n",
1182                    "    // comment 2\n",
1183                    "    c(y);\n",
1184                    "\n", // supporting diagnostic
1185                    "    d(x);\n",
1186                    "\n", // context ellipsis
1187                    // diagnostic group 2
1188                    "\n", // primary message
1189                    "\n", // filename
1190                    "fn main() {\n",
1191                    "    let x = vec![];\n",
1192                    "\n", // supporting diagnostic
1193                    "    let y = vec![];\n",
1194                    "    a(x);\n",
1195                    "\n", // supporting diagnostic
1196                    "    b(y);\n",
1197                    "\n", // context ellipsis
1198                    "    c(y);\n",
1199                    "    d(x);\n",
1200                    "\n", // supporting diagnostic
1201                    "}"
1202                )
1203            );
1204        });
1205    }
1206
1207    fn editor_blocks(
1208        editor: &ViewHandle<Editor>,
1209        cx: &mut MutableAppContext,
1210    ) -> Vec<(u32, String)> {
1211        let mut presenter = cx.build_presenter(editor.id(), 0.);
1212        let mut cx = presenter.build_layout_context(Default::default(), false, cx);
1213        cx.render(editor, |editor, cx| {
1214            let snapshot = editor.snapshot(cx);
1215            snapshot
1216                .blocks_in_range(0..snapshot.max_point().row())
1217                .filter_map(|(row, block)| {
1218                    let name = match block {
1219                        TransformBlock::Custom(block) => block
1220                            .render(&mut BlockContext {
1221                                cx,
1222                                anchor_x: 0.,
1223                                scroll_x: 0.,
1224                                gutter_padding: 0.,
1225                                gutter_width: 0.,
1226                                line_height: 0.,
1227                                em_width: 0.,
1228                            })
1229                            .name()?
1230                            .to_string(),
1231                        TransformBlock::ExcerptHeader {
1232                            starts_new_buffer, ..
1233                        } => {
1234                            if *starts_new_buffer {
1235                                "path header block".to_string()
1236                            } else {
1237                                "collapsed context".to_string()
1238                            }
1239                        }
1240                    };
1241
1242                    Some((row, name))
1243                })
1244                .collect()
1245        })
1246    }
1247}