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