diagnostics.rs

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