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