diagnostics.rs

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