diagnostics.rs

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