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