diagnostics.rs

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