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::AppState;
 711
 712    #[gpui::test]
 713    async fn test_diagnostics(cx: &mut TestAppContext) {
 714        let app_state = cx.update(AppState::test);
 715        app_state
 716            .fs
 717            .as_fake()
 718            .insert_tree(
 719                "/test",
 720                json!({
 721                    "consts.rs": "
 722                        const a: i32 = 'a';
 723                        const b: i32 = c;
 724                    "
 725                    .unindent(),
 726
 727                    "main.rs": "
 728                        fn main() {
 729                            let x = vec![];
 730                            let y = vec![];
 731                            a(x);
 732                            b(y);
 733                            // comment 1
 734                            // comment 2
 735                            c(y);
 736                            d(x);
 737                        }
 738                    "
 739                    .unindent(),
 740                }),
 741            )
 742            .await;
 743
 744        let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
 745        let workspace = cx.add_view(0, |cx| Workspace::new(project.clone(), cx));
 746
 747        // Create some diagnostics
 748        project.update(cx, |project, cx| {
 749            project
 750                .update_diagnostic_entries(
 751                    PathBuf::from("/test/main.rs"),
 752                    None,
 753                    vec![
 754                        DiagnosticEntry {
 755                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
 756                            diagnostic: Diagnostic {
 757                                message:
 758                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 759                                        .to_string(),
 760                                severity: DiagnosticSeverity::INFORMATION,
 761                                is_primary: false,
 762                                is_disk_based: true,
 763                                group_id: 1,
 764                                ..Default::default()
 765                            },
 766                        },
 767                        DiagnosticEntry {
 768                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
 769                            diagnostic: Diagnostic {
 770                                message:
 771                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 772                                        .to_string(),
 773                                severity: DiagnosticSeverity::INFORMATION,
 774                                is_primary: false,
 775                                is_disk_based: true,
 776                                group_id: 0,
 777                                ..Default::default()
 778                            },
 779                        },
 780                        DiagnosticEntry {
 781                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
 782                            diagnostic: Diagnostic {
 783                                message: "value moved here".to_string(),
 784                                severity: DiagnosticSeverity::INFORMATION,
 785                                is_primary: false,
 786                                is_disk_based: true,
 787                                group_id: 1,
 788                                ..Default::default()
 789                            },
 790                        },
 791                        DiagnosticEntry {
 792                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
 793                            diagnostic: Diagnostic {
 794                                message: "value moved here".to_string(),
 795                                severity: DiagnosticSeverity::INFORMATION,
 796                                is_primary: false,
 797                                is_disk_based: true,
 798                                group_id: 0,
 799                                ..Default::default()
 800                            },
 801                        },
 802                        DiagnosticEntry {
 803                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
 804                            diagnostic: Diagnostic {
 805                                message: "use of moved value\nvalue used here after move".to_string(),
 806                                severity: DiagnosticSeverity::ERROR,
 807                                is_primary: true,
 808                                is_disk_based: true,
 809                                group_id: 0,
 810                                ..Default::default()
 811                            },
 812                        },
 813                        DiagnosticEntry {
 814                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
 815                            diagnostic: Diagnostic {
 816                                message: "use of moved value\nvalue used here after move".to_string(),
 817                                severity: DiagnosticSeverity::ERROR,
 818                                is_primary: true,
 819                                is_disk_based: true,
 820                                group_id: 1,
 821                                ..Default::default()
 822                            },
 823                        },
 824                    ],
 825                    cx,
 826                )
 827                .unwrap();
 828        });
 829
 830        // Open the project diagnostics view while there are already diagnostics.
 831        let view = cx.add_view(0, |cx| {
 832            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
 833        });
 834
 835        view.next_notification(&cx).await;
 836        view.update(cx, |view, cx| {
 837            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
 838
 839            assert_eq!(
 840                editor_blocks(&editor, cx),
 841                [
 842                    (0, "path header block".into()),
 843                    (2, "diagnostic header".into()),
 844                    (15, "collapsed context".into()),
 845                    (16, "diagnostic header".into()),
 846                    (25, "collapsed context".into()),
 847                ]
 848            );
 849            assert_eq!(
 850                editor.text(),
 851                concat!(
 852                    //
 853                    // main.rs
 854                    //
 855                    "\n", // filename
 856                    "\n", // padding
 857                    // diagnostic group 1
 858                    "\n", // primary message
 859                    "\n", // padding
 860                    "    let x = vec![];\n",
 861                    "    let y = vec![];\n",
 862                    "\n", // supporting diagnostic
 863                    "    a(x);\n",
 864                    "    b(y);\n",
 865                    "\n", // supporting diagnostic
 866                    "    // comment 1\n",
 867                    "    // comment 2\n",
 868                    "    c(y);\n",
 869                    "\n", // supporting diagnostic
 870                    "    d(x);\n",
 871                    "\n", // context ellipsis
 872                    // diagnostic group 2
 873                    "\n", // primary message
 874                    "\n", // padding
 875                    "fn main() {\n",
 876                    "    let x = vec![];\n",
 877                    "\n", // supporting diagnostic
 878                    "    let y = vec![];\n",
 879                    "    a(x);\n",
 880                    "\n", // supporting diagnostic
 881                    "    b(y);\n",
 882                    "\n", // context ellipsis
 883                    "    c(y);\n",
 884                    "    d(x);\n",
 885                    "\n", // supporting diagnostic
 886                    "}"
 887                )
 888            );
 889
 890            // Cursor is at the first diagnostic
 891            view.editor.update(cx, |editor, cx| {
 892                assert_eq!(
 893                    editor.selections.display_ranges(cx),
 894                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
 895                );
 896            });
 897        });
 898
 899        // Diagnostics are added for another earlier path.
 900        project.update(cx, |project, cx| {
 901            project.disk_based_diagnostics_started(cx);
 902            project
 903                .update_diagnostic_entries(
 904                    PathBuf::from("/test/consts.rs"),
 905                    None,
 906                    vec![DiagnosticEntry {
 907                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
 908                        diagnostic: Diagnostic {
 909                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
 910                            severity: DiagnosticSeverity::ERROR,
 911                            is_primary: true,
 912                            is_disk_based: true,
 913                            group_id: 0,
 914                            ..Default::default()
 915                        },
 916                    }],
 917                    cx,
 918                )
 919                .unwrap();
 920            project.disk_based_diagnostics_finished(cx);
 921        });
 922
 923        view.next_notification(&cx).await;
 924        view.update(cx, |view, cx| {
 925            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
 926
 927            assert_eq!(
 928                editor_blocks(&editor, cx),
 929                [
 930                    (0, "path header block".into()),
 931                    (2, "diagnostic header".into()),
 932                    (7, "path header block".into()),
 933                    (9, "diagnostic header".into()),
 934                    (22, "collapsed context".into()),
 935                    (23, "diagnostic header".into()),
 936                    (32, "collapsed context".into()),
 937                ]
 938            );
 939            assert_eq!(
 940                editor.text(),
 941                concat!(
 942                    //
 943                    // consts.rs
 944                    //
 945                    "\n", // filename
 946                    "\n", // padding
 947                    // diagnostic group 1
 948                    "\n", // primary message
 949                    "\n", // padding
 950                    "const a: i32 = 'a';\n",
 951                    "\n", // supporting diagnostic
 952                    "const b: i32 = c;\n",
 953                    //
 954                    // main.rs
 955                    //
 956                    "\n", // filename
 957                    "\n", // padding
 958                    // diagnostic group 1
 959                    "\n", // primary message
 960                    "\n", // padding
 961                    "    let x = vec![];\n",
 962                    "    let y = vec![];\n",
 963                    "\n", // supporting diagnostic
 964                    "    a(x);\n",
 965                    "    b(y);\n",
 966                    "\n", // supporting diagnostic
 967                    "    // comment 1\n",
 968                    "    // comment 2\n",
 969                    "    c(y);\n",
 970                    "\n", // supporting diagnostic
 971                    "    d(x);\n",
 972                    "\n", // collapsed context
 973                    // diagnostic group 2
 974                    "\n", // primary message
 975                    "\n", // filename
 976                    "fn main() {\n",
 977                    "    let x = vec![];\n",
 978                    "\n", // supporting diagnostic
 979                    "    let y = vec![];\n",
 980                    "    a(x);\n",
 981                    "\n", // supporting diagnostic
 982                    "    b(y);\n",
 983                    "\n", // context ellipsis
 984                    "    c(y);\n",
 985                    "    d(x);\n",
 986                    "\n", // supporting diagnostic
 987                    "}"
 988                )
 989            );
 990
 991            // Cursor keeps its position.
 992            view.editor.update(cx, |editor, cx| {
 993                assert_eq!(
 994                    editor.selections.display_ranges(cx),
 995                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
 996                );
 997            });
 998        });
 999
1000        // Diagnostics are added to the first path
1001        project.update(cx, |project, cx| {
1002            project.disk_based_diagnostics_started(cx);
1003            project
1004                .update_diagnostic_entries(
1005                    PathBuf::from("/test/consts.rs"),
1006                    None,
1007                    vec![
1008                        DiagnosticEntry {
1009                            range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1010                            diagnostic: Diagnostic {
1011                                message: "mismatched types\nexpected `usize`, found `char`"
1012                                    .to_string(),
1013                                severity: DiagnosticSeverity::ERROR,
1014                                is_primary: true,
1015                                is_disk_based: true,
1016                                group_id: 0,
1017                                ..Default::default()
1018                            },
1019                        },
1020                        DiagnosticEntry {
1021                            range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1022                            diagnostic: Diagnostic {
1023                                message: "unresolved name `c`".to_string(),
1024                                severity: DiagnosticSeverity::ERROR,
1025                                is_primary: true,
1026                                is_disk_based: true,
1027                                group_id: 1,
1028                                ..Default::default()
1029                            },
1030                        },
1031                    ],
1032                    cx,
1033                )
1034                .unwrap();
1035            project.disk_based_diagnostics_finished(cx);
1036        });
1037
1038        view.next_notification(&cx).await;
1039        view.update(cx, |view, cx| {
1040            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1041
1042            assert_eq!(
1043                editor_blocks(&editor, cx),
1044                [
1045                    (0, "path header block".into()),
1046                    (2, "diagnostic header".into()),
1047                    (7, "collapsed context".into()),
1048                    (8, "diagnostic header".into()),
1049                    (13, "path header block".into()),
1050                    (15, "diagnostic header".into()),
1051                    (28, "collapsed context".into()),
1052                    (29, "diagnostic header".into()),
1053                    (38, "collapsed context".into()),
1054                ]
1055            );
1056            assert_eq!(
1057                editor.text(),
1058                concat!(
1059                    //
1060                    // consts.rs
1061                    //
1062                    "\n", // filename
1063                    "\n", // padding
1064                    // diagnostic group 1
1065                    "\n", // primary message
1066                    "\n", // padding
1067                    "const a: i32 = 'a';\n",
1068                    "\n", // supporting diagnostic
1069                    "const b: i32 = c;\n",
1070                    "\n", // context ellipsis
1071                    // diagnostic group 2
1072                    "\n", // primary message
1073                    "\n", // padding
1074                    "const a: i32 = 'a';\n",
1075                    "const b: i32 = c;\n",
1076                    "\n", // supporting diagnostic
1077                    //
1078                    // main.rs
1079                    //
1080                    "\n", // filename
1081                    "\n", // padding
1082                    // diagnostic group 1
1083                    "\n", // primary message
1084                    "\n", // padding
1085                    "    let x = vec![];\n",
1086                    "    let y = vec![];\n",
1087                    "\n", // supporting diagnostic
1088                    "    a(x);\n",
1089                    "    b(y);\n",
1090                    "\n", // supporting diagnostic
1091                    "    // comment 1\n",
1092                    "    // comment 2\n",
1093                    "    c(y);\n",
1094                    "\n", // supporting diagnostic
1095                    "    d(x);\n",
1096                    "\n", // context ellipsis
1097                    // diagnostic group 2
1098                    "\n", // primary message
1099                    "\n", // filename
1100                    "fn main() {\n",
1101                    "    let x = vec![];\n",
1102                    "\n", // supporting diagnostic
1103                    "    let y = vec![];\n",
1104                    "    a(x);\n",
1105                    "\n", // supporting diagnostic
1106                    "    b(y);\n",
1107                    "\n", // context ellipsis
1108                    "    c(y);\n",
1109                    "    d(x);\n",
1110                    "\n", // supporting diagnostic
1111                    "}"
1112                )
1113            );
1114        });
1115    }
1116
1117    fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1118        editor
1119            .blocks_in_range(0..editor.max_point().row())
1120            .filter_map(|(row, block)| {
1121                let name = match block {
1122                    TransformBlock::Custom(block) => block
1123                        .render(&BlockContext {
1124                            cx,
1125                            anchor_x: 0.,
1126                            scroll_x: 0.,
1127                            gutter_padding: 0.,
1128                            gutter_width: 0.,
1129                            line_height: 0.,
1130                            em_width: 0.,
1131                        })
1132                        .name()?
1133                        .to_string(),
1134                    TransformBlock::ExcerptHeader {
1135                        starts_new_buffer, ..
1136                    } => {
1137                        if *starts_new_buffer {
1138                            "path header block".to_string()
1139                        } else {
1140                            "collapsed context".to_string()
1141                        }
1142                    }
1143                };
1144
1145                Some((row, name))
1146            })
1147            .collect()
1148    }
1149}