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    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 = editor.refresh_selections(cx);
 421                selections = editor.local_selections::<usize>(cx);
 422            }
 423
 424            // If any selection has lost its position, move it to start of the next primary diagnostic.
 425            for selection in &mut selections {
 426                if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
 427                    let group_ix = match groups.binary_search_by(|probe| {
 428                        probe.excerpts.last().unwrap().cmp(&new_excerpt_id)
 429                    }) {
 430                        Ok(ix) | Err(ix) => ix,
 431                    };
 432                    if let Some(group) = groups.get(group_ix) {
 433                        let offset = excerpts_snapshot
 434                            .anchor_in_excerpt(
 435                                group.excerpts[group.primary_excerpt_ix].clone(),
 436                                group.primary_diagnostic.range.start.clone(),
 437                            )
 438                            .to_offset(&excerpts_snapshot);
 439                        selection.start = offset;
 440                        selection.end = offset;
 441                    }
 442                }
 443            }
 444            editor.update_selections(selections, None, cx);
 445            Some(())
 446        });
 447
 448        if self.path_states.is_empty() {
 449            if self.editor.is_focused(cx) {
 450                cx.focus_self();
 451            }
 452        } else {
 453            if cx.handle().is_focused(cx) {
 454                cx.focus(&self.editor);
 455            }
 456        }
 457        cx.notify();
 458    }
 459
 460    fn update_title(&mut self, cx: &mut ViewContext<Self>) {
 461        self.summary = self.project.read(cx).diagnostic_summary(cx);
 462        cx.emit(Event::TitleChanged);
 463    }
 464}
 465
 466impl workspace::Item for ProjectDiagnosticsEditor {
 467    fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox {
 468        render_summary(
 469            &self.summary,
 470            &style.label.text,
 471            &cx.global::<Settings>().theme.project_diagnostics,
 472        )
 473    }
 474
 475    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
 476        None
 477    }
 478
 479    fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
 480        None
 481    }
 482
 483    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
 484        self.editor
 485            .update(cx, |editor, cx| editor.navigate(data, cx))
 486    }
 487
 488    fn is_dirty(&self, cx: &AppContext) -> bool {
 489        self.excerpts.read(cx).read(cx).is_dirty()
 490    }
 491
 492    fn has_conflict(&self, cx: &AppContext) -> bool {
 493        self.excerpts.read(cx).read(cx).has_conflict()
 494    }
 495
 496    fn can_save(&self, _: &AppContext) -> bool {
 497        true
 498    }
 499
 500    fn save(
 501        &mut self,
 502        project: ModelHandle<Project>,
 503        cx: &mut ViewContext<Self>,
 504    ) -> Task<Result<()>> {
 505        self.editor.save(project, cx)
 506    }
 507
 508    fn reload(
 509        &mut self,
 510        project: ModelHandle<Project>,
 511        cx: &mut ViewContext<Self>,
 512    ) -> Task<Result<()>> {
 513        self.editor.reload(project, cx)
 514    }
 515
 516    fn can_save_as(&self, _: &AppContext) -> bool {
 517        false
 518    }
 519
 520    fn save_as(
 521        &mut self,
 522        _: ModelHandle<Project>,
 523        _: PathBuf,
 524        _: &mut ViewContext<Self>,
 525    ) -> Task<Result<()>> {
 526        unreachable!()
 527    }
 528
 529    fn should_activate_item_on_event(event: &Self::Event) -> bool {
 530        Editor::should_activate_item_on_event(event)
 531    }
 532
 533    fn should_update_tab_on_event(event: &Event) -> bool {
 534        matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
 535    }
 536
 537    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
 538        self.editor.update(cx, |editor, _| {
 539            editor.set_nav_history(Some(nav_history));
 540        });
 541    }
 542
 543    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
 544    where
 545        Self: Sized,
 546    {
 547        Some(ProjectDiagnosticsEditor::new(
 548            self.project.clone(),
 549            self.workspace.clone(),
 550            cx,
 551        ))
 552    }
 553
 554    fn act_as_type(
 555        &self,
 556        type_id: TypeId,
 557        self_handle: &ViewHandle<Self>,
 558        _: &AppContext,
 559    ) -> Option<AnyViewHandle> {
 560        if type_id == TypeId::of::<Self>() {
 561            Some(self_handle.into())
 562        } else if type_id == TypeId::of::<Editor>() {
 563            Some((&self.editor).into())
 564        } else {
 565            None
 566        }
 567    }
 568
 569    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 570        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
 571    }
 572}
 573
 574fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
 575    let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
 576    Arc::new(move |cx| {
 577        let settings = cx.global::<Settings>();
 578        let theme = &settings.theme.editor;
 579        let style = &theme.diagnostic_header;
 580        let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
 581        let icon_width = cx.em_width * style.icon_width_factor;
 582        let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
 583            Svg::new("icons/diagnostic-error-10.svg")
 584                .with_color(theme.error_diagnostic.message.text.color)
 585        } else {
 586            Svg::new("icons/diagnostic-warning-10.svg")
 587                .with_color(theme.warning_diagnostic.message.text.color)
 588        };
 589
 590        Flex::row()
 591            .with_child(
 592                icon.constrained()
 593                    .with_width(icon_width)
 594                    .aligned()
 595                    .contained()
 596                    .boxed(),
 597            )
 598            .with_child(
 599                Label::new(
 600                    message.clone(),
 601                    style.message.label.clone().with_font_size(font_size),
 602                )
 603                .with_highlights(highlights.clone())
 604                .contained()
 605                .with_style(style.message.container)
 606                .with_margin_left(cx.gutter_padding)
 607                .aligned()
 608                .boxed(),
 609            )
 610            .with_children(diagnostic.code.clone().map(|code| {
 611                Label::new(code, style.code.text.clone().with_font_size(font_size))
 612                    .contained()
 613                    .with_style(style.code.container)
 614                    .aligned()
 615                    .boxed()
 616            }))
 617            .contained()
 618            .with_style(style.container)
 619            .with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width)
 620            .expanded()
 621            .named("diagnostic header")
 622    })
 623}
 624
 625pub(crate) fn render_summary(
 626    summary: &DiagnosticSummary,
 627    text_style: &TextStyle,
 628    theme: &theme::ProjectDiagnostics,
 629) -> ElementBox {
 630    if summary.error_count == 0 && summary.warning_count == 0 {
 631        Label::new("No problems".to_string(), text_style.clone()).boxed()
 632    } else {
 633        let icon_width = theme.tab_icon_width;
 634        let icon_spacing = theme.tab_icon_spacing;
 635        let summary_spacing = theme.tab_summary_spacing;
 636        Flex::row()
 637            .with_children([
 638                Svg::new("icons/diagnostic-summary-error.svg")
 639                    .with_color(text_style.color)
 640                    .constrained()
 641                    .with_width(icon_width)
 642                    .aligned()
 643                    .contained()
 644                    .with_margin_right(icon_spacing)
 645                    .named("no-icon"),
 646                Label::new(
 647                    summary.error_count.to_string(),
 648                    LabelStyle {
 649                        text: text_style.clone(),
 650                        highlight_text: None,
 651                    },
 652                )
 653                .aligned()
 654                .boxed(),
 655                Svg::new("icons/diagnostic-summary-warning.svg")
 656                    .with_color(text_style.color)
 657                    .constrained()
 658                    .with_width(icon_width)
 659                    .aligned()
 660                    .contained()
 661                    .with_margin_left(summary_spacing)
 662                    .with_margin_right(icon_spacing)
 663                    .named("warn-icon"),
 664                Label::new(
 665                    summary.warning_count.to_string(),
 666                    LabelStyle {
 667                        text: text_style.clone(),
 668                        highlight_text: None,
 669                    },
 670                )
 671                .aligned()
 672                .boxed(),
 673            ])
 674            .boxed()
 675    }
 676}
 677
 678fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 679    lhs: &DiagnosticEntry<L>,
 680    rhs: &DiagnosticEntry<R>,
 681    snapshot: &language::BufferSnapshot,
 682) -> Ordering {
 683    lhs.range
 684        .start
 685        .to_offset(&snapshot)
 686        .cmp(&rhs.range.start.to_offset(snapshot))
 687        .then_with(|| {
 688            lhs.range
 689                .end
 690                .to_offset(&snapshot)
 691                .cmp(&rhs.range.end.to_offset(snapshot))
 692        })
 693        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 694}
 695
 696#[cfg(test)]
 697mod tests {
 698    use super::*;
 699    use editor::{
 700        display_map::{BlockContext, TransformBlock},
 701        DisplayPoint, EditorSnapshot,
 702    };
 703    use gpui::TestAppContext;
 704    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
 705    use serde_json::json;
 706    use unindent::Unindent as _;
 707    use workspace::WorkspaceParams;
 708
 709    #[gpui::test]
 710    async fn test_diagnostics(cx: &mut TestAppContext) {
 711        let params = cx.update(WorkspaceParams::test);
 712        let project = params.project.clone();
 713        let workspace = cx.add_view(0, |cx| Workspace::new(&params, cx));
 714
 715        params
 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        project
 745            .update(cx, |project, cx| {
 746                project.find_or_create_local_worktree("/test", true, cx)
 747            })
 748            .await
 749            .unwrap();
 750
 751        // Create some diagnostics
 752        project.update(cx, |project, cx| {
 753            project
 754                .update_diagnostic_entries(
 755                    PathBuf::from("/test/main.rs"),
 756                    None,
 757                    vec![
 758                        DiagnosticEntry {
 759                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
 760                            diagnostic: Diagnostic {
 761                                message:
 762                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 763                                        .to_string(),
 764                                severity: DiagnosticSeverity::INFORMATION,
 765                                is_primary: false,
 766                                is_disk_based: true,
 767                                group_id: 1,
 768                                ..Default::default()
 769                            },
 770                        },
 771                        DiagnosticEntry {
 772                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
 773                            diagnostic: Diagnostic {
 774                                message:
 775                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 776                                        .to_string(),
 777                                severity: DiagnosticSeverity::INFORMATION,
 778                                is_primary: false,
 779                                is_disk_based: true,
 780                                group_id: 0,
 781                                ..Default::default()
 782                            },
 783                        },
 784                        DiagnosticEntry {
 785                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
 786                            diagnostic: Diagnostic {
 787                                message: "value moved here".to_string(),
 788                                severity: DiagnosticSeverity::INFORMATION,
 789                                is_primary: false,
 790                                is_disk_based: true,
 791                                group_id: 1,
 792                                ..Default::default()
 793                            },
 794                        },
 795                        DiagnosticEntry {
 796                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
 797                            diagnostic: Diagnostic {
 798                                message: "value moved here".to_string(),
 799                                severity: DiagnosticSeverity::INFORMATION,
 800                                is_primary: false,
 801                                is_disk_based: true,
 802                                group_id: 0,
 803                                ..Default::default()
 804                            },
 805                        },
 806                        DiagnosticEntry {
 807                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
 808                            diagnostic: Diagnostic {
 809                                message: "use of moved value\nvalue used here after move".to_string(),
 810                                severity: DiagnosticSeverity::ERROR,
 811                                is_primary: true,
 812                                is_disk_based: true,
 813                                group_id: 0,
 814                                ..Default::default()
 815                            },
 816                        },
 817                        DiagnosticEntry {
 818                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
 819                            diagnostic: Diagnostic {
 820                                message: "use of moved value\nvalue used here after move".to_string(),
 821                                severity: DiagnosticSeverity::ERROR,
 822                                is_primary: true,
 823                                is_disk_based: true,
 824                                group_id: 1,
 825                                ..Default::default()
 826                            },
 827                        },
 828                    ],
 829                    cx,
 830                )
 831                .unwrap();
 832        });
 833
 834        // Open the project diagnostics view while there are already diagnostics.
 835        let view = cx.add_view(0, |cx| {
 836            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
 837        });
 838
 839        view.next_notification(&cx).await;
 840        view.update(cx, |view, cx| {
 841            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
 842
 843            assert_eq!(
 844                editor_blocks(&editor, cx),
 845                [
 846                    (0, "path header block".into()),
 847                    (2, "diagnostic header".into()),
 848                    (15, "collapsed context".into()),
 849                    (16, "diagnostic header".into()),
 850                    (25, "collapsed context".into()),
 851                ]
 852            );
 853            assert_eq!(
 854                editor.text(),
 855                concat!(
 856                    //
 857                    // main.rs
 858                    //
 859                    "\n", // filename
 860                    "\n", // padding
 861                    // diagnostic group 1
 862                    "\n", // primary message
 863                    "\n", // padding
 864                    "    let x = vec![];\n",
 865                    "    let y = vec![];\n",
 866                    "\n", // supporting diagnostic
 867                    "    a(x);\n",
 868                    "    b(y);\n",
 869                    "\n", // supporting diagnostic
 870                    "    // comment 1\n",
 871                    "    // comment 2\n",
 872                    "    c(y);\n",
 873                    "\n", // supporting diagnostic
 874                    "    d(x);\n",
 875                    "\n", // context ellipsis
 876                    // diagnostic group 2
 877                    "\n", // primary message
 878                    "\n", // padding
 879                    "fn main() {\n",
 880                    "    let x = vec![];\n",
 881                    "\n", // supporting diagnostic
 882                    "    let y = vec![];\n",
 883                    "    a(x);\n",
 884                    "\n", // supporting diagnostic
 885                    "    b(y);\n",
 886                    "\n", // context ellipsis
 887                    "    c(y);\n",
 888                    "    d(x);\n",
 889                    "\n", // supporting diagnostic
 890                    "}"
 891                )
 892            );
 893
 894            // Cursor is at the first diagnostic
 895            view.editor.update(cx, |editor, cx| {
 896                assert_eq!(
 897                    editor.selected_display_ranges(cx),
 898                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
 899                );
 900            });
 901        });
 902
 903        // Diagnostics are added for another earlier path.
 904        project.update(cx, |project, cx| {
 905            project.disk_based_diagnostics_started(cx);
 906            project
 907                .update_diagnostic_entries(
 908                    PathBuf::from("/test/consts.rs"),
 909                    None,
 910                    vec![DiagnosticEntry {
 911                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
 912                        diagnostic: Diagnostic {
 913                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
 914                            severity: DiagnosticSeverity::ERROR,
 915                            is_primary: true,
 916                            is_disk_based: true,
 917                            group_id: 0,
 918                            ..Default::default()
 919                        },
 920                    }],
 921                    cx,
 922                )
 923                .unwrap();
 924            project.disk_based_diagnostics_finished(cx);
 925        });
 926
 927        view.next_notification(&cx).await;
 928        view.update(cx, |view, cx| {
 929            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
 930
 931            assert_eq!(
 932                editor_blocks(&editor, cx),
 933                [
 934                    (0, "path header block".into()),
 935                    (2, "diagnostic header".into()),
 936                    (7, "path header block".into()),
 937                    (9, "diagnostic header".into()),
 938                    (22, "collapsed context".into()),
 939                    (23, "diagnostic header".into()),
 940                    (32, "collapsed context".into()),
 941                ]
 942            );
 943            assert_eq!(
 944                editor.text(),
 945                concat!(
 946                    //
 947                    // consts.rs
 948                    //
 949                    "\n", // filename
 950                    "\n", // padding
 951                    // diagnostic group 1
 952                    "\n", // primary message
 953                    "\n", // padding
 954                    "const a: i32 = 'a';\n",
 955                    "\n", // supporting diagnostic
 956                    "const b: i32 = c;\n",
 957                    //
 958                    // main.rs
 959                    //
 960                    "\n", // filename
 961                    "\n", // padding
 962                    // diagnostic group 1
 963                    "\n", // primary message
 964                    "\n", // padding
 965                    "    let x = vec![];\n",
 966                    "    let y = vec![];\n",
 967                    "\n", // supporting diagnostic
 968                    "    a(x);\n",
 969                    "    b(y);\n",
 970                    "\n", // supporting diagnostic
 971                    "    // comment 1\n",
 972                    "    // comment 2\n",
 973                    "    c(y);\n",
 974                    "\n", // supporting diagnostic
 975                    "    d(x);\n",
 976                    "\n", // collapsed context
 977                    // diagnostic group 2
 978                    "\n", // primary message
 979                    "\n", // filename
 980                    "fn main() {\n",
 981                    "    let x = vec![];\n",
 982                    "\n", // supporting diagnostic
 983                    "    let y = vec![];\n",
 984                    "    a(x);\n",
 985                    "\n", // supporting diagnostic
 986                    "    b(y);\n",
 987                    "\n", // context ellipsis
 988                    "    c(y);\n",
 989                    "    d(x);\n",
 990                    "\n", // supporting diagnostic
 991                    "}"
 992                )
 993            );
 994
 995            // Cursor keeps its position.
 996            view.editor.update(cx, |editor, cx| {
 997                assert_eq!(
 998                    editor.selected_display_ranges(cx),
 999                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1000                );
1001            });
1002        });
1003
1004        // Diagnostics are added to the first path
1005        project.update(cx, |project, cx| {
1006            project.disk_based_diagnostics_started(cx);
1007            project
1008                .update_diagnostic_entries(
1009                    PathBuf::from("/test/consts.rs"),
1010                    None,
1011                    vec![
1012                        DiagnosticEntry {
1013                            range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1014                            diagnostic: Diagnostic {
1015                                message: "mismatched types\nexpected `usize`, found `char`"
1016                                    .to_string(),
1017                                severity: DiagnosticSeverity::ERROR,
1018                                is_primary: true,
1019                                is_disk_based: true,
1020                                group_id: 0,
1021                                ..Default::default()
1022                            },
1023                        },
1024                        DiagnosticEntry {
1025                            range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1026                            diagnostic: Diagnostic {
1027                                message: "unresolved name `c`".to_string(),
1028                                severity: DiagnosticSeverity::ERROR,
1029                                is_primary: true,
1030                                is_disk_based: true,
1031                                group_id: 1,
1032                                ..Default::default()
1033                            },
1034                        },
1035                    ],
1036                    cx,
1037                )
1038                .unwrap();
1039            project.disk_based_diagnostics_finished(cx);
1040        });
1041
1042        view.next_notification(&cx).await;
1043        view.update(cx, |view, cx| {
1044            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1045
1046            assert_eq!(
1047                editor_blocks(&editor, cx),
1048                [
1049                    (0, "path header block".into()),
1050                    (2, "diagnostic header".into()),
1051                    (7, "collapsed context".into()),
1052                    (8, "diagnostic header".into()),
1053                    (13, "path header block".into()),
1054                    (15, "diagnostic header".into()),
1055                    (28, "collapsed context".into()),
1056                    (29, "diagnostic header".into()),
1057                    (38, "collapsed context".into()),
1058                ]
1059            );
1060            assert_eq!(
1061                editor.text(),
1062                concat!(
1063                    //
1064                    // consts.rs
1065                    //
1066                    "\n", // filename
1067                    "\n", // padding
1068                    // diagnostic group 1
1069                    "\n", // primary message
1070                    "\n", // padding
1071                    "const a: i32 = 'a';\n",
1072                    "\n", // supporting diagnostic
1073                    "const b: i32 = c;\n",
1074                    "\n", // context ellipsis
1075                    // diagnostic group 2
1076                    "\n", // primary message
1077                    "\n", // padding
1078                    "const a: i32 = 'a';\n",
1079                    "const b: i32 = c;\n",
1080                    "\n", // supporting diagnostic
1081                    //
1082                    // main.rs
1083                    //
1084                    "\n", // filename
1085                    "\n", // padding
1086                    // diagnostic group 1
1087                    "\n", // primary message
1088                    "\n", // padding
1089                    "    let x = vec![];\n",
1090                    "    let y = vec![];\n",
1091                    "\n", // supporting diagnostic
1092                    "    a(x);\n",
1093                    "    b(y);\n",
1094                    "\n", // supporting diagnostic
1095                    "    // comment 1\n",
1096                    "    // comment 2\n",
1097                    "    c(y);\n",
1098                    "\n", // supporting diagnostic
1099                    "    d(x);\n",
1100                    "\n", // context ellipsis
1101                    // diagnostic group 2
1102                    "\n", // primary message
1103                    "\n", // filename
1104                    "fn main() {\n",
1105                    "    let x = vec![];\n",
1106                    "\n", // supporting diagnostic
1107                    "    let y = vec![];\n",
1108                    "    a(x);\n",
1109                    "\n", // supporting diagnostic
1110                    "    b(y);\n",
1111                    "\n", // context ellipsis
1112                    "    c(y);\n",
1113                    "    d(x);\n",
1114                    "\n", // supporting diagnostic
1115                    "}"
1116                )
1117            );
1118        });
1119    }
1120
1121    fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1122        editor
1123            .blocks_in_range(0..editor.max_point().row())
1124            .filter_map(|(row, block)| {
1125                let name = match block {
1126                    TransformBlock::Custom(block) => block
1127                        .render(&BlockContext {
1128                            cx,
1129                            anchor_x: 0.,
1130                            scroll_x: 0.,
1131                            gutter_padding: 0.,
1132                            gutter_width: 0.,
1133                            line_height: 0.,
1134                            em_width: 0.,
1135                        })
1136                        .name()?
1137                        .to_string(),
1138                    TransformBlock::ExcerptHeader {
1139                        starts_new_buffer, ..
1140                    } => {
1141                        if *starts_new_buffer {
1142                            "path header block".to_string()
1143                        } else {
1144                            "collapsed context".to_string()
1145                        }
1146                    }
1147                };
1148
1149                Some((row, name))
1150            })
1151            .collect()
1152    }
1153}