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