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