diagnostics.rs

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