diagnostics.rs

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