diagnostics.rs

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