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