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