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