diagnostics.rs

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