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        let window_id = window.window_id();
 861
 862        // Create some diagnostics
 863        project.update(cx, |project, cx| {
 864            project
 865                .update_diagnostic_entries(
 866                    language_server_id,
 867                    PathBuf::from("/test/main.rs"),
 868                    None,
 869                    vec![
 870                        DiagnosticEntry {
 871                            range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
 872                            diagnostic: Diagnostic {
 873                                message:
 874                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 875                                        .to_string(),
 876                                severity: DiagnosticSeverity::INFORMATION,
 877                                is_primary: false,
 878                                is_disk_based: true,
 879                                group_id: 1,
 880                                ..Default::default()
 881                            },
 882                        },
 883                        DiagnosticEntry {
 884                            range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
 885                            diagnostic: Diagnostic {
 886                                message:
 887                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 888                                        .to_string(),
 889                                severity: DiagnosticSeverity::INFORMATION,
 890                                is_primary: false,
 891                                is_disk_based: true,
 892                                group_id: 0,
 893                                ..Default::default()
 894                            },
 895                        },
 896                        DiagnosticEntry {
 897                            range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
 898                            diagnostic: Diagnostic {
 899                                message: "value moved here".to_string(),
 900                                severity: DiagnosticSeverity::INFORMATION,
 901                                is_primary: false,
 902                                is_disk_based: true,
 903                                group_id: 1,
 904                                ..Default::default()
 905                            },
 906                        },
 907                        DiagnosticEntry {
 908                            range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
 909                            diagnostic: Diagnostic {
 910                                message: "value moved here".to_string(),
 911                                severity: DiagnosticSeverity::INFORMATION,
 912                                is_primary: false,
 913                                is_disk_based: true,
 914                                group_id: 0,
 915                                ..Default::default()
 916                            },
 917                        },
 918                        DiagnosticEntry {
 919                            range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
 920                            diagnostic: Diagnostic {
 921                                message: "use of moved value\nvalue used here after move".to_string(),
 922                                severity: DiagnosticSeverity::ERROR,
 923                                is_primary: true,
 924                                is_disk_based: true,
 925                                group_id: 0,
 926                                ..Default::default()
 927                            },
 928                        },
 929                        DiagnosticEntry {
 930                            range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
 931                            diagnostic: Diagnostic {
 932                                message: "use of moved value\nvalue used here after move".to_string(),
 933                                severity: DiagnosticSeverity::ERROR,
 934                                is_primary: true,
 935                                is_disk_based: true,
 936                                group_id: 1,
 937                                ..Default::default()
 938                            },
 939                        },
 940                    ],
 941                    cx,
 942                )
 943                .unwrap();
 944        });
 945
 946        // Open the project diagnostics view while there are already diagnostics.
 947        let view = cx.add_view(window_id, |cx| {
 948            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
 949        });
 950
 951        view.next_notification(cx).await;
 952        view.update(cx, |view, cx| {
 953            assert_eq!(
 954                editor_blocks(&view.editor, cx),
 955                [
 956                    (0, "path header block".into()),
 957                    (2, "diagnostic header".into()),
 958                    (15, "collapsed context".into()),
 959                    (16, "diagnostic header".into()),
 960                    (25, "collapsed context".into()),
 961                ]
 962            );
 963            assert_eq!(
 964                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
 965                concat!(
 966                    //
 967                    // main.rs
 968                    //
 969                    "\n", // filename
 970                    "\n", // padding
 971                    // diagnostic group 1
 972                    "\n", // primary message
 973                    "\n", // padding
 974                    "    let x = vec![];\n",
 975                    "    let y = vec![];\n",
 976                    "\n", // supporting diagnostic
 977                    "    a(x);\n",
 978                    "    b(y);\n",
 979                    "\n", // supporting diagnostic
 980                    "    // comment 1\n",
 981                    "    // comment 2\n",
 982                    "    c(y);\n",
 983                    "\n", // supporting diagnostic
 984                    "    d(x);\n",
 985                    "\n", // context ellipsis
 986                    // diagnostic group 2
 987                    "\n", // primary message
 988                    "\n", // padding
 989                    "fn main() {\n",
 990                    "    let x = vec![];\n",
 991                    "\n", // supporting diagnostic
 992                    "    let y = vec![];\n",
 993                    "    a(x);\n",
 994                    "\n", // supporting diagnostic
 995                    "    b(y);\n",
 996                    "\n", // context ellipsis
 997                    "    c(y);\n",
 998                    "    d(x);\n",
 999                    "\n", // supporting diagnostic
1000                    "}"
1001                )
1002            );
1003
1004            // Cursor is at the first diagnostic
1005            view.editor.update(cx, |editor, cx| {
1006                assert_eq!(
1007                    editor.selections.display_ranges(cx),
1008                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
1009                );
1010            });
1011        });
1012
1013        // Diagnostics are added for another earlier path.
1014        project.update(cx, |project, cx| {
1015            project.disk_based_diagnostics_started(language_server_id, cx);
1016            project
1017                .update_diagnostic_entries(
1018                    language_server_id,
1019                    PathBuf::from("/test/consts.rs"),
1020                    None,
1021                    vec![DiagnosticEntry {
1022                        range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
1023                        diagnostic: Diagnostic {
1024                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1025                            severity: DiagnosticSeverity::ERROR,
1026                            is_primary: true,
1027                            is_disk_based: true,
1028                            group_id: 0,
1029                            ..Default::default()
1030                        },
1031                    }],
1032                    cx,
1033                )
1034                .unwrap();
1035            project.disk_based_diagnostics_finished(language_server_id, cx);
1036        });
1037
1038        view.next_notification(cx).await;
1039        view.update(cx, |view, cx| {
1040            assert_eq!(
1041                editor_blocks(&view.editor, cx),
1042                [
1043                    (0, "path header block".into()),
1044                    (2, "diagnostic header".into()),
1045                    (7, "path header block".into()),
1046                    (9, "diagnostic header".into()),
1047                    (22, "collapsed context".into()),
1048                    (23, "diagnostic header".into()),
1049                    (32, "collapsed context".into()),
1050                ]
1051            );
1052            assert_eq!(
1053                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1054                concat!(
1055                    //
1056                    // consts.rs
1057                    //
1058                    "\n", // filename
1059                    "\n", // padding
1060                    // diagnostic group 1
1061                    "\n", // primary message
1062                    "\n", // padding
1063                    "const a: i32 = 'a';\n",
1064                    "\n", // supporting diagnostic
1065                    "const b: i32 = c;\n",
1066                    //
1067                    // main.rs
1068                    //
1069                    "\n", // filename
1070                    "\n", // padding
1071                    // diagnostic group 1
1072                    "\n", // primary message
1073                    "\n", // padding
1074                    "    let x = vec![];\n",
1075                    "    let y = vec![];\n",
1076                    "\n", // supporting diagnostic
1077                    "    a(x);\n",
1078                    "    b(y);\n",
1079                    "\n", // supporting diagnostic
1080                    "    // comment 1\n",
1081                    "    // comment 2\n",
1082                    "    c(y);\n",
1083                    "\n", // supporting diagnostic
1084                    "    d(x);\n",
1085                    "\n", // collapsed context
1086                    // diagnostic group 2
1087                    "\n", // primary message
1088                    "\n", // filename
1089                    "fn main() {\n",
1090                    "    let x = vec![];\n",
1091                    "\n", // supporting diagnostic
1092                    "    let y = vec![];\n",
1093                    "    a(x);\n",
1094                    "\n", // supporting diagnostic
1095                    "    b(y);\n",
1096                    "\n", // context ellipsis
1097                    "    c(y);\n",
1098                    "    d(x);\n",
1099                    "\n", // supporting diagnostic
1100                    "}"
1101                )
1102            );
1103
1104            // Cursor keeps its position.
1105            view.editor.update(cx, |editor, cx| {
1106                assert_eq!(
1107                    editor.selections.display_ranges(cx),
1108                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1109                );
1110            });
1111        });
1112
1113        // Diagnostics are added to the first path
1114        project.update(cx, |project, cx| {
1115            project.disk_based_diagnostics_started(language_server_id, cx);
1116            project
1117                .update_diagnostic_entries(
1118                    language_server_id,
1119                    PathBuf::from("/test/consts.rs"),
1120                    None,
1121                    vec![
1122                        DiagnosticEntry {
1123                            range: Unclipped(PointUtf16::new(0, 15))
1124                                ..Unclipped(PointUtf16::new(0, 15)),
1125                            diagnostic: Diagnostic {
1126                                message: "mismatched types\nexpected `usize`, found `char`"
1127                                    .to_string(),
1128                                severity: DiagnosticSeverity::ERROR,
1129                                is_primary: true,
1130                                is_disk_based: true,
1131                                group_id: 0,
1132                                ..Default::default()
1133                            },
1134                        },
1135                        DiagnosticEntry {
1136                            range: Unclipped(PointUtf16::new(1, 15))
1137                                ..Unclipped(PointUtf16::new(1, 15)),
1138                            diagnostic: Diagnostic {
1139                                message: "unresolved name `c`".to_string(),
1140                                severity: DiagnosticSeverity::ERROR,
1141                                is_primary: true,
1142                                is_disk_based: true,
1143                                group_id: 1,
1144                                ..Default::default()
1145                            },
1146                        },
1147                    ],
1148                    cx,
1149                )
1150                .unwrap();
1151            project.disk_based_diagnostics_finished(language_server_id, cx);
1152        });
1153
1154        view.next_notification(cx).await;
1155        view.update(cx, |view, cx| {
1156            assert_eq!(
1157                editor_blocks(&view.editor, cx),
1158                [
1159                    (0, "path header block".into()),
1160                    (2, "diagnostic header".into()),
1161                    (7, "collapsed context".into()),
1162                    (8, "diagnostic header".into()),
1163                    (13, "path header block".into()),
1164                    (15, "diagnostic header".into()),
1165                    (28, "collapsed context".into()),
1166                    (29, "diagnostic header".into()),
1167                    (38, "collapsed context".into()),
1168                ]
1169            );
1170            assert_eq!(
1171                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1172                concat!(
1173                    //
1174                    // consts.rs
1175                    //
1176                    "\n", // filename
1177                    "\n", // padding
1178                    // diagnostic group 1
1179                    "\n", // primary message
1180                    "\n", // padding
1181                    "const a: i32 = 'a';\n",
1182                    "\n", // supporting diagnostic
1183                    "const b: i32 = c;\n",
1184                    "\n", // context ellipsis
1185                    // diagnostic group 2
1186                    "\n", // primary message
1187                    "\n", // padding
1188                    "const a: i32 = 'a';\n",
1189                    "const b: i32 = c;\n",
1190                    "\n", // supporting diagnostic
1191                    //
1192                    // main.rs
1193                    //
1194                    "\n", // filename
1195                    "\n", // padding
1196                    // diagnostic group 1
1197                    "\n", // primary message
1198                    "\n", // padding
1199                    "    let x = vec![];\n",
1200                    "    let y = vec![];\n",
1201                    "\n", // supporting diagnostic
1202                    "    a(x);\n",
1203                    "    b(y);\n",
1204                    "\n", // supporting diagnostic
1205                    "    // comment 1\n",
1206                    "    // comment 2\n",
1207                    "    c(y);\n",
1208                    "\n", // supporting diagnostic
1209                    "    d(x);\n",
1210                    "\n", // context ellipsis
1211                    // diagnostic group 2
1212                    "\n", // primary message
1213                    "\n", // filename
1214                    "fn main() {\n",
1215                    "    let x = vec![];\n",
1216                    "\n", // supporting diagnostic
1217                    "    let y = vec![];\n",
1218                    "    a(x);\n",
1219                    "\n", // supporting diagnostic
1220                    "    b(y);\n",
1221                    "\n", // context ellipsis
1222                    "    c(y);\n",
1223                    "    d(x);\n",
1224                    "\n", // supporting diagnostic
1225                    "}"
1226                )
1227            );
1228        });
1229    }
1230
1231    #[gpui::test]
1232    async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
1233        init_test(cx);
1234
1235        let fs = FakeFs::new(cx.background());
1236        fs.insert_tree(
1237            "/test",
1238            json!({
1239                "main.js": "
1240                    a();
1241                    b();
1242                    c();
1243                    d();
1244                    e();
1245                ".unindent()
1246            }),
1247        )
1248        .await;
1249
1250        let server_id_1 = LanguageServerId(100);
1251        let server_id_2 = LanguageServerId(101);
1252        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
1253        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
1254        let workspace = window.root(cx);
1255        let window_id = window.window_id();
1256
1257        let view = cx.add_view(window_id, |cx| {
1258            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
1259        });
1260
1261        // Two language servers start updating diagnostics
1262        project.update(cx, |project, cx| {
1263            project.disk_based_diagnostics_started(server_id_1, cx);
1264            project.disk_based_diagnostics_started(server_id_2, cx);
1265            project
1266                .update_diagnostic_entries(
1267                    server_id_1,
1268                    PathBuf::from("/test/main.js"),
1269                    None,
1270                    vec![DiagnosticEntry {
1271                        range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
1272                        diagnostic: Diagnostic {
1273                            message: "error 1".to_string(),
1274                            severity: DiagnosticSeverity::WARNING,
1275                            is_primary: true,
1276                            is_disk_based: true,
1277                            group_id: 1,
1278                            ..Default::default()
1279                        },
1280                    }],
1281                    cx,
1282                )
1283                .unwrap();
1284            project
1285                .update_diagnostic_entries(
1286                    server_id_2,
1287                    PathBuf::from("/test/main.js"),
1288                    None,
1289                    vec![DiagnosticEntry {
1290                        range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
1291                        diagnostic: Diagnostic {
1292                            message: "warning 1".to_string(),
1293                            severity: DiagnosticSeverity::ERROR,
1294                            is_primary: true,
1295                            is_disk_based: true,
1296                            group_id: 2,
1297                            ..Default::default()
1298                        },
1299                    }],
1300                    cx,
1301                )
1302                .unwrap();
1303        });
1304
1305        // The first language server finishes
1306        project.update(cx, |project, cx| {
1307            project.disk_based_diagnostics_finished(server_id_1, cx);
1308        });
1309
1310        // Only the first language server's diagnostics are shown.
1311        cx.foreground().run_until_parked();
1312        view.update(cx, |view, cx| {
1313            assert_eq!(
1314                editor_blocks(&view.editor, cx),
1315                [
1316                    (0, "path header block".into()),
1317                    (2, "diagnostic header".into()),
1318                ]
1319            );
1320            assert_eq!(
1321                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1322                concat!(
1323                    "\n", // filename
1324                    "\n", // padding
1325                    // diagnostic group 1
1326                    "\n",     // primary message
1327                    "\n",     // padding
1328                    "a();\n", //
1329                    "b();",
1330                )
1331            );
1332        });
1333
1334        // The second language server finishes
1335        project.update(cx, |project, cx| {
1336            project.disk_based_diagnostics_finished(server_id_2, cx);
1337        });
1338
1339        // Both language server's diagnostics are shown.
1340        cx.foreground().run_until_parked();
1341        view.update(cx, |view, cx| {
1342            assert_eq!(
1343                editor_blocks(&view.editor, cx),
1344                [
1345                    (0, "path header block".into()),
1346                    (2, "diagnostic header".into()),
1347                    (6, "collapsed context".into()),
1348                    (7, "diagnostic header".into()),
1349                ]
1350            );
1351            assert_eq!(
1352                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1353                concat!(
1354                    "\n", // filename
1355                    "\n", // padding
1356                    // diagnostic group 1
1357                    "\n",     // primary message
1358                    "\n",     // padding
1359                    "a();\n", // location
1360                    "b();\n", //
1361                    "\n",     // collapsed context
1362                    // diagnostic group 2
1363                    "\n",     // primary message
1364                    "\n",     // padding
1365                    "a();\n", // context
1366                    "b();\n", //
1367                    "c();",   // context
1368                )
1369            );
1370        });
1371
1372        // Both language servers start updating diagnostics, and the first server finishes.
1373        project.update(cx, |project, cx| {
1374            project.disk_based_diagnostics_started(server_id_1, cx);
1375            project.disk_based_diagnostics_started(server_id_2, cx);
1376            project
1377                .update_diagnostic_entries(
1378                    server_id_1,
1379                    PathBuf::from("/test/main.js"),
1380                    None,
1381                    vec![DiagnosticEntry {
1382                        range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
1383                        diagnostic: Diagnostic {
1384                            message: "warning 2".to_string(),
1385                            severity: DiagnosticSeverity::WARNING,
1386                            is_primary: true,
1387                            is_disk_based: true,
1388                            group_id: 1,
1389                            ..Default::default()
1390                        },
1391                    }],
1392                    cx,
1393                )
1394                .unwrap();
1395            project
1396                .update_diagnostic_entries(
1397                    server_id_2,
1398                    PathBuf::from("/test/main.rs"),
1399                    None,
1400                    vec![],
1401                    cx,
1402                )
1403                .unwrap();
1404            project.disk_based_diagnostics_finished(server_id_1, cx);
1405        });
1406
1407        // Only the first language server's diagnostics are updated.
1408        cx.foreground().run_until_parked();
1409        view.update(cx, |view, cx| {
1410            assert_eq!(
1411                editor_blocks(&view.editor, cx),
1412                [
1413                    (0, "path header block".into()),
1414                    (2, "diagnostic header".into()),
1415                    (7, "collapsed context".into()),
1416                    (8, "diagnostic header".into()),
1417                ]
1418            );
1419            assert_eq!(
1420                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1421                concat!(
1422                    "\n", // filename
1423                    "\n", // padding
1424                    // diagnostic group 1
1425                    "\n",     // primary message
1426                    "\n",     // padding
1427                    "a();\n", // location
1428                    "b();\n", //
1429                    "c();\n", // context
1430                    "\n",     // collapsed context
1431                    // diagnostic group 2
1432                    "\n",     // primary message
1433                    "\n",     // padding
1434                    "b();\n", // context
1435                    "c();\n", //
1436                    "d();",   // context
1437                )
1438            );
1439        });
1440
1441        // The second language server finishes.
1442        project.update(cx, |project, cx| {
1443            project
1444                .update_diagnostic_entries(
1445                    server_id_2,
1446                    PathBuf::from("/test/main.js"),
1447                    None,
1448                    vec![DiagnosticEntry {
1449                        range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
1450                        diagnostic: Diagnostic {
1451                            message: "warning 2".to_string(),
1452                            severity: DiagnosticSeverity::WARNING,
1453                            is_primary: true,
1454                            is_disk_based: true,
1455                            group_id: 1,
1456                            ..Default::default()
1457                        },
1458                    }],
1459                    cx,
1460                )
1461                .unwrap();
1462            project.disk_based_diagnostics_finished(server_id_2, cx);
1463        });
1464
1465        // Both language servers' diagnostics are updated.
1466        cx.foreground().run_until_parked();
1467        view.update(cx, |view, cx| {
1468            assert_eq!(
1469                editor_blocks(&view.editor, cx),
1470                [
1471                    (0, "path header block".into()),
1472                    (2, "diagnostic header".into()),
1473                    (7, "collapsed context".into()),
1474                    (8, "diagnostic header".into()),
1475                ]
1476            );
1477            assert_eq!(
1478                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1479                concat!(
1480                    "\n", // filename
1481                    "\n", // padding
1482                    // diagnostic group 1
1483                    "\n",     // primary message
1484                    "\n",     // padding
1485                    "b();\n", // location
1486                    "c();\n", //
1487                    "d();\n", // context
1488                    "\n",     // collapsed context
1489                    // diagnostic group 2
1490                    "\n",     // primary message
1491                    "\n",     // padding
1492                    "c();\n", // context
1493                    "d();\n", //
1494                    "e();",   // context
1495                )
1496            );
1497        });
1498    }
1499
1500    fn init_test(cx: &mut TestAppContext) {
1501        cx.update(|cx| {
1502            cx.set_global(SettingsStore::test(cx));
1503            theme::init((), cx);
1504            language::init(cx);
1505            client::init_settings(cx);
1506            workspace::init_settings(cx);
1507            Project::init_settings(cx);
1508        });
1509    }
1510
1511    fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
1512        editor.update(cx, |editor, cx| {
1513            let snapshot = editor.snapshot(cx);
1514            snapshot
1515                .blocks_in_range(0..snapshot.max_point().row())
1516                .enumerate()
1517                .filter_map(|(ix, (row, block))| {
1518                    let name = match block {
1519                        TransformBlock::Custom(block) => block
1520                            .render(&mut BlockContext {
1521                                view_context: cx,
1522                                anchor_x: 0.,
1523                                scroll_x: 0.,
1524                                gutter_padding: 0.,
1525                                gutter_width: 0.,
1526                                line_height: 0.,
1527                                em_width: 0.,
1528                                block_id: ix,
1529                            })
1530                            .name()?
1531                            .to_string(),
1532                        TransformBlock::ExcerptHeader {
1533                            starts_new_buffer, ..
1534                        } => {
1535                            if *starts_new_buffer {
1536                                "path header block".to_string()
1537                            } else {
1538                                "collapsed context".to_string()
1539                            }
1540                        }
1541                    };
1542
1543                    Some((row, name))
1544                })
1545                .collect()
1546        })
1547    }
1548}