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