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 settings::SettingsStore;
 824    use unindent::Unindent as _;
 825
 826    #[gpui::test]
 827    async fn test_diagnostics(cx: &mut TestAppContext) {
 828        init_test(cx);
 829
 830        let fs = FakeFs::new(cx.background());
 831        fs.insert_tree(
 832            "/test",
 833            json!({
 834                "consts.rs": "
 835                        const a: i32 = 'a';
 836                        const b: i32 = c;
 837                    "
 838                .unindent(),
 839
 840                "main.rs": "
 841                        fn main() {
 842                            let x = vec![];
 843                            let y = vec![];
 844                            a(x);
 845                            b(y);
 846                            // comment 1
 847                            // comment 2
 848                            c(y);
 849                            d(x);
 850                        }
 851                    "
 852                .unindent(),
 853            }),
 854        )
 855        .await;
 856
 857        let language_server_id = LanguageServerId(0);
 858        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
 859        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 860
 861        // Create some diagnostics
 862        project.update(cx, |project, cx| {
 863            project
 864                .update_diagnostic_entries(
 865                    language_server_id,
 866                    PathBuf::from("/test/main.rs"),
 867                    None,
 868                    vec![
 869                        DiagnosticEntry {
 870                            range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
 871                            diagnostic: Diagnostic {
 872                                message:
 873                                    "move occurs because `x` 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: 1,
 879                                ..Default::default()
 880                            },
 881                        },
 882                        DiagnosticEntry {
 883                            range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
 884                            diagnostic: Diagnostic {
 885                                message:
 886                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 887                                        .to_string(),
 888                                severity: DiagnosticSeverity::INFORMATION,
 889                                is_primary: false,
 890                                is_disk_based: true,
 891                                group_id: 0,
 892                                ..Default::default()
 893                            },
 894                        },
 895                        DiagnosticEntry {
 896                            range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
 897                            diagnostic: Diagnostic {
 898                                message: "value moved here".to_string(),
 899                                severity: DiagnosticSeverity::INFORMATION,
 900                                is_primary: false,
 901                                is_disk_based: true,
 902                                group_id: 1,
 903                                ..Default::default()
 904                            },
 905                        },
 906                        DiagnosticEntry {
 907                            range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
 908                            diagnostic: Diagnostic {
 909                                message: "value moved here".to_string(),
 910                                severity: DiagnosticSeverity::INFORMATION,
 911                                is_primary: false,
 912                                is_disk_based: true,
 913                                group_id: 0,
 914                                ..Default::default()
 915                            },
 916                        },
 917                        DiagnosticEntry {
 918                            range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
 919                            diagnostic: Diagnostic {
 920                                message: "use of moved value\nvalue used here after move".to_string(),
 921                                severity: DiagnosticSeverity::ERROR,
 922                                is_primary: true,
 923                                is_disk_based: true,
 924                                group_id: 0,
 925                                ..Default::default()
 926                            },
 927                        },
 928                        DiagnosticEntry {
 929                            range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
 930                            diagnostic: Diagnostic {
 931                                message: "use of moved value\nvalue used here after move".to_string(),
 932                                severity: DiagnosticSeverity::ERROR,
 933                                is_primary: true,
 934                                is_disk_based: true,
 935                                group_id: 1,
 936                                ..Default::default()
 937                            },
 938                        },
 939                    ],
 940                    cx,
 941                )
 942                .unwrap();
 943        });
 944
 945        // Open the project diagnostics view while there are already diagnostics.
 946        let view = cx.add_view(window_id, |cx| {
 947            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
 948        });
 949
 950        view.next_notification(cx).await;
 951        view.update(cx, |view, cx| {
 952            assert_eq!(
 953                editor_blocks(&view.editor, cx),
 954                [
 955                    (0, "path header block".into()),
 956                    (2, "diagnostic header".into()),
 957                    (15, "collapsed context".into()),
 958                    (16, "diagnostic header".into()),
 959                    (25, "collapsed context".into()),
 960                ]
 961            );
 962            assert_eq!(
 963                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
 964                concat!(
 965                    //
 966                    // main.rs
 967                    //
 968                    "\n", // filename
 969                    "\n", // padding
 970                    // diagnostic group 1
 971                    "\n", // primary message
 972                    "\n", // padding
 973                    "    let x = vec![];\n",
 974                    "    let y = vec![];\n",
 975                    "\n", // supporting diagnostic
 976                    "    a(x);\n",
 977                    "    b(y);\n",
 978                    "\n", // supporting diagnostic
 979                    "    // comment 1\n",
 980                    "    // comment 2\n",
 981                    "    c(y);\n",
 982                    "\n", // supporting diagnostic
 983                    "    d(x);\n",
 984                    "\n", // context ellipsis
 985                    // diagnostic group 2
 986                    "\n", // primary message
 987                    "\n", // padding
 988                    "fn main() {\n",
 989                    "    let x = vec![];\n",
 990                    "\n", // supporting diagnostic
 991                    "    let y = vec![];\n",
 992                    "    a(x);\n",
 993                    "\n", // supporting diagnostic
 994                    "    b(y);\n",
 995                    "\n", // context ellipsis
 996                    "    c(y);\n",
 997                    "    d(x);\n",
 998                    "\n", // supporting diagnostic
 999                    "}"
1000                )
1001            );
1002
1003            // Cursor is at the first diagnostic
1004            view.editor.update(cx, |editor, cx| {
1005                assert_eq!(
1006                    editor.selections.display_ranges(cx),
1007                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
1008                );
1009            });
1010        });
1011
1012        // Diagnostics are added for another earlier path.
1013        project.update(cx, |project, cx| {
1014            project.disk_based_diagnostics_started(language_server_id, cx);
1015            project
1016                .update_diagnostic_entries(
1017                    language_server_id,
1018                    PathBuf::from("/test/consts.rs"),
1019                    None,
1020                    vec![DiagnosticEntry {
1021                        range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
1022                        diagnostic: Diagnostic {
1023                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1024                            severity: DiagnosticSeverity::ERROR,
1025                            is_primary: true,
1026                            is_disk_based: true,
1027                            group_id: 0,
1028                            ..Default::default()
1029                        },
1030                    }],
1031                    cx,
1032                )
1033                .unwrap();
1034            project.disk_based_diagnostics_finished(language_server_id, cx);
1035        });
1036
1037        view.next_notification(cx).await;
1038        view.update(cx, |view, cx| {
1039            assert_eq!(
1040                editor_blocks(&view.editor, cx),
1041                [
1042                    (0, "path header block".into()),
1043                    (2, "diagnostic header".into()),
1044                    (7, "path header block".into()),
1045                    (9, "diagnostic header".into()),
1046                    (22, "collapsed context".into()),
1047                    (23, "diagnostic header".into()),
1048                    (32, "collapsed context".into()),
1049                ]
1050            );
1051            assert_eq!(
1052                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1053                concat!(
1054                    //
1055                    // consts.rs
1056                    //
1057                    "\n", // filename
1058                    "\n", // padding
1059                    // diagnostic group 1
1060                    "\n", // primary message
1061                    "\n", // padding
1062                    "const a: i32 = 'a';\n",
1063                    "\n", // supporting diagnostic
1064                    "const b: i32 = c;\n",
1065                    //
1066                    // main.rs
1067                    //
1068                    "\n", // filename
1069                    "\n", // padding
1070                    // diagnostic group 1
1071                    "\n", // primary message
1072                    "\n", // padding
1073                    "    let x = vec![];\n",
1074                    "    let y = vec![];\n",
1075                    "\n", // supporting diagnostic
1076                    "    a(x);\n",
1077                    "    b(y);\n",
1078                    "\n", // supporting diagnostic
1079                    "    // comment 1\n",
1080                    "    // comment 2\n",
1081                    "    c(y);\n",
1082                    "\n", // supporting diagnostic
1083                    "    d(x);\n",
1084                    "\n", // collapsed context
1085                    // diagnostic group 2
1086                    "\n", // primary message
1087                    "\n", // filename
1088                    "fn main() {\n",
1089                    "    let x = vec![];\n",
1090                    "\n", // supporting diagnostic
1091                    "    let y = vec![];\n",
1092                    "    a(x);\n",
1093                    "\n", // supporting diagnostic
1094                    "    b(y);\n",
1095                    "\n", // context ellipsis
1096                    "    c(y);\n",
1097                    "    d(x);\n",
1098                    "\n", // supporting diagnostic
1099                    "}"
1100                )
1101            );
1102
1103            // Cursor keeps its position.
1104            view.editor.update(cx, |editor, cx| {
1105                assert_eq!(
1106                    editor.selections.display_ranges(cx),
1107                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1108                );
1109            });
1110        });
1111
1112        // Diagnostics are added to the first path
1113        project.update(cx, |project, cx| {
1114            project.disk_based_diagnostics_started(language_server_id, cx);
1115            project
1116                .update_diagnostic_entries(
1117                    language_server_id,
1118                    PathBuf::from("/test/consts.rs"),
1119                    None,
1120                    vec![
1121                        DiagnosticEntry {
1122                            range: Unclipped(PointUtf16::new(0, 15))
1123                                ..Unclipped(PointUtf16::new(0, 15)),
1124                            diagnostic: Diagnostic {
1125                                message: "mismatched types\nexpected `usize`, found `char`"
1126                                    .to_string(),
1127                                severity: DiagnosticSeverity::ERROR,
1128                                is_primary: true,
1129                                is_disk_based: true,
1130                                group_id: 0,
1131                                ..Default::default()
1132                            },
1133                        },
1134                        DiagnosticEntry {
1135                            range: Unclipped(PointUtf16::new(1, 15))
1136                                ..Unclipped(PointUtf16::new(1, 15)),
1137                            diagnostic: Diagnostic {
1138                                message: "unresolved name `c`".to_string(),
1139                                severity: DiagnosticSeverity::ERROR,
1140                                is_primary: true,
1141                                is_disk_based: true,
1142                                group_id: 1,
1143                                ..Default::default()
1144                            },
1145                        },
1146                    ],
1147                    cx,
1148                )
1149                .unwrap();
1150            project.disk_based_diagnostics_finished(language_server_id, cx);
1151        });
1152
1153        view.next_notification(cx).await;
1154        view.update(cx, |view, cx| {
1155            assert_eq!(
1156                editor_blocks(&view.editor, cx),
1157                [
1158                    (0, "path header block".into()),
1159                    (2, "diagnostic header".into()),
1160                    (7, "collapsed context".into()),
1161                    (8, "diagnostic header".into()),
1162                    (13, "path header block".into()),
1163                    (15, "diagnostic header".into()),
1164                    (28, "collapsed context".into()),
1165                    (29, "diagnostic header".into()),
1166                    (38, "collapsed context".into()),
1167                ]
1168            );
1169            assert_eq!(
1170                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1171                concat!(
1172                    //
1173                    // consts.rs
1174                    //
1175                    "\n", // filename
1176                    "\n", // padding
1177                    // diagnostic group 1
1178                    "\n", // primary message
1179                    "\n", // padding
1180                    "const a: i32 = 'a';\n",
1181                    "\n", // supporting diagnostic
1182                    "const b: i32 = c;\n",
1183                    "\n", // context ellipsis
1184                    // diagnostic group 2
1185                    "\n", // primary message
1186                    "\n", // padding
1187                    "const a: i32 = 'a';\n",
1188                    "const b: i32 = c;\n",
1189                    "\n", // supporting diagnostic
1190                    //
1191                    // main.rs
1192                    //
1193                    "\n", // filename
1194                    "\n", // padding
1195                    // diagnostic group 1
1196                    "\n", // primary message
1197                    "\n", // padding
1198                    "    let x = vec![];\n",
1199                    "    let y = vec![];\n",
1200                    "\n", // supporting diagnostic
1201                    "    a(x);\n",
1202                    "    b(y);\n",
1203                    "\n", // supporting diagnostic
1204                    "    // comment 1\n",
1205                    "    // comment 2\n",
1206                    "    c(y);\n",
1207                    "\n", // supporting diagnostic
1208                    "    d(x);\n",
1209                    "\n", // context ellipsis
1210                    // diagnostic group 2
1211                    "\n", // primary message
1212                    "\n", // filename
1213                    "fn main() {\n",
1214                    "    let x = vec![];\n",
1215                    "\n", // supporting diagnostic
1216                    "    let y = vec![];\n",
1217                    "    a(x);\n",
1218                    "\n", // supporting diagnostic
1219                    "    b(y);\n",
1220                    "\n", // context ellipsis
1221                    "    c(y);\n",
1222                    "    d(x);\n",
1223                    "\n", // supporting diagnostic
1224                    "}"
1225                )
1226            );
1227        });
1228    }
1229
1230    #[gpui::test]
1231    async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1232        init_test(cx);
1233
1234        let fs = FakeFs::new(cx.background());
1235        fs.insert_tree(
1236            "/test",
1237            json!({
1238                "main.js": "
1239                    a();
1240                    b();
1241                    c();
1242                    d();
1243                    e();
1244                ".unindent()
1245            }),
1246        )
1247        .await;
1248
1249        let server_id_1 = LanguageServerId(100);
1250        let server_id_2 = LanguageServerId(101);
1251        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1252        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1253
1254        let view = cx.add_view(window_id, |cx| {
1255            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
1256        });
1257
1258        // Two language servers start updating diagnostics
1259        project.update(cx, |project, cx| {
1260            project.disk_based_diagnostics_started(server_id_1, cx);
1261            project.disk_based_diagnostics_started(server_id_2, cx);
1262            project
1263                .update_diagnostic_entries(
1264                    server_id_1,
1265                    PathBuf::from("/test/main.js"),
1266                    None,
1267                    vec![DiagnosticEntry {
1268                        range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
1269                        diagnostic: Diagnostic {
1270                            message: "error 1".to_string(),
1271                            severity: DiagnosticSeverity::WARNING,
1272                            is_primary: true,
1273                            is_disk_based: true,
1274                            group_id: 1,
1275                            ..Default::default()
1276                        },
1277                    }],
1278                    cx,
1279                )
1280                .unwrap();
1281            project
1282                .update_diagnostic_entries(
1283                    server_id_2,
1284                    PathBuf::from("/test/main.js"),
1285                    None,
1286                    vec![DiagnosticEntry {
1287                        range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
1288                        diagnostic: Diagnostic {
1289                            message: "warning 1".to_string(),
1290                            severity: DiagnosticSeverity::ERROR,
1291                            is_primary: true,
1292                            is_disk_based: true,
1293                            group_id: 2,
1294                            ..Default::default()
1295                        },
1296                    }],
1297                    cx,
1298                )
1299                .unwrap();
1300        });
1301
1302        // The first language server finishes
1303        project.update(cx, |project, cx| {
1304            project.disk_based_diagnostics_finished(server_id_1, cx);
1305        });
1306
1307        // Only the first language server's diagnostics are shown.
1308        cx.foreground().run_until_parked();
1309        view.update(cx, |view, cx| {
1310            assert_eq!(
1311                editor_blocks(&view.editor, cx),
1312                [
1313                    (0, "path header block".into()),
1314                    (2, "diagnostic header".into()),
1315                ]
1316            );
1317            assert_eq!(
1318                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1319                concat!(
1320                    "\n", // filename
1321                    "\n", // padding
1322                    // diagnostic group 1
1323                    "\n",     // primary message
1324                    "\n",     // padding
1325                    "a();\n", //
1326                    "b();",
1327                )
1328            );
1329        });
1330
1331        // The second language server finishes
1332        project.update(cx, |project, cx| {
1333            project.disk_based_diagnostics_finished(server_id_2, cx);
1334        });
1335
1336        // Both language server's diagnostics are shown.
1337        cx.foreground().run_until_parked();
1338        view.update(cx, |view, cx| {
1339            assert_eq!(
1340                editor_blocks(&view.editor, cx),
1341                [
1342                    (0, "path header block".into()),
1343                    (2, "diagnostic header".into()),
1344                    (6, "collapsed context".into()),
1345                    (7, "diagnostic header".into()),
1346                ]
1347            );
1348            assert_eq!(
1349                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1350                concat!(
1351                    "\n", // filename
1352                    "\n", // padding
1353                    // diagnostic group 1
1354                    "\n",     // primary message
1355                    "\n",     // padding
1356                    "a();\n", // location
1357                    "b();\n", //
1358                    "\n",     // collapsed context
1359                    // diagnostic group 2
1360                    "\n",     // primary message
1361                    "\n",     // padding
1362                    "a();\n", // context
1363                    "b();\n", //
1364                    "c();",   // context
1365                )
1366            );
1367        });
1368
1369        // Both language servers start updating diagnostics, and the first server finishes.
1370        project.update(cx, |project, cx| {
1371            project.disk_based_diagnostics_started(server_id_1, cx);
1372            project.disk_based_diagnostics_started(server_id_2, cx);
1373            project
1374                .update_diagnostic_entries(
1375                    server_id_1,
1376                    PathBuf::from("/test/main.js"),
1377                    None,
1378                    vec![DiagnosticEntry {
1379                        range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
1380                        diagnostic: Diagnostic {
1381                            message: "warning 2".to_string(),
1382                            severity: DiagnosticSeverity::WARNING,
1383                            is_primary: true,
1384                            is_disk_based: true,
1385                            group_id: 1,
1386                            ..Default::default()
1387                        },
1388                    }],
1389                    cx,
1390                )
1391                .unwrap();
1392            project
1393                .update_diagnostic_entries(
1394                    server_id_2,
1395                    PathBuf::from("/test/main.rs"),
1396                    None,
1397                    vec![],
1398                    cx,
1399                )
1400                .unwrap();
1401            project.disk_based_diagnostics_finished(server_id_1, cx);
1402        });
1403
1404        // Only the first language server's diagnostics are updated.
1405        cx.foreground().run_until_parked();
1406        view.update(cx, |view, cx| {
1407            assert_eq!(
1408                editor_blocks(&view.editor, cx),
1409                [
1410                    (0, "path header block".into()),
1411                    (2, "diagnostic header".into()),
1412                    (7, "collapsed context".into()),
1413                    (8, "diagnostic header".into()),
1414                ]
1415            );
1416            assert_eq!(
1417                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1418                concat!(
1419                    "\n", // filename
1420                    "\n", // padding
1421                    // diagnostic group 1
1422                    "\n",     // primary message
1423                    "\n",     // padding
1424                    "a();\n", // location
1425                    "b();\n", //
1426                    "c();\n", // context
1427                    "\n",     // collapsed context
1428                    // diagnostic group 2
1429                    "\n",     // primary message
1430                    "\n",     // padding
1431                    "b();\n", // context
1432                    "c();\n", //
1433                    "d();",   // context
1434                )
1435            );
1436        });
1437
1438        // The second language server finishes.
1439        project.update(cx, |project, cx| {
1440            project
1441                .update_diagnostic_entries(
1442                    server_id_2,
1443                    PathBuf::from("/test/main.js"),
1444                    None,
1445                    vec![DiagnosticEntry {
1446                        range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
1447                        diagnostic: Diagnostic {
1448                            message: "warning 2".to_string(),
1449                            severity: DiagnosticSeverity::WARNING,
1450                            is_primary: true,
1451                            is_disk_based: true,
1452                            group_id: 1,
1453                            ..Default::default()
1454                        },
1455                    }],
1456                    cx,
1457                )
1458                .unwrap();
1459            project.disk_based_diagnostics_finished(server_id_2, cx);
1460        });
1461
1462        // Both language servers' diagnostics are updated.
1463        cx.foreground().run_until_parked();
1464        view.update(cx, |view, cx| {
1465            assert_eq!(
1466                editor_blocks(&view.editor, cx),
1467                [
1468                    (0, "path header block".into()),
1469                    (2, "diagnostic header".into()),
1470                    (7, "collapsed context".into()),
1471                    (8, "diagnostic header".into()),
1472                ]
1473            );
1474            assert_eq!(
1475                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1476                concat!(
1477                    "\n", // filename
1478                    "\n", // padding
1479                    // diagnostic group 1
1480                    "\n",     // primary message
1481                    "\n",     // padding
1482                    "b();\n", // location
1483                    "c();\n", //
1484                    "d();\n", // context
1485                    "\n",     // collapsed context
1486                    // diagnostic group 2
1487                    "\n",     // primary message
1488                    "\n",     // padding
1489                    "c();\n", // context
1490                    "d();\n", //
1491                    "e();",   // context
1492                )
1493            );
1494        });
1495    }
1496
1497    fn init_test(cx: &mut TestAppContext) {
1498        cx.update(|cx| {
1499            cx.set_global(Settings::test(cx));
1500            cx.set_global(SettingsStore::test(cx));
1501            language::init(cx);
1502            client::init_settings(cx);
1503            workspace::init_settings(cx);
1504        });
1505    }
1506
1507    fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
1508        editor.update(cx, |editor, cx| {
1509            let snapshot = editor.snapshot(cx);
1510            snapshot
1511                .blocks_in_range(0..snapshot.max_point().row())
1512                .filter_map(|(row, block)| {
1513                    let name = match block {
1514                        TransformBlock::Custom(block) => block
1515                            .render(&mut BlockContext {
1516                                view_context: cx,
1517                                anchor_x: 0.,
1518                                scroll_x: 0.,
1519                                gutter_padding: 0.,
1520                                gutter_width: 0.,
1521                                line_height: 0.,
1522                                em_width: 0.,
1523                            })
1524                            .name()?
1525                            .to_string(),
1526                        TransformBlock::ExcerptHeader {
1527                            starts_new_buffer, ..
1528                        } => {
1529                            if *starts_new_buffer {
1530                                "path header block".to_string()
1531                            } else {
1532                                "collapsed context".to_string()
1533                            }
1534                        }
1535                    };
1536
1537                    Some((row, name))
1538                })
1539                .collect()
1540        })
1541    }
1542}