diagnostics.rs

   1pub mod items;
   2
   3use anyhow::Result;
   4use collections::{BTreeSet, HashSet};
   5use editor::{
   6    diagnostic_block_renderer,
   7    display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
   8    highlight_diagnostic_message,
   9    scroll::autoscroll::Autoscroll,
  10    Editor, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
  11};
  12use gpui::{
  13    actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity,
  14    ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle,
  15};
  16use language::{
  17    Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
  18    SelectionGoal,
  19};
  20use lsp::LanguageServerId;
  21use project::{DiagnosticSummary, Project, ProjectPath};
  22use serde_json::json;
  23use smallvec::SmallVec;
  24use std::{
  25    any::{Any, TypeId},
  26    borrow::Cow,
  27    cmp::Ordering,
  28    ops::Range,
  29    path::PathBuf,
  30    sync::Arc,
  31};
  32use theme::ThemeSettings;
  33use util::TryFutureExt;
  34use workspace::{
  35    item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
  36    ItemNavHistory, Pane, PaneBackdrop, ToolbarItemLocation, Workspace,
  37};
  38
  39actions!(diagnostics, [Deploy]);
  40
  41const CONTEXT_LINE_COUNT: u32 = 1;
  42
  43pub fn init(cx: &mut AppContext) {
  44    cx.add_action(ProjectDiagnosticsEditor::deploy);
  45    items::init(cx);
  46}
  47
  48type Event = editor::Event;
  49
  50struct ProjectDiagnosticsEditor {
  51    project: ModelHandle<Project>,
  52    workspace: WeakViewHandle<Workspace>,
  53    editor: ViewHandle<Editor>,
  54    summary: DiagnosticSummary,
  55    excerpts: ModelHandle<MultiBuffer>,
  56    path_states: Vec<PathState>,
  57    paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
  58}
  59
  60struct PathState {
  61    path: ProjectPath,
  62    diagnostic_groups: Vec<DiagnosticGroupState>,
  63}
  64
  65#[derive(Clone, Debug, PartialEq)]
  66struct Jump {
  67    path: ProjectPath,
  68    position: Point,
  69    anchor: Anchor,
  70}
  71
  72struct DiagnosticGroupState {
  73    language_server_id: LanguageServerId,
  74    primary_diagnostic: DiagnosticEntry<language::Anchor>,
  75    primary_excerpt_ix: usize,
  76    excerpts: Vec<ExcerptId>,
  77    blocks: HashSet<BlockId>,
  78    block_count: usize,
  79}
  80
  81impl Entity for ProjectDiagnosticsEditor {
  82    type Event = Event;
  83}
  84
  85impl View for ProjectDiagnosticsEditor {
  86    fn ui_name() -> &'static str {
  87        "ProjectDiagnosticsEditor"
  88    }
  89
  90    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
  91        if self.path_states.is_empty() {
  92            let theme = &theme::current(cx).project_diagnostics;
  93            PaneBackdrop::new(
  94                cx.view_id(),
  95                Label::new("No problems in workspace", theme.empty_message.clone())
  96                    .aligned()
  97                    .contained()
  98                    .with_style(theme.container)
  99                    .into_any(),
 100            )
 101            .into_any()
 102        } else {
 103            ChildView::new(&self.editor, cx).into_any()
 104        }
 105    }
 106
 107    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
 108        if cx.is_self_focused() && !self.path_states.is_empty() {
 109            cx.focus(&self.editor);
 110        }
 111    }
 112
 113    fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
 114        let project = self.project.read(cx);
 115        json!({
 116            "project": json!({
 117                "language_servers": project.language_server_statuses().collect::<Vec<_>>(),
 118                "summary": project.diagnostic_summary(cx),
 119            }),
 120            "summary": self.summary,
 121            "paths_to_update": self.paths_to_update.iter().map(|(path, server_id)|
 122                (path.path.to_string_lossy(), server_id.0)
 123            ).collect::<Vec<_>>(),
 124            "paths_states": self.path_states.iter().map(|state|
 125                json!({
 126                    "path": state.path.path.to_string_lossy(),
 127                    "groups": state.diagnostic_groups.iter().map(|group|
 128                        json!({
 129                            "block_count": group.blocks.len(),
 130                            "excerpt_count": group.excerpts.len(),
 131                        })
 132                    ).collect::<Vec<_>>(),
 133                })
 134            ).collect::<Vec<_>>(),
 135        })
 136    }
 137}
 138
 139impl ProjectDiagnosticsEditor {
 140    fn new(
 141        project_handle: ModelHandle<Project>,
 142        workspace: WeakViewHandle<Workspace>,
 143        cx: &mut ViewContext<Self>,
 144    ) -> Self {
 145        cx.subscribe(&project_handle, |this, _, event, cx| match event {
 146            project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
 147                this.update_excerpts(Some(*language_server_id), cx);
 148                this.update_title(cx);
 149            }
 150            project::Event::DiagnosticsUpdated {
 151                language_server_id,
 152                path,
 153            } => {
 154                this.paths_to_update
 155                    .insert((path.clone(), *language_server_id));
 156            }
 157            _ => {}
 158        })
 159        .detach();
 160
 161        let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
 162        let editor = cx.add_view(|cx| {
 163            let mut editor =
 164                Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
 165            editor.set_vertical_scroll_margin(5, cx);
 166            editor
 167        });
 168        cx.subscribe(&editor, |this, _, event, cx| {
 169            cx.emit(event.clone());
 170            if event == &editor::Event::Focused && this.path_states.is_empty() {
 171                cx.focus_self()
 172            }
 173        })
 174        .detach();
 175
 176        let project = project_handle.read(cx);
 177        let paths_to_update = project
 178            .diagnostic_summaries(cx)
 179            .map(|(path, server_id, _)| (path, server_id))
 180            .collect();
 181        let summary = project.diagnostic_summary(cx);
 182        let mut this = Self {
 183            project: project_handle,
 184            summary,
 185            workspace,
 186            excerpts,
 187            editor,
 188            path_states: Default::default(),
 189            paths_to_update,
 190        };
 191        this.update_excerpts(None, cx);
 192        this
 193    }
 194
 195    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
 196        if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
 197            workspace.activate_item(&existing, cx);
 198        } else {
 199            let workspace_handle = cx.weak_handle();
 200            let diagnostics = cx.add_view(|cx| {
 201                ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
 202            });
 203            workspace.add_item(Box::new(diagnostics), cx);
 204        }
 205    }
 206
 207    fn update_excerpts(
 208        &mut self,
 209        language_server_id: Option<LanguageServerId>,
 210        cx: &mut ViewContext<Self>,
 211    ) {
 212        let mut paths = Vec::new();
 213        self.paths_to_update.retain(|(path, server_id)| {
 214            if language_server_id
 215                .map_or(true, |language_server_id| language_server_id == *server_id)
 216            {
 217                paths.push(path.clone());
 218                false
 219            } else {
 220                true
 221            }
 222        });
 223        let project = self.project.clone();
 224        cx.spawn(|this, mut cx| {
 225            async move {
 226                for path in paths {
 227                    let buffer = project
 228                        .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
 229                        .await?;
 230                    this.update(&mut cx, |this, cx| {
 231                        this.populate_excerpts(path, language_server_id, buffer, cx)
 232                    })?;
 233                }
 234                Result::<_, anyhow::Error>::Ok(())
 235            }
 236            .log_err()
 237        })
 238        .detach();
 239    }
 240
 241    fn populate_excerpts(
 242        &mut self,
 243        path: ProjectPath,
 244        language_server_id: Option<LanguageServerId>,
 245        buffer: ModelHandle<Buffer>,
 246        cx: &mut ViewContext<Self>,
 247    ) {
 248        let was_empty = self.path_states.is_empty();
 249        let snapshot = buffer.read(cx).snapshot();
 250        let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
 251            Ok(ix) => ix,
 252            Err(ix) => {
 253                self.path_states.insert(
 254                    ix,
 255                    PathState {
 256                        path: path.clone(),
 257                        diagnostic_groups: Default::default(),
 258                    },
 259                );
 260                ix
 261            }
 262        };
 263
 264        let mut prev_excerpt_id = if path_ix > 0 {
 265            let prev_path_last_group = &self.path_states[path_ix - 1]
 266                .diagnostic_groups
 267                .last()
 268                .unwrap();
 269            prev_path_last_group.excerpts.last().unwrap().clone()
 270        } else {
 271            ExcerptId::min()
 272        };
 273
 274        let path_state = &mut self.path_states[path_ix];
 275        let mut groups_to_add = Vec::new();
 276        let mut group_ixs_to_remove = Vec::new();
 277        let mut blocks_to_add = Vec::new();
 278        let mut blocks_to_remove = HashSet::default();
 279        let mut first_excerpt_id = None;
 280        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
 281            let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
 282            let mut new_groups = snapshot
 283                .diagnostic_groups(language_server_id)
 284                .into_iter()
 285                .filter(|(_, group)| {
 286                    group.entries[group.primary_ix].diagnostic.severity
 287                        <= DiagnosticSeverity::WARNING
 288                })
 289                .peekable();
 290            loop {
 291                let mut to_insert = None;
 292                let mut to_remove = None;
 293                let mut to_keep = None;
 294                match (old_groups.peek(), new_groups.peek()) {
 295                    (None, None) => break,
 296                    (None, Some(_)) => to_insert = new_groups.next(),
 297                    (Some((_, old_group)), None) => {
 298                        if language_server_id.map_or(true, |id| id == old_group.language_server_id)
 299                        {
 300                            to_remove = old_groups.next();
 301                        } else {
 302                            to_keep = old_groups.next();
 303                        }
 304                    }
 305                    (Some((_, old_group)), Some((_, new_group))) => {
 306                        let old_primary = &old_group.primary_diagnostic;
 307                        let new_primary = &new_group.entries[new_group.primary_ix];
 308                        match compare_diagnostics(old_primary, new_primary, &snapshot) {
 309                            Ordering::Less => {
 310                                if language_server_id
 311                                    .map_or(true, |id| id == old_group.language_server_id)
 312                                {
 313                                    to_remove = old_groups.next();
 314                                } else {
 315                                    to_keep = old_groups.next();
 316                                }
 317                            }
 318                            Ordering::Equal => {
 319                                to_keep = old_groups.next();
 320                                new_groups.next();
 321                            }
 322                            Ordering::Greater => to_insert = new_groups.next(),
 323                        }
 324                    }
 325                }
 326
 327                if let Some((language_server_id, group)) = to_insert {
 328                    let mut group_state = DiagnosticGroupState {
 329                        language_server_id,
 330                        primary_diagnostic: group.entries[group.primary_ix].clone(),
 331                        primary_excerpt_ix: 0,
 332                        excerpts: Default::default(),
 333                        blocks: Default::default(),
 334                        block_count: 0,
 335                    };
 336                    let mut pending_range: Option<(Range<Point>, usize)> = None;
 337                    let mut is_first_excerpt_for_group = true;
 338                    for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
 339                        let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
 340                        if let Some((range, start_ix)) = &mut pending_range {
 341                            if let Some(entry) = resolved_entry.as_ref() {
 342                                if entry.range.start.row
 343                                    <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
 344                                {
 345                                    range.end = range.end.max(entry.range.end);
 346                                    continue;
 347                                }
 348                            }
 349
 350                            let excerpt_start =
 351                                Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
 352                            let excerpt_end = snapshot.clip_point(
 353                                Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
 354                                Bias::Left,
 355                            );
 356                            let excerpt_id = excerpts
 357                                .insert_excerpts_after(
 358                                    prev_excerpt_id,
 359                                    buffer.clone(),
 360                                    [ExcerptRange {
 361                                        context: excerpt_start..excerpt_end,
 362                                        primary: Some(range.clone()),
 363                                    }],
 364                                    excerpts_cx,
 365                                )
 366                                .pop()
 367                                .unwrap();
 368
 369                            prev_excerpt_id = excerpt_id.clone();
 370                            first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
 371                            group_state.excerpts.push(excerpt_id.clone());
 372                            let header_position = (excerpt_id.clone(), language::Anchor::MIN);
 373
 374                            if is_first_excerpt_for_group {
 375                                is_first_excerpt_for_group = false;
 376                                let mut primary =
 377                                    group.entries[group.primary_ix].diagnostic.clone();
 378                                primary.message =
 379                                    primary.message.split('\n').next().unwrap().to_string();
 380                                group_state.block_count += 1;
 381                                blocks_to_add.push(BlockProperties {
 382                                    position: header_position,
 383                                    height: 2,
 384                                    style: BlockStyle::Sticky,
 385                                    render: diagnostic_header_renderer(primary),
 386                                    disposition: BlockDisposition::Above,
 387                                });
 388                            }
 389
 390                            for entry in &group.entries[*start_ix..ix] {
 391                                let mut diagnostic = entry.diagnostic.clone();
 392                                if diagnostic.is_primary {
 393                                    group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
 394                                    diagnostic.message =
 395                                        entry.diagnostic.message.split('\n').skip(1).collect();
 396                                }
 397
 398                                if !diagnostic.message.is_empty() {
 399                                    group_state.block_count += 1;
 400                                    blocks_to_add.push(BlockProperties {
 401                                        position: (excerpt_id.clone(), entry.range.start),
 402                                        height: diagnostic.message.matches('\n').count() as u8 + 1,
 403                                        style: BlockStyle::Fixed,
 404                                        render: diagnostic_block_renderer(diagnostic, true),
 405                                        disposition: BlockDisposition::Below,
 406                                    });
 407                                }
 408                            }
 409
 410                            pending_range.take();
 411                        }
 412
 413                        if let Some(entry) = resolved_entry {
 414                            pending_range = Some((entry.range.clone(), ix));
 415                        }
 416                    }
 417
 418                    groups_to_add.push(group_state);
 419                } else if let Some((group_ix, group_state)) = to_remove {
 420                    excerpts.remove_excerpts(group_state.excerpts.iter().copied(), excerpts_cx);
 421                    group_ixs_to_remove.push(group_ix);
 422                    blocks_to_remove.extend(group_state.blocks.iter().copied());
 423                } else if let Some((_, group)) = to_keep {
 424                    prev_excerpt_id = group.excerpts.last().unwrap().clone();
 425                    first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
 426                }
 427            }
 428
 429            excerpts.snapshot(excerpts_cx)
 430        });
 431
 432        self.editor.update(cx, |editor, cx| {
 433            editor.remove_blocks(blocks_to_remove, None, cx);
 434            let block_ids = editor.insert_blocks(
 435                blocks_to_add.into_iter().map(|block| {
 436                    let (excerpt_id, text_anchor) = block.position;
 437                    BlockProperties {
 438                        position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
 439                        height: block.height,
 440                        style: block.style,
 441                        render: block.render,
 442                        disposition: block.disposition,
 443                    }
 444                }),
 445                Some(Autoscroll::fit()),
 446                cx,
 447            );
 448
 449            let mut block_ids = block_ids.into_iter();
 450            for group_state in &mut groups_to_add {
 451                group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
 452            }
 453        });
 454
 455        for ix in group_ixs_to_remove.into_iter().rev() {
 456            path_state.diagnostic_groups.remove(ix);
 457        }
 458        path_state.diagnostic_groups.extend(groups_to_add);
 459        path_state.diagnostic_groups.sort_unstable_by(|a, b| {
 460            let range_a = &a.primary_diagnostic.range;
 461            let range_b = &b.primary_diagnostic.range;
 462            range_a
 463                .start
 464                .cmp(&range_b.start, &snapshot)
 465                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
 466        });
 467
 468        if path_state.diagnostic_groups.is_empty() {
 469            self.path_states.remove(path_ix);
 470        }
 471
 472        self.editor.update(cx, |editor, cx| {
 473            let groups;
 474            let mut selections;
 475            let new_excerpt_ids_by_selection_id;
 476            if was_empty {
 477                groups = self.path_states.first()?.diagnostic_groups.as_slice();
 478                new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
 479                selections = vec![Selection {
 480                    id: 0,
 481                    start: 0,
 482                    end: 0,
 483                    reversed: false,
 484                    goal: SelectionGoal::None,
 485                }];
 486            } else {
 487                groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
 488                new_excerpt_ids_by_selection_id =
 489                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
 490                selections = editor.selections.all::<usize>(cx);
 491            }
 492
 493            // If any selection has lost its position, move it to start of the next primary diagnostic.
 494            let snapshot = editor.snapshot(cx);
 495            for selection in &mut selections {
 496                if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
 497                    let group_ix = match groups.binary_search_by(|probe| {
 498                        probe
 499                            .excerpts
 500                            .last()
 501                            .unwrap()
 502                            .cmp(new_excerpt_id, &snapshot.buffer_snapshot)
 503                    }) {
 504                        Ok(ix) | Err(ix) => ix,
 505                    };
 506                    if let Some(group) = groups.get(group_ix) {
 507                        let offset = excerpts_snapshot
 508                            .anchor_in_excerpt(
 509                                group.excerpts[group.primary_excerpt_ix].clone(),
 510                                group.primary_diagnostic.range.start,
 511                            )
 512                            .to_offset(&excerpts_snapshot);
 513                        selection.start = offset;
 514                        selection.end = offset;
 515                    }
 516                }
 517            }
 518            editor.change_selections(None, cx, |s| {
 519                s.select(selections);
 520            });
 521            Some(())
 522        });
 523
 524        if self.path_states.is_empty() {
 525            if self.editor.is_focused(cx) {
 526                cx.focus_self();
 527            }
 528        } else if cx.handle().is_focused(cx) {
 529            cx.focus(&self.editor);
 530        }
 531        cx.notify();
 532    }
 533
 534    fn update_title(&mut self, cx: &mut ViewContext<Self>) {
 535        self.summary = self.project.read(cx).diagnostic_summary(cx);
 536        cx.emit(Event::TitleChanged);
 537    }
 538}
 539
 540impl Item for ProjectDiagnosticsEditor {
 541    fn tab_content<T: View>(
 542        &self,
 543        _detail: Option<usize>,
 544        style: &theme::Tab,
 545        cx: &AppContext,
 546    ) -> AnyElement<T> {
 547        render_summary(
 548            &self.summary,
 549            &style.label.text,
 550            &theme::current(cx).project_diagnostics,
 551        )
 552    }
 553
 554    fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
 555        self.editor.for_each_project_item(cx, f)
 556    }
 557
 558    fn is_singleton(&self, _: &AppContext) -> bool {
 559        false
 560    }
 561
 562    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
 563        self.editor
 564            .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
 565    }
 566
 567    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
 568        self.editor
 569            .update(cx, |editor, cx| editor.navigate(data, cx))
 570    }
 571
 572    fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
 573        Some("Project Diagnostics".into())
 574    }
 575
 576    fn is_dirty(&self, cx: &AppContext) -> bool {
 577        self.excerpts.read(cx).is_dirty(cx)
 578    }
 579
 580    fn has_conflict(&self, cx: &AppContext) -> bool {
 581        self.excerpts.read(cx).has_conflict(cx)
 582    }
 583
 584    fn can_save(&self, _: &AppContext) -> bool {
 585        true
 586    }
 587
 588    fn save(
 589        &mut self,
 590        project: ModelHandle<Project>,
 591        cx: &mut ViewContext<Self>,
 592    ) -> Task<Result<()>> {
 593        self.editor.save(project, cx)
 594    }
 595
 596    fn reload(
 597        &mut self,
 598        project: ModelHandle<Project>,
 599        cx: &mut ViewContext<Self>,
 600    ) -> Task<Result<()>> {
 601        self.editor.reload(project, cx)
 602    }
 603
 604    fn save_as(
 605        &mut self,
 606        _: ModelHandle<Project>,
 607        _: PathBuf,
 608        _: &mut ViewContext<Self>,
 609    ) -> Task<Result<()>> {
 610        unreachable!()
 611    }
 612
 613    fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
 614        Editor::to_item_events(event)
 615    }
 616
 617    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
 618        self.editor.update(cx, |editor, _| {
 619            editor.set_nav_history(Some(nav_history));
 620        });
 621    }
 622
 623    fn clone_on_split(
 624        &self,
 625        _workspace_id: workspace::WorkspaceId,
 626        cx: &mut ViewContext<Self>,
 627    ) -> Option<Self>
 628    where
 629        Self: Sized,
 630    {
 631        Some(ProjectDiagnosticsEditor::new(
 632            self.project.clone(),
 633            self.workspace.clone(),
 634            cx,
 635        ))
 636    }
 637
 638    fn act_as_type<'a>(
 639        &'a self,
 640        type_id: TypeId,
 641        self_handle: &'a ViewHandle<Self>,
 642        _: &'a AppContext,
 643    ) -> Option<&AnyViewHandle> {
 644        if type_id == TypeId::of::<Self>() {
 645            Some(self_handle)
 646        } else if type_id == TypeId::of::<Editor>() {
 647            Some(&self.editor)
 648        } else {
 649            None
 650        }
 651    }
 652
 653    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 654        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
 655    }
 656
 657    fn serialized_item_kind() -> Option<&'static str> {
 658        Some("diagnostics")
 659    }
 660
 661    fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
 662        self.editor.breadcrumbs(theme, cx)
 663    }
 664
 665    fn breadcrumb_location(&self) -> ToolbarItemLocation {
 666        ToolbarItemLocation::PrimaryLeft { flex: None }
 667    }
 668
 669    fn deserialize(
 670        project: ModelHandle<Project>,
 671        workspace: WeakViewHandle<Workspace>,
 672        _workspace_id: workspace::WorkspaceId,
 673        _item_id: workspace::ItemId,
 674        cx: &mut ViewContext<Pane>,
 675    ) -> Task<Result<ViewHandle<Self>>> {
 676        Task::ready(Ok(cx.add_view(|cx| Self::new(project, workspace, cx))))
 677    }
 678}
 679
 680fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
 681    let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
 682    Arc::new(move |cx| {
 683        let settings = settings::get::<ThemeSettings>(cx);
 684        let theme = &settings.theme.editor;
 685        let style = theme.diagnostic_header.clone();
 686        let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
 687        let icon_width = cx.em_width * style.icon_width_factor;
 688        let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
 689            Svg::new("icons/circle_x_mark_12.svg")
 690                .with_color(theme.error_diagnostic.message.text.color)
 691        } else {
 692            Svg::new("icons/triangle_exclamation_12.svg")
 693                .with_color(theme.warning_diagnostic.message.text.color)
 694        };
 695
 696        Flex::row()
 697            .with_child(
 698                icon.constrained()
 699                    .with_width(icon_width)
 700                    .aligned()
 701                    .contained()
 702                    .with_margin_right(cx.gutter_padding),
 703            )
 704            .with_children(diagnostic.source.as_ref().map(|source| {
 705                Label::new(
 706                    format!("{source}: "),
 707                    style.source.label.clone().with_font_size(font_size),
 708                )
 709                .contained()
 710                .with_style(style.message.container)
 711                .aligned()
 712            }))
 713            .with_child(
 714                Label::new(
 715                    message.clone(),
 716                    style.message.label.clone().with_font_size(font_size),
 717                )
 718                .with_highlights(highlights.clone())
 719                .contained()
 720                .with_style(style.message.container)
 721                .aligned(),
 722            )
 723            .with_children(diagnostic.code.clone().map(|code| {
 724                Label::new(code, style.code.text.clone().with_font_size(font_size))
 725                    .contained()
 726                    .with_style(style.code.container)
 727                    .aligned()
 728            }))
 729            .contained()
 730            .with_style(style.container)
 731            .with_padding_left(cx.gutter_padding)
 732            .with_padding_right(cx.gutter_padding)
 733            .expanded()
 734            .into_any_named("diagnostic header")
 735    })
 736}
 737
 738pub(crate) fn render_summary<T: View>(
 739    summary: &DiagnosticSummary,
 740    text_style: &TextStyle,
 741    theme: &theme::ProjectDiagnostics,
 742) -> AnyElement<T> {
 743    if summary.error_count == 0 && summary.warning_count == 0 {
 744        Label::new("No problems", text_style.clone()).into_any()
 745    } else {
 746        let icon_width = theme.tab_icon_width;
 747        let icon_spacing = theme.tab_icon_spacing;
 748        let summary_spacing = theme.tab_summary_spacing;
 749        Flex::row()
 750            .with_child(
 751                Svg::new("icons/circle_x_mark_12.svg")
 752                    .with_color(text_style.color)
 753                    .constrained()
 754                    .with_width(icon_width)
 755                    .aligned()
 756                    .contained()
 757                    .with_margin_right(icon_spacing),
 758            )
 759            .with_child(
 760                Label::new(
 761                    summary.error_count.to_string(),
 762                    LabelStyle {
 763                        text: text_style.clone(),
 764                        highlight_text: None,
 765                    },
 766                )
 767                .aligned(),
 768            )
 769            .with_child(
 770                Svg::new("icons/triangle_exclamation_12.svg")
 771                    .with_color(text_style.color)
 772                    .constrained()
 773                    .with_width(icon_width)
 774                    .aligned()
 775                    .contained()
 776                    .with_margin_left(summary_spacing)
 777                    .with_margin_right(icon_spacing),
 778            )
 779            .with_child(
 780                Label::new(
 781                    summary.warning_count.to_string(),
 782                    LabelStyle {
 783                        text: text_style.clone(),
 784                        highlight_text: None,
 785                    },
 786                )
 787                .aligned(),
 788            )
 789            .into_any()
 790    }
 791}
 792
 793fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 794    lhs: &DiagnosticEntry<L>,
 795    rhs: &DiagnosticEntry<R>,
 796    snapshot: &language::BufferSnapshot,
 797) -> Ordering {
 798    lhs.range
 799        .start
 800        .to_offset(snapshot)
 801        .cmp(&rhs.range.start.to_offset(snapshot))
 802        .then_with(|| {
 803            lhs.range
 804                .end
 805                .to_offset(snapshot)
 806                .cmp(&rhs.range.end.to_offset(snapshot))
 807        })
 808        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 809}
 810
 811#[cfg(test)]
 812mod tests {
 813    use super::*;
 814    use editor::{
 815        display_map::{BlockContext, TransformBlock},
 816        DisplayPoint,
 817    };
 818    use gpui::{TestAppContext, WindowContext};
 819    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
 820    use project::FakeFs;
 821    use serde_json::json;
 822    use settings::SettingsStore;
 823    use unindent::Unindent as _;
 824
 825    #[gpui::test]
 826    async fn test_diagnostics(cx: &mut TestAppContext) {
 827        init_test(cx);
 828
 829        let fs = FakeFs::new(cx.background());
 830        fs.insert_tree(
 831            "/test",
 832            json!({
 833                "consts.rs": "
 834                    const a: i32 = 'a';
 835                    const b: i32 = c;
 836                "
 837                .unindent(),
 838
 839                "main.rs": "
 840                    fn main() {
 841                        let x = vec![];
 842                        let y = vec![];
 843                        a(x);
 844                        b(y);
 845                        // comment 1
 846                        // comment 2
 847                        c(y);
 848                        d(x);
 849                    }
 850                "
 851                .unindent(),
 852            }),
 853        )
 854        .await;
 855
 856        let language_server_id = LanguageServerId(0);
 857        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
 858        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 859
 860        // Create some diagnostics
 861        project.update(cx, |project, cx| {
 862            project
 863                .update_diagnostic_entries(
 864                    language_server_id,
 865                    PathBuf::from("/test/main.rs"),
 866                    None,
 867                    vec![
 868                        DiagnosticEntry {
 869                            range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
 870                            diagnostic: Diagnostic {
 871                                message:
 872                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 873                                        .to_string(),
 874                                severity: DiagnosticSeverity::INFORMATION,
 875                                is_primary: false,
 876                                is_disk_based: true,
 877                                group_id: 1,
 878                                ..Default::default()
 879                            },
 880                        },
 881                        DiagnosticEntry {
 882                            range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
 883                            diagnostic: Diagnostic {
 884                                message:
 885                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 886                                        .to_string(),
 887                                severity: DiagnosticSeverity::INFORMATION,
 888                                is_primary: false,
 889                                is_disk_based: true,
 890                                group_id: 0,
 891                                ..Default::default()
 892                            },
 893                        },
 894                        DiagnosticEntry {
 895                            range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
 896                            diagnostic: Diagnostic {
 897                                message: "value moved here".to_string(),
 898                                severity: DiagnosticSeverity::INFORMATION,
 899                                is_primary: false,
 900                                is_disk_based: true,
 901                                group_id: 1,
 902                                ..Default::default()
 903                            },
 904                        },
 905                        DiagnosticEntry {
 906                            range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
 907                            diagnostic: Diagnostic {
 908                                message: "value moved here".to_string(),
 909                                severity: DiagnosticSeverity::INFORMATION,
 910                                is_primary: false,
 911                                is_disk_based: true,
 912                                group_id: 0,
 913                                ..Default::default()
 914                            },
 915                        },
 916                        DiagnosticEntry {
 917                            range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
 918                            diagnostic: Diagnostic {
 919                                message: "use of moved value\nvalue used here after move".to_string(),
 920                                severity: DiagnosticSeverity::ERROR,
 921                                is_primary: true,
 922                                is_disk_based: true,
 923                                group_id: 0,
 924                                ..Default::default()
 925                            },
 926                        },
 927                        DiagnosticEntry {
 928                            range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
 929                            diagnostic: Diagnostic {
 930                                message: "use of moved value\nvalue used here after move".to_string(),
 931                                severity: DiagnosticSeverity::ERROR,
 932                                is_primary: true,
 933                                is_disk_based: true,
 934                                group_id: 1,
 935                                ..Default::default()
 936                            },
 937                        },
 938                    ],
 939                    cx,
 940                )
 941                .unwrap();
 942        });
 943
 944        // Open the project diagnostics view while there are already diagnostics.
 945        let view = cx.add_view(window_id, |cx| {
 946            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
 947        });
 948
 949        view.next_notification(cx).await;
 950        view.update(cx, |view, cx| {
 951            assert_eq!(
 952                editor_blocks(&view.editor, cx),
 953                [
 954                    (0, "path header block".into()),
 955                    (2, "diagnostic header".into()),
 956                    (15, "collapsed context".into()),
 957                    (16, "diagnostic header".into()),
 958                    (25, "collapsed context".into()),
 959                ]
 960            );
 961            assert_eq!(
 962                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
 963                concat!(
 964                    //
 965                    // main.rs
 966                    //
 967                    "\n", // filename
 968                    "\n", // padding
 969                    // diagnostic group 1
 970                    "\n", // primary message
 971                    "\n", // padding
 972                    "    let x = vec![];\n",
 973                    "    let y = vec![];\n",
 974                    "\n", // supporting diagnostic
 975                    "    a(x);\n",
 976                    "    b(y);\n",
 977                    "\n", // supporting diagnostic
 978                    "    // comment 1\n",
 979                    "    // comment 2\n",
 980                    "    c(y);\n",
 981                    "\n", // supporting diagnostic
 982                    "    d(x);\n",
 983                    "\n", // context ellipsis
 984                    // diagnostic group 2
 985                    "\n", // primary message
 986                    "\n", // padding
 987                    "fn main() {\n",
 988                    "    let x = vec![];\n",
 989                    "\n", // supporting diagnostic
 990                    "    let y = vec![];\n",
 991                    "    a(x);\n",
 992                    "\n", // supporting diagnostic
 993                    "    b(y);\n",
 994                    "\n", // context ellipsis
 995                    "    c(y);\n",
 996                    "    d(x);\n",
 997                    "\n", // supporting diagnostic
 998                    "}"
 999                )
1000            );
1001
1002            // Cursor is at the first diagnostic
1003            view.editor.update(cx, |editor, cx| {
1004                assert_eq!(
1005                    editor.selections.display_ranges(cx),
1006                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
1007                );
1008            });
1009        });
1010
1011        // Diagnostics are added for another earlier path.
1012        project.update(cx, |project, cx| {
1013            project.disk_based_diagnostics_started(language_server_id, cx);
1014            project
1015                .update_diagnostic_entries(
1016                    language_server_id,
1017                    PathBuf::from("/test/consts.rs"),
1018                    None,
1019                    vec![DiagnosticEntry {
1020                        range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
1021                        diagnostic: Diagnostic {
1022                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1023                            severity: DiagnosticSeverity::ERROR,
1024                            is_primary: true,
1025                            is_disk_based: true,
1026                            group_id: 0,
1027                            ..Default::default()
1028                        },
1029                    }],
1030                    cx,
1031                )
1032                .unwrap();
1033            project.disk_based_diagnostics_finished(language_server_id, cx);
1034        });
1035
1036        view.next_notification(cx).await;
1037        view.update(cx, |view, cx| {
1038            assert_eq!(
1039                editor_blocks(&view.editor, cx),
1040                [
1041                    (0, "path header block".into()),
1042                    (2, "diagnostic header".into()),
1043                    (7, "path header block".into()),
1044                    (9, "diagnostic header".into()),
1045                    (22, "collapsed context".into()),
1046                    (23, "diagnostic header".into()),
1047                    (32, "collapsed context".into()),
1048                ]
1049            );
1050            assert_eq!(
1051                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1052                concat!(
1053                    //
1054                    // consts.rs
1055                    //
1056                    "\n", // filename
1057                    "\n", // padding
1058                    // diagnostic group 1
1059                    "\n", // primary message
1060                    "\n", // padding
1061                    "const a: i32 = 'a';\n",
1062                    "\n", // supporting diagnostic
1063                    "const b: i32 = c;\n",
1064                    //
1065                    // main.rs
1066                    //
1067                    "\n", // filename
1068                    "\n", // padding
1069                    // diagnostic group 1
1070                    "\n", // primary message
1071                    "\n", // padding
1072                    "    let x = vec![];\n",
1073                    "    let y = vec![];\n",
1074                    "\n", // supporting diagnostic
1075                    "    a(x);\n",
1076                    "    b(y);\n",
1077                    "\n", // supporting diagnostic
1078                    "    // comment 1\n",
1079                    "    // comment 2\n",
1080                    "    c(y);\n",
1081                    "\n", // supporting diagnostic
1082                    "    d(x);\n",
1083                    "\n", // collapsed context
1084                    // diagnostic group 2
1085                    "\n", // primary message
1086                    "\n", // filename
1087                    "fn main() {\n",
1088                    "    let x = vec![];\n",
1089                    "\n", // supporting diagnostic
1090                    "    let y = vec![];\n",
1091                    "    a(x);\n",
1092                    "\n", // supporting diagnostic
1093                    "    b(y);\n",
1094                    "\n", // context ellipsis
1095                    "    c(y);\n",
1096                    "    d(x);\n",
1097                    "\n", // supporting diagnostic
1098                    "}"
1099                )
1100            );
1101
1102            // Cursor keeps its position.
1103            view.editor.update(cx, |editor, cx| {
1104                assert_eq!(
1105                    editor.selections.display_ranges(cx),
1106                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1107                );
1108            });
1109        });
1110
1111        // Diagnostics are added to the first path
1112        project.update(cx, |project, cx| {
1113            project.disk_based_diagnostics_started(language_server_id, cx);
1114            project
1115                .update_diagnostic_entries(
1116                    language_server_id,
1117                    PathBuf::from("/test/consts.rs"),
1118                    None,
1119                    vec![
1120                        DiagnosticEntry {
1121                            range: Unclipped(PointUtf16::new(0, 15))
1122                                ..Unclipped(PointUtf16::new(0, 15)),
1123                            diagnostic: Diagnostic {
1124                                message: "mismatched types\nexpected `usize`, found `char`"
1125                                    .to_string(),
1126                                severity: DiagnosticSeverity::ERROR,
1127                                is_primary: true,
1128                                is_disk_based: true,
1129                                group_id: 0,
1130                                ..Default::default()
1131                            },
1132                        },
1133                        DiagnosticEntry {
1134                            range: Unclipped(PointUtf16::new(1, 15))
1135                                ..Unclipped(PointUtf16::new(1, 15)),
1136                            diagnostic: Diagnostic {
1137                                message: "unresolved name `c`".to_string(),
1138                                severity: DiagnosticSeverity::ERROR,
1139                                is_primary: true,
1140                                is_disk_based: true,
1141                                group_id: 1,
1142                                ..Default::default()
1143                            },
1144                        },
1145                    ],
1146                    cx,
1147                )
1148                .unwrap();
1149            project.disk_based_diagnostics_finished(language_server_id, cx);
1150        });
1151
1152        view.next_notification(cx).await;
1153        view.update(cx, |view, cx| {
1154            assert_eq!(
1155                editor_blocks(&view.editor, cx),
1156                [
1157                    (0, "path header block".into()),
1158                    (2, "diagnostic header".into()),
1159                    (7, "collapsed context".into()),
1160                    (8, "diagnostic header".into()),
1161                    (13, "path header block".into()),
1162                    (15, "diagnostic header".into()),
1163                    (28, "collapsed context".into()),
1164                    (29, "diagnostic header".into()),
1165                    (38, "collapsed context".into()),
1166                ]
1167            );
1168            assert_eq!(
1169                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1170                concat!(
1171                    //
1172                    // consts.rs
1173                    //
1174                    "\n", // filename
1175                    "\n", // padding
1176                    // diagnostic group 1
1177                    "\n", // primary message
1178                    "\n", // padding
1179                    "const a: i32 = 'a';\n",
1180                    "\n", // supporting diagnostic
1181                    "const b: i32 = c;\n",
1182                    "\n", // context ellipsis
1183                    // diagnostic group 2
1184                    "\n", // primary message
1185                    "\n", // padding
1186                    "const a: i32 = 'a';\n",
1187                    "const b: i32 = c;\n",
1188                    "\n", // supporting diagnostic
1189                    //
1190                    // main.rs
1191                    //
1192                    "\n", // filename
1193                    "\n", // padding
1194                    // diagnostic group 1
1195                    "\n", // primary message
1196                    "\n", // padding
1197                    "    let x = vec![];\n",
1198                    "    let y = vec![];\n",
1199                    "\n", // supporting diagnostic
1200                    "    a(x);\n",
1201                    "    b(y);\n",
1202                    "\n", // supporting diagnostic
1203                    "    // comment 1\n",
1204                    "    // comment 2\n",
1205                    "    c(y);\n",
1206                    "\n", // supporting diagnostic
1207                    "    d(x);\n",
1208                    "\n", // context ellipsis
1209                    // diagnostic group 2
1210                    "\n", // primary message
1211                    "\n", // filename
1212                    "fn main() {\n",
1213                    "    let x = vec![];\n",
1214                    "\n", // supporting diagnostic
1215                    "    let y = vec![];\n",
1216                    "    a(x);\n",
1217                    "\n", // supporting diagnostic
1218                    "    b(y);\n",
1219                    "\n", // context ellipsis
1220                    "    c(y);\n",
1221                    "    d(x);\n",
1222                    "\n", // supporting diagnostic
1223                    "}"
1224                )
1225            );
1226        });
1227    }
1228
1229    #[gpui::test]
1230    async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1231        init_test(cx);
1232
1233        let fs = FakeFs::new(cx.background());
1234        fs.insert_tree(
1235            "/test",
1236            json!({
1237                "main.js": "
1238                    a();
1239                    b();
1240                    c();
1241                    d();
1242                    e();
1243                ".unindent()
1244            }),
1245        )
1246        .await;
1247
1248        let server_id_1 = LanguageServerId(100);
1249        let server_id_2 = LanguageServerId(101);
1250        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1251        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1252
1253        let view = cx.add_view(window_id, |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                .filter_map(|(row, block)| {
1513                    let name = match block {
1514                        TransformBlock::Custom(block) => block
1515                            .render(&mut BlockContext {
1516                                view_context: cx,
1517                                anchor_x: 0.,
1518                                scroll_x: 0.,
1519                                gutter_padding: 0.,
1520                                gutter_width: 0.,
1521                                line_height: 0.,
1522                                em_width: 0.,
1523                            })
1524                            .name()?
1525                            .to_string(),
1526                        TransformBlock::ExcerptHeader {
1527                            starts_new_buffer, ..
1528                        } => {
1529                            if *starts_new_buffer {
1530                                "path header block".to_string()
1531                            } else {
1532                                "collapsed context".to_string()
1533                            }
1534                        }
1535                    };
1536
1537                    Some((row, name))
1538                })
1539                .collect()
1540        })
1541    }
1542}