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