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