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, 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                cx,
 446            );
 447
 448            let mut block_ids = block_ids.into_iter();
 449            for group_state in &mut groups_to_add {
 450                group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
 451            }
 452        });
 453
 454        for ix in group_ixs_to_remove.into_iter().rev() {
 455            path_state.diagnostic_groups.remove(ix);
 456        }
 457        path_state.diagnostic_groups.extend(groups_to_add);
 458        path_state.diagnostic_groups.sort_unstable_by(|a, b| {
 459            let range_a = &a.primary_diagnostic.range;
 460            let range_b = &b.primary_diagnostic.range;
 461            range_a
 462                .start
 463                .cmp(&range_b.start, &snapshot)
 464                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
 465        });
 466
 467        if path_state.diagnostic_groups.is_empty() {
 468            self.path_states.remove(path_ix);
 469        }
 470
 471        self.editor.update(cx, |editor, cx| {
 472            let groups;
 473            let mut selections;
 474            let new_excerpt_ids_by_selection_id;
 475            if was_empty {
 476                groups = self.path_states.first()?.diagnostic_groups.as_slice();
 477                new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
 478                selections = vec![Selection {
 479                    id: 0,
 480                    start: 0,
 481                    end: 0,
 482                    reversed: false,
 483                    goal: SelectionGoal::None,
 484                }];
 485            } else {
 486                groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
 487                new_excerpt_ids_by_selection_id =
 488                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
 489                selections = editor.selections.all::<usize>(cx);
 490            }
 491
 492            // If any selection has lost its position, move it to start of the next primary diagnostic.
 493            let snapshot = editor.snapshot(cx);
 494            for selection in &mut selections {
 495                if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
 496                    let group_ix = match groups.binary_search_by(|probe| {
 497                        probe
 498                            .excerpts
 499                            .last()
 500                            .unwrap()
 501                            .cmp(new_excerpt_id, &snapshot.buffer_snapshot)
 502                    }) {
 503                        Ok(ix) | Err(ix) => ix,
 504                    };
 505                    if let Some(group) = groups.get(group_ix) {
 506                        let offset = excerpts_snapshot
 507                            .anchor_in_excerpt(
 508                                group.excerpts[group.primary_excerpt_ix].clone(),
 509                                group.primary_diagnostic.range.start,
 510                            )
 511                            .to_offset(&excerpts_snapshot);
 512                        selection.start = offset;
 513                        selection.end = offset;
 514                    }
 515                }
 516            }
 517            editor.change_selections(None, cx, |s| {
 518                s.select(selections);
 519            });
 520            Some(())
 521        });
 522
 523        if self.path_states.is_empty() {
 524            if self.editor.is_focused(cx) {
 525                cx.focus_self();
 526            }
 527        } else if cx.handle().is_focused(cx) {
 528            cx.focus(&self.editor);
 529        }
 530        cx.notify();
 531    }
 532
 533    fn update_title(&mut self, cx: &mut ViewContext<Self>) {
 534        self.summary = self.project.read(cx).diagnostic_summary(cx);
 535        cx.emit(Event::TitleChanged);
 536    }
 537}
 538
 539impl Item for ProjectDiagnosticsEditor {
 540    fn tab_content<T: View>(
 541        &self,
 542        _detail: Option<usize>,
 543        style: &theme::Tab,
 544        cx: &AppContext,
 545    ) -> AnyElement<T> {
 546        render_summary(
 547            &self.summary,
 548            &style.label.text,
 549            &theme::current(cx).project_diagnostics,
 550        )
 551    }
 552
 553    fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
 554        self.editor.for_each_project_item(cx, f)
 555    }
 556
 557    fn is_singleton(&self, _: &AppContext) -> bool {
 558        false
 559    }
 560
 561    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
 562        self.editor
 563            .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
 564    }
 565
 566    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
 567        self.editor
 568            .update(cx, |editor, cx| editor.navigate(data, cx))
 569    }
 570
 571    fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
 572        Some("Project Diagnostics".into())
 573    }
 574
 575    fn is_dirty(&self, cx: &AppContext) -> bool {
 576        self.excerpts.read(cx).is_dirty(cx)
 577    }
 578
 579    fn has_conflict(&self, cx: &AppContext) -> bool {
 580        self.excerpts.read(cx).has_conflict(cx)
 581    }
 582
 583    fn can_save(&self, _: &AppContext) -> bool {
 584        true
 585    }
 586
 587    fn save(
 588        &mut self,
 589        project: ModelHandle<Project>,
 590        cx: &mut ViewContext<Self>,
 591    ) -> Task<Result<()>> {
 592        self.editor.save(project, cx)
 593    }
 594
 595    fn reload(
 596        &mut self,
 597        project: ModelHandle<Project>,
 598        cx: &mut ViewContext<Self>,
 599    ) -> Task<Result<()>> {
 600        self.editor.reload(project, cx)
 601    }
 602
 603    fn save_as(
 604        &mut self,
 605        _: ModelHandle<Project>,
 606        _: PathBuf,
 607        _: &mut ViewContext<Self>,
 608    ) -> Task<Result<()>> {
 609        unreachable!()
 610    }
 611
 612    fn git_diff_recalc(
 613        &mut self,
 614        project: ModelHandle<Project>,
 615        cx: &mut ViewContext<Self>,
 616    ) -> Task<Result<()>> {
 617        self.editor
 618            .update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
 619    }
 620
 621    fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
 622        Editor::to_item_events(event)
 623    }
 624
 625    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
 626        self.editor.update(cx, |editor, _| {
 627            editor.set_nav_history(Some(nav_history));
 628        });
 629    }
 630
 631    fn clone_on_split(
 632        &self,
 633        _workspace_id: workspace::WorkspaceId,
 634        cx: &mut ViewContext<Self>,
 635    ) -> Option<Self>
 636    where
 637        Self: Sized,
 638    {
 639        Some(ProjectDiagnosticsEditor::new(
 640            self.project.clone(),
 641            self.workspace.clone(),
 642            cx,
 643        ))
 644    }
 645
 646    fn act_as_type<'a>(
 647        &'a self,
 648        type_id: TypeId,
 649        self_handle: &'a ViewHandle<Self>,
 650        _: &'a AppContext,
 651    ) -> Option<&AnyViewHandle> {
 652        if type_id == TypeId::of::<Self>() {
 653            Some(self_handle)
 654        } else if type_id == TypeId::of::<Editor>() {
 655            Some(&self.editor)
 656        } else {
 657            None
 658        }
 659    }
 660
 661    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 662        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
 663    }
 664
 665    fn serialized_item_kind() -> Option<&'static str> {
 666        Some("diagnostics")
 667    }
 668
 669    fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
 670        self.editor.breadcrumbs(theme, cx)
 671    }
 672
 673    fn breadcrumb_location(&self) -> ToolbarItemLocation {
 674        ToolbarItemLocation::PrimaryLeft { flex: None }
 675    }
 676
 677    fn deserialize(
 678        project: ModelHandle<Project>,
 679        workspace: WeakViewHandle<Workspace>,
 680        _workspace_id: workspace::WorkspaceId,
 681        _item_id: workspace::ItemId,
 682        cx: &mut ViewContext<Pane>,
 683    ) -> Task<Result<ViewHandle<Self>>> {
 684        Task::ready(Ok(cx.add_view(|cx| Self::new(project, workspace, cx))))
 685    }
 686}
 687
 688fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
 689    let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
 690    Arc::new(move |cx| {
 691        let settings = settings::get::<ThemeSettings>(cx);
 692        let theme = &settings.theme.editor;
 693        let style = theme.diagnostic_header.clone();
 694        let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
 695        let icon_width = cx.em_width * style.icon_width_factor;
 696        let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
 697            Svg::new("icons/circle_x_mark_12.svg")
 698                .with_color(theme.error_diagnostic.message.text.color)
 699        } else {
 700            Svg::new("icons/triangle_exclamation_12.svg")
 701                .with_color(theme.warning_diagnostic.message.text.color)
 702        };
 703
 704        Flex::row()
 705            .with_child(
 706                icon.constrained()
 707                    .with_width(icon_width)
 708                    .aligned()
 709                    .contained()
 710                    .with_margin_right(cx.gutter_padding),
 711            )
 712            .with_children(diagnostic.source.as_ref().map(|source| {
 713                Label::new(
 714                    format!("{source}: "),
 715                    style.source.label.clone().with_font_size(font_size),
 716                )
 717                .contained()
 718                .with_style(style.message.container)
 719                .aligned()
 720            }))
 721            .with_child(
 722                Label::new(
 723                    message.clone(),
 724                    style.message.label.clone().with_font_size(font_size),
 725                )
 726                .with_highlights(highlights.clone())
 727                .contained()
 728                .with_style(style.message.container)
 729                .aligned(),
 730            )
 731            .with_children(diagnostic.code.clone().map(|code| {
 732                Label::new(code, style.code.text.clone().with_font_size(font_size))
 733                    .contained()
 734                    .with_style(style.code.container)
 735                    .aligned()
 736            }))
 737            .contained()
 738            .with_style(style.container)
 739            .with_padding_left(cx.gutter_padding)
 740            .with_padding_right(cx.gutter_padding)
 741            .expanded()
 742            .into_any_named("diagnostic header")
 743    })
 744}
 745
 746pub(crate) fn render_summary<T: View>(
 747    summary: &DiagnosticSummary,
 748    text_style: &TextStyle,
 749    theme: &theme::ProjectDiagnostics,
 750) -> AnyElement<T> {
 751    if summary.error_count == 0 && summary.warning_count == 0 {
 752        Label::new("No problems", text_style.clone()).into_any()
 753    } else {
 754        let icon_width = theme.tab_icon_width;
 755        let icon_spacing = theme.tab_icon_spacing;
 756        let summary_spacing = theme.tab_summary_spacing;
 757        Flex::row()
 758            .with_child(
 759                Svg::new("icons/circle_x_mark_12.svg")
 760                    .with_color(text_style.color)
 761                    .constrained()
 762                    .with_width(icon_width)
 763                    .aligned()
 764                    .contained()
 765                    .with_margin_right(icon_spacing),
 766            )
 767            .with_child(
 768                Label::new(
 769                    summary.error_count.to_string(),
 770                    LabelStyle {
 771                        text: text_style.clone(),
 772                        highlight_text: None,
 773                    },
 774                )
 775                .aligned(),
 776            )
 777            .with_child(
 778                Svg::new("icons/triangle_exclamation_12.svg")
 779                    .with_color(text_style.color)
 780                    .constrained()
 781                    .with_width(icon_width)
 782                    .aligned()
 783                    .contained()
 784                    .with_margin_left(summary_spacing)
 785                    .with_margin_right(icon_spacing),
 786            )
 787            .with_child(
 788                Label::new(
 789                    summary.warning_count.to_string(),
 790                    LabelStyle {
 791                        text: text_style.clone(),
 792                        highlight_text: None,
 793                    },
 794                )
 795                .aligned(),
 796            )
 797            .into_any()
 798    }
 799}
 800
 801fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 802    lhs: &DiagnosticEntry<L>,
 803    rhs: &DiagnosticEntry<R>,
 804    snapshot: &language::BufferSnapshot,
 805) -> Ordering {
 806    lhs.range
 807        .start
 808        .to_offset(snapshot)
 809        .cmp(&rhs.range.start.to_offset(snapshot))
 810        .then_with(|| {
 811            lhs.range
 812                .end
 813                .to_offset(snapshot)
 814                .cmp(&rhs.range.end.to_offset(snapshot))
 815        })
 816        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 817}
 818
 819#[cfg(test)]
 820mod tests {
 821    use super::*;
 822    use editor::{
 823        display_map::{BlockContext, TransformBlock},
 824        DisplayPoint,
 825    };
 826    use gpui::{TestAppContext, WindowContext};
 827    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
 828    use project::FakeFs;
 829    use serde_json::json;
 830    use settings::SettingsStore;
 831    use unindent::Unindent as _;
 832
 833    #[gpui::test]
 834    async fn test_diagnostics(cx: &mut TestAppContext) {
 835        init_test(cx);
 836
 837        let fs = FakeFs::new(cx.background());
 838        fs.insert_tree(
 839            "/test",
 840            json!({
 841                "consts.rs": "
 842                    const a: i32 = 'a';
 843                    const b: i32 = c;
 844                "
 845                .unindent(),
 846
 847                "main.rs": "
 848                    fn main() {
 849                        let x = vec![];
 850                        let y = vec![];
 851                        a(x);
 852                        b(y);
 853                        // comment 1
 854                        // comment 2
 855                        c(y);
 856                        d(x);
 857                    }
 858                "
 859                .unindent(),
 860            }),
 861        )
 862        .await;
 863
 864        let language_server_id = LanguageServerId(0);
 865        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
 866        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 867
 868        // Create some diagnostics
 869        project.update(cx, |project, cx| {
 870            project
 871                .update_diagnostic_entries(
 872                    language_server_id,
 873                    PathBuf::from("/test/main.rs"),
 874                    None,
 875                    vec![
 876                        DiagnosticEntry {
 877                            range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
 878                            diagnostic: Diagnostic {
 879                                message:
 880                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 881                                        .to_string(),
 882                                severity: DiagnosticSeverity::INFORMATION,
 883                                is_primary: false,
 884                                is_disk_based: true,
 885                                group_id: 1,
 886                                ..Default::default()
 887                            },
 888                        },
 889                        DiagnosticEntry {
 890                            range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
 891                            diagnostic: Diagnostic {
 892                                message:
 893                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 894                                        .to_string(),
 895                                severity: DiagnosticSeverity::INFORMATION,
 896                                is_primary: false,
 897                                is_disk_based: true,
 898                                group_id: 0,
 899                                ..Default::default()
 900                            },
 901                        },
 902                        DiagnosticEntry {
 903                            range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 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: 1,
 910                                ..Default::default()
 911                            },
 912                        },
 913                        DiagnosticEntry {
 914                            range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
 915                            diagnostic: Diagnostic {
 916                                message: "value moved here".to_string(),
 917                                severity: DiagnosticSeverity::INFORMATION,
 918                                is_primary: false,
 919                                is_disk_based: true,
 920                                group_id: 0,
 921                                ..Default::default()
 922                            },
 923                        },
 924                        DiagnosticEntry {
 925                            range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 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: 0,
 932                                ..Default::default()
 933                            },
 934                        },
 935                        DiagnosticEntry {
 936                            range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
 937                            diagnostic: Diagnostic {
 938                                message: "use of moved value\nvalue used here after move".to_string(),
 939                                severity: DiagnosticSeverity::ERROR,
 940                                is_primary: true,
 941                                is_disk_based: true,
 942                                group_id: 1,
 943                                ..Default::default()
 944                            },
 945                        },
 946                    ],
 947                    cx,
 948                )
 949                .unwrap();
 950        });
 951
 952        // Open the project diagnostics view while there are already diagnostics.
 953        let view = cx.add_view(window_id, |cx| {
 954            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
 955        });
 956
 957        view.next_notification(cx).await;
 958        view.update(cx, |view, cx| {
 959            assert_eq!(
 960                editor_blocks(&view.editor, cx),
 961                [
 962                    (0, "path header block".into()),
 963                    (2, "diagnostic header".into()),
 964                    (15, "collapsed context".into()),
 965                    (16, "diagnostic header".into()),
 966                    (25, "collapsed context".into()),
 967                ]
 968            );
 969            assert_eq!(
 970                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
 971                concat!(
 972                    //
 973                    // main.rs
 974                    //
 975                    "\n", // filename
 976                    "\n", // padding
 977                    // diagnostic group 1
 978                    "\n", // primary message
 979                    "\n", // padding
 980                    "    let x = vec![];\n",
 981                    "    let y = vec![];\n",
 982                    "\n", // supporting diagnostic
 983                    "    a(x);\n",
 984                    "    b(y);\n",
 985                    "\n", // supporting diagnostic
 986                    "    // comment 1\n",
 987                    "    // comment 2\n",
 988                    "    c(y);\n",
 989                    "\n", // supporting diagnostic
 990                    "    d(x);\n",
 991                    "\n", // context ellipsis
 992                    // diagnostic group 2
 993                    "\n", // primary message
 994                    "\n", // padding
 995                    "fn main() {\n",
 996                    "    let x = vec![];\n",
 997                    "\n", // supporting diagnostic
 998                    "    let y = vec![];\n",
 999                    "    a(x);\n",
1000                    "\n", // supporting diagnostic
1001                    "    b(y);\n",
1002                    "\n", // context ellipsis
1003                    "    c(y);\n",
1004                    "    d(x);\n",
1005                    "\n", // supporting diagnostic
1006                    "}"
1007                )
1008            );
1009
1010            // Cursor is at the first diagnostic
1011            view.editor.update(cx, |editor, cx| {
1012                assert_eq!(
1013                    editor.selections.display_ranges(cx),
1014                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
1015                );
1016            });
1017        });
1018
1019        // Diagnostics are added for another earlier path.
1020        project.update(cx, |project, cx| {
1021            project.disk_based_diagnostics_started(language_server_id, cx);
1022            project
1023                .update_diagnostic_entries(
1024                    language_server_id,
1025                    PathBuf::from("/test/consts.rs"),
1026                    None,
1027                    vec![DiagnosticEntry {
1028                        range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
1029                        diagnostic: Diagnostic {
1030                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1031                            severity: DiagnosticSeverity::ERROR,
1032                            is_primary: true,
1033                            is_disk_based: true,
1034                            group_id: 0,
1035                            ..Default::default()
1036                        },
1037                    }],
1038                    cx,
1039                )
1040                .unwrap();
1041            project.disk_based_diagnostics_finished(language_server_id, cx);
1042        });
1043
1044        view.next_notification(cx).await;
1045        view.update(cx, |view, cx| {
1046            assert_eq!(
1047                editor_blocks(&view.editor, cx),
1048                [
1049                    (0, "path header block".into()),
1050                    (2, "diagnostic header".into()),
1051                    (7, "path header block".into()),
1052                    (9, "diagnostic header".into()),
1053                    (22, "collapsed context".into()),
1054                    (23, "diagnostic header".into()),
1055                    (32, "collapsed context".into()),
1056                ]
1057            );
1058            assert_eq!(
1059                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1060                concat!(
1061                    //
1062                    // consts.rs
1063                    //
1064                    "\n", // filename
1065                    "\n", // padding
1066                    // diagnostic group 1
1067                    "\n", // primary message
1068                    "\n", // padding
1069                    "const a: i32 = 'a';\n",
1070                    "\n", // supporting diagnostic
1071                    "const b: i32 = c;\n",
1072                    //
1073                    // main.rs
1074                    //
1075                    "\n", // filename
1076                    "\n", // padding
1077                    // diagnostic group 1
1078                    "\n", // primary message
1079                    "\n", // padding
1080                    "    let x = vec![];\n",
1081                    "    let y = vec![];\n",
1082                    "\n", // supporting diagnostic
1083                    "    a(x);\n",
1084                    "    b(y);\n",
1085                    "\n", // supporting diagnostic
1086                    "    // comment 1\n",
1087                    "    // comment 2\n",
1088                    "    c(y);\n",
1089                    "\n", // supporting diagnostic
1090                    "    d(x);\n",
1091                    "\n", // collapsed context
1092                    // diagnostic group 2
1093                    "\n", // primary message
1094                    "\n", // filename
1095                    "fn main() {\n",
1096                    "    let x = vec![];\n",
1097                    "\n", // supporting diagnostic
1098                    "    let y = vec![];\n",
1099                    "    a(x);\n",
1100                    "\n", // supporting diagnostic
1101                    "    b(y);\n",
1102                    "\n", // context ellipsis
1103                    "    c(y);\n",
1104                    "    d(x);\n",
1105                    "\n", // supporting diagnostic
1106                    "}"
1107                )
1108            );
1109
1110            // Cursor keeps its position.
1111            view.editor.update(cx, |editor, cx| {
1112                assert_eq!(
1113                    editor.selections.display_ranges(cx),
1114                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1115                );
1116            });
1117        });
1118
1119        // Diagnostics are added to the first path
1120        project.update(cx, |project, cx| {
1121            project.disk_based_diagnostics_started(language_server_id, cx);
1122            project
1123                .update_diagnostic_entries(
1124                    language_server_id,
1125                    PathBuf::from("/test/consts.rs"),
1126                    None,
1127                    vec![
1128                        DiagnosticEntry {
1129                            range: Unclipped(PointUtf16::new(0, 15))
1130                                ..Unclipped(PointUtf16::new(0, 15)),
1131                            diagnostic: Diagnostic {
1132                                message: "mismatched types\nexpected `usize`, found `char`"
1133                                    .to_string(),
1134                                severity: DiagnosticSeverity::ERROR,
1135                                is_primary: true,
1136                                is_disk_based: true,
1137                                group_id: 0,
1138                                ..Default::default()
1139                            },
1140                        },
1141                        DiagnosticEntry {
1142                            range: Unclipped(PointUtf16::new(1, 15))
1143                                ..Unclipped(PointUtf16::new(1, 15)),
1144                            diagnostic: Diagnostic {
1145                                message: "unresolved name `c`".to_string(),
1146                                severity: DiagnosticSeverity::ERROR,
1147                                is_primary: true,
1148                                is_disk_based: true,
1149                                group_id: 1,
1150                                ..Default::default()
1151                            },
1152                        },
1153                    ],
1154                    cx,
1155                )
1156                .unwrap();
1157            project.disk_based_diagnostics_finished(language_server_id, cx);
1158        });
1159
1160        view.next_notification(cx).await;
1161        view.update(cx, |view, cx| {
1162            assert_eq!(
1163                editor_blocks(&view.editor, cx),
1164                [
1165                    (0, "path header block".into()),
1166                    (2, "diagnostic header".into()),
1167                    (7, "collapsed context".into()),
1168                    (8, "diagnostic header".into()),
1169                    (13, "path header block".into()),
1170                    (15, "diagnostic header".into()),
1171                    (28, "collapsed context".into()),
1172                    (29, "diagnostic header".into()),
1173                    (38, "collapsed context".into()),
1174                ]
1175            );
1176            assert_eq!(
1177                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1178                concat!(
1179                    //
1180                    // consts.rs
1181                    //
1182                    "\n", // filename
1183                    "\n", // padding
1184                    // diagnostic group 1
1185                    "\n", // primary message
1186                    "\n", // padding
1187                    "const a: i32 = 'a';\n",
1188                    "\n", // supporting diagnostic
1189                    "const b: i32 = c;\n",
1190                    "\n", // context ellipsis
1191                    // diagnostic group 2
1192                    "\n", // primary message
1193                    "\n", // padding
1194                    "const a: i32 = 'a';\n",
1195                    "const b: i32 = c;\n",
1196                    "\n", // supporting diagnostic
1197                    //
1198                    // main.rs
1199                    //
1200                    "\n", // filename
1201                    "\n", // padding
1202                    // diagnostic group 1
1203                    "\n", // primary message
1204                    "\n", // padding
1205                    "    let x = vec![];\n",
1206                    "    let y = vec![];\n",
1207                    "\n", // supporting diagnostic
1208                    "    a(x);\n",
1209                    "    b(y);\n",
1210                    "\n", // supporting diagnostic
1211                    "    // comment 1\n",
1212                    "    // comment 2\n",
1213                    "    c(y);\n",
1214                    "\n", // supporting diagnostic
1215                    "    d(x);\n",
1216                    "\n", // context ellipsis
1217                    // diagnostic group 2
1218                    "\n", // primary message
1219                    "\n", // filename
1220                    "fn main() {\n",
1221                    "    let x = vec![];\n",
1222                    "\n", // supporting diagnostic
1223                    "    let y = vec![];\n",
1224                    "    a(x);\n",
1225                    "\n", // supporting diagnostic
1226                    "    b(y);\n",
1227                    "\n", // context ellipsis
1228                    "    c(y);\n",
1229                    "    d(x);\n",
1230                    "\n", // supporting diagnostic
1231                    "}"
1232                )
1233            );
1234        });
1235    }
1236
1237    #[gpui::test]
1238    async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1239        init_test(cx);
1240
1241        let fs = FakeFs::new(cx.background());
1242        fs.insert_tree(
1243            "/test",
1244            json!({
1245                "main.js": "
1246                    a();
1247                    b();
1248                    c();
1249                    d();
1250                    e();
1251                ".unindent()
1252            }),
1253        )
1254        .await;
1255
1256        let server_id_1 = LanguageServerId(100);
1257        let server_id_2 = LanguageServerId(101);
1258        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1259        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1260
1261        let view = cx.add_view(window_id, |cx| {
1262            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
1263        });
1264
1265        // Two language servers start updating diagnostics
1266        project.update(cx, |project, cx| {
1267            project.disk_based_diagnostics_started(server_id_1, cx);
1268            project.disk_based_diagnostics_started(server_id_2, cx);
1269            project
1270                .update_diagnostic_entries(
1271                    server_id_1,
1272                    PathBuf::from("/test/main.js"),
1273                    None,
1274                    vec![DiagnosticEntry {
1275                        range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
1276                        diagnostic: Diagnostic {
1277                            message: "error 1".to_string(),
1278                            severity: DiagnosticSeverity::WARNING,
1279                            is_primary: true,
1280                            is_disk_based: true,
1281                            group_id: 1,
1282                            ..Default::default()
1283                        },
1284                    }],
1285                    cx,
1286                )
1287                .unwrap();
1288            project
1289                .update_diagnostic_entries(
1290                    server_id_2,
1291                    PathBuf::from("/test/main.js"),
1292                    None,
1293                    vec![DiagnosticEntry {
1294                        range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
1295                        diagnostic: Diagnostic {
1296                            message: "warning 1".to_string(),
1297                            severity: DiagnosticSeverity::ERROR,
1298                            is_primary: true,
1299                            is_disk_based: true,
1300                            group_id: 2,
1301                            ..Default::default()
1302                        },
1303                    }],
1304                    cx,
1305                )
1306                .unwrap();
1307        });
1308
1309        // The first language server finishes
1310        project.update(cx, |project, cx| {
1311            project.disk_based_diagnostics_finished(server_id_1, cx);
1312        });
1313
1314        // Only the first language server's diagnostics are shown.
1315        cx.foreground().run_until_parked();
1316        view.update(cx, |view, cx| {
1317            assert_eq!(
1318                editor_blocks(&view.editor, cx),
1319                [
1320                    (0, "path header block".into()),
1321                    (2, "diagnostic header".into()),
1322                ]
1323            );
1324            assert_eq!(
1325                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1326                concat!(
1327                    "\n", // filename
1328                    "\n", // padding
1329                    // diagnostic group 1
1330                    "\n",     // primary message
1331                    "\n",     // padding
1332                    "a();\n", //
1333                    "b();",
1334                )
1335            );
1336        });
1337
1338        // The second language server finishes
1339        project.update(cx, |project, cx| {
1340            project.disk_based_diagnostics_finished(server_id_2, cx);
1341        });
1342
1343        // Both language server's diagnostics are shown.
1344        cx.foreground().run_until_parked();
1345        view.update(cx, |view, cx| {
1346            assert_eq!(
1347                editor_blocks(&view.editor, cx),
1348                [
1349                    (0, "path header block".into()),
1350                    (2, "diagnostic header".into()),
1351                    (6, "collapsed context".into()),
1352                    (7, "diagnostic header".into()),
1353                ]
1354            );
1355            assert_eq!(
1356                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1357                concat!(
1358                    "\n", // filename
1359                    "\n", // padding
1360                    // diagnostic group 1
1361                    "\n",     // primary message
1362                    "\n",     // padding
1363                    "a();\n", // location
1364                    "b();\n", //
1365                    "\n",     // collapsed context
1366                    // diagnostic group 2
1367                    "\n",     // primary message
1368                    "\n",     // padding
1369                    "a();\n", // context
1370                    "b();\n", //
1371                    "c();",   // context
1372                )
1373            );
1374        });
1375
1376        // Both language servers start updating diagnostics, and the first server finishes.
1377        project.update(cx, |project, cx| {
1378            project.disk_based_diagnostics_started(server_id_1, cx);
1379            project.disk_based_diagnostics_started(server_id_2, cx);
1380            project
1381                .update_diagnostic_entries(
1382                    server_id_1,
1383                    PathBuf::from("/test/main.js"),
1384                    None,
1385                    vec![DiagnosticEntry {
1386                        range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
1387                        diagnostic: Diagnostic {
1388                            message: "warning 2".to_string(),
1389                            severity: DiagnosticSeverity::WARNING,
1390                            is_primary: true,
1391                            is_disk_based: true,
1392                            group_id: 1,
1393                            ..Default::default()
1394                        },
1395                    }],
1396                    cx,
1397                )
1398                .unwrap();
1399            project
1400                .update_diagnostic_entries(
1401                    server_id_2,
1402                    PathBuf::from("/test/main.rs"),
1403                    None,
1404                    vec![],
1405                    cx,
1406                )
1407                .unwrap();
1408            project.disk_based_diagnostics_finished(server_id_1, cx);
1409        });
1410
1411        // Only the first language server's diagnostics are updated.
1412        cx.foreground().run_until_parked();
1413        view.update(cx, |view, cx| {
1414            assert_eq!(
1415                editor_blocks(&view.editor, cx),
1416                [
1417                    (0, "path header block".into()),
1418                    (2, "diagnostic header".into()),
1419                    (7, "collapsed context".into()),
1420                    (8, "diagnostic header".into()),
1421                ]
1422            );
1423            assert_eq!(
1424                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1425                concat!(
1426                    "\n", // filename
1427                    "\n", // padding
1428                    // diagnostic group 1
1429                    "\n",     // primary message
1430                    "\n",     // padding
1431                    "a();\n", // location
1432                    "b();\n", //
1433                    "c();\n", // context
1434                    "\n",     // collapsed context
1435                    // diagnostic group 2
1436                    "\n",     // primary message
1437                    "\n",     // padding
1438                    "b();\n", // context
1439                    "c();\n", //
1440                    "d();",   // context
1441                )
1442            );
1443        });
1444
1445        // The second language server finishes.
1446        project.update(cx, |project, cx| {
1447            project
1448                .update_diagnostic_entries(
1449                    server_id_2,
1450                    PathBuf::from("/test/main.js"),
1451                    None,
1452                    vec![DiagnosticEntry {
1453                        range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
1454                        diagnostic: Diagnostic {
1455                            message: "warning 2".to_string(),
1456                            severity: DiagnosticSeverity::WARNING,
1457                            is_primary: true,
1458                            is_disk_based: true,
1459                            group_id: 1,
1460                            ..Default::default()
1461                        },
1462                    }],
1463                    cx,
1464                )
1465                .unwrap();
1466            project.disk_based_diagnostics_finished(server_id_2, cx);
1467        });
1468
1469        // Both language servers' diagnostics are updated.
1470        cx.foreground().run_until_parked();
1471        view.update(cx, |view, cx| {
1472            assert_eq!(
1473                editor_blocks(&view.editor, cx),
1474                [
1475                    (0, "path header block".into()),
1476                    (2, "diagnostic header".into()),
1477                    (7, "collapsed context".into()),
1478                    (8, "diagnostic header".into()),
1479                ]
1480            );
1481            assert_eq!(
1482                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1483                concat!(
1484                    "\n", // filename
1485                    "\n", // padding
1486                    // diagnostic group 1
1487                    "\n",     // primary message
1488                    "\n",     // padding
1489                    "b();\n", // location
1490                    "c();\n", //
1491                    "d();\n", // context
1492                    "\n",     // collapsed context
1493                    // diagnostic group 2
1494                    "\n",     // primary message
1495                    "\n",     // padding
1496                    "c();\n", // context
1497                    "d();\n", //
1498                    "e();",   // context
1499                )
1500            );
1501        });
1502    }
1503
1504    fn init_test(cx: &mut TestAppContext) {
1505        cx.update(|cx| {
1506            cx.set_global(SettingsStore::test(cx));
1507            theme::init((), cx);
1508            language::init(cx);
1509            client::init_settings(cx);
1510            workspace::init_settings(cx);
1511        });
1512    }
1513
1514    fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
1515        editor.update(cx, |editor, cx| {
1516            let snapshot = editor.snapshot(cx);
1517            snapshot
1518                .blocks_in_range(0..snapshot.max_point().row())
1519                .filter_map(|(row, block)| {
1520                    let name = match block {
1521                        TransformBlock::Custom(block) => block
1522                            .render(&mut BlockContext {
1523                                view_context: cx,
1524                                anchor_x: 0.,
1525                                scroll_x: 0.,
1526                                gutter_padding: 0.,
1527                                gutter_width: 0.,
1528                                line_height: 0.,
1529                                em_width: 0.,
1530                            })
1531                            .name()?
1532                            .to_string(),
1533                        TransformBlock::ExcerptHeader {
1534                            starts_new_buffer, ..
1535                        } => {
1536                            if *starts_new_buffer {
1537                                "path header block".to_string()
1538                            } else {
1539                                "collapsed context".to_string()
1540                            }
1541                        }
1542                    };
1543
1544                    Some((row, name))
1545                })
1546                .collect()
1547        })
1548    }
1549}