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