diagnostics.rs

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