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