diagnostics.rs

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