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