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