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