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