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