diagnostics.rs

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