diagnostics.rs

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