diagnostics.rs

   1pub mod items;
   2
   3use anyhow::Result;
   4use collections::{BTreeMap, HashSet};
   5use editor::{
   6    diagnostic_block_renderer,
   7    display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
   8    highlight_diagnostic_message, Autoscroll, Editor, ExcerptId, ExcerptRange, MultiBuffer,
   9    ToOffset,
  10};
  11use gpui::{
  12    actions, elements::*, fonts::TextStyle, impl_internal_actions, serde_json, AnyViewHandle,
  13    AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext,
  14    ViewHandle, WeakViewHandle,
  15};
  16use language::{
  17    Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Selection, SelectionGoal,
  18};
  19use project::{DiagnosticSummary, Project, ProjectPath};
  20use rope::point::Point;
  21use serde_json::json;
  22use settings::Settings;
  23use smallvec::SmallVec;
  24use std::{
  25    any::{Any, TypeId},
  26    cmp::Ordering,
  27    ops::Range,
  28    path::PathBuf,
  29    sync::Arc,
  30};
  31use util::TryFutureExt;
  32use workspace::{ItemHandle as _, ItemNavHistory, Workspace};
  33
  34actions!(diagnostics, [Deploy]);
  35
  36impl_internal_actions!(diagnostics, [Jump]);
  37
  38const CONTEXT_LINE_COUNT: u32 = 1;
  39
  40pub fn init(cx: &mut MutableAppContext) {
  41    cx.add_action(ProjectDiagnosticsEditor::deploy);
  42    items::init(cx);
  43}
  44
  45type Event = editor::Event;
  46
  47struct ProjectDiagnosticsEditor {
  48    project: ModelHandle<Project>,
  49    workspace: WeakViewHandle<Workspace>,
  50    editor: ViewHandle<Editor>,
  51    summary: DiagnosticSummary,
  52    excerpts: ModelHandle<MultiBuffer>,
  53    path_states: Vec<PathState>,
  54    paths_to_update: BTreeMap<ProjectPath, usize>,
  55}
  56
  57struct PathState {
  58    path: ProjectPath,
  59    diagnostic_groups: Vec<DiagnosticGroupState>,
  60}
  61
  62#[derive(Clone, Debug, PartialEq)]
  63struct Jump {
  64    path: ProjectPath,
  65    position: Point,
  66    anchor: Anchor,
  67}
  68
  69struct DiagnosticGroupState {
  70    primary_diagnostic: DiagnosticEntry<language::Anchor>,
  71    primary_excerpt_ix: usize,
  72    excerpts: Vec<ExcerptId>,
  73    blocks: HashSet<BlockId>,
  74    block_count: usize,
  75}
  76
  77impl Entity for ProjectDiagnosticsEditor {
  78    type Event = Event;
  79}
  80
  81impl View for ProjectDiagnosticsEditor {
  82    fn ui_name() -> &'static str {
  83        "ProjectDiagnosticsEditor"
  84    }
  85
  86    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
  87        if self.path_states.is_empty() {
  88            let theme = &cx.global::<Settings>().theme.project_diagnostics;
  89            Label::new(
  90                "No problems in workspace".to_string(),
  91                theme.empty_message.clone(),
  92            )
  93            .aligned()
  94            .contained()
  95            .with_style(theme.container)
  96            .boxed()
  97        } else {
  98            ChildView::new(&self.editor).boxed()
  99        }
 100    }
 101
 102    fn on_focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
 103        if !self.path_states.is_empty() {
 104            cx.focus(&self.editor);
 105        }
 106    }
 107
 108    fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
 109        let project = self.project.read(cx);
 110        json!({
 111            "project": json!({
 112                "language_servers": project.language_server_statuses().collect::<Vec<_>>(),
 113                "summary": project.diagnostic_summary(cx),
 114            }),
 115            "summary": self.summary,
 116            "paths_to_update": self.paths_to_update.iter().map(|(path, server_id)|
 117                (path.path.to_string_lossy(), server_id)
 118            ).collect::<Vec<_>>(),
 119            "paths_states": self.path_states.iter().map(|state|
 120                json!({
 121                    "path": state.path.path.to_string_lossy(),
 122                    "groups": state.diagnostic_groups.iter().map(|group|
 123                        json!({
 124                            "block_count": group.blocks.len(),
 125                            "excerpt_count": group.excerpts.len(),
 126                        })
 127                    ).collect::<Vec<_>>(),
 128                })
 129            ).collect::<Vec<_>>(),
 130        })
 131    }
 132}
 133
 134impl ProjectDiagnosticsEditor {
 135    fn new(
 136        project_handle: ModelHandle<Project>,
 137        workspace: WeakViewHandle<Workspace>,
 138        cx: &mut ViewContext<Self>,
 139    ) -> Self {
 140        cx.subscribe(&project_handle, |this, _, event, cx| match event {
 141            project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
 142                this.update_excerpts(Some(*language_server_id), cx);
 143                this.update_title(cx);
 144            }
 145            project::Event::DiagnosticsUpdated {
 146                language_server_id,
 147                path,
 148            } => {
 149                this.paths_to_update
 150                    .insert(path.clone(), *language_server_id);
 151            }
 152            _ => {}
 153        })
 154        .detach();
 155
 156        let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
 157        let editor = cx.add_view(|cx| {
 158            let mut editor =
 159                Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
 160            editor.set_vertical_scroll_margin(5, cx);
 161            editor
 162        });
 163        cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
 164            .detach();
 165
 166        let project = project_handle.read(cx);
 167        let paths_to_update = project
 168            .diagnostic_summaries(cx)
 169            .map(|e| (e.0, e.1.language_server_id))
 170            .collect();
 171        let summary = project.diagnostic_summary(cx);
 172        let mut this = Self {
 173            project: project_handle,
 174            summary,
 175            workspace,
 176            excerpts,
 177            editor,
 178            path_states: Default::default(),
 179            paths_to_update,
 180        };
 181        this.update_excerpts(None, cx);
 182        this
 183    }
 184
 185    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
 186        if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
 187            workspace.activate_item(&existing, cx);
 188        } else {
 189            let workspace_handle = cx.weak_handle();
 190            let diagnostics = cx.add_view(|cx| {
 191                ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
 192            });
 193            workspace.add_item(Box::new(diagnostics), cx);
 194        }
 195    }
 196
 197    fn update_excerpts(&mut self, language_server_id: Option<usize>, cx: &mut ViewContext<Self>) {
 198        let mut paths = Vec::new();
 199        self.paths_to_update.retain(|path, server_id| {
 200            if language_server_id
 201                .map_or(true, |language_server_id| language_server_id == *server_id)
 202            {
 203                paths.push(path.clone());
 204                false
 205            } else {
 206                true
 207            }
 208        });
 209        let project = self.project.clone();
 210        cx.spawn(|this, mut cx| {
 211            async move {
 212                for path in paths {
 213                    let buffer = project
 214                        .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
 215                        .await?;
 216                    this.update(&mut cx, |this, cx| this.populate_excerpts(path, buffer, cx))
 217                }
 218                Result::<_, anyhow::Error>::Ok(())
 219            }
 220            .log_err()
 221        })
 222        .detach();
 223    }
 224
 225    fn populate_excerpts(
 226        &mut self,
 227        path: ProjectPath,
 228        buffer: ModelHandle<Buffer>,
 229        cx: &mut ViewContext<Self>,
 230    ) {
 231        let was_empty = self.path_states.is_empty();
 232        let snapshot = buffer.read(cx).snapshot();
 233        let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
 234            Ok(ix) => ix,
 235            Err(ix) => {
 236                self.path_states.insert(
 237                    ix,
 238                    PathState {
 239                        path: path.clone(),
 240                        diagnostic_groups: Default::default(),
 241                    },
 242                );
 243                ix
 244            }
 245        };
 246
 247        let mut prev_excerpt_id = if path_ix > 0 {
 248            let prev_path_last_group = &self.path_states[path_ix - 1]
 249                .diagnostic_groups
 250                .last()
 251                .unwrap();
 252            prev_path_last_group.excerpts.last().unwrap().clone()
 253        } else {
 254            ExcerptId::min()
 255        };
 256
 257        let path_state = &mut self.path_states[path_ix];
 258        let mut groups_to_add = Vec::new();
 259        let mut group_ixs_to_remove = Vec::new();
 260        let mut blocks_to_add = Vec::new();
 261        let mut blocks_to_remove = HashSet::default();
 262        let mut first_excerpt_id = None;
 263        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
 264            let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
 265            let mut new_groups = snapshot
 266                .diagnostic_groups()
 267                .into_iter()
 268                .filter(|group| {
 269                    group.entries[group.primary_ix].diagnostic.severity
 270                        <= DiagnosticSeverity::WARNING
 271                })
 272                .peekable();
 273            loop {
 274                let mut to_insert = None;
 275                let mut to_remove = None;
 276                let mut to_keep = None;
 277                match (old_groups.peek(), new_groups.peek()) {
 278                    (None, None) => break,
 279                    (None, Some(_)) => to_insert = new_groups.next(),
 280                    (Some(_), None) => to_remove = old_groups.next(),
 281                    (Some((_, old_group)), Some(new_group)) => {
 282                        let old_primary = &old_group.primary_diagnostic;
 283                        let new_primary = &new_group.entries[new_group.primary_ix];
 284                        match compare_diagnostics(old_primary, new_primary, &snapshot) {
 285                            Ordering::Less => to_remove = old_groups.next(),
 286                            Ordering::Equal => {
 287                                to_keep = old_groups.next();
 288                                new_groups.next();
 289                            }
 290                            Ordering::Greater => to_insert = new_groups.next(),
 291                        }
 292                    }
 293                }
 294
 295                if let Some(group) = to_insert {
 296                    let mut group_state = DiagnosticGroupState {
 297                        primary_diagnostic: group.entries[group.primary_ix].clone(),
 298                        primary_excerpt_ix: 0,
 299                        excerpts: Default::default(),
 300                        blocks: Default::default(),
 301                        block_count: 0,
 302                    };
 303                    let mut pending_range: Option<(Range<Point>, usize)> = None;
 304                    let mut is_first_excerpt_for_group = true;
 305                    for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
 306                        let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
 307                        if let Some((range, start_ix)) = &mut pending_range {
 308                            if let Some(entry) = resolved_entry.as_ref() {
 309                                if entry.range.start.row
 310                                    <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
 311                                {
 312                                    range.end = range.end.max(entry.range.end);
 313                                    continue;
 314                                }
 315                            }
 316
 317                            let excerpt_start =
 318                                Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
 319                            let excerpt_end = snapshot.clip_point(
 320                                Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
 321                                Bias::Left,
 322                            );
 323                            let excerpt_id = excerpts
 324                                .insert_excerpts_after(
 325                                    &prev_excerpt_id,
 326                                    buffer.clone(),
 327                                    [ExcerptRange {
 328                                        context: excerpt_start..excerpt_end,
 329                                        primary: Some(range.clone()),
 330                                    }],
 331                                    excerpts_cx,
 332                                )
 333                                .pop()
 334                                .unwrap();
 335
 336                            prev_excerpt_id = excerpt_id.clone();
 337                            first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
 338                            group_state.excerpts.push(excerpt_id.clone());
 339                            let header_position = (excerpt_id.clone(), language::Anchor::MIN);
 340
 341                            if is_first_excerpt_for_group {
 342                                is_first_excerpt_for_group = false;
 343                                let mut primary =
 344                                    group.entries[group.primary_ix].diagnostic.clone();
 345                                primary.message =
 346                                    primary.message.split('\n').next().unwrap().to_string();
 347                                group_state.block_count += 1;
 348                                blocks_to_add.push(BlockProperties {
 349                                    position: header_position,
 350                                    height: 2,
 351                                    style: BlockStyle::Sticky,
 352                                    render: diagnostic_header_renderer(primary),
 353                                    disposition: BlockDisposition::Above,
 354                                });
 355                            }
 356
 357                            for entry in &group.entries[*start_ix..ix] {
 358                                let mut diagnostic = entry.diagnostic.clone();
 359                                if diagnostic.is_primary {
 360                                    group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
 361                                    diagnostic.message =
 362                                        entry.diagnostic.message.split('\n').skip(1).collect();
 363                                }
 364
 365                                if !diagnostic.message.is_empty() {
 366                                    group_state.block_count += 1;
 367                                    blocks_to_add.push(BlockProperties {
 368                                        position: (excerpt_id.clone(), entry.range.start),
 369                                        height: diagnostic.message.matches('\n').count() as u8 + 1,
 370                                        style: BlockStyle::Fixed,
 371                                        render: diagnostic_block_renderer(diagnostic, true),
 372                                        disposition: BlockDisposition::Below,
 373                                    });
 374                                }
 375                            }
 376
 377                            pending_range.take();
 378                        }
 379
 380                        if let Some(entry) = resolved_entry {
 381                            pending_range = Some((entry.range.clone(), ix));
 382                        }
 383                    }
 384
 385                    groups_to_add.push(group_state);
 386                } else if let Some((group_ix, group_state)) = to_remove {
 387                    excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
 388                    group_ixs_to_remove.push(group_ix);
 389                    blocks_to_remove.extend(group_state.blocks.iter().copied());
 390                } else if let Some((_, group)) = to_keep {
 391                    prev_excerpt_id = group.excerpts.last().unwrap().clone();
 392                    first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
 393                }
 394            }
 395
 396            excerpts.snapshot(excerpts_cx)
 397        });
 398
 399        self.editor.update(cx, |editor, cx| {
 400            editor.remove_blocks(blocks_to_remove, cx);
 401            let block_ids = editor.insert_blocks(
 402                blocks_to_add.into_iter().map(|block| {
 403                    let (excerpt_id, text_anchor) = block.position;
 404                    BlockProperties {
 405                        position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
 406                        height: block.height,
 407                        style: block.style,
 408                        render: block.render,
 409                        disposition: block.disposition,
 410                    }
 411                }),
 412                cx,
 413            );
 414
 415            let mut block_ids = block_ids.into_iter();
 416            for group_state in &mut groups_to_add {
 417                group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
 418            }
 419        });
 420
 421        for ix in group_ixs_to_remove.into_iter().rev() {
 422            path_state.diagnostic_groups.remove(ix);
 423        }
 424        path_state.diagnostic_groups.extend(groups_to_add);
 425        path_state.diagnostic_groups.sort_unstable_by(|a, b| {
 426            let range_a = &a.primary_diagnostic.range;
 427            let range_b = &b.primary_diagnostic.range;
 428            range_a
 429                .start
 430                .cmp(&range_b.start, &snapshot)
 431                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
 432        });
 433
 434        if path_state.diagnostic_groups.is_empty() {
 435            self.path_states.remove(path_ix);
 436        }
 437
 438        self.editor.update(cx, |editor, cx| {
 439            let groups;
 440            let mut selections;
 441            let new_excerpt_ids_by_selection_id;
 442            if was_empty {
 443                groups = self.path_states.first()?.diagnostic_groups.as_slice();
 444                new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
 445                selections = vec![Selection {
 446                    id: 0,
 447                    start: 0,
 448                    end: 0,
 449                    reversed: false,
 450                    goal: SelectionGoal::None,
 451                }];
 452            } else {
 453                groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
 454                new_excerpt_ids_by_selection_id =
 455                    editor.change_selections(Some(Autoscroll::Fit), cx, |s| s.refresh());
 456                selections = editor.selections.all::<usize>(cx);
 457            }
 458
 459            // If any selection has lost its position, move it to start of the next primary diagnostic.
 460            for selection in &mut selections {
 461                if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
 462                    let group_ix = match groups.binary_search_by(|probe| {
 463                        probe.excerpts.last().unwrap().cmp(new_excerpt_id)
 464                    }) {
 465                        Ok(ix) | Err(ix) => ix,
 466                    };
 467                    if let Some(group) = groups.get(group_ix) {
 468                        let offset = excerpts_snapshot
 469                            .anchor_in_excerpt(
 470                                group.excerpts[group.primary_excerpt_ix].clone(),
 471                                group.primary_diagnostic.range.start,
 472                            )
 473                            .to_offset(&excerpts_snapshot);
 474                        selection.start = offset;
 475                        selection.end = offset;
 476                    }
 477                }
 478            }
 479            editor.change_selections(None, cx, |s| {
 480                s.select(selections);
 481            });
 482            Some(())
 483        });
 484
 485        if self.path_states.is_empty() {
 486            if self.editor.is_focused(cx) {
 487                cx.focus_self();
 488            }
 489        } else if cx.handle().is_focused(cx) {
 490            cx.focus(&self.editor);
 491        }
 492        cx.notify();
 493    }
 494
 495    fn update_title(&mut self, cx: &mut ViewContext<Self>) {
 496        self.summary = self.project.read(cx).diagnostic_summary(cx);
 497        cx.emit(Event::TitleChanged);
 498    }
 499}
 500
 501impl workspace::Item for ProjectDiagnosticsEditor {
 502    fn tab_content(
 503        &self,
 504        _detail: Option<usize>,
 505        style: &theme::Tab,
 506        cx: &AppContext,
 507    ) -> ElementBox {
 508        render_summary(
 509            &self.summary,
 510            &style.label.text,
 511            &cx.global::<Settings>().theme.project_diagnostics,
 512        )
 513    }
 514
 515    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
 516        None
 517    }
 518
 519    fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
 520        self.editor.project_entry_ids(cx)
 521    }
 522
 523    fn is_singleton(&self, _: &AppContext) -> bool {
 524        false
 525    }
 526
 527    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
 528        self.editor
 529            .update(cx, |editor, cx| editor.navigate(data, cx))
 530    }
 531
 532    fn is_dirty(&self, cx: &AppContext) -> bool {
 533        self.excerpts.read(cx).is_dirty(cx)
 534    }
 535
 536    fn has_conflict(&self, cx: &AppContext) -> bool {
 537        self.excerpts.read(cx).has_conflict(cx)
 538    }
 539
 540    fn can_save(&self, _: &AppContext) -> bool {
 541        true
 542    }
 543
 544    fn save(
 545        &mut self,
 546        project: ModelHandle<Project>,
 547        cx: &mut ViewContext<Self>,
 548    ) -> Task<Result<()>> {
 549        self.editor.save(project, cx)
 550    }
 551
 552    fn reload(
 553        &mut self,
 554        project: ModelHandle<Project>,
 555        cx: &mut ViewContext<Self>,
 556    ) -> Task<Result<()>> {
 557        self.editor.reload(project, cx)
 558    }
 559
 560    fn save_as(
 561        &mut self,
 562        _: ModelHandle<Project>,
 563        _: PathBuf,
 564        _: &mut ViewContext<Self>,
 565    ) -> Task<Result<()>> {
 566        unreachable!()
 567    }
 568
 569    fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
 570        Editor::to_item_events(event)
 571    }
 572
 573    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
 574        self.editor.update(cx, |editor, _| {
 575            editor.set_nav_history(Some(nav_history));
 576        });
 577    }
 578
 579    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
 580    where
 581        Self: Sized,
 582    {
 583        Some(ProjectDiagnosticsEditor::new(
 584            self.project.clone(),
 585            self.workspace.clone(),
 586            cx,
 587        ))
 588    }
 589
 590    fn act_as_type(
 591        &self,
 592        type_id: TypeId,
 593        self_handle: &ViewHandle<Self>,
 594        _: &AppContext,
 595    ) -> Option<AnyViewHandle> {
 596        if type_id == TypeId::of::<Self>() {
 597            Some(self_handle.into())
 598        } else if type_id == TypeId::of::<Editor>() {
 599            Some((&self.editor).into())
 600        } else {
 601            None
 602        }
 603    }
 604
 605    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 606        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
 607    }
 608}
 609
 610fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
 611    let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
 612    Arc::new(move |cx| {
 613        let settings = cx.global::<Settings>();
 614        let theme = &settings.theme.editor;
 615        let style = theme.diagnostic_header.clone();
 616        let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
 617        let icon_width = cx.em_width * style.icon_width_factor;
 618        let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
 619            Svg::new("icons/circle_x_mark_12.svg")
 620                .with_color(theme.error_diagnostic.message.text.color)
 621        } else {
 622            Svg::new("icons/triangle_exclamation_12.svg")
 623                .with_color(theme.warning_diagnostic.message.text.color)
 624        };
 625
 626        Flex::row()
 627            .with_child(
 628                icon.constrained()
 629                    .with_width(icon_width)
 630                    .aligned()
 631                    .contained()
 632                    .boxed(),
 633            )
 634            .with_child(
 635                Label::new(
 636                    message.clone(),
 637                    style.message.label.clone().with_font_size(font_size),
 638                )
 639                .with_highlights(highlights.clone())
 640                .contained()
 641                .with_style(style.message.container)
 642                .with_margin_left(cx.gutter_padding)
 643                .aligned()
 644                .boxed(),
 645            )
 646            .with_children(diagnostic.code.clone().map(|code| {
 647                Label::new(code, style.code.text.clone().with_font_size(font_size))
 648                    .contained()
 649                    .with_style(style.code.container)
 650                    .aligned()
 651                    .boxed()
 652            }))
 653            .contained()
 654            .with_style(style.container)
 655            .with_padding_left(cx.gutter_padding)
 656            .with_padding_right(cx.gutter_padding)
 657            .expanded()
 658            .named("diagnostic header")
 659    })
 660}
 661
 662pub(crate) fn render_summary(
 663    summary: &DiagnosticSummary,
 664    text_style: &TextStyle,
 665    theme: &theme::ProjectDiagnostics,
 666) -> ElementBox {
 667    if summary.error_count == 0 && summary.warning_count == 0 {
 668        Label::new("No problems".to_string(), text_style.clone()).boxed()
 669    } else {
 670        let icon_width = theme.tab_icon_width;
 671        let icon_spacing = theme.tab_icon_spacing;
 672        let summary_spacing = theme.tab_summary_spacing;
 673        Flex::row()
 674            .with_children([
 675                Svg::new("icons/circle_x_mark_12.svg")
 676                    .with_color(text_style.color)
 677                    .constrained()
 678                    .with_width(icon_width)
 679                    .aligned()
 680                    .contained()
 681                    .with_margin_right(icon_spacing)
 682                    .named("no-icon"),
 683                Label::new(
 684                    summary.error_count.to_string(),
 685                    LabelStyle {
 686                        text: text_style.clone(),
 687                        highlight_text: None,
 688                    },
 689                )
 690                .aligned()
 691                .boxed(),
 692                Svg::new("icons/triangle_exclamation_12.svg")
 693                    .with_color(text_style.color)
 694                    .constrained()
 695                    .with_width(icon_width)
 696                    .aligned()
 697                    .contained()
 698                    .with_margin_left(summary_spacing)
 699                    .with_margin_right(icon_spacing)
 700                    .named("warn-icon"),
 701                Label::new(
 702                    summary.warning_count.to_string(),
 703                    LabelStyle {
 704                        text: text_style.clone(),
 705                        highlight_text: None,
 706                    },
 707                )
 708                .aligned()
 709                .boxed(),
 710            ])
 711            .boxed()
 712    }
 713}
 714
 715fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 716    lhs: &DiagnosticEntry<L>,
 717    rhs: &DiagnosticEntry<R>,
 718    snapshot: &language::BufferSnapshot,
 719) -> Ordering {
 720    lhs.range
 721        .start
 722        .to_offset(snapshot)
 723        .cmp(&rhs.range.start.to_offset(snapshot))
 724        .then_with(|| {
 725            lhs.range
 726                .end
 727                .to_offset(snapshot)
 728                .cmp(&rhs.range.end.to_offset(snapshot))
 729        })
 730        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 731}
 732
 733#[cfg(test)]
 734mod tests {
 735    use super::*;
 736    use editor::{
 737        display_map::{BlockContext, TransformBlock},
 738        DisplayPoint,
 739    };
 740    use gpui::TestAppContext;
 741    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity};
 742    use rope::point_utf16::PointUtf16;
 743    use serde_json::json;
 744    use unindent::Unindent as _;
 745    use workspace::AppState;
 746
 747    #[gpui::test]
 748    async fn test_diagnostics(cx: &mut TestAppContext) {
 749        let app_state = cx.update(AppState::test);
 750        app_state
 751            .fs
 752            .as_fake()
 753            .insert_tree(
 754                "/test",
 755                json!({
 756                    "consts.rs": "
 757                        const a: i32 = 'a';
 758                        const b: i32 = c;
 759                    "
 760                    .unindent(),
 761
 762                    "main.rs": "
 763                        fn main() {
 764                            let x = vec![];
 765                            let y = vec![];
 766                            a(x);
 767                            b(y);
 768                            // comment 1
 769                            // comment 2
 770                            c(y);
 771                            d(x);
 772                        }
 773                    "
 774                    .unindent(),
 775                }),
 776            )
 777            .await;
 778
 779        let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
 780        let (_, workspace) =
 781            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
 782
 783        // Create some diagnostics
 784        project.update(cx, |project, cx| {
 785            project
 786                .update_diagnostic_entries(
 787                    0,
 788                    PathBuf::from("/test/main.rs"),
 789                    None,
 790                    vec![
 791                        DiagnosticEntry {
 792                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
 793                            diagnostic: Diagnostic {
 794                                message:
 795                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 796                                        .to_string(),
 797                                severity: DiagnosticSeverity::INFORMATION,
 798                                is_primary: false,
 799                                is_disk_based: true,
 800                                group_id: 1,
 801                                ..Default::default()
 802                            },
 803                        },
 804                        DiagnosticEntry {
 805                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
 806                            diagnostic: Diagnostic {
 807                                message:
 808                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 809                                        .to_string(),
 810                                severity: DiagnosticSeverity::INFORMATION,
 811                                is_primary: false,
 812                                is_disk_based: true,
 813                                group_id: 0,
 814                                ..Default::default()
 815                            },
 816                        },
 817                        DiagnosticEntry {
 818                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
 819                            diagnostic: Diagnostic {
 820                                message: "value moved here".to_string(),
 821                                severity: DiagnosticSeverity::INFORMATION,
 822                                is_primary: false,
 823                                is_disk_based: true,
 824                                group_id: 1,
 825                                ..Default::default()
 826                            },
 827                        },
 828                        DiagnosticEntry {
 829                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
 830                            diagnostic: Diagnostic {
 831                                message: "value moved here".to_string(),
 832                                severity: DiagnosticSeverity::INFORMATION,
 833                                is_primary: false,
 834                                is_disk_based: true,
 835                                group_id: 0,
 836                                ..Default::default()
 837                            },
 838                        },
 839                        DiagnosticEntry {
 840                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
 841                            diagnostic: Diagnostic {
 842                                message: "use of moved value\nvalue used here after move".to_string(),
 843                                severity: DiagnosticSeverity::ERROR,
 844                                is_primary: true,
 845                                is_disk_based: true,
 846                                group_id: 0,
 847                                ..Default::default()
 848                            },
 849                        },
 850                        DiagnosticEntry {
 851                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
 852                            diagnostic: Diagnostic {
 853                                message: "use of moved value\nvalue used here after move".to_string(),
 854                                severity: DiagnosticSeverity::ERROR,
 855                                is_primary: true,
 856                                is_disk_based: true,
 857                                group_id: 1,
 858                                ..Default::default()
 859                            },
 860                        },
 861                    ],
 862                    cx,
 863                )
 864                .unwrap();
 865        });
 866
 867        // Open the project diagnostics view while there are already diagnostics.
 868        let view = cx.add_view(&workspace, |cx| {
 869            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
 870        });
 871
 872        view.next_notification(cx).await;
 873        view.update(cx, |view, cx| {
 874            assert_eq!(
 875                editor_blocks(&view.editor, cx),
 876                [
 877                    (0, "path header block".into()),
 878                    (2, "diagnostic header".into()),
 879                    (15, "collapsed context".into()),
 880                    (16, "diagnostic header".into()),
 881                    (25, "collapsed context".into()),
 882                ]
 883            );
 884            assert_eq!(
 885                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
 886                concat!(
 887                    //
 888                    // main.rs
 889                    //
 890                    "\n", // filename
 891                    "\n", // padding
 892                    // diagnostic group 1
 893                    "\n", // primary message
 894                    "\n", // padding
 895                    "    let x = vec![];\n",
 896                    "    let y = vec![];\n",
 897                    "\n", // supporting diagnostic
 898                    "    a(x);\n",
 899                    "    b(y);\n",
 900                    "\n", // supporting diagnostic
 901                    "    // comment 1\n",
 902                    "    // comment 2\n",
 903                    "    c(y);\n",
 904                    "\n", // supporting diagnostic
 905                    "    d(x);\n",
 906                    "\n", // context ellipsis
 907                    // diagnostic group 2
 908                    "\n", // primary message
 909                    "\n", // padding
 910                    "fn main() {\n",
 911                    "    let x = vec![];\n",
 912                    "\n", // supporting diagnostic
 913                    "    let y = vec![];\n",
 914                    "    a(x);\n",
 915                    "\n", // supporting diagnostic
 916                    "    b(y);\n",
 917                    "\n", // context ellipsis
 918                    "    c(y);\n",
 919                    "    d(x);\n",
 920                    "\n", // supporting diagnostic
 921                    "}"
 922                )
 923            );
 924
 925            // Cursor is at the first diagnostic
 926            view.editor.update(cx, |editor, cx| {
 927                assert_eq!(
 928                    editor.selections.display_ranges(cx),
 929                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
 930                );
 931            });
 932        });
 933
 934        // Diagnostics are added for another earlier path.
 935        project.update(cx, |project, cx| {
 936            project.disk_based_diagnostics_started(0, cx);
 937            project
 938                .update_diagnostic_entries(
 939                    0,
 940                    PathBuf::from("/test/consts.rs"),
 941                    None,
 942                    vec![DiagnosticEntry {
 943                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
 944                        diagnostic: Diagnostic {
 945                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
 946                            severity: DiagnosticSeverity::ERROR,
 947                            is_primary: true,
 948                            is_disk_based: true,
 949                            group_id: 0,
 950                            ..Default::default()
 951                        },
 952                    }],
 953                    cx,
 954                )
 955                .unwrap();
 956            project.disk_based_diagnostics_finished(0, cx);
 957        });
 958
 959        view.next_notification(cx).await;
 960        view.update(cx, |view, cx| {
 961            assert_eq!(
 962                editor_blocks(&view.editor, cx),
 963                [
 964                    (0, "path header block".into()),
 965                    (2, "diagnostic header".into()),
 966                    (7, "path header block".into()),
 967                    (9, "diagnostic header".into()),
 968                    (22, "collapsed context".into()),
 969                    (23, "diagnostic header".into()),
 970                    (32, "collapsed context".into()),
 971                ]
 972            );
 973            assert_eq!(
 974                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
 975                concat!(
 976                    //
 977                    // consts.rs
 978                    //
 979                    "\n", // filename
 980                    "\n", // padding
 981                    // diagnostic group 1
 982                    "\n", // primary message
 983                    "\n", // padding
 984                    "const a: i32 = 'a';\n",
 985                    "\n", // supporting diagnostic
 986                    "const b: i32 = c;\n",
 987                    //
 988                    // main.rs
 989                    //
 990                    "\n", // filename
 991                    "\n", // padding
 992                    // diagnostic group 1
 993                    "\n", // primary message
 994                    "\n", // padding
 995                    "    let x = vec![];\n",
 996                    "    let y = vec![];\n",
 997                    "\n", // supporting diagnostic
 998                    "    a(x);\n",
 999                    "    b(y);\n",
1000                    "\n", // supporting diagnostic
1001                    "    // comment 1\n",
1002                    "    // comment 2\n",
1003                    "    c(y);\n",
1004                    "\n", // supporting diagnostic
1005                    "    d(x);\n",
1006                    "\n", // collapsed context
1007                    // diagnostic group 2
1008                    "\n", // primary message
1009                    "\n", // filename
1010                    "fn main() {\n",
1011                    "    let x = vec![];\n",
1012                    "\n", // supporting diagnostic
1013                    "    let y = vec![];\n",
1014                    "    a(x);\n",
1015                    "\n", // supporting diagnostic
1016                    "    b(y);\n",
1017                    "\n", // context ellipsis
1018                    "    c(y);\n",
1019                    "    d(x);\n",
1020                    "\n", // supporting diagnostic
1021                    "}"
1022                )
1023            );
1024
1025            // Cursor keeps its position.
1026            view.editor.update(cx, |editor, cx| {
1027                assert_eq!(
1028                    editor.selections.display_ranges(cx),
1029                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1030                );
1031            });
1032        });
1033
1034        // Diagnostics are added to the first path
1035        project.update(cx, |project, cx| {
1036            project.disk_based_diagnostics_started(0, cx);
1037            project
1038                .update_diagnostic_entries(
1039                    0,
1040                    PathBuf::from("/test/consts.rs"),
1041                    None,
1042                    vec![
1043                        DiagnosticEntry {
1044                            range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1045                            diagnostic: Diagnostic {
1046                                message: "mismatched types\nexpected `usize`, found `char`"
1047                                    .to_string(),
1048                                severity: DiagnosticSeverity::ERROR,
1049                                is_primary: true,
1050                                is_disk_based: true,
1051                                group_id: 0,
1052                                ..Default::default()
1053                            },
1054                        },
1055                        DiagnosticEntry {
1056                            range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1057                            diagnostic: Diagnostic {
1058                                message: "unresolved name `c`".to_string(),
1059                                severity: DiagnosticSeverity::ERROR,
1060                                is_primary: true,
1061                                is_disk_based: true,
1062                                group_id: 1,
1063                                ..Default::default()
1064                            },
1065                        },
1066                    ],
1067                    cx,
1068                )
1069                .unwrap();
1070            project.disk_based_diagnostics_finished(0, cx);
1071        });
1072
1073        view.next_notification(cx).await;
1074        view.update(cx, |view, cx| {
1075            assert_eq!(
1076                editor_blocks(&view.editor, cx),
1077                [
1078                    (0, "path header block".into()),
1079                    (2, "diagnostic header".into()),
1080                    (7, "collapsed context".into()),
1081                    (8, "diagnostic header".into()),
1082                    (13, "path header block".into()),
1083                    (15, "diagnostic header".into()),
1084                    (28, "collapsed context".into()),
1085                    (29, "diagnostic header".into()),
1086                    (38, "collapsed context".into()),
1087                ]
1088            );
1089            assert_eq!(
1090                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1091                concat!(
1092                    //
1093                    // consts.rs
1094                    //
1095                    "\n", // filename
1096                    "\n", // padding
1097                    // diagnostic group 1
1098                    "\n", // primary message
1099                    "\n", // padding
1100                    "const a: i32 = 'a';\n",
1101                    "\n", // supporting diagnostic
1102                    "const b: i32 = c;\n",
1103                    "\n", // context ellipsis
1104                    // diagnostic group 2
1105                    "\n", // primary message
1106                    "\n", // padding
1107                    "const a: i32 = 'a';\n",
1108                    "const b: i32 = c;\n",
1109                    "\n", // supporting diagnostic
1110                    //
1111                    // main.rs
1112                    //
1113                    "\n", // filename
1114                    "\n", // padding
1115                    // diagnostic group 1
1116                    "\n", // primary message
1117                    "\n", // padding
1118                    "    let x = vec![];\n",
1119                    "    let y = vec![];\n",
1120                    "\n", // supporting diagnostic
1121                    "    a(x);\n",
1122                    "    b(y);\n",
1123                    "\n", // supporting diagnostic
1124                    "    // comment 1\n",
1125                    "    // comment 2\n",
1126                    "    c(y);\n",
1127                    "\n", // supporting diagnostic
1128                    "    d(x);\n",
1129                    "\n", // context ellipsis
1130                    // diagnostic group 2
1131                    "\n", // primary message
1132                    "\n", // filename
1133                    "fn main() {\n",
1134                    "    let x = vec![];\n",
1135                    "\n", // supporting diagnostic
1136                    "    let y = vec![];\n",
1137                    "    a(x);\n",
1138                    "\n", // supporting diagnostic
1139                    "    b(y);\n",
1140                    "\n", // context ellipsis
1141                    "    c(y);\n",
1142                    "    d(x);\n",
1143                    "\n", // supporting diagnostic
1144                    "}"
1145                )
1146            );
1147        });
1148    }
1149
1150    fn editor_blocks(
1151        editor: &ViewHandle<Editor>,
1152        cx: &mut MutableAppContext,
1153    ) -> Vec<(u32, String)> {
1154        let mut presenter = cx.build_presenter(editor.id(), 0., Default::default());
1155        let mut cx = presenter.build_layout_context(Default::default(), false, cx);
1156        cx.render(editor, |editor, cx| {
1157            let snapshot = editor.snapshot(cx);
1158            snapshot
1159                .blocks_in_range(0..snapshot.max_point().row())
1160                .filter_map(|(row, block)| {
1161                    let name = match block {
1162                        TransformBlock::Custom(block) => block
1163                            .render(&mut BlockContext {
1164                                cx,
1165                                anchor_x: 0.,
1166                                scroll_x: 0.,
1167                                gutter_padding: 0.,
1168                                gutter_width: 0.,
1169                                line_height: 0.,
1170                                em_width: 0.,
1171                            })
1172                            .name()?
1173                            .to_string(),
1174                        TransformBlock::ExcerptHeader {
1175                            starts_new_buffer, ..
1176                        } => {
1177                            if *starts_new_buffer {
1178                                "path header block".to_string()
1179                            } else {
1180                                "collapsed context".to_string()
1181                            }
1182                        }
1183                    };
1184
1185                    Some((row, name))
1186                })
1187                .collect()
1188        })
1189    }
1190}