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