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