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