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