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