diagnostics.rs

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