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