diagnostics.rs

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