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