diagnostics.rs

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