diagnostics.rs

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