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