diagnostics.rs

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