diagnostics.rs

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