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<L: language::ToOffset, R: language::ToOffset>(
 891    lhs: &DiagnosticEntry<L>,
 892    rhs: &DiagnosticEntry<R>,
 893    snapshot: &language::BufferSnapshot,
 894) -> Ordering {
 895    lhs.range
 896        .start
 897        .to_offset(snapshot)
 898        .cmp(&rhs.range.start.to_offset(snapshot))
 899        .then_with(|| {
 900            lhs.range
 901                .end
 902                .to_offset(snapshot)
 903                .cmp(&rhs.range.end.to_offset(snapshot))
 904        })
 905        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 906}
 907
 908#[cfg(test)]
 909mod tests {
 910    use super::*;
 911    use editor::{
 912        display_map::{BlockContext, TransformBlock},
 913        DisplayPoint, GutterDimensions,
 914    };
 915    use gpui::{px, Stateful, TestAppContext, VisualTestContext, WindowContext};
 916    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
 917    use project::FakeFs;
 918    use serde_json::json;
 919    use settings::SettingsStore;
 920    use unindent::Unindent as _;
 921
 922    #[gpui::test]
 923    async fn test_diagnostics(cx: &mut TestAppContext) {
 924        init_test(cx);
 925
 926        let fs = FakeFs::new(cx.executor());
 927        fs.insert_tree(
 928            "/test",
 929            json!({
 930                "consts.rs": "
 931                    const a: i32 = 'a';
 932                    const b: i32 = c;
 933                "
 934                .unindent(),
 935
 936                "main.rs": "
 937                    fn main() {
 938                        let x = vec![];
 939                        let y = vec![];
 940                        a(x);
 941                        b(y);
 942                        // comment 1
 943                        // comment 2
 944                        c(y);
 945                        d(x);
 946                    }
 947                "
 948                .unindent(),
 949            }),
 950        )
 951        .await;
 952
 953        let language_server_id = LanguageServerId(0);
 954        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
 955        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 956        let cx = &mut VisualTestContext::from_window(*window, cx);
 957        let workspace = window.root(cx).unwrap();
 958
 959        // Create some diagnostics
 960        project.update(cx, |project, cx| {
 961            project
 962                .update_diagnostic_entries(
 963                    language_server_id,
 964                    PathBuf::from("/test/main.rs"),
 965                    None,
 966                    vec![
 967                        DiagnosticEntry {
 968                            range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
 969                            diagnostic: Diagnostic {
 970                                message:
 971                                    "move occurs because `x` 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: 1,
 977                                ..Default::default()
 978                            },
 979                        },
 980                        DiagnosticEntry {
 981                            range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
 982                            diagnostic: Diagnostic {
 983                                message:
 984                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 985                                        .to_string(),
 986                                severity: DiagnosticSeverity::INFORMATION,
 987                                is_primary: false,
 988                                is_disk_based: true,
 989                                group_id: 0,
 990                                ..Default::default()
 991                            },
 992                        },
 993                        DiagnosticEntry {
 994                            range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
 995                            diagnostic: Diagnostic {
 996                                message: "value moved here".to_string(),
 997                                severity: DiagnosticSeverity::INFORMATION,
 998                                is_primary: false,
 999                                is_disk_based: true,
1000                                group_id: 1,
1001                                ..Default::default()
1002                            },
1003                        },
1004                        DiagnosticEntry {
1005                            range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
1006                            diagnostic: Diagnostic {
1007                                message: "value moved here".to_string(),
1008                                severity: DiagnosticSeverity::INFORMATION,
1009                                is_primary: false,
1010                                is_disk_based: true,
1011                                group_id: 0,
1012                                ..Default::default()
1013                            },
1014                        },
1015                        DiagnosticEntry {
1016                            range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
1017                            diagnostic: Diagnostic {
1018                                message: "use of moved value\nvalue used here after move".to_string(),
1019                                severity: DiagnosticSeverity::ERROR,
1020                                is_primary: true,
1021                                is_disk_based: true,
1022                                group_id: 0,
1023                                ..Default::default()
1024                            },
1025                        },
1026                        DiagnosticEntry {
1027                            range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
1028                            diagnostic: Diagnostic {
1029                                message: "use of moved value\nvalue used here after move".to_string(),
1030                                severity: DiagnosticSeverity::ERROR,
1031                                is_primary: true,
1032                                is_disk_based: true,
1033                                group_id: 1,
1034                                ..Default::default()
1035                            },
1036                        },
1037                    ],
1038                    cx,
1039                )
1040                .unwrap();
1041        });
1042
1043        // Open the project diagnostics view while there are already diagnostics.
1044        let view = window.build_view(cx, |cx| {
1045            ProjectDiagnosticsEditor::new_with_context(
1046                1,
1047                project.clone(),
1048                workspace.downgrade(),
1049                cx,
1050            )
1051        });
1052
1053        view.next_notification(cx).await;
1054        view.update(cx, |view, cx| {
1055            assert_eq!(
1056                editor_blocks(&view.editor, cx),
1057                [
1058                    (0, "path header block".into()),
1059                    (2, "diagnostic header".into()),
1060                    (15, "collapsed context".into()),
1061                    (16, "diagnostic header".into()),
1062                    (25, "collapsed context".into()),
1063                ]
1064            );
1065            assert_eq!(
1066                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1067                concat!(
1068                    //
1069                    // main.rs
1070                    //
1071                    "\n", // filename
1072                    "\n", // padding
1073                    // diagnostic group 1
1074                    "\n", // primary message
1075                    "\n", // padding
1076                    "    let x = vec![];\n",
1077                    "    let y = vec![];\n",
1078                    "\n", // supporting diagnostic
1079                    "    a(x);\n",
1080                    "    b(y);\n",
1081                    "\n", // supporting diagnostic
1082                    "    // comment 1\n",
1083                    "    // comment 2\n",
1084                    "    c(y);\n",
1085                    "\n", // supporting diagnostic
1086                    "    d(x);\n",
1087                    "\n", // context ellipsis
1088                    // diagnostic group 2
1089                    "\n", // primary message
1090                    "\n", // padding
1091                    "fn main() {\n",
1092                    "    let x = vec![];\n",
1093                    "\n", // supporting diagnostic
1094                    "    let y = vec![];\n",
1095                    "    a(x);\n",
1096                    "\n", // supporting diagnostic
1097                    "    b(y);\n",
1098                    "\n", // context ellipsis
1099                    "    c(y);\n",
1100                    "    d(x);\n",
1101                    "\n", // supporting diagnostic
1102                    "}"
1103                )
1104            );
1105
1106            // Cursor is at the first diagnostic
1107            view.editor.update(cx, |editor, cx| {
1108                assert_eq!(
1109                    editor.selections.display_ranges(cx),
1110                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
1111                );
1112            });
1113        });
1114
1115        // Diagnostics are added for another earlier path.
1116        project.update(cx, |project, cx| {
1117            project.disk_based_diagnostics_started(language_server_id, cx);
1118            project
1119                .update_diagnostic_entries(
1120                    language_server_id,
1121                    PathBuf::from("/test/consts.rs"),
1122                    None,
1123                    vec![DiagnosticEntry {
1124                        range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
1125                        diagnostic: Diagnostic {
1126                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1127                            severity: DiagnosticSeverity::ERROR,
1128                            is_primary: true,
1129                            is_disk_based: true,
1130                            group_id: 0,
1131                            ..Default::default()
1132                        },
1133                    }],
1134                    cx,
1135                )
1136                .unwrap();
1137            project.disk_based_diagnostics_finished(language_server_id, cx);
1138        });
1139
1140        view.next_notification(cx).await;
1141        view.update(cx, |view, cx| {
1142            assert_eq!(
1143                editor_blocks(&view.editor, cx),
1144                [
1145                    (0, "path header block".into()),
1146                    (2, "diagnostic header".into()),
1147                    (7, "path header block".into()),
1148                    (9, "diagnostic header".into()),
1149                    (22, "collapsed context".into()),
1150                    (23, "diagnostic header".into()),
1151                    (32, "collapsed context".into()),
1152                ]
1153            );
1154            assert_eq!(
1155                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1156                concat!(
1157                    //
1158                    // consts.rs
1159                    //
1160                    "\n", // filename
1161                    "\n", // padding
1162                    // diagnostic group 1
1163                    "\n", // primary message
1164                    "\n", // padding
1165                    "const a: i32 = 'a';\n",
1166                    "\n", // supporting diagnostic
1167                    "const b: i32 = c;\n",
1168                    //
1169                    // main.rs
1170                    //
1171                    "\n", // filename
1172                    "\n", // padding
1173                    // diagnostic group 1
1174                    "\n", // primary message
1175                    "\n", // padding
1176                    "    let x = vec![];\n",
1177                    "    let y = vec![];\n",
1178                    "\n", // supporting diagnostic
1179                    "    a(x);\n",
1180                    "    b(y);\n",
1181                    "\n", // supporting diagnostic
1182                    "    // comment 1\n",
1183                    "    // comment 2\n",
1184                    "    c(y);\n",
1185                    "\n", // supporting diagnostic
1186                    "    d(x);\n",
1187                    "\n", // collapsed context
1188                    // diagnostic group 2
1189                    "\n", // primary message
1190                    "\n", // filename
1191                    "fn main() {\n",
1192                    "    let x = vec![];\n",
1193                    "\n", // supporting diagnostic
1194                    "    let y = vec![];\n",
1195                    "    a(x);\n",
1196                    "\n", // supporting diagnostic
1197                    "    b(y);\n",
1198                    "\n", // context ellipsis
1199                    "    c(y);\n",
1200                    "    d(x);\n",
1201                    "\n", // supporting diagnostic
1202                    "}"
1203                )
1204            );
1205
1206            // Cursor keeps its position.
1207            view.editor.update(cx, |editor, cx| {
1208                assert_eq!(
1209                    editor.selections.display_ranges(cx),
1210                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1211                );
1212            });
1213        });
1214
1215        // Diagnostics are added to the first path
1216        project.update(cx, |project, cx| {
1217            project.disk_based_diagnostics_started(language_server_id, cx);
1218            project
1219                .update_diagnostic_entries(
1220                    language_server_id,
1221                    PathBuf::from("/test/consts.rs"),
1222                    None,
1223                    vec![
1224                        DiagnosticEntry {
1225                            range: Unclipped(PointUtf16::new(0, 15))
1226                                ..Unclipped(PointUtf16::new(0, 15)),
1227                            diagnostic: Diagnostic {
1228                                message: "mismatched types\nexpected `usize`, found `char`"
1229                                    .to_string(),
1230                                severity: DiagnosticSeverity::ERROR,
1231                                is_primary: true,
1232                                is_disk_based: true,
1233                                group_id: 0,
1234                                ..Default::default()
1235                            },
1236                        },
1237                        DiagnosticEntry {
1238                            range: Unclipped(PointUtf16::new(1, 15))
1239                                ..Unclipped(PointUtf16::new(1, 15)),
1240                            diagnostic: Diagnostic {
1241                                message: "unresolved name `c`".to_string(),
1242                                severity: DiagnosticSeverity::ERROR,
1243                                is_primary: true,
1244                                is_disk_based: true,
1245                                group_id: 1,
1246                                ..Default::default()
1247                            },
1248                        },
1249                    ],
1250                    cx,
1251                )
1252                .unwrap();
1253            project.disk_based_diagnostics_finished(language_server_id, cx);
1254        });
1255
1256        view.next_notification(cx).await;
1257        view.update(cx, |view, cx| {
1258            assert_eq!(
1259                editor_blocks(&view.editor, cx),
1260                [
1261                    (0, "path header block".into()),
1262                    (2, "diagnostic header".into()),
1263                    (7, "collapsed context".into()),
1264                    (8, "diagnostic header".into()),
1265                    (13, "path header block".into()),
1266                    (15, "diagnostic header".into()),
1267                    (28, "collapsed context".into()),
1268                    (29, "diagnostic header".into()),
1269                    (38, "collapsed context".into()),
1270                ]
1271            );
1272            assert_eq!(
1273                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1274                concat!(
1275                    //
1276                    // consts.rs
1277                    //
1278                    "\n", // filename
1279                    "\n", // padding
1280                    // diagnostic group 1
1281                    "\n", // primary message
1282                    "\n", // padding
1283                    "const a: i32 = 'a';\n",
1284                    "\n", // supporting diagnostic
1285                    "const b: i32 = c;\n",
1286                    "\n", // context ellipsis
1287                    // diagnostic group 2
1288                    "\n", // primary message
1289                    "\n", // padding
1290                    "const a: i32 = 'a';\n",
1291                    "const b: i32 = c;\n",
1292                    "\n", // supporting diagnostic
1293                    //
1294                    // main.rs
1295                    //
1296                    "\n", // filename
1297                    "\n", // padding
1298                    // diagnostic group 1
1299                    "\n", // primary message
1300                    "\n", // padding
1301                    "    let x = vec![];\n",
1302                    "    let y = vec![];\n",
1303                    "\n", // supporting diagnostic
1304                    "    a(x);\n",
1305                    "    b(y);\n",
1306                    "\n", // supporting diagnostic
1307                    "    // comment 1\n",
1308                    "    // comment 2\n",
1309                    "    c(y);\n",
1310                    "\n", // supporting diagnostic
1311                    "    d(x);\n",
1312                    "\n", // context ellipsis
1313                    // diagnostic group 2
1314                    "\n", // primary message
1315                    "\n", // filename
1316                    "fn main() {\n",
1317                    "    let x = vec![];\n",
1318                    "\n", // supporting diagnostic
1319                    "    let y = vec![];\n",
1320                    "    a(x);\n",
1321                    "\n", // supporting diagnostic
1322                    "    b(y);\n",
1323                    "\n", // context ellipsis
1324                    "    c(y);\n",
1325                    "    d(x);\n",
1326                    "\n", // supporting diagnostic
1327                    "}"
1328                )
1329            );
1330        });
1331    }
1332
1333    #[gpui::test]
1334    async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1335        init_test(cx);
1336
1337        let fs = FakeFs::new(cx.executor());
1338        fs.insert_tree(
1339            "/test",
1340            json!({
1341                "main.js": "
1342                    a();
1343                    b();
1344                    c();
1345                    d();
1346                    e();
1347                ".unindent()
1348            }),
1349        )
1350        .await;
1351
1352        let server_id_1 = LanguageServerId(100);
1353        let server_id_2 = LanguageServerId(101);
1354        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1355        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1356        let cx = &mut VisualTestContext::from_window(*window, cx);
1357        let workspace = window.root(cx).unwrap();
1358
1359        let view = window.build_view(cx, |cx| {
1360            ProjectDiagnosticsEditor::new_with_context(
1361                1,
1362                project.clone(),
1363                workspace.downgrade(),
1364                cx,
1365            )
1366        });
1367
1368        // Two language servers start updating diagnostics
1369        project.update(cx, |project, cx| {
1370            project.disk_based_diagnostics_started(server_id_1, cx);
1371            project.disk_based_diagnostics_started(server_id_2, cx);
1372            project
1373                .update_diagnostic_entries(
1374                    server_id_1,
1375                    PathBuf::from("/test/main.js"),
1376                    None,
1377                    vec![DiagnosticEntry {
1378                        range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
1379                        diagnostic: Diagnostic {
1380                            message: "error 1".to_string(),
1381                            severity: DiagnosticSeverity::WARNING,
1382                            is_primary: true,
1383                            is_disk_based: true,
1384                            group_id: 1,
1385                            ..Default::default()
1386                        },
1387                    }],
1388                    cx,
1389                )
1390                .unwrap();
1391        });
1392
1393        // The first language server finishes
1394        project.update(cx, |project, cx| {
1395            project.disk_based_diagnostics_finished(server_id_1, cx);
1396        });
1397
1398        // Only the first language server's diagnostics are shown.
1399        cx.executor().run_until_parked();
1400        view.update(cx, |view, cx| {
1401            assert_eq!(
1402                editor_blocks(&view.editor, cx),
1403                [
1404                    (0, "path header block".into()),
1405                    (2, "diagnostic header".into()),
1406                ]
1407            );
1408            assert_eq!(
1409                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1410                concat!(
1411                    "\n", // filename
1412                    "\n", // padding
1413                    // diagnostic group 1
1414                    "\n",     // primary message
1415                    "\n",     // padding
1416                    "a();\n", //
1417                    "b();",
1418                )
1419            );
1420        });
1421
1422        // The second language server finishes
1423        project.update(cx, |project, cx| {
1424            project
1425                .update_diagnostic_entries(
1426                    server_id_2,
1427                    PathBuf::from("/test/main.js"),
1428                    None,
1429                    vec![DiagnosticEntry {
1430                        range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
1431                        diagnostic: Diagnostic {
1432                            message: "warning 1".to_string(),
1433                            severity: DiagnosticSeverity::ERROR,
1434                            is_primary: true,
1435                            is_disk_based: true,
1436                            group_id: 2,
1437                            ..Default::default()
1438                        },
1439                    }],
1440                    cx,
1441                )
1442                .unwrap();
1443            project.disk_based_diagnostics_finished(server_id_2, cx);
1444        });
1445
1446        // Both language server's diagnostics are shown.
1447        cx.executor().run_until_parked();
1448        view.update(cx, |view, cx| {
1449            assert_eq!(
1450                editor_blocks(&view.editor, cx),
1451                [
1452                    (0, "path header block".into()),
1453                    (2, "diagnostic header".into()),
1454                    (6, "collapsed context".into()),
1455                    (7, "diagnostic header".into()),
1456                ]
1457            );
1458            assert_eq!(
1459                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1460                concat!(
1461                    "\n", // filename
1462                    "\n", // padding
1463                    // diagnostic group 1
1464                    "\n",     // primary message
1465                    "\n",     // padding
1466                    "a();\n", // location
1467                    "b();\n", //
1468                    "\n",     // collapsed context
1469                    // diagnostic group 2
1470                    "\n",     // primary message
1471                    "\n",     // padding
1472                    "a();\n", // context
1473                    "b();\n", //
1474                    "c();",   // context
1475                )
1476            );
1477        });
1478
1479        // Both language servers start updating diagnostics, and the first server finishes.
1480        project.update(cx, |project, cx| {
1481            project.disk_based_diagnostics_started(server_id_1, cx);
1482            project.disk_based_diagnostics_started(server_id_2, cx);
1483            project
1484                .update_diagnostic_entries(
1485                    server_id_1,
1486                    PathBuf::from("/test/main.js"),
1487                    None,
1488                    vec![DiagnosticEntry {
1489                        range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
1490                        diagnostic: Diagnostic {
1491                            message: "warning 2".to_string(),
1492                            severity: DiagnosticSeverity::WARNING,
1493                            is_primary: true,
1494                            is_disk_based: true,
1495                            group_id: 1,
1496                            ..Default::default()
1497                        },
1498                    }],
1499                    cx,
1500                )
1501                .unwrap();
1502            project
1503                .update_diagnostic_entries(
1504                    server_id_2,
1505                    PathBuf::from("/test/main.rs"),
1506                    None,
1507                    vec![],
1508                    cx,
1509                )
1510                .unwrap();
1511            project.disk_based_diagnostics_finished(server_id_1, cx);
1512        });
1513
1514        // Only the first language server's diagnostics are updated.
1515        cx.executor().run_until_parked();
1516        view.update(cx, |view, cx| {
1517            assert_eq!(
1518                editor_blocks(&view.editor, cx),
1519                [
1520                    (0, "path header block".into()),
1521                    (2, "diagnostic header".into()),
1522                    (7, "collapsed context".into()),
1523                    (8, "diagnostic header".into()),
1524                ]
1525            );
1526            assert_eq!(
1527                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1528                concat!(
1529                    "\n", // filename
1530                    "\n", // padding
1531                    // diagnostic group 1
1532                    "\n",     // primary message
1533                    "\n",     // padding
1534                    "a();\n", // location
1535                    "b();\n", //
1536                    "c();\n", // context
1537                    "\n",     // collapsed context
1538                    // diagnostic group 2
1539                    "\n",     // primary message
1540                    "\n",     // padding
1541                    "b();\n", // context
1542                    "c();\n", //
1543                    "d();",   // context
1544                )
1545            );
1546        });
1547
1548        // The second language server finishes.
1549        project.update(cx, |project, cx| {
1550            project
1551                .update_diagnostic_entries(
1552                    server_id_2,
1553                    PathBuf::from("/test/main.js"),
1554                    None,
1555                    vec![DiagnosticEntry {
1556                        range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
1557                        diagnostic: Diagnostic {
1558                            message: "warning 2".to_string(),
1559                            severity: DiagnosticSeverity::WARNING,
1560                            is_primary: true,
1561                            is_disk_based: true,
1562                            group_id: 1,
1563                            ..Default::default()
1564                        },
1565                    }],
1566                    cx,
1567                )
1568                .unwrap();
1569            project.disk_based_diagnostics_finished(server_id_2, cx);
1570        });
1571
1572        // Both language servers' diagnostics are updated.
1573        cx.executor().run_until_parked();
1574        view.update(cx, |view, cx| {
1575            assert_eq!(
1576                editor_blocks(&view.editor, cx),
1577                [
1578                    (0, "path header block".into()),
1579                    (2, "diagnostic header".into()),
1580                    (7, "collapsed context".into()),
1581                    (8, "diagnostic header".into()),
1582                ]
1583            );
1584            assert_eq!(
1585                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1586                concat!(
1587                    "\n", // filename
1588                    "\n", // padding
1589                    // diagnostic group 1
1590                    "\n",     // primary message
1591                    "\n",     // padding
1592                    "b();\n", // location
1593                    "c();\n", //
1594                    "d();\n", // context
1595                    "\n",     // collapsed context
1596                    // diagnostic group 2
1597                    "\n",     // primary message
1598                    "\n",     // padding
1599                    "c();\n", // context
1600                    "d();\n", //
1601                    "e();",   // context
1602                )
1603            );
1604        });
1605    }
1606
1607    fn init_test(cx: &mut TestAppContext) {
1608        cx.update(|cx| {
1609            let settings = SettingsStore::test(cx);
1610            cx.set_global(settings);
1611            theme::init(theme::LoadThemes::JustBase, cx);
1612            language::init(cx);
1613            client::init_settings(cx);
1614            workspace::init_settings(cx);
1615            Project::init_settings(cx);
1616            crate::init(cx);
1617            editor::init(cx);
1618        });
1619    }
1620
1621    fn editor_blocks(editor: &View<Editor>, cx: &mut WindowContext) -> Vec<(u32, SharedString)> {
1622        editor.update(cx, |editor, cx| {
1623            let snapshot = editor.snapshot(cx);
1624            snapshot
1625                .blocks_in_range(0..snapshot.max_point().row())
1626                .enumerate()
1627                .filter_map(|(ix, (row, block))| {
1628                    let name: SharedString = match block {
1629                        TransformBlock::Custom(block) => cx.with_element_context({
1630                            |cx| -> Option<SharedString> {
1631                                let mut element = block.render(&mut BlockContext {
1632                                    context: cx,
1633                                    anchor_x: px(0.),
1634                                    gutter_dimensions: &GutterDimensions::default(),
1635                                    line_height: px(0.),
1636                                    em_width: px(0.),
1637                                    max_width: px(0.),
1638                                    block_id: ix,
1639                                    editor_style: &editor::EditorStyle::default(),
1640                                });
1641                                let element = element.downcast_mut::<Stateful<Div>>().unwrap();
1642                                element.interactivity().element_id.clone()?.try_into().ok()
1643                            }
1644                        })?,
1645
1646                        TransformBlock::ExcerptHeader {
1647                            starts_new_buffer, ..
1648                        } => {
1649                            if *starts_new_buffer {
1650                                "path header block".into()
1651                            } else {
1652                                "collapsed context".into()
1653                            }
1654                        }
1655                    };
1656
1657                    Some((row, name))
1658                })
1659                .collect()
1660        })
1661    }
1662}