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