diagnostics.rs

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