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 smallvec::SmallVec;
  24use std::{
  25    any::{Any, TypeId},
  26    borrow::Cow,
  27    cmp::Ordering,
  28    ops::Range,
  29    path::PathBuf,
  30    sync::Arc,
  31};
  32use theme::ThemeSettings;
  33use util::TryFutureExt;
  34use workspace::{
  35    item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
  36    ItemNavHistory, Pane, PaneBackdrop, 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 = &theme::current(cx).project_diagnostics;
  93            PaneBackdrop::new(
  94                cx.view_id(),
  95                Label::new("No problems in workspace", theme.empty_message.clone())
  96                    .aligned()
  97                    .contained()
  98                    .with_style(theme.container)
  99                    .into_any(),
 100            )
 101            .into_any()
 102        } else {
 103            ChildView::new(&self.editor, cx).into_any()
 104        }
 105    }
 106
 107    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
 108        if cx.is_self_focused() && !self.path_states.is_empty() {
 109            cx.focus(&self.editor);
 110        }
 111    }
 112
 113    fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
 114        let project = self.project.read(cx);
 115        json!({
 116            "project": json!({
 117                "language_servers": project.language_server_statuses().collect::<Vec<_>>(),
 118                "summary": project.diagnostic_summary(cx),
 119            }),
 120            "summary": self.summary,
 121            "paths_to_update": self.paths_to_update.iter().map(|(path, server_id)|
 122                (path.path.to_string_lossy(), server_id.0)
 123            ).collect::<Vec<_>>(),
 124            "paths_states": self.path_states.iter().map(|state|
 125                json!({
 126                    "path": state.path.path.to_string_lossy(),
 127                    "groups": state.diagnostic_groups.iter().map(|group|
 128                        json!({
 129                            "block_count": group.blocks.len(),
 130                            "excerpt_count": group.excerpts.len(),
 131                        })
 132                    ).collect::<Vec<_>>(),
 133                })
 134            ).collect::<Vec<_>>(),
 135        })
 136    }
 137}
 138
 139impl ProjectDiagnosticsEditor {
 140    fn new(
 141        project_handle: ModelHandle<Project>,
 142        workspace: WeakViewHandle<Workspace>,
 143        cx: &mut ViewContext<Self>,
 144    ) -> Self {
 145        cx.subscribe(&project_handle, |this, _, event, cx| match event {
 146            project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
 147                this.update_excerpts(Some(*language_server_id), cx);
 148                this.update_title(cx);
 149            }
 150            project::Event::DiagnosticsUpdated {
 151                language_server_id,
 152                path,
 153            } => {
 154                this.paths_to_update
 155                    .insert((path.clone(), *language_server_id));
 156            }
 157            _ => {}
 158        })
 159        .detach();
 160
 161        let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
 162        let editor = cx.add_view(|cx| {
 163            let mut editor =
 164                Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
 165            editor.set_vertical_scroll_margin(5, cx);
 166            editor
 167        });
 168        cx.subscribe(&editor, |this, _, event, cx| {
 169            cx.emit(event.clone());
 170            if event == &editor::Event::Focused && this.path_states.is_empty() {
 171                cx.focus_self()
 172            }
 173        })
 174        .detach();
 175
 176        let project = project_handle.read(cx);
 177        let paths_to_update = project
 178            .diagnostic_summaries(cx)
 179            .map(|(path, server_id, _)| (path, server_id))
 180            .collect();
 181        let summary = project.diagnostic_summary(cx);
 182        let mut this = Self {
 183            project: project_handle,
 184            summary,
 185            workspace,
 186            excerpts,
 187            editor,
 188            path_states: Default::default(),
 189            paths_to_update,
 190        };
 191        this.update_excerpts(None, cx);
 192        this
 193    }
 194
 195    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
 196        if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
 197            workspace.activate_item(&existing, cx);
 198        } else {
 199            let workspace_handle = cx.weak_handle();
 200            let diagnostics = cx.add_view(|cx| {
 201                ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
 202            });
 203            workspace.add_item(Box::new(diagnostics), cx);
 204        }
 205    }
 206
 207    fn update_excerpts(
 208        &mut self,
 209        language_server_id: Option<LanguageServerId>,
 210        cx: &mut ViewContext<Self>,
 211    ) {
 212        let mut paths = Vec::new();
 213        self.paths_to_update.retain(|(path, server_id)| {
 214            if language_server_id
 215                .map_or(true, |language_server_id| language_server_id == *server_id)
 216            {
 217                paths.push(path.clone());
 218                false
 219            } else {
 220                true
 221            }
 222        });
 223        let project = self.project.clone();
 224        cx.spawn(|this, mut cx| {
 225            async move {
 226                for path in paths {
 227                    let buffer = project
 228                        .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
 229                        .await?;
 230                    this.update(&mut cx, |this, cx| {
 231                        this.populate_excerpts(path, language_server_id, buffer, cx)
 232                    })?;
 233                }
 234                Result::<_, anyhow::Error>::Ok(())
 235            }
 236            .log_err()
 237        })
 238        .detach();
 239    }
 240
 241    fn populate_excerpts(
 242        &mut self,
 243        path: ProjectPath,
 244        language_server_id: Option<LanguageServerId>,
 245        buffer: ModelHandle<Buffer>,
 246        cx: &mut ViewContext<Self>,
 247    ) {
 248        let was_empty = self.path_states.is_empty();
 249        let snapshot = buffer.read(cx).snapshot();
 250        let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
 251            Ok(ix) => ix,
 252            Err(ix) => {
 253                self.path_states.insert(
 254                    ix,
 255                    PathState {
 256                        path: path.clone(),
 257                        diagnostic_groups: Default::default(),
 258                    },
 259                );
 260                ix
 261            }
 262        };
 263
 264        let mut prev_excerpt_id = if path_ix > 0 {
 265            let prev_path_last_group = &self.path_states[path_ix - 1]
 266                .diagnostic_groups
 267                .last()
 268                .unwrap();
 269            prev_path_last_group.excerpts.last().unwrap().clone()
 270        } else {
 271            ExcerptId::min()
 272        };
 273
 274        let path_state = &mut self.path_states[path_ix];
 275        let mut groups_to_add = Vec::new();
 276        let mut group_ixs_to_remove = Vec::new();
 277        let mut blocks_to_add = Vec::new();
 278        let mut blocks_to_remove = HashSet::default();
 279        let mut first_excerpt_id = None;
 280        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
 281            let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
 282            let mut new_groups = snapshot
 283                .diagnostic_groups(language_server_id)
 284                .into_iter()
 285                .filter(|(_, group)| {
 286                    group.entries[group.primary_ix].diagnostic.severity
 287                        <= DiagnosticSeverity::WARNING
 288                })
 289                .peekable();
 290            loop {
 291                let mut to_insert = None;
 292                let mut to_remove = None;
 293                let mut to_keep = None;
 294                match (old_groups.peek(), new_groups.peek()) {
 295                    (None, None) => break,
 296                    (None, Some(_)) => to_insert = new_groups.next(),
 297                    (Some((_, old_group)), None) => {
 298                        if language_server_id.map_or(true, |id| id == old_group.language_server_id)
 299                        {
 300                            to_remove = old_groups.next();
 301                        } else {
 302                            to_keep = old_groups.next();
 303                        }
 304                    }
 305                    (Some((_, old_group)), Some((_, new_group))) => {
 306                        let old_primary = &old_group.primary_diagnostic;
 307                        let new_primary = &new_group.entries[new_group.primary_ix];
 308                        match compare_diagnostics(old_primary, new_primary, &snapshot) {
 309                            Ordering::Less => {
 310                                if language_server_id
 311                                    .map_or(true, |id| id == old_group.language_server_id)
 312                                {
 313                                    to_remove = old_groups.next();
 314                                } else {
 315                                    to_keep = old_groups.next();
 316                                }
 317                            }
 318                            Ordering::Equal => {
 319                                to_keep = old_groups.next();
 320                                new_groups.next();
 321                            }
 322                            Ordering::Greater => to_insert = new_groups.next(),
 323                        }
 324                    }
 325                }
 326
 327                if let Some((language_server_id, group)) = to_insert {
 328                    let mut group_state = DiagnosticGroupState {
 329                        language_server_id,
 330                        primary_diagnostic: group.entries[group.primary_ix].clone(),
 331                        primary_excerpt_ix: 0,
 332                        excerpts: Default::default(),
 333                        blocks: Default::default(),
 334                        block_count: 0,
 335                    };
 336                    let mut pending_range: Option<(Range<Point>, usize)> = None;
 337                    let mut is_first_excerpt_for_group = true;
 338                    for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
 339                        let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
 340                        if let Some((range, start_ix)) = &mut pending_range {
 341                            if let Some(entry) = resolved_entry.as_ref() {
 342                                if entry.range.start.row
 343                                    <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
 344                                {
 345                                    range.end = range.end.max(entry.range.end);
 346                                    continue;
 347                                }
 348                            }
 349
 350                            let excerpt_start =
 351                                Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
 352                            let excerpt_end = snapshot.clip_point(
 353                                Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
 354                                Bias::Left,
 355                            );
 356                            let excerpt_id = excerpts
 357                                .insert_excerpts_after(
 358                                    prev_excerpt_id,
 359                                    buffer.clone(),
 360                                    [ExcerptRange {
 361                                        context: excerpt_start..excerpt_end,
 362                                        primary: Some(range.clone()),
 363                                    }],
 364                                    excerpts_cx,
 365                                )
 366                                .pop()
 367                                .unwrap();
 368
 369                            prev_excerpt_id = excerpt_id.clone();
 370                            first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
 371                            group_state.excerpts.push(excerpt_id.clone());
 372                            let header_position = (excerpt_id.clone(), language::Anchor::MIN);
 373
 374                            if is_first_excerpt_for_group {
 375                                is_first_excerpt_for_group = false;
 376                                let mut primary =
 377                                    group.entries[group.primary_ix].diagnostic.clone();
 378                                primary.message =
 379                                    primary.message.split('\n').next().unwrap().to_string();
 380                                group_state.block_count += 1;
 381                                blocks_to_add.push(BlockProperties {
 382                                    position: header_position,
 383                                    height: 2,
 384                                    style: BlockStyle::Sticky,
 385                                    render: diagnostic_header_renderer(primary),
 386                                    disposition: BlockDisposition::Above,
 387                                });
 388                            }
 389
 390                            for entry in &group.entries[*start_ix..ix] {
 391                                let mut diagnostic = entry.diagnostic.clone();
 392                                if diagnostic.is_primary {
 393                                    group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
 394                                    diagnostic.message =
 395                                        entry.diagnostic.message.split('\n').skip(1).collect();
 396                                }
 397
 398                                if !diagnostic.message.is_empty() {
 399                                    group_state.block_count += 1;
 400                                    blocks_to_add.push(BlockProperties {
 401                                        position: (excerpt_id.clone(), entry.range.start),
 402                                        height: diagnostic.message.matches('\n').count() as u8 + 1,
 403                                        style: BlockStyle::Fixed,
 404                                        render: diagnostic_block_renderer(diagnostic, true),
 405                                        disposition: BlockDisposition::Below,
 406                                    });
 407                                }
 408                            }
 409
 410                            pending_range.take();
 411                        }
 412
 413                        if let Some(entry) = resolved_entry {
 414                            pending_range = Some((entry.range.clone(), ix));
 415                        }
 416                    }
 417
 418                    groups_to_add.push(group_state);
 419                } else if let Some((group_ix, group_state)) = to_remove {
 420                    excerpts.remove_excerpts(group_state.excerpts.iter().copied(), excerpts_cx);
 421                    group_ixs_to_remove.push(group_ix);
 422                    blocks_to_remove.extend(group_state.blocks.iter().copied());
 423                } else if let Some((_, group)) = to_keep {
 424                    prev_excerpt_id = group.excerpts.last().unwrap().clone();
 425                    first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
 426                }
 427            }
 428
 429            excerpts.snapshot(excerpts_cx)
 430        });
 431
 432        self.editor.update(cx, |editor, cx| {
 433            editor.remove_blocks(blocks_to_remove, None, cx);
 434            let block_ids = editor.insert_blocks(
 435                blocks_to_add.into_iter().map(|block| {
 436                    let (excerpt_id, text_anchor) = block.position;
 437                    BlockProperties {
 438                        position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
 439                        height: block.height,
 440                        style: block.style,
 441                        render: block.render,
 442                        disposition: block.disposition,
 443                    }
 444                }),
 445                Some(Autoscroll::fit()),
 446                cx,
 447            );
 448
 449            let mut block_ids = block_ids.into_iter();
 450            for group_state in &mut groups_to_add {
 451                group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
 452            }
 453        });
 454
 455        for ix in group_ixs_to_remove.into_iter().rev() {
 456            path_state.diagnostic_groups.remove(ix);
 457        }
 458        path_state.diagnostic_groups.extend(groups_to_add);
 459        path_state.diagnostic_groups.sort_unstable_by(|a, b| {
 460            let range_a = &a.primary_diagnostic.range;
 461            let range_b = &b.primary_diagnostic.range;
 462            range_a
 463                .start
 464                .cmp(&range_b.start, &snapshot)
 465                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
 466        });
 467
 468        if path_state.diagnostic_groups.is_empty() {
 469            self.path_states.remove(path_ix);
 470        }
 471
 472        self.editor.update(cx, |editor, cx| {
 473            let groups;
 474            let mut selections;
 475            let new_excerpt_ids_by_selection_id;
 476            if was_empty {
 477                groups = self.path_states.first()?.diagnostic_groups.as_slice();
 478                new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
 479                selections = vec![Selection {
 480                    id: 0,
 481                    start: 0,
 482                    end: 0,
 483                    reversed: false,
 484                    goal: SelectionGoal::None,
 485                }];
 486            } else {
 487                groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
 488                new_excerpt_ids_by_selection_id =
 489                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
 490                selections = editor.selections.all::<usize>(cx);
 491            }
 492
 493            // If any selection has lost its position, move it to start of the next primary diagnostic.
 494            let snapshot = editor.snapshot(cx);
 495            for selection in &mut selections {
 496                if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
 497                    let group_ix = match groups.binary_search_by(|probe| {
 498                        probe
 499                            .excerpts
 500                            .last()
 501                            .unwrap()
 502                            .cmp(new_excerpt_id, &snapshot.buffer_snapshot)
 503                    }) {
 504                        Ok(ix) | Err(ix) => ix,
 505                    };
 506                    if let Some(group) = groups.get(group_ix) {
 507                        let offset = excerpts_snapshot
 508                            .anchor_in_excerpt(
 509                                group.excerpts[group.primary_excerpt_ix].clone(),
 510                                group.primary_diagnostic.range.start,
 511                            )
 512                            .to_offset(&excerpts_snapshot);
 513                        selection.start = offset;
 514                        selection.end = offset;
 515                    }
 516                }
 517            }
 518            editor.change_selections(None, cx, |s| {
 519                s.select(selections);
 520            });
 521            Some(())
 522        });
 523
 524        if self.path_states.is_empty() {
 525            if self.editor.is_focused(cx) {
 526                cx.focus_self();
 527            }
 528        } else if cx.handle().is_focused(cx) {
 529            cx.focus(&self.editor);
 530        }
 531        cx.notify();
 532    }
 533
 534    fn update_title(&mut self, cx: &mut ViewContext<Self>) {
 535        self.summary = self.project.read(cx).diagnostic_summary(cx);
 536        cx.emit(Event::TitleChanged);
 537    }
 538}
 539
 540impl Item for ProjectDiagnosticsEditor {
 541    fn tab_content<T: View>(
 542        &self,
 543        _detail: Option<usize>,
 544        style: &theme::Tab,
 545        cx: &AppContext,
 546    ) -> AnyElement<T> {
 547        render_summary(
 548            &self.summary,
 549            &style.label.text,
 550            &theme::current(cx).project_diagnostics,
 551        )
 552    }
 553
 554    fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
 555        self.editor.for_each_project_item(cx, f)
 556    }
 557
 558    fn is_singleton(&self, _: &AppContext) -> bool {
 559        false
 560    }
 561
 562    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
 563        self.editor
 564            .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
 565    }
 566
 567    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
 568        self.editor
 569            .update(cx, |editor, cx| editor.navigate(data, cx))
 570    }
 571
 572    fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
 573        Some("Project Diagnostics".into())
 574    }
 575
 576    fn is_dirty(&self, cx: &AppContext) -> bool {
 577        self.excerpts.read(cx).is_dirty(cx)
 578    }
 579
 580    fn has_conflict(&self, cx: &AppContext) -> bool {
 581        self.excerpts.read(cx).has_conflict(cx)
 582    }
 583
 584    fn can_save(&self, _: &AppContext) -> bool {
 585        true
 586    }
 587
 588    fn save(
 589        &mut self,
 590        project: ModelHandle<Project>,
 591        cx: &mut ViewContext<Self>,
 592    ) -> Task<Result<()>> {
 593        self.editor.save(project, cx)
 594    }
 595
 596    fn reload(
 597        &mut self,
 598        project: ModelHandle<Project>,
 599        cx: &mut ViewContext<Self>,
 600    ) -> Task<Result<()>> {
 601        self.editor.reload(project, cx)
 602    }
 603
 604    fn save_as(
 605        &mut self,
 606        _: ModelHandle<Project>,
 607        _: PathBuf,
 608        _: &mut ViewContext<Self>,
 609    ) -> Task<Result<()>> {
 610        unreachable!()
 611    }
 612
 613    fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
 614        Editor::to_item_events(event)
 615    }
 616
 617    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
 618        self.editor.update(cx, |editor, _| {
 619            editor.set_nav_history(Some(nav_history));
 620        });
 621    }
 622
 623    fn clone_on_split(
 624        &self,
 625        _workspace_id: workspace::WorkspaceId,
 626        cx: &mut ViewContext<Self>,
 627    ) -> Option<Self>
 628    where
 629        Self: Sized,
 630    {
 631        Some(ProjectDiagnosticsEditor::new(
 632            self.project.clone(),
 633            self.workspace.clone(),
 634            cx,
 635        ))
 636    }
 637
 638    fn act_as_type<'a>(
 639        &'a self,
 640        type_id: TypeId,
 641        self_handle: &'a ViewHandle<Self>,
 642        _: &'a AppContext,
 643    ) -> Option<&AnyViewHandle> {
 644        if type_id == TypeId::of::<Self>() {
 645            Some(self_handle)
 646        } else if type_id == TypeId::of::<Editor>() {
 647            Some(&self.editor)
 648        } else {
 649            None
 650        }
 651    }
 652
 653    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 654        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
 655    }
 656
 657    fn serialized_item_kind() -> Option<&'static str> {
 658        Some("diagnostics")
 659    }
 660
 661    fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
 662        self.editor.breadcrumbs(theme, cx)
 663    }
 664
 665    fn breadcrumb_location(&self) -> ToolbarItemLocation {
 666        ToolbarItemLocation::PrimaryLeft { flex: None }
 667    }
 668
 669    fn deserialize(
 670        project: ModelHandle<Project>,
 671        workspace: WeakViewHandle<Workspace>,
 672        _workspace_id: workspace::WorkspaceId,
 673        _item_id: workspace::ItemId,
 674        cx: &mut ViewContext<Pane>,
 675    ) -> Task<Result<ViewHandle<Self>>> {
 676        Task::ready(Ok(cx.add_view(|cx| Self::new(project, workspace, cx))))
 677    }
 678}
 679
 680fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
 681    let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
 682    Arc::new(move |cx| {
 683        let settings = settings::get::<ThemeSettings>(cx);
 684        let theme = &settings.theme.editor;
 685        let style = theme.diagnostic_header.clone();
 686        let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
 687        let icon_width = cx.em_width * style.icon_width_factor;
 688        let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
 689            Svg::new("icons/circle_x_mark_12.svg")
 690                .with_color(theme.error_diagnostic.message.text.color)
 691        } else {
 692            Svg::new("icons/triangle_exclamation_12.svg")
 693                .with_color(theme.warning_diagnostic.message.text.color)
 694        };
 695
 696        Flex::row()
 697            .with_child(
 698                icon.constrained()
 699                    .with_width(icon_width)
 700                    .aligned()
 701                    .contained()
 702                    .with_margin_right(cx.gutter_padding),
 703            )
 704            .with_children(diagnostic.source.as_ref().map(|source| {
 705                Label::new(
 706                    format!("{source}: "),
 707                    style.source.label.clone().with_font_size(font_size),
 708                )
 709                .contained()
 710                .with_style(style.message.container)
 711                .aligned()
 712            }))
 713            .with_child(
 714                Label::new(
 715                    message.clone(),
 716                    style.message.label.clone().with_font_size(font_size),
 717                )
 718                .with_highlights(highlights.clone())
 719                .contained()
 720                .with_style(style.message.container)
 721                .aligned(),
 722            )
 723            .with_children(diagnostic.code.clone().map(|code| {
 724                Label::new(code, style.code.text.clone().with_font_size(font_size))
 725                    .contained()
 726                    .with_style(style.code.container)
 727                    .aligned()
 728            }))
 729            .contained()
 730            .with_style(style.container)
 731            .with_padding_left(cx.gutter_padding)
 732            .with_padding_right(cx.gutter_padding)
 733            .expanded()
 734            .into_any_named("diagnostic header")
 735    })
 736}
 737
 738pub(crate) fn render_summary<T: View>(
 739    summary: &DiagnosticSummary,
 740    text_style: &TextStyle,
 741    theme: &theme::ProjectDiagnostics,
 742) -> AnyElement<T> {
 743    if summary.error_count == 0 && summary.warning_count == 0 {
 744        Label::new("No problems", text_style.clone()).into_any()
 745    } else {
 746        let icon_width = theme.tab_icon_width;
 747        let icon_spacing = theme.tab_icon_spacing;
 748        let summary_spacing = theme.tab_summary_spacing;
 749        Flex::row()
 750            .with_child(
 751                Svg::new("icons/circle_x_mark_12.svg")
 752                    .with_color(text_style.color)
 753                    .constrained()
 754                    .with_width(icon_width)
 755                    .aligned()
 756                    .contained()
 757                    .with_margin_right(icon_spacing),
 758            )
 759            .with_child(
 760                Label::new(
 761                    summary.error_count.to_string(),
 762                    LabelStyle {
 763                        text: text_style.clone(),
 764                        highlight_text: None,
 765                    },
 766                )
 767                .aligned(),
 768            )
 769            .with_child(
 770                Svg::new("icons/triangle_exclamation_12.svg")
 771                    .with_color(text_style.color)
 772                    .constrained()
 773                    .with_width(icon_width)
 774                    .aligned()
 775                    .contained()
 776                    .with_margin_left(summary_spacing)
 777                    .with_margin_right(icon_spacing),
 778            )
 779            .with_child(
 780                Label::new(
 781                    summary.warning_count.to_string(),
 782                    LabelStyle {
 783                        text: text_style.clone(),
 784                        highlight_text: None,
 785                    },
 786                )
 787                .aligned(),
 788            )
 789            .into_any()
 790    }
 791}
 792
 793fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 794    lhs: &DiagnosticEntry<L>,
 795    rhs: &DiagnosticEntry<R>,
 796    snapshot: &language::BufferSnapshot,
 797) -> Ordering {
 798    lhs.range
 799        .start
 800        .to_offset(snapshot)
 801        .cmp(&rhs.range.start.to_offset(snapshot))
 802        .then_with(|| {
 803            lhs.range
 804                .end
 805                .to_offset(snapshot)
 806                .cmp(&rhs.range.end.to_offset(snapshot))
 807        })
 808        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 809}
 810
 811#[cfg(test)]
 812mod tests {
 813    use super::*;
 814    use editor::{
 815        display_map::{BlockContext, TransformBlock},
 816        DisplayPoint,
 817    };
 818    use gpui::{TestAppContext, WindowContext};
 819    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
 820    use project::FakeFs;
 821    use serde_json::json;
 822    use settings::SettingsStore;
 823    use unindent::Unindent as _;
 824
 825    #[gpui::test]
 826    async fn test_diagnostics(cx: &mut TestAppContext) {
 827        init_test(cx);
 828
 829        let fs = FakeFs::new(cx.background());
 830        fs.insert_tree(
 831            "/test",
 832            json!({
 833                "consts.rs": "
 834                    const a: i32 = 'a';
 835                    const b: i32 = c;
 836                "
 837                .unindent(),
 838
 839                "main.rs": "
 840                    fn main() {
 841                        let x = vec![];
 842                        let y = vec![];
 843                        a(x);
 844                        b(y);
 845                        // comment 1
 846                        // comment 2
 847                        c(y);
 848                        d(x);
 849                    }
 850                "
 851                .unindent(),
 852            }),
 853        )
 854        .await;
 855
 856        let language_server_id = LanguageServerId(0);
 857        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
 858        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 859        let workspace = window.root(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 = window.add_view(cx, |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 = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1253        let workspace = window.root(cx);
1254
1255        let view = window.add_view(cx, |cx| {
1256            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
1257        });
1258
1259        // Two language servers start updating diagnostics
1260        project.update(cx, |project, cx| {
1261            project.disk_based_diagnostics_started(server_id_1, cx);
1262            project.disk_based_diagnostics_started(server_id_2, cx);
1263            project
1264                .update_diagnostic_entries(
1265                    server_id_1,
1266                    PathBuf::from("/test/main.js"),
1267                    None,
1268                    vec![DiagnosticEntry {
1269                        range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
1270                        diagnostic: Diagnostic {
1271                            message: "error 1".to_string(),
1272                            severity: DiagnosticSeverity::WARNING,
1273                            is_primary: true,
1274                            is_disk_based: true,
1275                            group_id: 1,
1276                            ..Default::default()
1277                        },
1278                    }],
1279                    cx,
1280                )
1281                .unwrap();
1282            project
1283                .update_diagnostic_entries(
1284                    server_id_2,
1285                    PathBuf::from("/test/main.js"),
1286                    None,
1287                    vec![DiagnosticEntry {
1288                        range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
1289                        diagnostic: Diagnostic {
1290                            message: "warning 1".to_string(),
1291                            severity: DiagnosticSeverity::ERROR,
1292                            is_primary: true,
1293                            is_disk_based: true,
1294                            group_id: 2,
1295                            ..Default::default()
1296                        },
1297                    }],
1298                    cx,
1299                )
1300                .unwrap();
1301        });
1302
1303        // The first language server finishes
1304        project.update(cx, |project, cx| {
1305            project.disk_based_diagnostics_finished(server_id_1, cx);
1306        });
1307
1308        // Only the first language server's diagnostics are shown.
1309        cx.foreground().run_until_parked();
1310        view.update(cx, |view, cx| {
1311            assert_eq!(
1312                editor_blocks(&view.editor, cx),
1313                [
1314                    (0, "path header block".into()),
1315                    (2, "diagnostic header".into()),
1316                ]
1317            );
1318            assert_eq!(
1319                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1320                concat!(
1321                    "\n", // filename
1322                    "\n", // padding
1323                    // diagnostic group 1
1324                    "\n",     // primary message
1325                    "\n",     // padding
1326                    "a();\n", //
1327                    "b();",
1328                )
1329            );
1330        });
1331
1332        // The second language server finishes
1333        project.update(cx, |project, cx| {
1334            project.disk_based_diagnostics_finished(server_id_2, cx);
1335        });
1336
1337        // Both language server's diagnostics are shown.
1338        cx.foreground().run_until_parked();
1339        view.update(cx, |view, cx| {
1340            assert_eq!(
1341                editor_blocks(&view.editor, cx),
1342                [
1343                    (0, "path header block".into()),
1344                    (2, "diagnostic header".into()),
1345                    (6, "collapsed context".into()),
1346                    (7, "diagnostic header".into()),
1347                ]
1348            );
1349            assert_eq!(
1350                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1351                concat!(
1352                    "\n", // filename
1353                    "\n", // padding
1354                    // diagnostic group 1
1355                    "\n",     // primary message
1356                    "\n",     // padding
1357                    "a();\n", // location
1358                    "b();\n", //
1359                    "\n",     // collapsed context
1360                    // diagnostic group 2
1361                    "\n",     // primary message
1362                    "\n",     // padding
1363                    "a();\n", // context
1364                    "b();\n", //
1365                    "c();",   // context
1366                )
1367            );
1368        });
1369
1370        // Both language servers start updating diagnostics, and the first server finishes.
1371        project.update(cx, |project, cx| {
1372            project.disk_based_diagnostics_started(server_id_1, cx);
1373            project.disk_based_diagnostics_started(server_id_2, cx);
1374            project
1375                .update_diagnostic_entries(
1376                    server_id_1,
1377                    PathBuf::from("/test/main.js"),
1378                    None,
1379                    vec![DiagnosticEntry {
1380                        range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
1381                        diagnostic: Diagnostic {
1382                            message: "warning 2".to_string(),
1383                            severity: DiagnosticSeverity::WARNING,
1384                            is_primary: true,
1385                            is_disk_based: true,
1386                            group_id: 1,
1387                            ..Default::default()
1388                        },
1389                    }],
1390                    cx,
1391                )
1392                .unwrap();
1393            project
1394                .update_diagnostic_entries(
1395                    server_id_2,
1396                    PathBuf::from("/test/main.rs"),
1397                    None,
1398                    vec![],
1399                    cx,
1400                )
1401                .unwrap();
1402            project.disk_based_diagnostics_finished(server_id_1, cx);
1403        });
1404
1405        // Only the first language server's diagnostics are updated.
1406        cx.foreground().run_until_parked();
1407        view.update(cx, |view, cx| {
1408            assert_eq!(
1409                editor_blocks(&view.editor, cx),
1410                [
1411                    (0, "path header block".into()),
1412                    (2, "diagnostic header".into()),
1413                    (7, "collapsed context".into()),
1414                    (8, "diagnostic header".into()),
1415                ]
1416            );
1417            assert_eq!(
1418                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1419                concat!(
1420                    "\n", // filename
1421                    "\n", // padding
1422                    // diagnostic group 1
1423                    "\n",     // primary message
1424                    "\n",     // padding
1425                    "a();\n", // location
1426                    "b();\n", //
1427                    "c();\n", // context
1428                    "\n",     // collapsed context
1429                    // diagnostic group 2
1430                    "\n",     // primary message
1431                    "\n",     // padding
1432                    "b();\n", // context
1433                    "c();\n", //
1434                    "d();",   // context
1435                )
1436            );
1437        });
1438
1439        // The second language server finishes.
1440        project.update(cx, |project, cx| {
1441            project
1442                .update_diagnostic_entries(
1443                    server_id_2,
1444                    PathBuf::from("/test/main.js"),
1445                    None,
1446                    vec![DiagnosticEntry {
1447                        range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
1448                        diagnostic: Diagnostic {
1449                            message: "warning 2".to_string(),
1450                            severity: DiagnosticSeverity::WARNING,
1451                            is_primary: true,
1452                            is_disk_based: true,
1453                            group_id: 1,
1454                            ..Default::default()
1455                        },
1456                    }],
1457                    cx,
1458                )
1459                .unwrap();
1460            project.disk_based_diagnostics_finished(server_id_2, cx);
1461        });
1462
1463        // Both language servers' diagnostics are updated.
1464        cx.foreground().run_until_parked();
1465        view.update(cx, |view, cx| {
1466            assert_eq!(
1467                editor_blocks(&view.editor, cx),
1468                [
1469                    (0, "path header block".into()),
1470                    (2, "diagnostic header".into()),
1471                    (7, "collapsed context".into()),
1472                    (8, "diagnostic header".into()),
1473                ]
1474            );
1475            assert_eq!(
1476                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1477                concat!(
1478                    "\n", // filename
1479                    "\n", // padding
1480                    // diagnostic group 1
1481                    "\n",     // primary message
1482                    "\n",     // padding
1483                    "b();\n", // location
1484                    "c();\n", //
1485                    "d();\n", // context
1486                    "\n",     // collapsed context
1487                    // diagnostic group 2
1488                    "\n",     // primary message
1489                    "\n",     // padding
1490                    "c();\n", // context
1491                    "d();\n", //
1492                    "e();",   // context
1493                )
1494            );
1495        });
1496    }
1497
1498    fn init_test(cx: &mut TestAppContext) {
1499        cx.update(|cx| {
1500            cx.set_global(SettingsStore::test(cx));
1501            theme::init((), cx);
1502            language::init(cx);
1503            client::init_settings(cx);
1504            workspace::init_settings(cx);
1505            Project::init_settings(cx);
1506        });
1507    }
1508
1509    fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
1510        editor.update(cx, |editor, cx| {
1511            let snapshot = editor.snapshot(cx);
1512            snapshot
1513                .blocks_in_range(0..snapshot.max_point().row())
1514                .enumerate()
1515                .filter_map(|(ix, (row, block))| {
1516                    let name = match block {
1517                        TransformBlock::Custom(block) => block
1518                            .render(&mut BlockContext {
1519                                view_context: cx,
1520                                anchor_x: 0.,
1521                                scroll_x: 0.,
1522                                gutter_padding: 0.,
1523                                gutter_width: 0.,
1524                                line_height: 0.,
1525                                em_width: 0.,
1526                                block_id: ix,
1527                            })
1528                            .name()?
1529                            .to_string(),
1530                        TransformBlock::ExcerptHeader {
1531                            starts_new_buffer, ..
1532                        } => {
1533                            if *starts_new_buffer {
1534                                "path header block".to_string()
1535                            } else {
1536                                "collapsed context".to_string()
1537                            }
1538                        }
1539                    };
1540
1541                    Some((row, name))
1542                })
1543                .collect()
1544        })
1545    }
1546}