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