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