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, InteractiveElement, IntoElement, Model, ParentElement, Render,
  18    SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
  19    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!(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, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
 789    Arc::new(move |_| {
 790        h_stack()
 791            .id("diagnostic header")
 792            .py_2()
 793            .pl_10()
 794            .pr_5()
 795            .w_full()
 796            .justify_between()
 797            .gap_2()
 798            .child(
 799                h_stack()
 800                    .gap_3()
 801                    .map(|stack| {
 802                        let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
 803                            IconElement::new(Icon::XCircle).color(Color::Error)
 804                        } else {
 805                            IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
 806                        };
 807                        stack.child(icon)
 808                    })
 809                    .child(
 810                        h_stack()
 811                            .gap_1()
 812                            .child(HighlightedLabel::new(message.clone(), highlights.clone()))
 813                            .when_some(diagnostic.code.as_ref(), |stack, code| {
 814                                stack.child(Label::new(format!("({code})")).color(Color::Muted))
 815                            }),
 816                    ),
 817            )
 818            .child(
 819                h_stack()
 820                    .gap_1()
 821                    .when_some(diagnostic.source.as_ref(), |stack, source| {
 822                        stack.child(Label::new(format!("{source}")).color(Color::Muted))
 823                    }),
 824            )
 825            .into_any_element()
 826    })
 827}
 828
 829fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 830    lhs: &DiagnosticEntry<L>,
 831    rhs: &DiagnosticEntry<R>,
 832    snapshot: &language::BufferSnapshot,
 833) -> Ordering {
 834    lhs.range
 835        .start
 836        .to_offset(snapshot)
 837        .cmp(&rhs.range.start.to_offset(snapshot))
 838        .then_with(|| {
 839            lhs.range
 840                .end
 841                .to_offset(snapshot)
 842                .cmp(&rhs.range.end.to_offset(snapshot))
 843        })
 844        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 845}
 846
 847#[cfg(test)]
 848mod tests {
 849    use super::*;
 850    use editor::{
 851        display_map::{BlockContext, TransformBlock},
 852        DisplayPoint,
 853    };
 854    use gpui::{px, TestAppContext, VisualTestContext, WindowContext};
 855    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
 856    use project::FakeFs;
 857    use serde_json::json;
 858    use settings::SettingsStore;
 859    use unindent::Unindent as _;
 860
 861    #[gpui::test]
 862    async fn test_diagnostics(cx: &mut TestAppContext) {
 863        init_test(cx);
 864
 865        let fs = FakeFs::new(cx.executor());
 866        fs.insert_tree(
 867            "/test",
 868            json!({
 869                "consts.rs": "
 870                    const a: i32 = 'a';
 871                    const b: i32 = c;
 872                "
 873                .unindent(),
 874
 875                "main.rs": "
 876                    fn main() {
 877                        let x = vec![];
 878                        let y = vec![];
 879                        a(x);
 880                        b(y);
 881                        // comment 1
 882                        // comment 2
 883                        c(y);
 884                        d(x);
 885                    }
 886                "
 887                .unindent(),
 888            }),
 889        )
 890        .await;
 891
 892        let language_server_id = LanguageServerId(0);
 893        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
 894        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 895        let cx = &mut VisualTestContext::from_window(*window, cx);
 896        let workspace = window.root(cx).unwrap();
 897
 898        // Create some diagnostics
 899        project.update(cx, |project, cx| {
 900            project
 901                .update_diagnostic_entries(
 902                    language_server_id,
 903                    PathBuf::from("/test/main.rs"),
 904                    None,
 905                    vec![
 906                        DiagnosticEntry {
 907                            range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
 908                            diagnostic: Diagnostic {
 909                                message:
 910                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 911                                        .to_string(),
 912                                severity: DiagnosticSeverity::INFORMATION,
 913                                is_primary: false,
 914                                is_disk_based: true,
 915                                group_id: 1,
 916                                ..Default::default()
 917                            },
 918                        },
 919                        DiagnosticEntry {
 920                            range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
 921                            diagnostic: Diagnostic {
 922                                message:
 923                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 924                                        .to_string(),
 925                                severity: DiagnosticSeverity::INFORMATION,
 926                                is_primary: false,
 927                                is_disk_based: true,
 928                                group_id: 0,
 929                                ..Default::default()
 930                            },
 931                        },
 932                        DiagnosticEntry {
 933                            range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
 934                            diagnostic: Diagnostic {
 935                                message: "value moved here".to_string(),
 936                                severity: DiagnosticSeverity::INFORMATION,
 937                                is_primary: false,
 938                                is_disk_based: true,
 939                                group_id: 1,
 940                                ..Default::default()
 941                            },
 942                        },
 943                        DiagnosticEntry {
 944                            range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
 945                            diagnostic: Diagnostic {
 946                                message: "value moved here".to_string(),
 947                                severity: DiagnosticSeverity::INFORMATION,
 948                                is_primary: false,
 949                                is_disk_based: true,
 950                                group_id: 0,
 951                                ..Default::default()
 952                            },
 953                        },
 954                        DiagnosticEntry {
 955                            range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
 956                            diagnostic: Diagnostic {
 957                                message: "use of moved value\nvalue used here after move".to_string(),
 958                                severity: DiagnosticSeverity::ERROR,
 959                                is_primary: true,
 960                                is_disk_based: true,
 961                                group_id: 0,
 962                                ..Default::default()
 963                            },
 964                        },
 965                        DiagnosticEntry {
 966                            range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
 967                            diagnostic: Diagnostic {
 968                                message: "use of moved value\nvalue used here after move".to_string(),
 969                                severity: DiagnosticSeverity::ERROR,
 970                                is_primary: true,
 971                                is_disk_based: true,
 972                                group_id: 1,
 973                                ..Default::default()
 974                            },
 975                        },
 976                    ],
 977                    cx,
 978                )
 979                .unwrap();
 980        });
 981
 982        // Open the project diagnostics view while there are already diagnostics.
 983        let view = window.build_view(cx, |cx| {
 984            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
 985        });
 986
 987        view.next_notification(cx).await;
 988        view.update(cx, |view, cx| {
 989            assert_eq!(
 990                editor_blocks(&view.editor, cx),
 991                [
 992                    (0, "path header block".into()),
 993                    (2, "diagnostic header".into()),
 994                    (15, "collapsed context".into()),
 995                    (16, "diagnostic header".into()),
 996                    (25, "collapsed context".into()),
 997                ]
 998            );
 999            assert_eq!(
1000                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1001                concat!(
1002                    //
1003                    // main.rs
1004                    //
1005                    "\n", // filename
1006                    "\n", // padding
1007                    // diagnostic group 1
1008                    "\n", // primary message
1009                    "\n", // padding
1010                    "    let x = vec![];\n",
1011                    "    let y = vec![];\n",
1012                    "\n", // supporting diagnostic
1013                    "    a(x);\n",
1014                    "    b(y);\n",
1015                    "\n", // supporting diagnostic
1016                    "    // comment 1\n",
1017                    "    // comment 2\n",
1018                    "    c(y);\n",
1019                    "\n", // supporting diagnostic
1020                    "    d(x);\n",
1021                    "\n", // context ellipsis
1022                    // diagnostic group 2
1023                    "\n", // primary message
1024                    "\n", // padding
1025                    "fn main() {\n",
1026                    "    let x = vec![];\n",
1027                    "\n", // supporting diagnostic
1028                    "    let y = vec![];\n",
1029                    "    a(x);\n",
1030                    "\n", // supporting diagnostic
1031                    "    b(y);\n",
1032                    "\n", // context ellipsis
1033                    "    c(y);\n",
1034                    "    d(x);\n",
1035                    "\n", // supporting diagnostic
1036                    "}"
1037                )
1038            );
1039
1040            // Cursor is at the first diagnostic
1041            view.editor.update(cx, |editor, cx| {
1042                assert_eq!(
1043                    editor.selections.display_ranges(cx),
1044                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
1045                );
1046            });
1047        });
1048
1049        // Diagnostics are added for another earlier path.
1050        project.update(cx, |project, cx| {
1051            project.disk_based_diagnostics_started(language_server_id, cx);
1052            project
1053                .update_diagnostic_entries(
1054                    language_server_id,
1055                    PathBuf::from("/test/consts.rs"),
1056                    None,
1057                    vec![DiagnosticEntry {
1058                        range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
1059                        diagnostic: Diagnostic {
1060                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1061                            severity: DiagnosticSeverity::ERROR,
1062                            is_primary: true,
1063                            is_disk_based: true,
1064                            group_id: 0,
1065                            ..Default::default()
1066                        },
1067                    }],
1068                    cx,
1069                )
1070                .unwrap();
1071            project.disk_based_diagnostics_finished(language_server_id, cx);
1072        });
1073
1074        view.next_notification(cx).await;
1075        view.update(cx, |view, cx| {
1076            assert_eq!(
1077                editor_blocks(&view.editor, cx),
1078                [
1079                    (0, "path header block".into()),
1080                    (2, "diagnostic header".into()),
1081                    (7, "path header block".into()),
1082                    (9, "diagnostic header".into()),
1083                    (22, "collapsed context".into()),
1084                    (23, "diagnostic header".into()),
1085                    (32, "collapsed context".into()),
1086                ]
1087            );
1088            assert_eq!(
1089                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1090                concat!(
1091                    //
1092                    // consts.rs
1093                    //
1094                    "\n", // filename
1095                    "\n", // padding
1096                    // diagnostic group 1
1097                    "\n", // primary message
1098                    "\n", // padding
1099                    "const a: i32 = 'a';\n",
1100                    "\n", // supporting diagnostic
1101                    "const b: i32 = c;\n",
1102                    //
1103                    // main.rs
1104                    //
1105                    "\n", // filename
1106                    "\n", // padding
1107                    // diagnostic group 1
1108                    "\n", // primary message
1109                    "\n", // padding
1110                    "    let x = vec![];\n",
1111                    "    let y = vec![];\n",
1112                    "\n", // supporting diagnostic
1113                    "    a(x);\n",
1114                    "    b(y);\n",
1115                    "\n", // supporting diagnostic
1116                    "    // comment 1\n",
1117                    "    // comment 2\n",
1118                    "    c(y);\n",
1119                    "\n", // supporting diagnostic
1120                    "    d(x);\n",
1121                    "\n", // collapsed context
1122                    // diagnostic group 2
1123                    "\n", // primary message
1124                    "\n", // filename
1125                    "fn main() {\n",
1126                    "    let x = vec![];\n",
1127                    "\n", // supporting diagnostic
1128                    "    let y = vec![];\n",
1129                    "    a(x);\n",
1130                    "\n", // supporting diagnostic
1131                    "    b(y);\n",
1132                    "\n", // context ellipsis
1133                    "    c(y);\n",
1134                    "    d(x);\n",
1135                    "\n", // supporting diagnostic
1136                    "}"
1137                )
1138            );
1139
1140            // Cursor keeps its position.
1141            view.editor.update(cx, |editor, cx| {
1142                assert_eq!(
1143                    editor.selections.display_ranges(cx),
1144                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1145                );
1146            });
1147        });
1148
1149        // Diagnostics are added to the first path
1150        project.update(cx, |project, cx| {
1151            project.disk_based_diagnostics_started(language_server_id, cx);
1152            project
1153                .update_diagnostic_entries(
1154                    language_server_id,
1155                    PathBuf::from("/test/consts.rs"),
1156                    None,
1157                    vec![
1158                        DiagnosticEntry {
1159                            range: Unclipped(PointUtf16::new(0, 15))
1160                                ..Unclipped(PointUtf16::new(0, 15)),
1161                            diagnostic: Diagnostic {
1162                                message: "mismatched types\nexpected `usize`, found `char`"
1163                                    .to_string(),
1164                                severity: DiagnosticSeverity::ERROR,
1165                                is_primary: true,
1166                                is_disk_based: true,
1167                                group_id: 0,
1168                                ..Default::default()
1169                            },
1170                        },
1171                        DiagnosticEntry {
1172                            range: Unclipped(PointUtf16::new(1, 15))
1173                                ..Unclipped(PointUtf16::new(1, 15)),
1174                            diagnostic: Diagnostic {
1175                                message: "unresolved name `c`".to_string(),
1176                                severity: DiagnosticSeverity::ERROR,
1177                                is_primary: true,
1178                                is_disk_based: true,
1179                                group_id: 1,
1180                                ..Default::default()
1181                            },
1182                        },
1183                    ],
1184                    cx,
1185                )
1186                .unwrap();
1187            project.disk_based_diagnostics_finished(language_server_id, cx);
1188        });
1189
1190        view.next_notification(cx).await;
1191        view.update(cx, |view, cx| {
1192            assert_eq!(
1193                editor_blocks(&view.editor, cx),
1194                [
1195                    (0, "path header block".into()),
1196                    (2, "diagnostic header".into()),
1197                    (7, "collapsed context".into()),
1198                    (8, "diagnostic header".into()),
1199                    (13, "path header block".into()),
1200                    (15, "diagnostic header".into()),
1201                    (28, "collapsed context".into()),
1202                    (29, "diagnostic header".into()),
1203                    (38, "collapsed context".into()),
1204                ]
1205            );
1206            assert_eq!(
1207                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1208                concat!(
1209                    //
1210                    // consts.rs
1211                    //
1212                    "\n", // filename
1213                    "\n", // padding
1214                    // diagnostic group 1
1215                    "\n", // primary message
1216                    "\n", // padding
1217                    "const a: i32 = 'a';\n",
1218                    "\n", // supporting diagnostic
1219                    "const b: i32 = c;\n",
1220                    "\n", // context ellipsis
1221                    // diagnostic group 2
1222                    "\n", // primary message
1223                    "\n", // padding
1224                    "const a: i32 = 'a';\n",
1225                    "const b: i32 = c;\n",
1226                    "\n", // supporting diagnostic
1227                    //
1228                    // main.rs
1229                    //
1230                    "\n", // filename
1231                    "\n", // padding
1232                    // diagnostic group 1
1233                    "\n", // primary message
1234                    "\n", // padding
1235                    "    let x = vec![];\n",
1236                    "    let y = vec![];\n",
1237                    "\n", // supporting diagnostic
1238                    "    a(x);\n",
1239                    "    b(y);\n",
1240                    "\n", // supporting diagnostic
1241                    "    // comment 1\n",
1242                    "    // comment 2\n",
1243                    "    c(y);\n",
1244                    "\n", // supporting diagnostic
1245                    "    d(x);\n",
1246                    "\n", // context ellipsis
1247                    // diagnostic group 2
1248                    "\n", // primary message
1249                    "\n", // filename
1250                    "fn main() {\n",
1251                    "    let x = vec![];\n",
1252                    "\n", // supporting diagnostic
1253                    "    let y = vec![];\n",
1254                    "    a(x);\n",
1255                    "\n", // supporting diagnostic
1256                    "    b(y);\n",
1257                    "\n", // context ellipsis
1258                    "    c(y);\n",
1259                    "    d(x);\n",
1260                    "\n", // supporting diagnostic
1261                    "}"
1262                )
1263            );
1264        });
1265    }
1266
1267    #[gpui::test]
1268    async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1269        init_test(cx);
1270
1271        let fs = FakeFs::new(cx.executor());
1272        fs.insert_tree(
1273            "/test",
1274            json!({
1275                "main.js": "
1276                    a();
1277                    b();
1278                    c();
1279                    d();
1280                    e();
1281                ".unindent()
1282            }),
1283        )
1284        .await;
1285
1286        let server_id_1 = LanguageServerId(100);
1287        let server_id_2 = LanguageServerId(101);
1288        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1289        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1290        let cx = &mut VisualTestContext::from_window(*window, cx);
1291        let workspace = window.root(cx).unwrap();
1292
1293        let view = window.build_view(cx, |cx| {
1294            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
1295        });
1296
1297        // Two language servers start updating diagnostics
1298        project.update(cx, |project, cx| {
1299            project.disk_based_diagnostics_started(server_id_1, cx);
1300            project.disk_based_diagnostics_started(server_id_2, cx);
1301            project
1302                .update_diagnostic_entries(
1303                    server_id_1,
1304                    PathBuf::from("/test/main.js"),
1305                    None,
1306                    vec![DiagnosticEntry {
1307                        range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
1308                        diagnostic: Diagnostic {
1309                            message: "error 1".to_string(),
1310                            severity: DiagnosticSeverity::WARNING,
1311                            is_primary: true,
1312                            is_disk_based: true,
1313                            group_id: 1,
1314                            ..Default::default()
1315                        },
1316                    }],
1317                    cx,
1318                )
1319                .unwrap();
1320        });
1321
1322        // The first language server finishes
1323        project.update(cx, |project, cx| {
1324            project.disk_based_diagnostics_finished(server_id_1, cx);
1325        });
1326
1327        // Only the first language server's diagnostics are shown.
1328        cx.executor().run_until_parked();
1329        view.update(cx, |view, cx| {
1330            assert_eq!(
1331                editor_blocks(&view.editor, cx),
1332                [
1333                    (0, "path header block".into()),
1334                    (2, "diagnostic header".into()),
1335                ]
1336            );
1337            assert_eq!(
1338                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1339                concat!(
1340                    "\n", // filename
1341                    "\n", // padding
1342                    // diagnostic group 1
1343                    "\n",     // primary message
1344                    "\n",     // padding
1345                    "a();\n", //
1346                    "b();",
1347                )
1348            );
1349        });
1350
1351        // The second language server finishes
1352        project.update(cx, |project, cx| {
1353            project
1354                .update_diagnostic_entries(
1355                    server_id_2,
1356                    PathBuf::from("/test/main.js"),
1357                    None,
1358                    vec![DiagnosticEntry {
1359                        range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
1360                        diagnostic: Diagnostic {
1361                            message: "warning 1".to_string(),
1362                            severity: DiagnosticSeverity::ERROR,
1363                            is_primary: true,
1364                            is_disk_based: true,
1365                            group_id: 2,
1366                            ..Default::default()
1367                        },
1368                    }],
1369                    cx,
1370                )
1371                .unwrap();
1372            project.disk_based_diagnostics_finished(server_id_2, cx);
1373        });
1374
1375        // Both language server's diagnostics are shown.
1376        cx.executor().run_until_parked();
1377        view.update(cx, |view, cx| {
1378            assert_eq!(
1379                editor_blocks(&view.editor, cx),
1380                [
1381                    (0, "path header block".into()),
1382                    (2, "diagnostic header".into()),
1383                    (6, "collapsed context".into()),
1384                    (7, "diagnostic header".into()),
1385                ]
1386            );
1387            assert_eq!(
1388                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1389                concat!(
1390                    "\n", // filename
1391                    "\n", // padding
1392                    // diagnostic group 1
1393                    "\n",     // primary message
1394                    "\n",     // padding
1395                    "a();\n", // location
1396                    "b();\n", //
1397                    "\n",     // collapsed context
1398                    // diagnostic group 2
1399                    "\n",     // primary message
1400                    "\n",     // padding
1401                    "a();\n", // context
1402                    "b();\n", //
1403                    "c();",   // context
1404                )
1405            );
1406        });
1407
1408        // Both language servers start updating diagnostics, and the first server finishes.
1409        project.update(cx, |project, cx| {
1410            project.disk_based_diagnostics_started(server_id_1, cx);
1411            project.disk_based_diagnostics_started(server_id_2, cx);
1412            project
1413                .update_diagnostic_entries(
1414                    server_id_1,
1415                    PathBuf::from("/test/main.js"),
1416                    None,
1417                    vec![DiagnosticEntry {
1418                        range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
1419                        diagnostic: Diagnostic {
1420                            message: "warning 2".to_string(),
1421                            severity: DiagnosticSeverity::WARNING,
1422                            is_primary: true,
1423                            is_disk_based: true,
1424                            group_id: 1,
1425                            ..Default::default()
1426                        },
1427                    }],
1428                    cx,
1429                )
1430                .unwrap();
1431            project
1432                .update_diagnostic_entries(
1433                    server_id_2,
1434                    PathBuf::from("/test/main.rs"),
1435                    None,
1436                    vec![],
1437                    cx,
1438                )
1439                .unwrap();
1440            project.disk_based_diagnostics_finished(server_id_1, cx);
1441        });
1442
1443        // Only the first language server's diagnostics are updated.
1444        cx.executor().run_until_parked();
1445        view.update(cx, |view, cx| {
1446            assert_eq!(
1447                editor_blocks(&view.editor, cx),
1448                [
1449                    (0, "path header block".into()),
1450                    (2, "diagnostic header".into()),
1451                    (7, "collapsed context".into()),
1452                    (8, "diagnostic header".into()),
1453                ]
1454            );
1455            assert_eq!(
1456                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1457                concat!(
1458                    "\n", // filename
1459                    "\n", // padding
1460                    // diagnostic group 1
1461                    "\n",     // primary message
1462                    "\n",     // padding
1463                    "a();\n", // location
1464                    "b();\n", //
1465                    "c();\n", // context
1466                    "\n",     // collapsed context
1467                    // diagnostic group 2
1468                    "\n",     // primary message
1469                    "\n",     // padding
1470                    "b();\n", // context
1471                    "c();\n", //
1472                    "d();",   // context
1473                )
1474            );
1475        });
1476
1477        // The second language server finishes.
1478        project.update(cx, |project, cx| {
1479            project
1480                .update_diagnostic_entries(
1481                    server_id_2,
1482                    PathBuf::from("/test/main.js"),
1483                    None,
1484                    vec![DiagnosticEntry {
1485                        range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
1486                        diagnostic: Diagnostic {
1487                            message: "warning 2".to_string(),
1488                            severity: DiagnosticSeverity::WARNING,
1489                            is_primary: true,
1490                            is_disk_based: true,
1491                            group_id: 1,
1492                            ..Default::default()
1493                        },
1494                    }],
1495                    cx,
1496                )
1497                .unwrap();
1498            project.disk_based_diagnostics_finished(server_id_2, cx);
1499        });
1500
1501        // Both language servers' diagnostics are updated.
1502        cx.executor().run_until_parked();
1503        view.update(cx, |view, cx| {
1504            assert_eq!(
1505                editor_blocks(&view.editor, cx),
1506                [
1507                    (0, "path header block".into()),
1508                    (2, "diagnostic header".into()),
1509                    (7, "collapsed context".into()),
1510                    (8, "diagnostic header".into()),
1511                ]
1512            );
1513            assert_eq!(
1514                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1515                concat!(
1516                    "\n", // filename
1517                    "\n", // padding
1518                    // diagnostic group 1
1519                    "\n",     // primary message
1520                    "\n",     // padding
1521                    "b();\n", // location
1522                    "c();\n", //
1523                    "d();\n", // context
1524                    "\n",     // collapsed context
1525                    // diagnostic group 2
1526                    "\n",     // primary message
1527                    "\n",     // padding
1528                    "c();\n", // context
1529                    "d();\n", //
1530                    "e();",   // context
1531                )
1532            );
1533        });
1534    }
1535
1536    fn init_test(cx: &mut TestAppContext) {
1537        cx.update(|cx| {
1538            let settings = SettingsStore::test(cx);
1539            cx.set_global(settings);
1540            theme::init(theme::LoadThemes::JustBase, cx);
1541            language::init(cx);
1542            client::init_settings(cx);
1543            workspace::init_settings(cx);
1544            Project::init_settings(cx);
1545            crate::init(cx);
1546        });
1547    }
1548
1549    fn editor_blocks(editor: &View<Editor>, cx: &mut WindowContext) -> Vec<(u32, SharedString)> {
1550        editor.update(cx, |editor, cx| {
1551            let snapshot = editor.snapshot(cx);
1552            snapshot
1553                .blocks_in_range(0..snapshot.max_point().row())
1554                .enumerate()
1555                .filter_map(|(ix, (row, block))| {
1556                    let name = match block {
1557                        TransformBlock::Custom(block) => block
1558                            .render(&mut BlockContext {
1559                                view_context: cx,
1560                                anchor_x: px(0.),
1561                                gutter_padding: px(0.),
1562                                gutter_width: px(0.),
1563                                line_height: px(0.),
1564                                em_width: px(0.),
1565                                block_id: ix,
1566                                editor_style: &editor::EditorStyle::default(),
1567                            })
1568                            .inner_id()?
1569                            .try_into()
1570                            .ok()?,
1571
1572                        TransformBlock::ExcerptHeader {
1573                            starts_new_buffer, ..
1574                        } => {
1575                            if *starts_new_buffer {
1576                                "path header block".into()
1577                            } else {
1578                                "collapsed context".into()
1579                            }
1580                        }
1581                    };
1582
1583                    Some((row, name))
1584                })
1585                .collect()
1586        })
1587    }
1588}