diagnostics.rs

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