diagnostics.rs

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