diagnostics.rs

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