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