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(&mut self, cx: &mut ViewContext<Self>) {
 103        if !self.path_states.is_empty() {
 104            cx.focus(&self.editor);
 105        }
 106    }
 107
 108    fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
 109        let project = self.project.read(cx);
 110        json!({
 111            "project": json!({
 112                "language_servers": project.language_server_statuses().collect::<Vec<_>>(),
 113                "summary": project.diagnostic_summary(cx),
 114            }),
 115            "summary": self.summary,
 116            "paths_to_update": self.paths_to_update.iter().map(|(path, server_id)|
 117                (path.path.to_string_lossy(), server_id)
 118            ).collect::<Vec<_>>(),
 119            "paths_states": self.path_states.iter().map(|state|
 120                json!({
 121                    "path": state.path.path.to_string_lossy(),
 122                    "groups": state.diagnostic_groups.iter().map(|group|
 123                        json!({
 124                            "block_count": group.blocks.len(),
 125                            "excerpt_count": group.excerpts.len(),
 126                        })
 127                    ).collect::<Vec<_>>(),
 128                })
 129            ).collect::<Vec<_>>(),
 130        })
 131    }
 132}
 133
 134impl ProjectDiagnosticsEditor {
 135    fn new(
 136        project_handle: ModelHandle<Project>,
 137        workspace: WeakViewHandle<Workspace>,
 138        cx: &mut ViewContext<Self>,
 139    ) -> Self {
 140        cx.subscribe(&project_handle, |this, _, event, cx| match event {
 141            project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
 142                this.update_excerpts(Some(*language_server_id), cx);
 143                this.update_title(cx);
 144            }
 145            project::Event::DiagnosticsUpdated {
 146                language_server_id,
 147                path,
 148            } => {
 149                this.paths_to_update
 150                    .insert(path.clone(), *language_server_id);
 151            }
 152            _ => {}
 153        })
 154        .detach();
 155
 156        let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
 157        let editor = cx.add_view(|cx| {
 158            let mut editor =
 159                Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
 160            editor.set_vertical_scroll_margin(5, cx);
 161            editor
 162        });
 163        cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
 164            .detach();
 165
 166        let project = project_handle.read(cx);
 167        let paths_to_update = project
 168            .diagnostic_summaries(cx)
 169            .map(|e| (e.0, e.1.language_server_id))
 170            .collect();
 171        let summary = project.diagnostic_summary(cx);
 172        let mut this = Self {
 173            project: project_handle,
 174            summary,
 175            workspace,
 176            excerpts,
 177            editor,
 178            path_states: Default::default(),
 179            paths_to_update,
 180        };
 181        this.update_excerpts(None, cx);
 182        this
 183    }
 184
 185    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
 186        if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
 187            workspace.activate_item(&existing, cx);
 188        } else {
 189            let workspace_handle = cx.weak_handle();
 190            let diagnostics = cx.add_view(|cx| {
 191                ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
 192            });
 193            workspace.add_item(Box::new(diagnostics), cx);
 194        }
 195    }
 196
 197    fn update_excerpts(&mut self, language_server_id: Option<usize>, cx: &mut ViewContext<Self>) {
 198        let mut paths = Vec::new();
 199        self.paths_to_update.retain(|path, server_id| {
 200            if language_server_id
 201                .map_or(true, |language_server_id| language_server_id == *server_id)
 202            {
 203                paths.push(path.clone());
 204                false
 205            } else {
 206                true
 207            }
 208        });
 209        let project = self.project.clone();
 210        cx.spawn(|this, mut cx| {
 211            async move {
 212                for path in paths {
 213                    let buffer = project
 214                        .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
 215                        .await?;
 216                    this.update(&mut cx, |this, cx| this.populate_excerpts(path, buffer, cx))
 217                }
 218                Result::<_, anyhow::Error>::Ok(())
 219            }
 220            .log_err()
 221        })
 222        .detach();
 223    }
 224
 225    fn populate_excerpts(
 226        &mut self,
 227        path: ProjectPath,
 228        buffer: ModelHandle<Buffer>,
 229        cx: &mut ViewContext<Self>,
 230    ) {
 231        let was_empty = self.path_states.is_empty();
 232        let snapshot = buffer.read(cx).snapshot();
 233        let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
 234            Ok(ix) => ix,
 235            Err(ix) => {
 236                self.path_states.insert(
 237                    ix,
 238                    PathState {
 239                        path: path.clone(),
 240                        diagnostic_groups: Default::default(),
 241                    },
 242                );
 243                ix
 244            }
 245        };
 246
 247        let mut prev_excerpt_id = if path_ix > 0 {
 248            let prev_path_last_group = &self.path_states[path_ix - 1]
 249                .diagnostic_groups
 250                .last()
 251                .unwrap();
 252            prev_path_last_group.excerpts.last().unwrap().clone()
 253        } else {
 254            ExcerptId::min()
 255        };
 256
 257        let path_state = &mut self.path_states[path_ix];
 258        let mut groups_to_add = Vec::new();
 259        let mut group_ixs_to_remove = Vec::new();
 260        let mut blocks_to_add = Vec::new();
 261        let mut blocks_to_remove = HashSet::default();
 262        let mut first_excerpt_id = None;
 263        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
 264            let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
 265            let mut new_groups = snapshot
 266                .diagnostic_groups()
 267                .into_iter()
 268                .filter(|group| {
 269                    group.entries[group.primary_ix].diagnostic.severity
 270                        <= DiagnosticSeverity::WARNING
 271                })
 272                .peekable();
 273            loop {
 274                let mut to_insert = None;
 275                let mut to_remove = None;
 276                let mut to_keep = None;
 277                match (old_groups.peek(), new_groups.peek()) {
 278                    (None, None) => break,
 279                    (None, Some(_)) => to_insert = new_groups.next(),
 280                    (Some(_), None) => to_remove = old_groups.next(),
 281                    (Some((_, old_group)), Some(new_group)) => {
 282                        let old_primary = &old_group.primary_diagnostic;
 283                        let new_primary = &new_group.entries[new_group.primary_ix];
 284                        match compare_diagnostics(old_primary, new_primary, &snapshot) {
 285                            Ordering::Less => to_remove = old_groups.next(),
 286                            Ordering::Equal => {
 287                                to_keep = old_groups.next();
 288                                new_groups.next();
 289                            }
 290                            Ordering::Greater => to_insert = new_groups.next(),
 291                        }
 292                    }
 293                }
 294
 295                if let Some(group) = to_insert {
 296                    let mut group_state = DiagnosticGroupState {
 297                        primary_diagnostic: group.entries[group.primary_ix].clone(),
 298                        primary_excerpt_ix: 0,
 299                        excerpts: Default::default(),
 300                        blocks: Default::default(),
 301                        block_count: 0,
 302                    };
 303                    let mut pending_range: Option<(Range<Point>, usize)> = None;
 304                    let mut is_first_excerpt_for_group = true;
 305                    for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
 306                        let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
 307                        if let Some((range, start_ix)) = &mut pending_range {
 308                            if let Some(entry) = resolved_entry.as_ref() {
 309                                if entry.range.start.row
 310                                    <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
 311                                {
 312                                    range.end = range.end.max(entry.range.end);
 313                                    continue;
 314                                }
 315                            }
 316
 317                            let excerpt_start =
 318                                Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
 319                            let excerpt_end = snapshot.clip_point(
 320                                Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
 321                                Bias::Left,
 322                            );
 323                            let excerpt_id = excerpts
 324                                .insert_excerpts_after(
 325                                    &prev_excerpt_id,
 326                                    buffer.clone(),
 327                                    [ExcerptRange {
 328                                        context: excerpt_start..excerpt_end,
 329                                        primary: Some(range.clone()),
 330                                    }],
 331                                    excerpts_cx,
 332                                )
 333                                .pop()
 334                                .unwrap();
 335
 336                            prev_excerpt_id = excerpt_id.clone();
 337                            first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
 338                            group_state.excerpts.push(excerpt_id.clone());
 339                            let header_position = (excerpt_id.clone(), language::Anchor::MIN);
 340
 341                            if is_first_excerpt_for_group {
 342                                is_first_excerpt_for_group = false;
 343                                let mut primary =
 344                                    group.entries[group.primary_ix].diagnostic.clone();
 345                                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.clone()),
 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.clone(),
 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 {
 490            if cx.handle().is_focused(cx) {
 491                cx.focus(&self.editor);
 492            }
 493        }
 494        cx.notify();
 495    }
 496
 497    fn update_title(&mut self, cx: &mut ViewContext<Self>) {
 498        self.summary = self.project.read(cx).diagnostic_summary(cx);
 499        cx.emit(Event::TitleChanged);
 500    }
 501}
 502
 503impl workspace::Item for ProjectDiagnosticsEditor {
 504    fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
 505        render_summary(
 506            &self.summary,
 507            &style.label.text,
 508            &cx.global::<Settings>().theme.project_diagnostics,
 509        )
 510    }
 511
 512    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
 513        None
 514    }
 515
 516    fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
 517        self.editor.project_entry_ids(cx)
 518    }
 519
 520    fn is_singleton(&self, _: &AppContext) -> bool {
 521        false
 522    }
 523
 524    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
 525        self.editor
 526            .update(cx, |editor, cx| editor.navigate(data, cx))
 527    }
 528
 529    fn is_dirty(&self, cx: &AppContext) -> bool {
 530        self.excerpts.read(cx).is_dirty(cx)
 531    }
 532
 533    fn has_conflict(&self, cx: &AppContext) -> bool {
 534        self.excerpts.read(cx).has_conflict(cx)
 535    }
 536
 537    fn can_save(&self, _: &AppContext) -> bool {
 538        true
 539    }
 540
 541    fn save(
 542        &mut self,
 543        project: ModelHandle<Project>,
 544        cx: &mut ViewContext<Self>,
 545    ) -> Task<Result<()>> {
 546        self.editor.save(project, cx)
 547    }
 548
 549    fn reload(
 550        &mut self,
 551        project: ModelHandle<Project>,
 552        cx: &mut ViewContext<Self>,
 553    ) -> Task<Result<()>> {
 554        self.editor.reload(project, cx)
 555    }
 556
 557    fn save_as(
 558        &mut self,
 559        _: ModelHandle<Project>,
 560        _: PathBuf,
 561        _: &mut ViewContext<Self>,
 562    ) -> Task<Result<()>> {
 563        unreachable!()
 564    }
 565
 566    fn should_activate_item_on_event(event: &Self::Event) -> bool {
 567        Editor::should_activate_item_on_event(event)
 568    }
 569
 570    fn should_update_tab_on_event(event: &Event) -> bool {
 571        matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
 572    }
 573
 574    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
 575        self.editor.update(cx, |editor, _| {
 576            editor.set_nav_history(Some(nav_history));
 577        });
 578    }
 579
 580    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
 581    where
 582        Self: Sized,
 583    {
 584        Some(ProjectDiagnosticsEditor::new(
 585            self.project.clone(),
 586            self.workspace.clone(),
 587            cx,
 588        ))
 589    }
 590
 591    fn act_as_type(
 592        &self,
 593        type_id: TypeId,
 594        self_handle: &ViewHandle<Self>,
 595        _: &AppContext,
 596    ) -> Option<AnyViewHandle> {
 597        if type_id == TypeId::of::<Self>() {
 598            Some(self_handle.into())
 599        } else if type_id == TypeId::of::<Editor>() {
 600            Some((&self.editor).into())
 601        } else {
 602            None
 603        }
 604    }
 605
 606    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 607        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
 608    }
 609}
 610
 611fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
 612    let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
 613    Arc::new(move |cx| {
 614        let settings = cx.global::<Settings>();
 615        let theme = &settings.theme.editor;
 616        let style = theme.diagnostic_header.clone();
 617        let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
 618        let icon_width = cx.em_width * style.icon_width_factor;
 619        let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
 620            Svg::new("icons/diagnostic-error-10.svg")
 621                .with_color(theme.error_diagnostic.message.text.color)
 622        } else {
 623            Svg::new("icons/diagnostic-warning-10.svg")
 624                .with_color(theme.warning_diagnostic.message.text.color)
 625        };
 626
 627        Flex::row()
 628            .with_child(
 629                icon.constrained()
 630                    .with_width(icon_width)
 631                    .aligned()
 632                    .contained()
 633                    .boxed(),
 634            )
 635            .with_child(
 636                Label::new(
 637                    message.clone(),
 638                    style.message.label.clone().with_font_size(font_size),
 639                )
 640                .with_highlights(highlights.clone())
 641                .contained()
 642                .with_style(style.message.container)
 643                .with_margin_left(cx.gutter_padding)
 644                .aligned()
 645                .boxed(),
 646            )
 647            .with_children(diagnostic.code.clone().map(|code| {
 648                Label::new(code, style.code.text.clone().with_font_size(font_size))
 649                    .contained()
 650                    .with_style(style.code.container)
 651                    .aligned()
 652                    .boxed()
 653            }))
 654            .contained()
 655            .with_style(style.container)
 656            .with_padding_left(cx.gutter_padding)
 657            .with_padding_right(cx.gutter_padding)
 658            .expanded()
 659            .named("diagnostic header")
 660    })
 661}
 662
 663pub(crate) fn render_summary(
 664    summary: &DiagnosticSummary,
 665    text_style: &TextStyle,
 666    theme: &theme::ProjectDiagnostics,
 667) -> ElementBox {
 668    if summary.error_count == 0 && summary.warning_count == 0 {
 669        Label::new("No problems".to_string(), text_style.clone()).boxed()
 670    } else {
 671        let icon_width = theme.tab_icon_width;
 672        let icon_spacing = theme.tab_icon_spacing;
 673        let summary_spacing = theme.tab_summary_spacing;
 674        Flex::row()
 675            .with_children([
 676                Svg::new("icons/diagnostic-summary-error.svg")
 677                    .with_color(text_style.color)
 678                    .constrained()
 679                    .with_width(icon_width)
 680                    .aligned()
 681                    .contained()
 682                    .with_margin_right(icon_spacing)
 683                    .named("no-icon"),
 684                Label::new(
 685                    summary.error_count.to_string(),
 686                    LabelStyle {
 687                        text: text_style.clone(),
 688                        highlight_text: None,
 689                    },
 690                )
 691                .aligned()
 692                .boxed(),
 693                Svg::new("icons/diagnostic-summary-warning.svg")
 694                    .with_color(text_style.color)
 695                    .constrained()
 696                    .with_width(icon_width)
 697                    .aligned()
 698                    .contained()
 699                    .with_margin_left(summary_spacing)
 700                    .with_margin_right(icon_spacing)
 701                    .named("warn-icon"),
 702                Label::new(
 703                    summary.warning_count.to_string(),
 704                    LabelStyle {
 705                        text: text_style.clone(),
 706                        highlight_text: None,
 707                    },
 708                )
 709                .aligned()
 710                .boxed(),
 711            ])
 712            .boxed()
 713    }
 714}
 715
 716fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 717    lhs: &DiagnosticEntry<L>,
 718    rhs: &DiagnosticEntry<R>,
 719    snapshot: &language::BufferSnapshot,
 720) -> Ordering {
 721    lhs.range
 722        .start
 723        .to_offset(&snapshot)
 724        .cmp(&rhs.range.start.to_offset(snapshot))
 725        .then_with(|| {
 726            lhs.range
 727                .end
 728                .to_offset(&snapshot)
 729                .cmp(&rhs.range.end.to_offset(snapshot))
 730        })
 731        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 732}
 733
 734#[cfg(test)]
 735mod tests {
 736    use super::*;
 737    use editor::{
 738        display_map::{BlockContext, TransformBlock},
 739        DisplayPoint,
 740    };
 741    use gpui::TestAppContext;
 742    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
 743    use serde_json::json;
 744    use unindent::Unindent as _;
 745    use workspace::AppState;
 746
 747    #[gpui::test]
 748    async fn test_diagnostics(cx: &mut TestAppContext) {
 749        let app_state = cx.update(AppState::test);
 750        app_state
 751            .fs
 752            .as_fake()
 753            .insert_tree(
 754                "/test",
 755                json!({
 756                    "consts.rs": "
 757                        const a: i32 = 'a';
 758                        const b: i32 = c;
 759                    "
 760                    .unindent(),
 761
 762                    "main.rs": "
 763                        fn main() {
 764                            let x = vec![];
 765                            let y = vec![];
 766                            a(x);
 767                            b(y);
 768                            // comment 1
 769                            // comment 2
 770                            c(y);
 771                            d(x);
 772                        }
 773                    "
 774                    .unindent(),
 775                }),
 776            )
 777            .await;
 778
 779        let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
 780        let workspace = cx.add_view(0, |cx| Workspace::new(project.clone(), 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(0, |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.);
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}