diagnostics.rs

   1pub mod items;
   2mod project_diagnostics_settings;
   3mod toolbar_controls;
   4
   5use anyhow::{Context as _, Result};
   6use collections::{HashMap, HashSet};
   7use editor::{
   8    diagnostic_block_renderer,
   9    display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
  10    highlight_diagnostic_message,
  11    scroll::autoscroll::Autoscroll,
  12    Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
  13};
  14use futures::future::try_join_all;
  15use gpui::{
  16    actions, div, AnyElement, AnyView, AppContext, Component, Context, Div, EventEmitter,
  17    FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView, InteractiveComponent,
  18    Model, ParentComponent, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
  19    VisualContext, WeakView,
  20};
  21use language::{
  22    Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
  23    SelectionGoal,
  24};
  25use lsp::LanguageServerId;
  26use project::{DiagnosticSummary, Project, ProjectPath};
  27use project_diagnostics_settings::ProjectDiagnosticsSettings;
  28use settings::Settings;
  29use std::{
  30    any::{Any, TypeId},
  31    cmp::Ordering,
  32    mem,
  33    ops::Range,
  34    path::PathBuf,
  35    sync::Arc,
  36};
  37pub use toolbar_controls::ToolbarControls;
  38use ui::{h_stack, HighlightedLabel, Icon, IconElement, Label, TextColor};
  39use util::TryFutureExt;
  40use workspace::{
  41    item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
  42    ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
  43};
  44
  45actions!(Deploy, ToggleWarnings);
  46
  47const CONTEXT_LINE_COUNT: u32 = 1;
  48
  49pub fn init(cx: &mut AppContext) {
  50    ProjectDiagnosticsSettings::register(cx);
  51    cx.observe_new_views(ProjectDiagnosticsEditor::register)
  52        .detach();
  53}
  54
  55struct ProjectDiagnosticsEditor {
  56    project: Model<Project>,
  57    workspace: WeakView<Workspace>,
  58    focus_handle: FocusHandle,
  59    editor: View<Editor>,
  60    summary: DiagnosticSummary,
  61    excerpts: Model<MultiBuffer>,
  62    path_states: Vec<PathState>,
  63    paths_to_update: HashMap<LanguageServerId, HashSet<ProjectPath>>,
  64    current_diagnostics: HashMap<LanguageServerId, HashSet<ProjectPath>>,
  65    include_warnings: bool,
  66    _subscriptions: Vec<Subscription>,
  67}
  68
  69struct PathState {
  70    path: ProjectPath,
  71    diagnostic_groups: Vec<DiagnosticGroupState>,
  72}
  73
  74#[derive(Clone, Debug, PartialEq)]
  75struct Jump {
  76    path: ProjectPath,
  77    position: Point,
  78    anchor: Anchor,
  79}
  80
  81struct DiagnosticGroupState {
  82    language_server_id: LanguageServerId,
  83    primary_diagnostic: DiagnosticEntry<language::Anchor>,
  84    primary_excerpt_ix: usize,
  85    excerpts: Vec<ExcerptId>,
  86    blocks: HashSet<BlockId>,
  87    block_count: usize,
  88}
  89
  90impl EventEmitter<ItemEvent> for ProjectDiagnosticsEditor {}
  91
  92impl Render for ProjectDiagnosticsEditor {
  93    type Element = Focusable<Self, Div<Self>>;
  94
  95    fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
  96        let child = if self.path_states.is_empty() {
  97            div()
  98                .flex()
  99                .items_center()
 100                .justify_center()
 101                .size_full()
 102                .child(Label::new("No problems in workspace"))
 103        } else {
 104            div().size_full().child(self.editor.clone())
 105        };
 106
 107        div()
 108            .track_focus(&self.focus_handle)
 109            .size_full()
 110            .on_focus_in(Self::focus_in)
 111            .on_action(Self::toggle_warnings)
 112            .child(child)
 113    }
 114}
 115
 116impl ProjectDiagnosticsEditor {
 117    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 118        workspace.register_action(Self::deploy);
 119    }
 120
 121    fn new(
 122        project_handle: Model<Project>,
 123        workspace: WeakView<Workspace>,
 124        cx: &mut ViewContext<Self>,
 125    ) -> Self {
 126        let project_event_subscription =
 127            cx.subscribe(&project_handle, |this, _, event, cx| match event {
 128                project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
 129                    log::debug!("Disk based diagnostics finished for server {language_server_id}");
 130                    this.update_excerpts(Some(*language_server_id), cx);
 131                }
 132                project::Event::DiagnosticsUpdated {
 133                    language_server_id,
 134                    path,
 135                } => {
 136                    log::debug!("Adding path {path:?} to update for server {language_server_id}");
 137                    this.paths_to_update
 138                        .entry(*language_server_id)
 139                        .or_default()
 140                        .insert(path.clone());
 141                    if this.editor.read(cx).selections.all::<usize>(cx).is_empty()
 142                        && !this.is_dirty(cx)
 143                    {
 144                        this.update_excerpts(Some(*language_server_id), cx);
 145                    }
 146                }
 147                _ => {}
 148            });
 149
 150        let excerpts = cx.build_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
 151        let editor = cx.build_view(|cx| {
 152            let mut editor =
 153                Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
 154            editor.set_vertical_scroll_margin(5, cx);
 155            editor
 156        });
 157        let editor_event_subscription =
 158            cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
 159                Self::emit_item_event_for_editor_event(event, cx);
 160                if event == &EditorEvent::Focused && this.path_states.is_empty() {
 161                    cx.focus(&this.focus_handle);
 162                }
 163            });
 164
 165        let project = project_handle.read(cx);
 166        let summary = project.diagnostic_summary(cx);
 167        let mut this = Self {
 168            project: project_handle,
 169            summary,
 170            workspace,
 171            excerpts,
 172            focus_handle: cx.focus_handle(),
 173            editor,
 174            path_states: Default::default(),
 175            paths_to_update: HashMap::default(),
 176            include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings,
 177            current_diagnostics: HashMap::default(),
 178            _subscriptions: vec![project_event_subscription, editor_event_subscription],
 179        };
 180        this.update_excerpts(None, cx);
 181        this
 182    }
 183
 184    fn emit_item_event_for_editor_event(event: &EditorEvent, cx: &mut ViewContext<Self>) {
 185        match event {
 186            EditorEvent::Closed => cx.emit(ItemEvent::CloseItem),
 187
 188            EditorEvent::Saved | EditorEvent::TitleChanged => {
 189                cx.emit(ItemEvent::UpdateTab);
 190                cx.emit(ItemEvent::UpdateBreadcrumbs);
 191            }
 192
 193            EditorEvent::Reparsed => {
 194                cx.emit(ItemEvent::UpdateBreadcrumbs);
 195            }
 196
 197            EditorEvent::SelectionsChanged { local } if *local => {
 198                cx.emit(ItemEvent::UpdateBreadcrumbs);
 199            }
 200
 201            EditorEvent::DirtyChanged => {
 202                cx.emit(ItemEvent::UpdateTab);
 203            }
 204
 205            EditorEvent::BufferEdited => {
 206                cx.emit(ItemEvent::Edit);
 207                cx.emit(ItemEvent::UpdateBreadcrumbs);
 208            }
 209
 210            EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => {
 211                cx.emit(ItemEvent::Edit);
 212            }
 213
 214            _ => {}
 215        }
 216    }
 217
 218    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
 219        if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
 220            workspace.activate_item(&existing, cx);
 221        } else {
 222            let workspace_handle = cx.view().downgrade();
 223            let diagnostics = cx.build_view(|cx| {
 224                ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
 225            });
 226            workspace.add_item(Box::new(diagnostics), cx);
 227        }
 228    }
 229
 230    fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
 231        self.include_warnings = !self.include_warnings;
 232        self.paths_to_update = self.current_diagnostics.clone();
 233        self.update_excerpts(None, cx);
 234        cx.notify();
 235    }
 236
 237    fn focus_in(&mut self, _: &FocusEvent, cx: &mut ViewContext<Self>) {
 238        if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() {
 239            self.editor.focus_handle(cx).focus(cx)
 240        }
 241    }
 242
 243    fn update_excerpts(
 244        &mut self,
 245        language_server_id: Option<LanguageServerId>,
 246        cx: &mut ViewContext<Self>,
 247    ) {
 248        log::debug!("Updating excerpts for server {language_server_id:?}");
 249        let mut paths_to_recheck = HashSet::default();
 250        let mut new_summaries: HashMap<LanguageServerId, HashSet<ProjectPath>> = self
 251            .project
 252            .read(cx)
 253            .diagnostic_summaries(cx)
 254            .fold(HashMap::default(), |mut summaries, (path, server_id, _)| {
 255                summaries.entry(server_id).or_default().insert(path);
 256                summaries
 257            });
 258        let mut old_diagnostics = if let Some(language_server_id) = language_server_id {
 259            new_summaries.retain(|server_id, _| server_id == &language_server_id);
 260            self.paths_to_update.retain(|server_id, paths| {
 261                if server_id == &language_server_id {
 262                    paths_to_recheck.extend(paths.drain());
 263                    false
 264                } else {
 265                    true
 266                }
 267            });
 268            let mut old_diagnostics = HashMap::default();
 269            if let Some(new_paths) = new_summaries.get(&language_server_id) {
 270                if let Some(old_paths) = self
 271                    .current_diagnostics
 272                    .insert(language_server_id, new_paths.clone())
 273                {
 274                    old_diagnostics.insert(language_server_id, old_paths);
 275                }
 276            } else {
 277                if let Some(old_paths) = self.current_diagnostics.remove(&language_server_id) {
 278                    old_diagnostics.insert(language_server_id, old_paths);
 279                }
 280            }
 281            old_diagnostics
 282        } else {
 283            paths_to_recheck.extend(self.paths_to_update.drain().flat_map(|(_, paths)| paths));
 284            mem::replace(&mut self.current_diagnostics, new_summaries.clone())
 285        };
 286        for (server_id, new_paths) in new_summaries {
 287            match old_diagnostics.remove(&server_id) {
 288                Some(mut old_paths) => {
 289                    paths_to_recheck.extend(
 290                        new_paths
 291                            .into_iter()
 292                            .filter(|new_path| !old_paths.remove(new_path)),
 293                    );
 294                    paths_to_recheck.extend(old_paths);
 295                }
 296                None => paths_to_recheck.extend(new_paths),
 297            }
 298        }
 299        paths_to_recheck.extend(old_diagnostics.into_iter().flat_map(|(_, paths)| paths));
 300
 301        if paths_to_recheck.is_empty() {
 302            log::debug!("No paths to recheck for language server {language_server_id:?}");
 303            return;
 304        }
 305        log::debug!(
 306            "Rechecking {} paths for language server {:?}",
 307            paths_to_recheck.len(),
 308            language_server_id
 309        );
 310        let project = self.project.clone();
 311        cx.spawn(|this, mut cx| {
 312            async move {
 313                let _: Vec<()> = try_join_all(paths_to_recheck.into_iter().map(|path| {
 314                    let mut cx = cx.clone();
 315                    let project = project.clone();
 316                    let this = this.clone();
 317                    async move {
 318                        let buffer = project
 319                            .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))?
 320                            .await
 321                            .with_context(|| format!("opening buffer for path {path:?}"))?;
 322                        this.update(&mut cx, |this, cx| {
 323                            this.populate_excerpts(path, language_server_id, buffer, cx);
 324                        })
 325                        .context("missing project")?;
 326                        anyhow::Ok(())
 327                    }
 328                }))
 329                .await
 330                .context("rechecking diagnostics for paths")?;
 331
 332                this.update(&mut cx, |this, cx| {
 333                    this.summary = this.project.read(cx).diagnostic_summary(cx);
 334                    cx.emit(ItemEvent::UpdateTab);
 335                    cx.emit(ItemEvent::UpdateBreadcrumbs);
 336                })?;
 337                anyhow::Ok(())
 338            }
 339            .log_err()
 340        })
 341        .detach();
 342    }
 343
 344    fn populate_excerpts(
 345        &mut self,
 346        path: ProjectPath,
 347        language_server_id: Option<LanguageServerId>,
 348        buffer: Model<Buffer>,
 349        cx: &mut ViewContext<Self>,
 350    ) {
 351        let was_empty = self.path_states.is_empty();
 352        let snapshot = buffer.read(cx).snapshot();
 353        let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
 354            Ok(ix) => ix,
 355            Err(ix) => {
 356                self.path_states.insert(
 357                    ix,
 358                    PathState {
 359                        path: path.clone(),
 360                        diagnostic_groups: Default::default(),
 361                    },
 362                );
 363                ix
 364            }
 365        };
 366
 367        let mut prev_excerpt_id = if path_ix > 0 {
 368            let prev_path_last_group = &self.path_states[path_ix - 1]
 369                .diagnostic_groups
 370                .last()
 371                .unwrap();
 372            prev_path_last_group.excerpts.last().unwrap().clone()
 373        } else {
 374            ExcerptId::min()
 375        };
 376
 377        let path_state = &mut self.path_states[path_ix];
 378        let mut groups_to_add = Vec::new();
 379        let mut group_ixs_to_remove = Vec::new();
 380        let mut blocks_to_add = Vec::new();
 381        let mut blocks_to_remove = HashSet::default();
 382        let mut first_excerpt_id = None;
 383        let max_severity = if self.include_warnings {
 384            DiagnosticSeverity::WARNING
 385        } else {
 386            DiagnosticSeverity::ERROR
 387        };
 388        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
 389            let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
 390            let mut new_groups = snapshot
 391                .diagnostic_groups(language_server_id)
 392                .into_iter()
 393                .filter(|(_, group)| {
 394                    group.entries[group.primary_ix].diagnostic.severity <= max_severity
 395                })
 396                .peekable();
 397            loop {
 398                let mut to_insert = None;
 399                let mut to_remove = None;
 400                let mut to_keep = None;
 401                match (old_groups.peek(), new_groups.peek()) {
 402                    (None, None) => break,
 403                    (None, Some(_)) => to_insert = new_groups.next(),
 404                    (Some((_, old_group)), None) => {
 405                        if language_server_id.map_or(true, |id| id == old_group.language_server_id)
 406                        {
 407                            to_remove = old_groups.next();
 408                        } else {
 409                            to_keep = old_groups.next();
 410                        }
 411                    }
 412                    (Some((_, old_group)), Some((_, new_group))) => {
 413                        let old_primary = &old_group.primary_diagnostic;
 414                        let new_primary = &new_group.entries[new_group.primary_ix];
 415                        match compare_diagnostics(old_primary, new_primary, &snapshot) {
 416                            Ordering::Less => {
 417                                if language_server_id
 418                                    .map_or(true, |id| id == old_group.language_server_id)
 419                                {
 420                                    to_remove = old_groups.next();
 421                                } else {
 422                                    to_keep = old_groups.next();
 423                                }
 424                            }
 425                            Ordering::Equal => {
 426                                to_keep = old_groups.next();
 427                                new_groups.next();
 428                            }
 429                            Ordering::Greater => to_insert = new_groups.next(),
 430                        }
 431                    }
 432                }
 433
 434                if let Some((language_server_id, group)) = to_insert {
 435                    let mut group_state = DiagnosticGroupState {
 436                        language_server_id,
 437                        primary_diagnostic: group.entries[group.primary_ix].clone(),
 438                        primary_excerpt_ix: 0,
 439                        excerpts: Default::default(),
 440                        blocks: Default::default(),
 441                        block_count: 0,
 442                    };
 443                    let mut pending_range: Option<(Range<Point>, usize)> = None;
 444                    let mut is_first_excerpt_for_group = true;
 445                    for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
 446                        let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
 447                        if let Some((range, start_ix)) = &mut pending_range {
 448                            if let Some(entry) = resolved_entry.as_ref() {
 449                                if entry.range.start.row
 450                                    <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
 451                                {
 452                                    range.end = range.end.max(entry.range.end);
 453                                    continue;
 454                                }
 455                            }
 456
 457                            let excerpt_start =
 458                                Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
 459                            let excerpt_end = snapshot.clip_point(
 460                                Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
 461                                Bias::Left,
 462                            );
 463                            let excerpt_id = excerpts
 464                                .insert_excerpts_after(
 465                                    prev_excerpt_id,
 466                                    buffer.clone(),
 467                                    [ExcerptRange {
 468                                        context: excerpt_start..excerpt_end,
 469                                        primary: Some(range.clone()),
 470                                    }],
 471                                    excerpts_cx,
 472                                )
 473                                .pop()
 474                                .unwrap();
 475
 476                            prev_excerpt_id = excerpt_id.clone();
 477                            first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
 478                            group_state.excerpts.push(excerpt_id.clone());
 479                            let header_position = (excerpt_id.clone(), language::Anchor::MIN);
 480
 481                            if is_first_excerpt_for_group {
 482                                is_first_excerpt_for_group = false;
 483                                let mut primary =
 484                                    group.entries[group.primary_ix].diagnostic.clone();
 485                                primary.message =
 486                                    primary.message.split('\n').next().unwrap().to_string();
 487                                group_state.block_count += 1;
 488                                blocks_to_add.push(BlockProperties {
 489                                    position: header_position,
 490                                    height: 2,
 491                                    style: BlockStyle::Sticky,
 492                                    render: diagnostic_header_renderer(primary),
 493                                    disposition: BlockDisposition::Above,
 494                                });
 495                            }
 496
 497                            for entry in &group.entries[*start_ix..ix] {
 498                                let mut diagnostic = entry.diagnostic.clone();
 499                                if diagnostic.is_primary {
 500                                    group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
 501                                    diagnostic.message =
 502                                        entry.diagnostic.message.split('\n').skip(1).collect();
 503                                }
 504
 505                                if !diagnostic.message.is_empty() {
 506                                    group_state.block_count += 1;
 507                                    blocks_to_add.push(BlockProperties {
 508                                        position: (excerpt_id.clone(), entry.range.start),
 509                                        height: diagnostic.message.matches('\n').count() as u8 + 1,
 510                                        style: BlockStyle::Fixed,
 511                                        render: diagnostic_block_renderer(diagnostic, true),
 512                                        disposition: BlockDisposition::Below,
 513                                    });
 514                                }
 515                            }
 516
 517                            pending_range.take();
 518                        }
 519
 520                        if let Some(entry) = resolved_entry {
 521                            pending_range = Some((entry.range.clone(), ix));
 522                        }
 523                    }
 524
 525                    groups_to_add.push(group_state);
 526                } else if let Some((group_ix, group_state)) = to_remove {
 527                    excerpts.remove_excerpts(group_state.excerpts.iter().copied(), excerpts_cx);
 528                    group_ixs_to_remove.push(group_ix);
 529                    blocks_to_remove.extend(group_state.blocks.iter().copied());
 530                } else if let Some((_, group)) = to_keep {
 531                    prev_excerpt_id = group.excerpts.last().unwrap().clone();
 532                    first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
 533                }
 534            }
 535
 536            excerpts.snapshot(excerpts_cx)
 537        });
 538
 539        self.editor.update(cx, |editor, cx| {
 540            editor.remove_blocks(blocks_to_remove, None, cx);
 541            let block_ids = editor.insert_blocks(
 542                blocks_to_add.into_iter().map(|block| {
 543                    let (excerpt_id, text_anchor) = block.position;
 544                    BlockProperties {
 545                        position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
 546                        height: block.height,
 547                        style: block.style,
 548                        render: block.render,
 549                        disposition: block.disposition,
 550                    }
 551                }),
 552                Some(Autoscroll::fit()),
 553                cx,
 554            );
 555
 556            let mut block_ids = block_ids.into_iter();
 557            for group_state in &mut groups_to_add {
 558                group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
 559            }
 560        });
 561
 562        for ix in group_ixs_to_remove.into_iter().rev() {
 563            path_state.diagnostic_groups.remove(ix);
 564        }
 565        path_state.diagnostic_groups.extend(groups_to_add);
 566        path_state.diagnostic_groups.sort_unstable_by(|a, b| {
 567            let range_a = &a.primary_diagnostic.range;
 568            let range_b = &b.primary_diagnostic.range;
 569            range_a
 570                .start
 571                .cmp(&range_b.start, &snapshot)
 572                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
 573        });
 574
 575        if path_state.diagnostic_groups.is_empty() {
 576            self.path_states.remove(path_ix);
 577        }
 578
 579        self.editor.update(cx, |editor, cx| {
 580            let groups;
 581            let mut selections;
 582            let new_excerpt_ids_by_selection_id;
 583            if was_empty {
 584                groups = self.path_states.first()?.diagnostic_groups.as_slice();
 585                new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
 586                selections = vec![Selection {
 587                    id: 0,
 588                    start: 0,
 589                    end: 0,
 590                    reversed: false,
 591                    goal: SelectionGoal::None,
 592                }];
 593            } else {
 594                groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
 595                new_excerpt_ids_by_selection_id =
 596                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
 597                selections = editor.selections.all::<usize>(cx);
 598            }
 599
 600            // If any selection has lost its position, move it to start of the next primary diagnostic.
 601            let snapshot = editor.snapshot(cx);
 602            for selection in &mut selections {
 603                if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
 604                    let group_ix = match groups.binary_search_by(|probe| {
 605                        probe
 606                            .excerpts
 607                            .last()
 608                            .unwrap()
 609                            .cmp(new_excerpt_id, &snapshot.buffer_snapshot)
 610                    }) {
 611                        Ok(ix) | Err(ix) => ix,
 612                    };
 613                    if let Some(group) = groups.get(group_ix) {
 614                        let offset = excerpts_snapshot
 615                            .anchor_in_excerpt(
 616                                group.excerpts[group.primary_excerpt_ix].clone(),
 617                                group.primary_diagnostic.range.start,
 618                            )
 619                            .to_offset(&excerpts_snapshot);
 620                        selection.start = offset;
 621                        selection.end = offset;
 622                    }
 623                }
 624            }
 625            editor.change_selections(None, cx, |s| {
 626                s.select(selections);
 627            });
 628            Some(())
 629        });
 630
 631        if self.path_states.is_empty() {
 632            if self.editor.focus_handle(cx).is_focused(cx) {
 633                cx.focus(&self.focus_handle);
 634            }
 635        } else if self.focus_handle.is_focused(cx) {
 636            let focus_handle = self.editor.focus_handle(cx);
 637            cx.focus(&focus_handle);
 638        }
 639        cx.notify();
 640    }
 641}
 642
 643impl FocusableView for ProjectDiagnosticsEditor {
 644    fn focus_handle(&self, _: &AppContext) -> FocusHandle {
 645        self.focus_handle.clone()
 646    }
 647}
 648
 649impl Item for ProjectDiagnosticsEditor {
 650    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 651        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
 652    }
 653
 654    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
 655        self.editor
 656            .update(cx, |editor, cx| editor.navigate(data, cx))
 657    }
 658
 659    fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
 660        Some("Project Diagnostics".into())
 661    }
 662
 663    fn tab_content<T: 'static>(&self, _detail: Option<usize>, _: &AppContext) -> AnyElement<T> {
 664        render_summary(&self.summary)
 665    }
 666
 667    fn for_each_project_item(
 668        &self,
 669        cx: &AppContext,
 670        f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item),
 671    ) {
 672        self.editor.for_each_project_item(cx, f)
 673    }
 674
 675    fn is_singleton(&self, _: &AppContext) -> bool {
 676        false
 677    }
 678
 679    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
 680        self.editor.update(cx, |editor, _| {
 681            editor.set_nav_history(Some(nav_history));
 682        });
 683    }
 684
 685    fn clone_on_split(
 686        &self,
 687        _workspace_id: workspace::WorkspaceId,
 688        cx: &mut ViewContext<Self>,
 689    ) -> Option<View<Self>>
 690    where
 691        Self: Sized,
 692    {
 693        Some(cx.build_view(|cx| {
 694            ProjectDiagnosticsEditor::new(self.project.clone(), self.workspace.clone(), cx)
 695        }))
 696    }
 697
 698    fn is_dirty(&self, cx: &AppContext) -> bool {
 699        self.excerpts.read(cx).is_dirty(cx)
 700    }
 701
 702    fn has_conflict(&self, cx: &AppContext) -> bool {
 703        self.excerpts.read(cx).has_conflict(cx)
 704    }
 705
 706    fn can_save(&self, _: &AppContext) -> bool {
 707        true
 708    }
 709
 710    fn save(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
 711        self.editor.save(project, cx)
 712    }
 713
 714    fn save_as(
 715        &mut self,
 716        _: Model<Project>,
 717        _: PathBuf,
 718        _: &mut ViewContext<Self>,
 719    ) -> Task<Result<()>> {
 720        unreachable!()
 721    }
 722
 723    fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
 724        self.editor.reload(project, cx)
 725    }
 726
 727    fn act_as_type<'a>(
 728        &'a self,
 729        type_id: TypeId,
 730        self_handle: &'a View<Self>,
 731        _: &'a AppContext,
 732    ) -> Option<AnyView> {
 733        if type_id == TypeId::of::<Self>() {
 734            Some(self_handle.to_any())
 735        } else if type_id == TypeId::of::<Editor>() {
 736            Some(self.editor.to_any())
 737        } else {
 738            None
 739        }
 740    }
 741
 742    fn breadcrumb_location(&self) -> ToolbarItemLocation {
 743        ToolbarItemLocation::PrimaryLeft { flex: None }
 744    }
 745
 746    fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
 747        self.editor.breadcrumbs(theme, cx)
 748    }
 749
 750    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
 751        self.editor
 752            .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
 753    }
 754
 755    fn serialized_item_kind() -> Option<&'static str> {
 756        Some("diagnostics")
 757    }
 758
 759    fn deserialize(
 760        project: Model<Project>,
 761        workspace: WeakView<Workspace>,
 762        _workspace_id: workspace::WorkspaceId,
 763        _item_id: workspace::ItemId,
 764        cx: &mut ViewContext<Pane>,
 765    ) -> Task<Result<View<Self>>> {
 766        Task::ready(Ok(cx.build_view(|cx| Self::new(project, workspace, cx))))
 767    }
 768}
 769
 770fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
 771    let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
 772    Arc::new(move |_| {
 773        h_stack()
 774            .id("diagnostic header")
 775            .gap_3()
 776            .bg(gpui::red())
 777            .map(|stack| {
 778                let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
 779                    IconElement::new(Icon::XCircle).color(TextColor::Error)
 780                } else {
 781                    IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning)
 782                };
 783
 784                stack.child(div().pl_8().child(icon))
 785            })
 786            .when_some(diagnostic.source.as_ref(), |stack, source| {
 787                stack.child(Label::new(format!("{source}:")).color(TextColor::Accent))
 788            })
 789            .child(HighlightedLabel::new(message.clone(), highlights.clone()))
 790            .when_some(diagnostic.code.as_ref(), |stack, code| {
 791                stack.child(Label::new(code.clone()))
 792            })
 793            .render()
 794    })
 795}
 796
 797pub(crate) fn render_summary<T: 'static>(summary: &DiagnosticSummary) -> AnyElement<T> {
 798    if summary.error_count == 0 && summary.warning_count == 0 {
 799        Label::new("No problems").render()
 800    } else {
 801        h_stack()
 802            .bg(gpui::red())
 803            .child(IconElement::new(Icon::XCircle))
 804            .child(Label::new(summary.error_count.to_string()))
 805            .child(IconElement::new(Icon::ExclamationTriangle))
 806            .child(Label::new(summary.warning_count.to_string()))
 807            .render()
 808    }
 809}
 810
 811fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 812    lhs: &DiagnosticEntry<L>,
 813    rhs: &DiagnosticEntry<R>,
 814    snapshot: &language::BufferSnapshot,
 815) -> Ordering {
 816    lhs.range
 817        .start
 818        .to_offset(snapshot)
 819        .cmp(&rhs.range.start.to_offset(snapshot))
 820        .then_with(|| {
 821            lhs.range
 822                .end
 823                .to_offset(snapshot)
 824                .cmp(&rhs.range.end.to_offset(snapshot))
 825        })
 826        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 827}
 828
 829#[cfg(test)]
 830mod tests {
 831    use super::*;
 832    use editor::{
 833        display_map::{BlockContext, TransformBlock},
 834        DisplayPoint,
 835    };
 836    use gpui::{px, TestAppContext, VisualTestContext, WindowContext};
 837    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
 838    use project::FakeFs;
 839    use serde_json::json;
 840    use settings::SettingsStore;
 841    use unindent::Unindent as _;
 842
 843    #[gpui::test]
 844    async fn test_diagnostics(cx: &mut TestAppContext) {
 845        init_test(cx);
 846
 847        let fs = FakeFs::new(cx.executor());
 848        fs.insert_tree(
 849            "/test",
 850            json!({
 851                "consts.rs": "
 852                    const a: i32 = 'a';
 853                    const b: i32 = c;
 854                "
 855                .unindent(),
 856
 857                "main.rs": "
 858                    fn main() {
 859                        let x = vec![];
 860                        let y = vec![];
 861                        a(x);
 862                        b(y);
 863                        // comment 1
 864                        // comment 2
 865                        c(y);
 866                        d(x);
 867                    }
 868                "
 869                .unindent(),
 870            }),
 871        )
 872        .await;
 873
 874        let language_server_id = LanguageServerId(0);
 875        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
 876        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 877        let cx = &mut VisualTestContext::from_window(*window, cx);
 878        let workspace = window.root(cx).unwrap();
 879
 880        // Create some diagnostics
 881        project.update(cx, |project, cx| {
 882            project
 883                .update_diagnostic_entries(
 884                    language_server_id,
 885                    PathBuf::from("/test/main.rs"),
 886                    None,
 887                    vec![
 888                        DiagnosticEntry {
 889                            range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
 890                            diagnostic: Diagnostic {
 891                                message:
 892                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 893                                        .to_string(),
 894                                severity: DiagnosticSeverity::INFORMATION,
 895                                is_primary: false,
 896                                is_disk_based: true,
 897                                group_id: 1,
 898                                ..Default::default()
 899                            },
 900                        },
 901                        DiagnosticEntry {
 902                            range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
 903                            diagnostic: Diagnostic {
 904                                message:
 905                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 906                                        .to_string(),
 907                                severity: DiagnosticSeverity::INFORMATION,
 908                                is_primary: false,
 909                                is_disk_based: true,
 910                                group_id: 0,
 911                                ..Default::default()
 912                            },
 913                        },
 914                        DiagnosticEntry {
 915                            range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
 916                            diagnostic: Diagnostic {
 917                                message: "value moved here".to_string(),
 918                                severity: DiagnosticSeverity::INFORMATION,
 919                                is_primary: false,
 920                                is_disk_based: true,
 921                                group_id: 1,
 922                                ..Default::default()
 923                            },
 924                        },
 925                        DiagnosticEntry {
 926                            range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
 927                            diagnostic: Diagnostic {
 928                                message: "value moved here".to_string(),
 929                                severity: DiagnosticSeverity::INFORMATION,
 930                                is_primary: false,
 931                                is_disk_based: true,
 932                                group_id: 0,
 933                                ..Default::default()
 934                            },
 935                        },
 936                        DiagnosticEntry {
 937                            range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
 938                            diagnostic: Diagnostic {
 939                                message: "use of moved value\nvalue used here after move".to_string(),
 940                                severity: DiagnosticSeverity::ERROR,
 941                                is_primary: true,
 942                                is_disk_based: true,
 943                                group_id: 0,
 944                                ..Default::default()
 945                            },
 946                        },
 947                        DiagnosticEntry {
 948                            range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
 949                            diagnostic: Diagnostic {
 950                                message: "use of moved value\nvalue used here after move".to_string(),
 951                                severity: DiagnosticSeverity::ERROR,
 952                                is_primary: true,
 953                                is_disk_based: true,
 954                                group_id: 1,
 955                                ..Default::default()
 956                            },
 957                        },
 958                    ],
 959                    cx,
 960                )
 961                .unwrap();
 962        });
 963
 964        // Open the project diagnostics view while there are already diagnostics.
 965        let view = window.build_view(cx, |cx| {
 966            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
 967        });
 968
 969        view.next_notification(cx).await;
 970        view.update(cx, |view, cx| {
 971            assert_eq!(
 972                editor_blocks(&view.editor, cx),
 973                [
 974                    (0, "path header block".into()),
 975                    (2, "diagnostic header".into()),
 976                    (15, "collapsed context".into()),
 977                    (16, "diagnostic header".into()),
 978                    (25, "collapsed context".into()),
 979                ]
 980            );
 981            assert_eq!(
 982                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
 983                concat!(
 984                    //
 985                    // main.rs
 986                    //
 987                    "\n", // filename
 988                    "\n", // padding
 989                    // diagnostic group 1
 990                    "\n", // primary message
 991                    "\n", // padding
 992                    "    let x = vec![];\n",
 993                    "    let y = vec![];\n",
 994                    "\n", // supporting diagnostic
 995                    "    a(x);\n",
 996                    "    b(y);\n",
 997                    "\n", // supporting diagnostic
 998                    "    // comment 1\n",
 999                    "    // comment 2\n",
1000                    "    c(y);\n",
1001                    "\n", // supporting diagnostic
1002                    "    d(x);\n",
1003                    "\n", // context ellipsis
1004                    // diagnostic group 2
1005                    "\n", // primary message
1006                    "\n", // padding
1007                    "fn main() {\n",
1008                    "    let x = vec![];\n",
1009                    "\n", // supporting diagnostic
1010                    "    let y = vec![];\n",
1011                    "    a(x);\n",
1012                    "\n", // supporting diagnostic
1013                    "    b(y);\n",
1014                    "\n", // context ellipsis
1015                    "    c(y);\n",
1016                    "    d(x);\n",
1017                    "\n", // supporting diagnostic
1018                    "}"
1019                )
1020            );
1021
1022            // Cursor is at the first diagnostic
1023            view.editor.update(cx, |editor, cx| {
1024                assert_eq!(
1025                    editor.selections.display_ranges(cx),
1026                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
1027                );
1028            });
1029        });
1030
1031        // Diagnostics are added for another earlier path.
1032        project.update(cx, |project, cx| {
1033            project.disk_based_diagnostics_started(language_server_id, cx);
1034            project
1035                .update_diagnostic_entries(
1036                    language_server_id,
1037                    PathBuf::from("/test/consts.rs"),
1038                    None,
1039                    vec![DiagnosticEntry {
1040                        range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
1041                        diagnostic: Diagnostic {
1042                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1043                            severity: DiagnosticSeverity::ERROR,
1044                            is_primary: true,
1045                            is_disk_based: true,
1046                            group_id: 0,
1047                            ..Default::default()
1048                        },
1049                    }],
1050                    cx,
1051                )
1052                .unwrap();
1053            project.disk_based_diagnostics_finished(language_server_id, cx);
1054        });
1055
1056        view.next_notification(cx).await;
1057        view.update(cx, |view, cx| {
1058            assert_eq!(
1059                editor_blocks(&view.editor, cx),
1060                [
1061                    (0, "path header block".into()),
1062                    (2, "diagnostic header".into()),
1063                    (7, "path header block".into()),
1064                    (9, "diagnostic header".into()),
1065                    (22, "collapsed context".into()),
1066                    (23, "diagnostic header".into()),
1067                    (32, "collapsed context".into()),
1068                ]
1069            );
1070            assert_eq!(
1071                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1072                concat!(
1073                    //
1074                    // consts.rs
1075                    //
1076                    "\n", // filename
1077                    "\n", // padding
1078                    // diagnostic group 1
1079                    "\n", // primary message
1080                    "\n", // padding
1081                    "const a: i32 = 'a';\n",
1082                    "\n", // supporting diagnostic
1083                    "const b: i32 = c;\n",
1084                    //
1085                    // main.rs
1086                    //
1087                    "\n", // filename
1088                    "\n", // padding
1089                    // diagnostic group 1
1090                    "\n", // primary message
1091                    "\n", // padding
1092                    "    let x = vec![];\n",
1093                    "    let y = vec![];\n",
1094                    "\n", // supporting diagnostic
1095                    "    a(x);\n",
1096                    "    b(y);\n",
1097                    "\n", // supporting diagnostic
1098                    "    // comment 1\n",
1099                    "    // comment 2\n",
1100                    "    c(y);\n",
1101                    "\n", // supporting diagnostic
1102                    "    d(x);\n",
1103                    "\n", // collapsed context
1104                    // diagnostic group 2
1105                    "\n", // primary message
1106                    "\n", // filename
1107                    "fn main() {\n",
1108                    "    let x = vec![];\n",
1109                    "\n", // supporting diagnostic
1110                    "    let y = vec![];\n",
1111                    "    a(x);\n",
1112                    "\n", // supporting diagnostic
1113                    "    b(y);\n",
1114                    "\n", // context ellipsis
1115                    "    c(y);\n",
1116                    "    d(x);\n",
1117                    "\n", // supporting diagnostic
1118                    "}"
1119                )
1120            );
1121
1122            // Cursor keeps its position.
1123            view.editor.update(cx, |editor, cx| {
1124                assert_eq!(
1125                    editor.selections.display_ranges(cx),
1126                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1127                );
1128            });
1129        });
1130
1131        // Diagnostics are added to the first path
1132        project.update(cx, |project, cx| {
1133            project.disk_based_diagnostics_started(language_server_id, cx);
1134            project
1135                .update_diagnostic_entries(
1136                    language_server_id,
1137                    PathBuf::from("/test/consts.rs"),
1138                    None,
1139                    vec![
1140                        DiagnosticEntry {
1141                            range: Unclipped(PointUtf16::new(0, 15))
1142                                ..Unclipped(PointUtf16::new(0, 15)),
1143                            diagnostic: Diagnostic {
1144                                message: "mismatched types\nexpected `usize`, found `char`"
1145                                    .to_string(),
1146                                severity: DiagnosticSeverity::ERROR,
1147                                is_primary: true,
1148                                is_disk_based: true,
1149                                group_id: 0,
1150                                ..Default::default()
1151                            },
1152                        },
1153                        DiagnosticEntry {
1154                            range: Unclipped(PointUtf16::new(1, 15))
1155                                ..Unclipped(PointUtf16::new(1, 15)),
1156                            diagnostic: Diagnostic {
1157                                message: "unresolved name `c`".to_string(),
1158                                severity: DiagnosticSeverity::ERROR,
1159                                is_primary: true,
1160                                is_disk_based: true,
1161                                group_id: 1,
1162                                ..Default::default()
1163                            },
1164                        },
1165                    ],
1166                    cx,
1167                )
1168                .unwrap();
1169            project.disk_based_diagnostics_finished(language_server_id, cx);
1170        });
1171
1172        view.next_notification(cx).await;
1173        view.update(cx, |view, cx| {
1174            assert_eq!(
1175                editor_blocks(&view.editor, cx),
1176                [
1177                    (0, "path header block".into()),
1178                    (2, "diagnostic header".into()),
1179                    (7, "collapsed context".into()),
1180                    (8, "diagnostic header".into()),
1181                    (13, "path header block".into()),
1182                    (15, "diagnostic header".into()),
1183                    (28, "collapsed context".into()),
1184                    (29, "diagnostic header".into()),
1185                    (38, "collapsed context".into()),
1186                ]
1187            );
1188            assert_eq!(
1189                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1190                concat!(
1191                    //
1192                    // consts.rs
1193                    //
1194                    "\n", // filename
1195                    "\n", // padding
1196                    // diagnostic group 1
1197                    "\n", // primary message
1198                    "\n", // padding
1199                    "const a: i32 = 'a';\n",
1200                    "\n", // supporting diagnostic
1201                    "const b: i32 = c;\n",
1202                    "\n", // context ellipsis
1203                    // diagnostic group 2
1204                    "\n", // primary message
1205                    "\n", // padding
1206                    "const a: i32 = 'a';\n",
1207                    "const b: i32 = c;\n",
1208                    "\n", // supporting diagnostic
1209                    //
1210                    // main.rs
1211                    //
1212                    "\n", // filename
1213                    "\n", // padding
1214                    // diagnostic group 1
1215                    "\n", // primary message
1216                    "\n", // padding
1217                    "    let x = vec![];\n",
1218                    "    let y = vec![];\n",
1219                    "\n", // supporting diagnostic
1220                    "    a(x);\n",
1221                    "    b(y);\n",
1222                    "\n", // supporting diagnostic
1223                    "    // comment 1\n",
1224                    "    // comment 2\n",
1225                    "    c(y);\n",
1226                    "\n", // supporting diagnostic
1227                    "    d(x);\n",
1228                    "\n", // context ellipsis
1229                    // diagnostic group 2
1230                    "\n", // primary message
1231                    "\n", // filename
1232                    "fn main() {\n",
1233                    "    let x = vec![];\n",
1234                    "\n", // supporting diagnostic
1235                    "    let y = vec![];\n",
1236                    "    a(x);\n",
1237                    "\n", // supporting diagnostic
1238                    "    b(y);\n",
1239                    "\n", // context ellipsis
1240                    "    c(y);\n",
1241                    "    d(x);\n",
1242                    "\n", // supporting diagnostic
1243                    "}"
1244                )
1245            );
1246        });
1247    }
1248
1249    #[gpui::test]
1250    async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1251        init_test(cx);
1252
1253        let fs = FakeFs::new(cx.executor());
1254        fs.insert_tree(
1255            "/test",
1256            json!({
1257                "main.js": "
1258                    a();
1259                    b();
1260                    c();
1261                    d();
1262                    e();
1263                ".unindent()
1264            }),
1265        )
1266        .await;
1267
1268        let server_id_1 = LanguageServerId(100);
1269        let server_id_2 = LanguageServerId(101);
1270        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1271        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1272        let cx = &mut VisualTestContext::from_window(*window, cx);
1273        let workspace = window.root(cx).unwrap();
1274
1275        let view = window.build_view(cx, |cx| {
1276            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
1277        });
1278
1279        // Two language servers start updating diagnostics
1280        project.update(cx, |project, cx| {
1281            project.disk_based_diagnostics_started(server_id_1, cx);
1282            project.disk_based_diagnostics_started(server_id_2, cx);
1283            project
1284                .update_diagnostic_entries(
1285                    server_id_1,
1286                    PathBuf::from("/test/main.js"),
1287                    None,
1288                    vec![DiagnosticEntry {
1289                        range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
1290                        diagnostic: Diagnostic {
1291                            message: "error 1".to_string(),
1292                            severity: DiagnosticSeverity::WARNING,
1293                            is_primary: true,
1294                            is_disk_based: true,
1295                            group_id: 1,
1296                            ..Default::default()
1297                        },
1298                    }],
1299                    cx,
1300                )
1301                .unwrap();
1302        });
1303
1304        // The first language server finishes
1305        project.update(cx, |project, cx| {
1306            project.disk_based_diagnostics_finished(server_id_1, cx);
1307        });
1308
1309        // Only the first language server's diagnostics are shown.
1310        cx.executor().run_until_parked();
1311        view.update(cx, |view, cx| {
1312            assert_eq!(
1313                editor_blocks(&view.editor, cx),
1314                [
1315                    (0, "path header block".into()),
1316                    (2, "diagnostic header".into()),
1317                ]
1318            );
1319            assert_eq!(
1320                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1321                concat!(
1322                    "\n", // filename
1323                    "\n", // padding
1324                    // diagnostic group 1
1325                    "\n",     // primary message
1326                    "\n",     // padding
1327                    "a();\n", //
1328                    "b();",
1329                )
1330            );
1331        });
1332
1333        // The second language server finishes
1334        project.update(cx, |project, cx| {
1335            project
1336                .update_diagnostic_entries(
1337                    server_id_2,
1338                    PathBuf::from("/test/main.js"),
1339                    None,
1340                    vec![DiagnosticEntry {
1341                        range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
1342                        diagnostic: Diagnostic {
1343                            message: "warning 1".to_string(),
1344                            severity: DiagnosticSeverity::ERROR,
1345                            is_primary: true,
1346                            is_disk_based: true,
1347                            group_id: 2,
1348                            ..Default::default()
1349                        },
1350                    }],
1351                    cx,
1352                )
1353                .unwrap();
1354            project.disk_based_diagnostics_finished(server_id_2, cx);
1355        });
1356
1357        // Both language server's diagnostics are shown.
1358        cx.executor().run_until_parked();
1359        view.update(cx, |view, cx| {
1360            assert_eq!(
1361                editor_blocks(&view.editor, cx),
1362                [
1363                    (0, "path header block".into()),
1364                    (2, "diagnostic header".into()),
1365                    (6, "collapsed context".into()),
1366                    (7, "diagnostic header".into()),
1367                ]
1368            );
1369            assert_eq!(
1370                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1371                concat!(
1372                    "\n", // filename
1373                    "\n", // padding
1374                    // diagnostic group 1
1375                    "\n",     // primary message
1376                    "\n",     // padding
1377                    "a();\n", // location
1378                    "b();\n", //
1379                    "\n",     // collapsed context
1380                    // diagnostic group 2
1381                    "\n",     // primary message
1382                    "\n",     // padding
1383                    "a();\n", // context
1384                    "b();\n", //
1385                    "c();",   // context
1386                )
1387            );
1388        });
1389
1390        // Both language servers start updating diagnostics, and the first server finishes.
1391        project.update(cx, |project, cx| {
1392            project.disk_based_diagnostics_started(server_id_1, cx);
1393            project.disk_based_diagnostics_started(server_id_2, cx);
1394            project
1395                .update_diagnostic_entries(
1396                    server_id_1,
1397                    PathBuf::from("/test/main.js"),
1398                    None,
1399                    vec![DiagnosticEntry {
1400                        range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
1401                        diagnostic: Diagnostic {
1402                            message: "warning 2".to_string(),
1403                            severity: DiagnosticSeverity::WARNING,
1404                            is_primary: true,
1405                            is_disk_based: true,
1406                            group_id: 1,
1407                            ..Default::default()
1408                        },
1409                    }],
1410                    cx,
1411                )
1412                .unwrap();
1413            project
1414                .update_diagnostic_entries(
1415                    server_id_2,
1416                    PathBuf::from("/test/main.rs"),
1417                    None,
1418                    vec![],
1419                    cx,
1420                )
1421                .unwrap();
1422            project.disk_based_diagnostics_finished(server_id_1, cx);
1423        });
1424
1425        // Only the first language server's diagnostics are updated.
1426        cx.executor().run_until_parked();
1427        view.update(cx, |view, cx| {
1428            assert_eq!(
1429                editor_blocks(&view.editor, cx),
1430                [
1431                    (0, "path header block".into()),
1432                    (2, "diagnostic header".into()),
1433                    (7, "collapsed context".into()),
1434                    (8, "diagnostic header".into()),
1435                ]
1436            );
1437            assert_eq!(
1438                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1439                concat!(
1440                    "\n", // filename
1441                    "\n", // padding
1442                    // diagnostic group 1
1443                    "\n",     // primary message
1444                    "\n",     // padding
1445                    "a();\n", // location
1446                    "b();\n", //
1447                    "c();\n", // context
1448                    "\n",     // collapsed context
1449                    // diagnostic group 2
1450                    "\n",     // primary message
1451                    "\n",     // padding
1452                    "b();\n", // context
1453                    "c();\n", //
1454                    "d();",   // context
1455                )
1456            );
1457        });
1458
1459        // The second language server finishes.
1460        project.update(cx, |project, cx| {
1461            project
1462                .update_diagnostic_entries(
1463                    server_id_2,
1464                    PathBuf::from("/test/main.js"),
1465                    None,
1466                    vec![DiagnosticEntry {
1467                        range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
1468                        diagnostic: Diagnostic {
1469                            message: "warning 2".to_string(),
1470                            severity: DiagnosticSeverity::WARNING,
1471                            is_primary: true,
1472                            is_disk_based: true,
1473                            group_id: 1,
1474                            ..Default::default()
1475                        },
1476                    }],
1477                    cx,
1478                )
1479                .unwrap();
1480            project.disk_based_diagnostics_finished(server_id_2, cx);
1481        });
1482
1483        // Both language servers' diagnostics are updated.
1484        cx.executor().run_until_parked();
1485        view.update(cx, |view, cx| {
1486            assert_eq!(
1487                editor_blocks(&view.editor, cx),
1488                [
1489                    (0, "path header block".into()),
1490                    (2, "diagnostic header".into()),
1491                    (7, "collapsed context".into()),
1492                    (8, "diagnostic header".into()),
1493                ]
1494            );
1495            assert_eq!(
1496                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1497                concat!(
1498                    "\n", // filename
1499                    "\n", // padding
1500                    // diagnostic group 1
1501                    "\n",     // primary message
1502                    "\n",     // padding
1503                    "b();\n", // location
1504                    "c();\n", //
1505                    "d();\n", // context
1506                    "\n",     // collapsed context
1507                    // diagnostic group 2
1508                    "\n",     // primary message
1509                    "\n",     // padding
1510                    "c();\n", // context
1511                    "d();\n", //
1512                    "e();",   // context
1513                )
1514            );
1515        });
1516    }
1517
1518    fn init_test(cx: &mut TestAppContext) {
1519        cx.update(|cx| {
1520            let settings = SettingsStore::test(cx);
1521            cx.set_global(settings);
1522            theme::init(theme::LoadThemes::JustBase, cx);
1523            language::init(cx);
1524            client::init_settings(cx);
1525            workspace::init_settings(cx);
1526            Project::init_settings(cx);
1527            crate::init(cx);
1528        });
1529    }
1530
1531    fn editor_blocks(editor: &View<Editor>, cx: &mut WindowContext) -> Vec<(u32, SharedString)> {
1532        editor.update(cx, |editor, cx| {
1533            let snapshot = editor.snapshot(cx);
1534            snapshot
1535                .blocks_in_range(0..snapshot.max_point().row())
1536                .enumerate()
1537                .filter_map(|(ix, (row, block))| {
1538                    let name = match block {
1539                        TransformBlock::Custom(block) => block
1540                            .render(&mut BlockContext {
1541                                view_context: cx,
1542                                anchor_x: px(0.),
1543                                gutter_padding: px(0.),
1544                                gutter_width: px(0.),
1545                                line_height: px(0.),
1546                                em_width: px(0.),
1547                                block_id: ix,
1548                                editor_style: &editor::EditorStyle::default(),
1549                            })
1550                            .element_id()?
1551                            .try_into()
1552                            .ok()?,
1553
1554                        TransformBlock::ExcerptHeader {
1555                            starts_new_buffer, ..
1556                        } => {
1557                            if *starts_new_buffer {
1558                                "path header block".into()
1559                            } else {
1560                                "collapsed context".into()
1561                            }
1562                        }
1563                    };
1564
1565                    Some((row, name))
1566                })
1567                .collect()
1568        })
1569    }
1570}