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::{Item, ItemEvent, ItemHandle},
  36    ItemNavHistory, Pane, 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>) -> Element<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                .boxed()
 100        } else {
 101            ChildView::new(&self.editor, cx).boxed()
 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(
 534        &self,
 535        _detail: Option<usize>,
 536        style: &theme::Tab,
 537        cx: &AppContext,
 538    ) -> Element<Pane> {
 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 deserialize(
 658        project: ModelHandle<Project>,
 659        workspace: WeakViewHandle<Workspace>,
 660        _workspace_id: workspace::WorkspaceId,
 661        _item_id: workspace::ItemId,
 662        cx: &mut ViewContext<Pane>,
 663    ) -> Task<Result<ViewHandle<Self>>> {
 664        Task::ready(Ok(cx.add_view(|cx| Self::new(project, workspace, cx))))
 665    }
 666}
 667
 668fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
 669    let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
 670    Arc::new(move |cx| {
 671        let settings = cx.global::<Settings>();
 672        let theme = &settings.theme.editor;
 673        let style = theme.diagnostic_header.clone();
 674        let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
 675        let icon_width = cx.em_width * style.icon_width_factor;
 676        let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
 677            Svg::new("icons/circle_x_mark_12.svg")
 678                .with_color(theme.error_diagnostic.message.text.color)
 679        } else {
 680            Svg::new("icons/triangle_exclamation_12.svg")
 681                .with_color(theme.warning_diagnostic.message.text.color)
 682        };
 683
 684        Flex::row()
 685            .with_child(
 686                icon.constrained()
 687                    .with_width(icon_width)
 688                    .aligned()
 689                    .contained()
 690                    .boxed(),
 691            )
 692            .with_child(
 693                Label::new(
 694                    message.clone(),
 695                    style.message.label.clone().with_font_size(font_size),
 696                )
 697                .with_highlights(highlights.clone())
 698                .contained()
 699                .with_style(style.message.container)
 700                .with_margin_left(cx.gutter_padding)
 701                .aligned()
 702                .boxed(),
 703            )
 704            .with_children(diagnostic.code.clone().map(|code| {
 705                Label::new(code, style.code.text.clone().with_font_size(font_size))
 706                    .contained()
 707                    .with_style(style.code.container)
 708                    .aligned()
 709                    .boxed()
 710            }))
 711            .contained()
 712            .with_style(style.container)
 713            .with_padding_left(cx.gutter_padding)
 714            .with_padding_right(cx.gutter_padding)
 715            .expanded()
 716            .named("diagnostic header")
 717    })
 718}
 719
 720pub(crate) fn render_summary(
 721    summary: &DiagnosticSummary,
 722    text_style: &TextStyle,
 723    theme: &theme::ProjectDiagnostics,
 724) -> Element<Pane> {
 725    if summary.error_count == 0 && summary.warning_count == 0 {
 726        Label::new("No problems", text_style.clone()).boxed()
 727    } else {
 728        let icon_width = theme.tab_icon_width;
 729        let icon_spacing = theme.tab_icon_spacing;
 730        let summary_spacing = theme.tab_summary_spacing;
 731        Flex::row()
 732            .with_children([
 733                Svg::new("icons/circle_x_mark_12.svg")
 734                    .with_color(text_style.color)
 735                    .constrained()
 736                    .with_width(icon_width)
 737                    .aligned()
 738                    .contained()
 739                    .with_margin_right(icon_spacing)
 740                    .named("no-icon"),
 741                Label::new(
 742                    summary.error_count.to_string(),
 743                    LabelStyle {
 744                        text: text_style.clone(),
 745                        highlight_text: None,
 746                    },
 747                )
 748                .aligned()
 749                .boxed(),
 750                Svg::new("icons/triangle_exclamation_12.svg")
 751                    .with_color(text_style.color)
 752                    .constrained()
 753                    .with_width(icon_width)
 754                    .aligned()
 755                    .contained()
 756                    .with_margin_left(summary_spacing)
 757                    .with_margin_right(icon_spacing)
 758                    .named("warn-icon"),
 759                Label::new(
 760                    summary.warning_count.to_string(),
 761                    LabelStyle {
 762                        text: text_style.clone(),
 763                        highlight_text: None,
 764                    },
 765                )
 766                .aligned()
 767                .boxed(),
 768            ])
 769            .boxed()
 770    }
 771}
 772
 773fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 774    lhs: &DiagnosticEntry<L>,
 775    rhs: &DiagnosticEntry<R>,
 776    snapshot: &language::BufferSnapshot,
 777) -> Ordering {
 778    lhs.range
 779        .start
 780        .to_offset(snapshot)
 781        .cmp(&rhs.range.start.to_offset(snapshot))
 782        .then_with(|| {
 783            lhs.range
 784                .end
 785                .to_offset(snapshot)
 786                .cmp(&rhs.range.end.to_offset(snapshot))
 787        })
 788        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 789}
 790
 791#[cfg(test)]
 792mod tests {
 793    use super::*;
 794    use editor::{
 795        display_map::{BlockContext, TransformBlock},
 796        DisplayPoint,
 797    };
 798    use gpui::{TestAppContext, WindowContext};
 799    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
 800    use project::FakeFs;
 801    use serde_json::json;
 802    use unindent::Unindent as _;
 803
 804    #[gpui::test]
 805    async fn test_diagnostics(cx: &mut TestAppContext) {
 806        Settings::test_async(cx);
 807        let fs = FakeFs::new(cx.background());
 808        fs.insert_tree(
 809            "/test",
 810            json!({
 811                "consts.rs": "
 812                        const a: i32 = 'a';
 813                        const b: i32 = c;
 814                    "
 815                .unindent(),
 816
 817                "main.rs": "
 818                        fn main() {
 819                            let x = vec![];
 820                            let y = vec![];
 821                            a(x);
 822                            b(y);
 823                            // comment 1
 824                            // comment 2
 825                            c(y);
 826                            d(x);
 827                        }
 828                    "
 829                .unindent(),
 830            }),
 831        )
 832        .await;
 833
 834        let language_server_id = LanguageServerId(0);
 835        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
 836        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 837
 838        // Create some diagnostics
 839        project.update(cx, |project, cx| {
 840            project
 841                .update_diagnostic_entries(
 842                    language_server_id,
 843                    PathBuf::from("/test/main.rs"),
 844                    None,
 845                    vec![
 846                        DiagnosticEntry {
 847                            range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
 848                            diagnostic: Diagnostic {
 849                                message:
 850                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 851                                        .to_string(),
 852                                severity: DiagnosticSeverity::INFORMATION,
 853                                is_primary: false,
 854                                is_disk_based: true,
 855                                group_id: 1,
 856                                ..Default::default()
 857                            },
 858                        },
 859                        DiagnosticEntry {
 860                            range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
 861                            diagnostic: Diagnostic {
 862                                message:
 863                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 864                                        .to_string(),
 865                                severity: DiagnosticSeverity::INFORMATION,
 866                                is_primary: false,
 867                                is_disk_based: true,
 868                                group_id: 0,
 869                                ..Default::default()
 870                            },
 871                        },
 872                        DiagnosticEntry {
 873                            range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
 874                            diagnostic: Diagnostic {
 875                                message: "value moved here".to_string(),
 876                                severity: DiagnosticSeverity::INFORMATION,
 877                                is_primary: false,
 878                                is_disk_based: true,
 879                                group_id: 1,
 880                                ..Default::default()
 881                            },
 882                        },
 883                        DiagnosticEntry {
 884                            range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
 885                            diagnostic: Diagnostic {
 886                                message: "value moved here".to_string(),
 887                                severity: DiagnosticSeverity::INFORMATION,
 888                                is_primary: false,
 889                                is_disk_based: true,
 890                                group_id: 0,
 891                                ..Default::default()
 892                            },
 893                        },
 894                        DiagnosticEntry {
 895                            range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
 896                            diagnostic: Diagnostic {
 897                                message: "use of moved value\nvalue used here after move".to_string(),
 898                                severity: DiagnosticSeverity::ERROR,
 899                                is_primary: true,
 900                                is_disk_based: true,
 901                                group_id: 0,
 902                                ..Default::default()
 903                            },
 904                        },
 905                        DiagnosticEntry {
 906                            range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
 907                            diagnostic: Diagnostic {
 908                                message: "use of moved value\nvalue used here after move".to_string(),
 909                                severity: DiagnosticSeverity::ERROR,
 910                                is_primary: true,
 911                                is_disk_based: true,
 912                                group_id: 1,
 913                                ..Default::default()
 914                            },
 915                        },
 916                    ],
 917                    cx,
 918                )
 919                .unwrap();
 920        });
 921
 922        // Open the project diagnostics view while there are already diagnostics.
 923        let view = cx.add_view(&workspace, |cx| {
 924            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
 925        });
 926
 927        view.next_notification(cx).await;
 928        view.update(cx, |view, cx| {
 929            assert_eq!(
 930                editor_blocks(&view.editor, cx),
 931                [
 932                    (0, "path header block".into()),
 933                    (2, "diagnostic header".into()),
 934                    (15, "collapsed context".into()),
 935                    (16, "diagnostic header".into()),
 936                    (25, "collapsed context".into()),
 937                ]
 938            );
 939            assert_eq!(
 940                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
 941                concat!(
 942                    //
 943                    // main.rs
 944                    //
 945                    "\n", // filename
 946                    "\n", // padding
 947                    // diagnostic group 1
 948                    "\n", // primary message
 949                    "\n", // padding
 950                    "    let x = vec![];\n",
 951                    "    let y = vec![];\n",
 952                    "\n", // supporting diagnostic
 953                    "    a(x);\n",
 954                    "    b(y);\n",
 955                    "\n", // supporting diagnostic
 956                    "    // comment 1\n",
 957                    "    // comment 2\n",
 958                    "    c(y);\n",
 959                    "\n", // supporting diagnostic
 960                    "    d(x);\n",
 961                    "\n", // context ellipsis
 962                    // diagnostic group 2
 963                    "\n", // primary message
 964                    "\n", // padding
 965                    "fn main() {\n",
 966                    "    let x = vec![];\n",
 967                    "\n", // supporting diagnostic
 968                    "    let y = vec![];\n",
 969                    "    a(x);\n",
 970                    "\n", // supporting diagnostic
 971                    "    b(y);\n",
 972                    "\n", // context ellipsis
 973                    "    c(y);\n",
 974                    "    d(x);\n",
 975                    "\n", // supporting diagnostic
 976                    "}"
 977                )
 978            );
 979
 980            // Cursor is at the first diagnostic
 981            view.editor.update(cx, |editor, cx| {
 982                assert_eq!(
 983                    editor.selections.display_ranges(cx),
 984                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
 985                );
 986            });
 987        });
 988
 989        // Diagnostics are added for another earlier path.
 990        project.update(cx, |project, cx| {
 991            project.disk_based_diagnostics_started(language_server_id, cx);
 992            project
 993                .update_diagnostic_entries(
 994                    language_server_id,
 995                    PathBuf::from("/test/consts.rs"),
 996                    None,
 997                    vec![DiagnosticEntry {
 998                        range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
 999                        diagnostic: Diagnostic {
1000                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1001                            severity: DiagnosticSeverity::ERROR,
1002                            is_primary: true,
1003                            is_disk_based: true,
1004                            group_id: 0,
1005                            ..Default::default()
1006                        },
1007                    }],
1008                    cx,
1009                )
1010                .unwrap();
1011            project.disk_based_diagnostics_finished(language_server_id, cx);
1012        });
1013
1014        view.next_notification(cx).await;
1015        view.update(cx, |view, cx| {
1016            assert_eq!(
1017                editor_blocks(&view.editor, cx),
1018                [
1019                    (0, "path header block".into()),
1020                    (2, "diagnostic header".into()),
1021                    (7, "path header block".into()),
1022                    (9, "diagnostic header".into()),
1023                    (22, "collapsed context".into()),
1024                    (23, "diagnostic header".into()),
1025                    (32, "collapsed context".into()),
1026                ]
1027            );
1028            assert_eq!(
1029                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1030                concat!(
1031                    //
1032                    // consts.rs
1033                    //
1034                    "\n", // filename
1035                    "\n", // padding
1036                    // diagnostic group 1
1037                    "\n", // primary message
1038                    "\n", // padding
1039                    "const a: i32 = 'a';\n",
1040                    "\n", // supporting diagnostic
1041                    "const b: i32 = c;\n",
1042                    //
1043                    // main.rs
1044                    //
1045                    "\n", // filename
1046                    "\n", // padding
1047                    // diagnostic group 1
1048                    "\n", // primary message
1049                    "\n", // padding
1050                    "    let x = vec![];\n",
1051                    "    let y = vec![];\n",
1052                    "\n", // supporting diagnostic
1053                    "    a(x);\n",
1054                    "    b(y);\n",
1055                    "\n", // supporting diagnostic
1056                    "    // comment 1\n",
1057                    "    // comment 2\n",
1058                    "    c(y);\n",
1059                    "\n", // supporting diagnostic
1060                    "    d(x);\n",
1061                    "\n", // collapsed context
1062                    // diagnostic group 2
1063                    "\n", // primary message
1064                    "\n", // filename
1065                    "fn main() {\n",
1066                    "    let x = vec![];\n",
1067                    "\n", // supporting diagnostic
1068                    "    let y = vec![];\n",
1069                    "    a(x);\n",
1070                    "\n", // supporting diagnostic
1071                    "    b(y);\n",
1072                    "\n", // context ellipsis
1073                    "    c(y);\n",
1074                    "    d(x);\n",
1075                    "\n", // supporting diagnostic
1076                    "}"
1077                )
1078            );
1079
1080            // Cursor keeps its position.
1081            view.editor.update(cx, |editor, cx| {
1082                assert_eq!(
1083                    editor.selections.display_ranges(cx),
1084                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1085                );
1086            });
1087        });
1088
1089        // Diagnostics are added to the first path
1090        project.update(cx, |project, cx| {
1091            project.disk_based_diagnostics_started(language_server_id, cx);
1092            project
1093                .update_diagnostic_entries(
1094                    language_server_id,
1095                    PathBuf::from("/test/consts.rs"),
1096                    None,
1097                    vec![
1098                        DiagnosticEntry {
1099                            range: Unclipped(PointUtf16::new(0, 15))
1100                                ..Unclipped(PointUtf16::new(0, 15)),
1101                            diagnostic: Diagnostic {
1102                                message: "mismatched types\nexpected `usize`, found `char`"
1103                                    .to_string(),
1104                                severity: DiagnosticSeverity::ERROR,
1105                                is_primary: true,
1106                                is_disk_based: true,
1107                                group_id: 0,
1108                                ..Default::default()
1109                            },
1110                        },
1111                        DiagnosticEntry {
1112                            range: Unclipped(PointUtf16::new(1, 15))
1113                                ..Unclipped(PointUtf16::new(1, 15)),
1114                            diagnostic: Diagnostic {
1115                                message: "unresolved name `c`".to_string(),
1116                                severity: DiagnosticSeverity::ERROR,
1117                                is_primary: true,
1118                                is_disk_based: true,
1119                                group_id: 1,
1120                                ..Default::default()
1121                            },
1122                        },
1123                    ],
1124                    cx,
1125                )
1126                .unwrap();
1127            project.disk_based_diagnostics_finished(language_server_id, cx);
1128        });
1129
1130        view.next_notification(cx).await;
1131        view.update(cx, |view, cx| {
1132            assert_eq!(
1133                editor_blocks(&view.editor, cx),
1134                [
1135                    (0, "path header block".into()),
1136                    (2, "diagnostic header".into()),
1137                    (7, "collapsed context".into()),
1138                    (8, "diagnostic header".into()),
1139                    (13, "path header block".into()),
1140                    (15, "diagnostic header".into()),
1141                    (28, "collapsed context".into()),
1142                    (29, "diagnostic header".into()),
1143                    (38, "collapsed context".into()),
1144                ]
1145            );
1146            assert_eq!(
1147                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1148                concat!(
1149                    //
1150                    // consts.rs
1151                    //
1152                    "\n", // filename
1153                    "\n", // padding
1154                    // diagnostic group 1
1155                    "\n", // primary message
1156                    "\n", // padding
1157                    "const a: i32 = 'a';\n",
1158                    "\n", // supporting diagnostic
1159                    "const b: i32 = c;\n",
1160                    "\n", // context ellipsis
1161                    // diagnostic group 2
1162                    "\n", // primary message
1163                    "\n", // padding
1164                    "const a: i32 = 'a';\n",
1165                    "const b: i32 = c;\n",
1166                    "\n", // supporting diagnostic
1167                    //
1168                    // main.rs
1169                    //
1170                    "\n", // filename
1171                    "\n", // padding
1172                    // diagnostic group 1
1173                    "\n", // primary message
1174                    "\n", // padding
1175                    "    let x = vec![];\n",
1176                    "    let y = vec![];\n",
1177                    "\n", // supporting diagnostic
1178                    "    a(x);\n",
1179                    "    b(y);\n",
1180                    "\n", // supporting diagnostic
1181                    "    // comment 1\n",
1182                    "    // comment 2\n",
1183                    "    c(y);\n",
1184                    "\n", // supporting diagnostic
1185                    "    d(x);\n",
1186                    "\n", // context ellipsis
1187                    // diagnostic group 2
1188                    "\n", // primary message
1189                    "\n", // filename
1190                    "fn main() {\n",
1191                    "    let x = vec![];\n",
1192                    "\n", // supporting diagnostic
1193                    "    let y = vec![];\n",
1194                    "    a(x);\n",
1195                    "\n", // supporting diagnostic
1196                    "    b(y);\n",
1197                    "\n", // context ellipsis
1198                    "    c(y);\n",
1199                    "    d(x);\n",
1200                    "\n", // supporting diagnostic
1201                    "}"
1202                )
1203            );
1204        });
1205    }
1206
1207    #[gpui::test]
1208    async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1209        Settings::test_async(cx);
1210        let fs = FakeFs::new(cx.background());
1211        fs.insert_tree(
1212            "/test",
1213            json!({
1214                "main.js": "
1215                    a();
1216                    b();
1217                    c();
1218                    d();
1219                    e();
1220                ".unindent()
1221            }),
1222        )
1223        .await;
1224
1225        let server_id_1 = LanguageServerId(100);
1226        let server_id_2 = LanguageServerId(101);
1227        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1228        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1229
1230        let view = cx.add_view(&workspace, |cx| {
1231            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
1232        });
1233
1234        // Two language servers start updating diagnostics
1235        project.update(cx, |project, cx| {
1236            project.disk_based_diagnostics_started(server_id_1, cx);
1237            project.disk_based_diagnostics_started(server_id_2, cx);
1238            project
1239                .update_diagnostic_entries(
1240                    server_id_1,
1241                    PathBuf::from("/test/main.js"),
1242                    None,
1243                    vec![DiagnosticEntry {
1244                        range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
1245                        diagnostic: Diagnostic {
1246                            message: "error 1".to_string(),
1247                            severity: DiagnosticSeverity::WARNING,
1248                            is_primary: true,
1249                            is_disk_based: true,
1250                            group_id: 1,
1251                            ..Default::default()
1252                        },
1253                    }],
1254                    cx,
1255                )
1256                .unwrap();
1257            project
1258                .update_diagnostic_entries(
1259                    server_id_2,
1260                    PathBuf::from("/test/main.js"),
1261                    None,
1262                    vec![DiagnosticEntry {
1263                        range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
1264                        diagnostic: Diagnostic {
1265                            message: "warning 1".to_string(),
1266                            severity: DiagnosticSeverity::ERROR,
1267                            is_primary: true,
1268                            is_disk_based: true,
1269                            group_id: 2,
1270                            ..Default::default()
1271                        },
1272                    }],
1273                    cx,
1274                )
1275                .unwrap();
1276        });
1277
1278        // The first language server finishes
1279        project.update(cx, |project, cx| {
1280            project.disk_based_diagnostics_finished(server_id_1, cx);
1281        });
1282
1283        // Only the first language server's diagnostics are shown.
1284        cx.foreground().run_until_parked();
1285        view.update(cx, |view, cx| {
1286            assert_eq!(
1287                editor_blocks(&view.editor, cx),
1288                [
1289                    (0, "path header block".into()),
1290                    (2, "diagnostic header".into()),
1291                ]
1292            );
1293            assert_eq!(
1294                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1295                concat!(
1296                    "\n", // filename
1297                    "\n", // padding
1298                    // diagnostic group 1
1299                    "\n",     // primary message
1300                    "\n",     // padding
1301                    "a();\n", //
1302                    "b();",
1303                )
1304            );
1305        });
1306
1307        // The second language server finishes
1308        project.update(cx, |project, cx| {
1309            project.disk_based_diagnostics_finished(server_id_2, cx);
1310        });
1311
1312        // Both language server's diagnostics are shown.
1313        cx.foreground().run_until_parked();
1314        view.update(cx, |view, cx| {
1315            assert_eq!(
1316                editor_blocks(&view.editor, cx),
1317                [
1318                    (0, "path header block".into()),
1319                    (2, "diagnostic header".into()),
1320                    (6, "collapsed context".into()),
1321                    (7, "diagnostic header".into()),
1322                ]
1323            );
1324            assert_eq!(
1325                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1326                concat!(
1327                    "\n", // filename
1328                    "\n", // padding
1329                    // diagnostic group 1
1330                    "\n",     // primary message
1331                    "\n",     // padding
1332                    "a();\n", // location
1333                    "b();\n", //
1334                    "\n",     // collapsed context
1335                    // diagnostic group 2
1336                    "\n",     // primary message
1337                    "\n",     // padding
1338                    "a();\n", // context
1339                    "b();\n", //
1340                    "c();",   // context
1341                )
1342            );
1343        });
1344
1345        // Both language servers start updating diagnostics, and the first server finishes.
1346        project.update(cx, |project, cx| {
1347            project.disk_based_diagnostics_started(server_id_1, cx);
1348            project.disk_based_diagnostics_started(server_id_2, cx);
1349            project
1350                .update_diagnostic_entries(
1351                    server_id_1,
1352                    PathBuf::from("/test/main.js"),
1353                    None,
1354                    vec![DiagnosticEntry {
1355                        range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
1356                        diagnostic: Diagnostic {
1357                            message: "warning 2".to_string(),
1358                            severity: DiagnosticSeverity::WARNING,
1359                            is_primary: true,
1360                            is_disk_based: true,
1361                            group_id: 1,
1362                            ..Default::default()
1363                        },
1364                    }],
1365                    cx,
1366                )
1367                .unwrap();
1368            project
1369                .update_diagnostic_entries(
1370                    server_id_2,
1371                    PathBuf::from("/test/main.rs"),
1372                    None,
1373                    vec![],
1374                    cx,
1375                )
1376                .unwrap();
1377            project.disk_based_diagnostics_finished(server_id_1, cx);
1378        });
1379
1380        // Only the first language server's diagnostics are updated.
1381        cx.foreground().run_until_parked();
1382        view.update(cx, |view, cx| {
1383            assert_eq!(
1384                editor_blocks(&view.editor, cx),
1385                [
1386                    (0, "path header block".into()),
1387                    (2, "diagnostic header".into()),
1388                    (7, "collapsed context".into()),
1389                    (8, "diagnostic header".into()),
1390                ]
1391            );
1392            assert_eq!(
1393                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1394                concat!(
1395                    "\n", // filename
1396                    "\n", // padding
1397                    // diagnostic group 1
1398                    "\n",     // primary message
1399                    "\n",     // padding
1400                    "a();\n", // location
1401                    "b();\n", //
1402                    "c();\n", // context
1403                    "\n",     // collapsed context
1404                    // diagnostic group 2
1405                    "\n",     // primary message
1406                    "\n",     // padding
1407                    "b();\n", // context
1408                    "c();\n", //
1409                    "d();",   // context
1410                )
1411            );
1412        });
1413
1414        // The second language server finishes.
1415        project.update(cx, |project, cx| {
1416            project
1417                .update_diagnostic_entries(
1418                    server_id_2,
1419                    PathBuf::from("/test/main.js"),
1420                    None,
1421                    vec![DiagnosticEntry {
1422                        range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
1423                        diagnostic: Diagnostic {
1424                            message: "warning 2".to_string(),
1425                            severity: DiagnosticSeverity::WARNING,
1426                            is_primary: true,
1427                            is_disk_based: true,
1428                            group_id: 1,
1429                            ..Default::default()
1430                        },
1431                    }],
1432                    cx,
1433                )
1434                .unwrap();
1435            project.disk_based_diagnostics_finished(server_id_2, cx);
1436        });
1437
1438        // Both language servers' diagnostics are updated.
1439        cx.foreground().run_until_parked();
1440        view.update(cx, |view, cx| {
1441            assert_eq!(
1442                editor_blocks(&view.editor, cx),
1443                [
1444                    (0, "path header block".into()),
1445                    (2, "diagnostic header".into()),
1446                    (7, "collapsed context".into()),
1447                    (8, "diagnostic header".into()),
1448                ]
1449            );
1450            assert_eq!(
1451                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1452                concat!(
1453                    "\n", // filename
1454                    "\n", // padding
1455                    // diagnostic group 1
1456                    "\n",     // primary message
1457                    "\n",     // padding
1458                    "b();\n", // location
1459                    "c();\n", //
1460                    "d();\n", // context
1461                    "\n",     // collapsed context
1462                    // diagnostic group 2
1463                    "\n",     // primary message
1464                    "\n",     // padding
1465                    "c();\n", // context
1466                    "d();\n", //
1467                    "e();",   // context
1468                )
1469            );
1470        });
1471    }
1472
1473    fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
1474        editor.update(cx, |editor, cx| {
1475            let snapshot = editor.snapshot(cx);
1476            snapshot
1477                .blocks_in_range(0..snapshot.max_point().row())
1478                .filter_map(|(row, block)| {
1479                    let name = match block {
1480                        TransformBlock::Custom(block) => block
1481                            .render(&mut BlockContext {
1482                                view_context: cx,
1483                                anchor_x: 0.,
1484                                scroll_x: 0.,
1485                                gutter_padding: 0.,
1486                                gutter_width: 0.,
1487                                line_height: 0.,
1488                                em_width: 0.,
1489                            })
1490                            .name()?
1491                            .to_string(),
1492                        TransformBlock::ExcerptHeader {
1493                            starts_new_buffer, ..
1494                        } => {
1495                            if *starts_new_buffer {
1496                                "path header block".to_string()
1497                            } else {
1498                                "collapsed context".to_string()
1499                            }
1500                        }
1501                    };
1502
1503                    Some((row, name))
1504                })
1505                .collect()
1506        })
1507    }
1508}