diagnostics.rs

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