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