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