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, BlockStyle, RenderBlock},
   8    highlight_diagnostic_message, Autoscroll, Editor, ExcerptId, ExcerptRange, MultiBuffer,
   9    ToOffset,
  10};
  11use gpui::{
  12    actions, elements::*, fonts::TextStyle, impl_internal_actions, serde_json, AnyViewHandle,
  13    AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext,
  14    ViewHandle, WeakViewHandle,
  15};
  16use language::{
  17    Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
  18    SelectionGoal,
  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_in(&mut self, _: AnyViewHandle, 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                                primary.message =
 346                                    primary.message.split('\n').next().unwrap().to_string();
 347                                group_state.block_count += 1;
 348                                blocks_to_add.push(BlockProperties {
 349                                    position: header_position,
 350                                    height: 2,
 351                                    style: BlockStyle::Sticky,
 352                                    render: diagnostic_header_renderer(primary),
 353                                    disposition: BlockDisposition::Above,
 354                                });
 355                            }
 356
 357                            for entry in &group.entries[*start_ix..ix] {
 358                                let mut diagnostic = entry.diagnostic.clone();
 359                                if diagnostic.is_primary {
 360                                    group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
 361                                    diagnostic.message =
 362                                        entry.diagnostic.message.split('\n').skip(1).collect();
 363                                }
 364
 365                                if !diagnostic.message.is_empty() {
 366                                    group_state.block_count += 1;
 367                                    blocks_to_add.push(BlockProperties {
 368                                        position: (excerpt_id.clone(), entry.range.start),
 369                                        height: diagnostic.message.matches('\n').count() as u8 + 1,
 370                                        style: BlockStyle::Fixed,
 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                        style: block.style,
 408                        render: block.render,
 409                        disposition: block.disposition,
 410                    }
 411                }),
 412                cx,
 413            );
 414
 415            let mut block_ids = block_ids.into_iter();
 416            for group_state in &mut groups_to_add {
 417                group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
 418            }
 419        });
 420
 421        for ix in group_ixs_to_remove.into_iter().rev() {
 422            path_state.diagnostic_groups.remove(ix);
 423        }
 424        path_state.diagnostic_groups.extend(groups_to_add);
 425        path_state.diagnostic_groups.sort_unstable_by(|a, b| {
 426            let range_a = &a.primary_diagnostic.range;
 427            let range_b = &b.primary_diagnostic.range;
 428            range_a
 429                .start
 430                .cmp(&range_b.start, &snapshot)
 431                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
 432        });
 433
 434        if path_state.diagnostic_groups.is_empty() {
 435            self.path_states.remove(path_ix);
 436        }
 437
 438        self.editor.update(cx, |editor, cx| {
 439            let groups;
 440            let mut selections;
 441            let new_excerpt_ids_by_selection_id;
 442            if was_empty {
 443                groups = self.path_states.first()?.diagnostic_groups.as_slice();
 444                new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
 445                selections = vec![Selection {
 446                    id: 0,
 447                    start: 0,
 448                    end: 0,
 449                    reversed: false,
 450                    goal: SelectionGoal::None,
 451                }];
 452            } else {
 453                groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
 454                new_excerpt_ids_by_selection_id =
 455                    editor.change_selections(Some(Autoscroll::Fit), cx, |s| s.refresh());
 456                selections = editor.selections.all::<usize>(cx);
 457            }
 458
 459            // If any selection has lost its position, move it to start of the next primary diagnostic.
 460            for selection in &mut selections {
 461                if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
 462                    let group_ix = match groups.binary_search_by(|probe| {
 463                        probe.excerpts.last().unwrap().cmp(new_excerpt_id)
 464                    }) {
 465                        Ok(ix) | Err(ix) => ix,
 466                    };
 467                    if let Some(group) = groups.get(group_ix) {
 468                        let offset = excerpts_snapshot
 469                            .anchor_in_excerpt(
 470                                group.excerpts[group.primary_excerpt_ix].clone(),
 471                                group.primary_diagnostic.range.start,
 472                            )
 473                            .to_offset(&excerpts_snapshot);
 474                        selection.start = offset;
 475                        selection.end = offset;
 476                    }
 477                }
 478            }
 479            editor.change_selections(None, cx, |s| {
 480                s.select(selections);
 481            });
 482            Some(())
 483        });
 484
 485        if self.path_states.is_empty() {
 486            if self.editor.is_focused(cx) {
 487                cx.focus_self();
 488            }
 489        } else if cx.handle().is_focused(cx) {
 490            cx.focus(&self.editor);
 491        }
 492        cx.notify();
 493    }
 494
 495    fn update_title(&mut self, cx: &mut ViewContext<Self>) {
 496        self.summary = self.project.read(cx).diagnostic_summary(cx);
 497        cx.emit(Event::TitleChanged);
 498    }
 499}
 500
 501impl workspace::Item for ProjectDiagnosticsEditor {
 502    fn tab_content(
 503        &self,
 504        _detail: Option<usize>,
 505        style: &theme::Tab,
 506        cx: &AppContext,
 507    ) -> ElementBox {
 508        render_summary(
 509            &self.summary,
 510            &style.label.text,
 511            &cx.global::<Settings>().theme.project_diagnostics,
 512        )
 513    }
 514
 515    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
 516        None
 517    }
 518
 519    fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
 520        self.editor.project_entry_ids(cx)
 521    }
 522
 523    fn is_singleton(&self, _: &AppContext) -> bool {
 524        false
 525    }
 526
 527    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
 528        self.editor
 529            .update(cx, |editor, cx| editor.navigate(data, cx))
 530    }
 531
 532    fn is_dirty(&self, cx: &AppContext) -> bool {
 533        self.excerpts.read(cx).is_dirty(cx)
 534    }
 535
 536    fn has_conflict(&self, cx: &AppContext) -> bool {
 537        self.excerpts.read(cx).has_conflict(cx)
 538    }
 539
 540    fn can_save(&self, _: &AppContext) -> bool {
 541        true
 542    }
 543
 544    fn save(
 545        &mut self,
 546        project: ModelHandle<Project>,
 547        cx: &mut ViewContext<Self>,
 548    ) -> Task<Result<()>> {
 549        self.editor.save(project, cx)
 550    }
 551
 552    fn reload(
 553        &mut self,
 554        project: ModelHandle<Project>,
 555        cx: &mut ViewContext<Self>,
 556    ) -> Task<Result<()>> {
 557        self.editor.reload(project, cx)
 558    }
 559
 560    fn save_as(
 561        &mut self,
 562        _: ModelHandle<Project>,
 563        _: PathBuf,
 564        _: &mut ViewContext<Self>,
 565    ) -> Task<Result<()>> {
 566        unreachable!()
 567    }
 568
 569    fn should_update_tab_on_event(event: &Event) -> bool {
 570        Editor::should_update_tab_on_event(event)
 571    }
 572
 573    fn is_edit_event(event: &Self::Event) -> bool {
 574        Editor::is_edit_event(event)
 575    }
 576
 577    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
 578        self.editor.update(cx, |editor, _| {
 579            editor.set_nav_history(Some(nav_history));
 580        });
 581    }
 582
 583    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
 584    where
 585        Self: Sized,
 586    {
 587        Some(ProjectDiagnosticsEditor::new(
 588            self.project.clone(),
 589            self.workspace.clone(),
 590            cx,
 591        ))
 592    }
 593
 594    fn act_as_type(
 595        &self,
 596        type_id: TypeId,
 597        self_handle: &ViewHandle<Self>,
 598        _: &AppContext,
 599    ) -> Option<AnyViewHandle> {
 600        if type_id == TypeId::of::<Self>() {
 601            Some(self_handle.into())
 602        } else if type_id == TypeId::of::<Editor>() {
 603            Some((&self.editor).into())
 604        } else {
 605            None
 606        }
 607    }
 608
 609    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 610        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
 611    }
 612}
 613
 614fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
 615    let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
 616    Arc::new(move |cx| {
 617        let settings = cx.global::<Settings>();
 618        let theme = &settings.theme.editor;
 619        let style = theme.diagnostic_header.clone();
 620        let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
 621        let icon_width = cx.em_width * style.icon_width_factor;
 622        let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
 623            Svg::new("icons/circle_x_mark_12.svg")
 624                .with_color(theme.error_diagnostic.message.text.color)
 625        } else {
 626            Svg::new("icons/triangle_exclamation_12.svg")
 627                .with_color(theme.warning_diagnostic.message.text.color)
 628        };
 629
 630        Flex::row()
 631            .with_child(
 632                icon.constrained()
 633                    .with_width(icon_width)
 634                    .aligned()
 635                    .contained()
 636                    .boxed(),
 637            )
 638            .with_child(
 639                Label::new(
 640                    message.clone(),
 641                    style.message.label.clone().with_font_size(font_size),
 642                )
 643                .with_highlights(highlights.clone())
 644                .contained()
 645                .with_style(style.message.container)
 646                .with_margin_left(cx.gutter_padding)
 647                .aligned()
 648                .boxed(),
 649            )
 650            .with_children(diagnostic.code.clone().map(|code| {
 651                Label::new(code, style.code.text.clone().with_font_size(font_size))
 652                    .contained()
 653                    .with_style(style.code.container)
 654                    .aligned()
 655                    .boxed()
 656            }))
 657            .contained()
 658            .with_style(style.container)
 659            .with_padding_left(cx.gutter_padding)
 660            .with_padding_right(cx.gutter_padding)
 661            .expanded()
 662            .named("diagnostic header")
 663    })
 664}
 665
 666pub(crate) fn render_summary(
 667    summary: &DiagnosticSummary,
 668    text_style: &TextStyle,
 669    theme: &theme::ProjectDiagnostics,
 670) -> ElementBox {
 671    if summary.error_count == 0 && summary.warning_count == 0 {
 672        Label::new("No problems".to_string(), text_style.clone()).boxed()
 673    } else {
 674        let icon_width = theme.tab_icon_width;
 675        let icon_spacing = theme.tab_icon_spacing;
 676        let summary_spacing = theme.tab_summary_spacing;
 677        Flex::row()
 678            .with_children([
 679                Svg::new("icons/circle_x_mark_12.svg")
 680                    .with_color(text_style.color)
 681                    .constrained()
 682                    .with_width(icon_width)
 683                    .aligned()
 684                    .contained()
 685                    .with_margin_right(icon_spacing)
 686                    .named("no-icon"),
 687                Label::new(
 688                    summary.error_count.to_string(),
 689                    LabelStyle {
 690                        text: text_style.clone(),
 691                        highlight_text: None,
 692                    },
 693                )
 694                .aligned()
 695                .boxed(),
 696                Svg::new("icons/triangle_exclamation_12.svg")
 697                    .with_color(text_style.color)
 698                    .constrained()
 699                    .with_width(icon_width)
 700                    .aligned()
 701                    .contained()
 702                    .with_margin_left(summary_spacing)
 703                    .with_margin_right(icon_spacing)
 704                    .named("warn-icon"),
 705                Label::new(
 706                    summary.warning_count.to_string(),
 707                    LabelStyle {
 708                        text: text_style.clone(),
 709                        highlight_text: None,
 710                    },
 711                )
 712                .aligned()
 713                .boxed(),
 714            ])
 715            .boxed()
 716    }
 717}
 718
 719fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 720    lhs: &DiagnosticEntry<L>,
 721    rhs: &DiagnosticEntry<R>,
 722    snapshot: &language::BufferSnapshot,
 723) -> Ordering {
 724    lhs.range
 725        .start
 726        .to_offset(snapshot)
 727        .cmp(&rhs.range.start.to_offset(snapshot))
 728        .then_with(|| {
 729            lhs.range
 730                .end
 731                .to_offset(snapshot)
 732                .cmp(&rhs.range.end.to_offset(snapshot))
 733        })
 734        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 735}
 736
 737#[cfg(test)]
 738mod tests {
 739    use super::*;
 740    use editor::{
 741        display_map::{BlockContext, TransformBlock},
 742        DisplayPoint,
 743    };
 744    use gpui::TestAppContext;
 745    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
 746    use serde_json::json;
 747    use unindent::Unindent as _;
 748    use workspace::AppState;
 749
 750    #[gpui::test]
 751    async fn test_diagnostics(cx: &mut TestAppContext) {
 752        let app_state = cx.update(AppState::test);
 753        app_state
 754            .fs
 755            .as_fake()
 756            .insert_tree(
 757                "/test",
 758                json!({
 759                    "consts.rs": "
 760                        const a: i32 = 'a';
 761                        const b: i32 = c;
 762                    "
 763                    .unindent(),
 764
 765                    "main.rs": "
 766                        fn main() {
 767                            let x = vec![];
 768                            let y = vec![];
 769                            a(x);
 770                            b(y);
 771                            // comment 1
 772                            // comment 2
 773                            c(y);
 774                            d(x);
 775                        }
 776                    "
 777                    .unindent(),
 778                }),
 779            )
 780            .await;
 781
 782        let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
 783        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
 784
 785        // Create some diagnostics
 786        project.update(cx, |project, cx| {
 787            project
 788                .update_diagnostic_entries(
 789                    0,
 790                    PathBuf::from("/test/main.rs"),
 791                    None,
 792                    vec![
 793                        DiagnosticEntry {
 794                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
 795                            diagnostic: Diagnostic {
 796                                message:
 797                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 798                                        .to_string(),
 799                                severity: DiagnosticSeverity::INFORMATION,
 800                                is_primary: false,
 801                                is_disk_based: true,
 802                                group_id: 1,
 803                                ..Default::default()
 804                            },
 805                        },
 806                        DiagnosticEntry {
 807                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
 808                            diagnostic: Diagnostic {
 809                                message:
 810                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 811                                        .to_string(),
 812                                severity: DiagnosticSeverity::INFORMATION,
 813                                is_primary: false,
 814                                is_disk_based: true,
 815                                group_id: 0,
 816                                ..Default::default()
 817                            },
 818                        },
 819                        DiagnosticEntry {
 820                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
 821                            diagnostic: Diagnostic {
 822                                message: "value moved here".to_string(),
 823                                severity: DiagnosticSeverity::INFORMATION,
 824                                is_primary: false,
 825                                is_disk_based: true,
 826                                group_id: 1,
 827                                ..Default::default()
 828                            },
 829                        },
 830                        DiagnosticEntry {
 831                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
 832                            diagnostic: Diagnostic {
 833                                message: "value moved here".to_string(),
 834                                severity: DiagnosticSeverity::INFORMATION,
 835                                is_primary: false,
 836                                is_disk_based: true,
 837                                group_id: 0,
 838                                ..Default::default()
 839                            },
 840                        },
 841                        DiagnosticEntry {
 842                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
 843                            diagnostic: Diagnostic {
 844                                message: "use of moved value\nvalue used here after move".to_string(),
 845                                severity: DiagnosticSeverity::ERROR,
 846                                is_primary: true,
 847                                is_disk_based: true,
 848                                group_id: 0,
 849                                ..Default::default()
 850                            },
 851                        },
 852                        DiagnosticEntry {
 853                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
 854                            diagnostic: Diagnostic {
 855                                message: "use of moved value\nvalue used here after move".to_string(),
 856                                severity: DiagnosticSeverity::ERROR,
 857                                is_primary: true,
 858                                is_disk_based: true,
 859                                group_id: 1,
 860                                ..Default::default()
 861                            },
 862                        },
 863                    ],
 864                    cx,
 865                )
 866                .unwrap();
 867        });
 868
 869        // Open the project diagnostics view while there are already diagnostics.
 870        let view = cx.add_view(&workspace, |cx| {
 871            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
 872        });
 873
 874        view.next_notification(cx).await;
 875        view.update(cx, |view, cx| {
 876            assert_eq!(
 877                editor_blocks(&view.editor, cx),
 878                [
 879                    (0, "path header block".into()),
 880                    (2, "diagnostic header".into()),
 881                    (15, "collapsed context".into()),
 882                    (16, "diagnostic header".into()),
 883                    (25, "collapsed context".into()),
 884                ]
 885            );
 886            assert_eq!(
 887                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
 888                concat!(
 889                    //
 890                    // main.rs
 891                    //
 892                    "\n", // filename
 893                    "\n", // padding
 894                    // diagnostic group 1
 895                    "\n", // primary message
 896                    "\n", // padding
 897                    "    let x = vec![];\n",
 898                    "    let y = vec![];\n",
 899                    "\n", // supporting diagnostic
 900                    "    a(x);\n",
 901                    "    b(y);\n",
 902                    "\n", // supporting diagnostic
 903                    "    // comment 1\n",
 904                    "    // comment 2\n",
 905                    "    c(y);\n",
 906                    "\n", // supporting diagnostic
 907                    "    d(x);\n",
 908                    "\n", // context ellipsis
 909                    // diagnostic group 2
 910                    "\n", // primary message
 911                    "\n", // padding
 912                    "fn main() {\n",
 913                    "    let x = vec![];\n",
 914                    "\n", // supporting diagnostic
 915                    "    let y = vec![];\n",
 916                    "    a(x);\n",
 917                    "\n", // supporting diagnostic
 918                    "    b(y);\n",
 919                    "\n", // context ellipsis
 920                    "    c(y);\n",
 921                    "    d(x);\n",
 922                    "\n", // supporting diagnostic
 923                    "}"
 924                )
 925            );
 926
 927            // Cursor is at the first diagnostic
 928            view.editor.update(cx, |editor, cx| {
 929                assert_eq!(
 930                    editor.selections.display_ranges(cx),
 931                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
 932                );
 933            });
 934        });
 935
 936        // Diagnostics are added for another earlier path.
 937        project.update(cx, |project, cx| {
 938            project.disk_based_diagnostics_started(0, cx);
 939            project
 940                .update_diagnostic_entries(
 941                    0,
 942                    PathBuf::from("/test/consts.rs"),
 943                    None,
 944                    vec![DiagnosticEntry {
 945                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
 946                        diagnostic: Diagnostic {
 947                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
 948                            severity: DiagnosticSeverity::ERROR,
 949                            is_primary: true,
 950                            is_disk_based: true,
 951                            group_id: 0,
 952                            ..Default::default()
 953                        },
 954                    }],
 955                    cx,
 956                )
 957                .unwrap();
 958            project.disk_based_diagnostics_finished(0, cx);
 959        });
 960
 961        view.next_notification(cx).await;
 962        view.update(cx, |view, cx| {
 963            assert_eq!(
 964                editor_blocks(&view.editor, cx),
 965                [
 966                    (0, "path header block".into()),
 967                    (2, "diagnostic header".into()),
 968                    (7, "path header block".into()),
 969                    (9, "diagnostic header".into()),
 970                    (22, "collapsed context".into()),
 971                    (23, "diagnostic header".into()),
 972                    (32, "collapsed context".into()),
 973                ]
 974            );
 975            assert_eq!(
 976                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
 977                concat!(
 978                    //
 979                    // consts.rs
 980                    //
 981                    "\n", // filename
 982                    "\n", // padding
 983                    // diagnostic group 1
 984                    "\n", // primary message
 985                    "\n", // padding
 986                    "const a: i32 = 'a';\n",
 987                    "\n", // supporting diagnostic
 988                    "const b: i32 = c;\n",
 989                    //
 990                    // main.rs
 991                    //
 992                    "\n", // filename
 993                    "\n", // padding
 994                    // diagnostic group 1
 995                    "\n", // primary message
 996                    "\n", // padding
 997                    "    let x = vec![];\n",
 998                    "    let y = vec![];\n",
 999                    "\n", // supporting diagnostic
1000                    "    a(x);\n",
1001                    "    b(y);\n",
1002                    "\n", // supporting diagnostic
1003                    "    // comment 1\n",
1004                    "    // comment 2\n",
1005                    "    c(y);\n",
1006                    "\n", // supporting diagnostic
1007                    "    d(x);\n",
1008                    "\n", // collapsed context
1009                    // diagnostic group 2
1010                    "\n", // primary message
1011                    "\n", // filename
1012                    "fn main() {\n",
1013                    "    let x = vec![];\n",
1014                    "\n", // supporting diagnostic
1015                    "    let y = vec![];\n",
1016                    "    a(x);\n",
1017                    "\n", // supporting diagnostic
1018                    "    b(y);\n",
1019                    "\n", // context ellipsis
1020                    "    c(y);\n",
1021                    "    d(x);\n",
1022                    "\n", // supporting diagnostic
1023                    "}"
1024                )
1025            );
1026
1027            // Cursor keeps its position.
1028            view.editor.update(cx, |editor, cx| {
1029                assert_eq!(
1030                    editor.selections.display_ranges(cx),
1031                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1032                );
1033            });
1034        });
1035
1036        // Diagnostics are added to the first path
1037        project.update(cx, |project, cx| {
1038            project.disk_based_diagnostics_started(0, cx);
1039            project
1040                .update_diagnostic_entries(
1041                    0,
1042                    PathBuf::from("/test/consts.rs"),
1043                    None,
1044                    vec![
1045                        DiagnosticEntry {
1046                            range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1047                            diagnostic: Diagnostic {
1048                                message: "mismatched types\nexpected `usize`, found `char`"
1049                                    .to_string(),
1050                                severity: DiagnosticSeverity::ERROR,
1051                                is_primary: true,
1052                                is_disk_based: true,
1053                                group_id: 0,
1054                                ..Default::default()
1055                            },
1056                        },
1057                        DiagnosticEntry {
1058                            range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1059                            diagnostic: Diagnostic {
1060                                message: "unresolved name `c`".to_string(),
1061                                severity: DiagnosticSeverity::ERROR,
1062                                is_primary: true,
1063                                is_disk_based: true,
1064                                group_id: 1,
1065                                ..Default::default()
1066                            },
1067                        },
1068                    ],
1069                    cx,
1070                )
1071                .unwrap();
1072            project.disk_based_diagnostics_finished(0, cx);
1073        });
1074
1075        view.next_notification(cx).await;
1076        view.update(cx, |view, cx| {
1077            assert_eq!(
1078                editor_blocks(&view.editor, cx),
1079                [
1080                    (0, "path header block".into()),
1081                    (2, "diagnostic header".into()),
1082                    (7, "collapsed context".into()),
1083                    (8, "diagnostic header".into()),
1084                    (13, "path header block".into()),
1085                    (15, "diagnostic header".into()),
1086                    (28, "collapsed context".into()),
1087                    (29, "diagnostic header".into()),
1088                    (38, "collapsed context".into()),
1089                ]
1090            );
1091            assert_eq!(
1092                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1093                concat!(
1094                    //
1095                    // consts.rs
1096                    //
1097                    "\n", // filename
1098                    "\n", // padding
1099                    // diagnostic group 1
1100                    "\n", // primary message
1101                    "\n", // padding
1102                    "const a: i32 = 'a';\n",
1103                    "\n", // supporting diagnostic
1104                    "const b: i32 = c;\n",
1105                    "\n", // context ellipsis
1106                    // diagnostic group 2
1107                    "\n", // primary message
1108                    "\n", // padding
1109                    "const a: i32 = 'a';\n",
1110                    "const b: i32 = c;\n",
1111                    "\n", // supporting diagnostic
1112                    //
1113                    // main.rs
1114                    //
1115                    "\n", // filename
1116                    "\n", // padding
1117                    // diagnostic group 1
1118                    "\n", // primary message
1119                    "\n", // padding
1120                    "    let x = vec![];\n",
1121                    "    let y = vec![];\n",
1122                    "\n", // supporting diagnostic
1123                    "    a(x);\n",
1124                    "    b(y);\n",
1125                    "\n", // supporting diagnostic
1126                    "    // comment 1\n",
1127                    "    // comment 2\n",
1128                    "    c(y);\n",
1129                    "\n", // supporting diagnostic
1130                    "    d(x);\n",
1131                    "\n", // context ellipsis
1132                    // diagnostic group 2
1133                    "\n", // primary message
1134                    "\n", // filename
1135                    "fn main() {\n",
1136                    "    let x = vec![];\n",
1137                    "\n", // supporting diagnostic
1138                    "    let y = vec![];\n",
1139                    "    a(x);\n",
1140                    "\n", // supporting diagnostic
1141                    "    b(y);\n",
1142                    "\n", // context ellipsis
1143                    "    c(y);\n",
1144                    "    d(x);\n",
1145                    "\n", // supporting diagnostic
1146                    "}"
1147                )
1148            );
1149        });
1150    }
1151
1152    fn editor_blocks(
1153        editor: &ViewHandle<Editor>,
1154        cx: &mut MutableAppContext,
1155    ) -> Vec<(u32, String)> {
1156        let mut presenter = cx.build_presenter(editor.id(), 0.);
1157        let mut cx = presenter.build_layout_context(Default::default(), false, cx);
1158        cx.render(editor, |editor, cx| {
1159            let snapshot = editor.snapshot(cx);
1160            snapshot
1161                .blocks_in_range(0..snapshot.max_point().row())
1162                .filter_map(|(row, block)| {
1163                    let name = match block {
1164                        TransformBlock::Custom(block) => block
1165                            .render(&mut BlockContext {
1166                                cx,
1167                                anchor_x: 0.,
1168                                scroll_x: 0.,
1169                                gutter_padding: 0.,
1170                                gutter_width: 0.,
1171                                line_height: 0.,
1172                                em_width: 0.,
1173                            })
1174                            .name()?
1175                            .to_string(),
1176                        TransformBlock::ExcerptHeader {
1177                            starts_new_buffer, ..
1178                        } => {
1179                            if *starts_new_buffer {
1180                                "path header block".to_string()
1181                            } else {
1182                                "collapsed context".to_string()
1183                            }
1184                        }
1185                    };
1186
1187                    Some((row, name))
1188                })
1189                .collect()
1190        })
1191    }
1192}