diagnostics.rs

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