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, RenderContext, Task, View, ViewContext, ViewHandle,
  15    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 AppContext) {
  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<'a>(
 606        &'a self,
 607        type_id: TypeId,
 608        self_handle: &'a ViewHandle<Self>,
 609        _: &'a AppContext,
 610    ) -> Option<&AnyViewHandle> {
 611        if type_id == TypeId::of::<Self>() {
 612            Some(self_handle)
 613        } else if type_id == TypeId::of::<Editor>() {
 614            Some(&self.editor)
 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| Workspace::test_new(project.clone(), cx));
 809
 810        // Create some diagnostics
 811        project.update(cx, |project, cx| {
 812            project
 813                .update_diagnostic_entries(
 814                    0,
 815                    PathBuf::from("/test/main.rs"),
 816                    None,
 817                    vec![
 818                        DiagnosticEntry {
 819                            range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
 820                            diagnostic: Diagnostic {
 821                                message:
 822                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 823                                        .to_string(),
 824                                severity: DiagnosticSeverity::INFORMATION,
 825                                is_primary: false,
 826                                is_disk_based: true,
 827                                group_id: 1,
 828                                ..Default::default()
 829                            },
 830                        },
 831                        DiagnosticEntry {
 832                            range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
 833                            diagnostic: Diagnostic {
 834                                message:
 835                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 836                                        .to_string(),
 837                                severity: DiagnosticSeverity::INFORMATION,
 838                                is_primary: false,
 839                                is_disk_based: true,
 840                                group_id: 0,
 841                                ..Default::default()
 842                            },
 843                        },
 844                        DiagnosticEntry {
 845                            range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
 846                            diagnostic: Diagnostic {
 847                                message: "value moved here".to_string(),
 848                                severity: DiagnosticSeverity::INFORMATION,
 849                                is_primary: false,
 850                                is_disk_based: true,
 851                                group_id: 1,
 852                                ..Default::default()
 853                            },
 854                        },
 855                        DiagnosticEntry {
 856                            range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
 857                            diagnostic: Diagnostic {
 858                                message: "value moved here".to_string(),
 859                                severity: DiagnosticSeverity::INFORMATION,
 860                                is_primary: false,
 861                                is_disk_based: true,
 862                                group_id: 0,
 863                                ..Default::default()
 864                            },
 865                        },
 866                        DiagnosticEntry {
 867                            range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
 868                            diagnostic: Diagnostic {
 869                                message: "use of moved value\nvalue used here after move".to_string(),
 870                                severity: DiagnosticSeverity::ERROR,
 871                                is_primary: true,
 872                                is_disk_based: true,
 873                                group_id: 0,
 874                                ..Default::default()
 875                            },
 876                        },
 877                        DiagnosticEntry {
 878                            range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
 879                            diagnostic: Diagnostic {
 880                                message: "use of moved value\nvalue used here after move".to_string(),
 881                                severity: DiagnosticSeverity::ERROR,
 882                                is_primary: true,
 883                                is_disk_based: true,
 884                                group_id: 1,
 885                                ..Default::default()
 886                            },
 887                        },
 888                    ],
 889                    cx,
 890                )
 891                .unwrap();
 892        });
 893
 894        // Open the project diagnostics view while there are already diagnostics.
 895        let view = cx.add_view(&workspace, |cx| {
 896            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
 897        });
 898
 899        view.next_notification(cx).await;
 900        view.update(cx, |view, cx| {
 901            assert_eq!(
 902                editor_blocks(&view.editor, cx),
 903                [
 904                    (0, "path header block".into()),
 905                    (2, "diagnostic header".into()),
 906                    (15, "collapsed context".into()),
 907                    (16, "diagnostic header".into()),
 908                    (25, "collapsed context".into()),
 909                ]
 910            );
 911            assert_eq!(
 912                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
 913                concat!(
 914                    //
 915                    // main.rs
 916                    //
 917                    "\n", // filename
 918                    "\n", // padding
 919                    // diagnostic group 1
 920                    "\n", // primary message
 921                    "\n", // padding
 922                    "    let x = vec![];\n",
 923                    "    let y = vec![];\n",
 924                    "\n", // supporting diagnostic
 925                    "    a(x);\n",
 926                    "    b(y);\n",
 927                    "\n", // supporting diagnostic
 928                    "    // comment 1\n",
 929                    "    // comment 2\n",
 930                    "    c(y);\n",
 931                    "\n", // supporting diagnostic
 932                    "    d(x);\n",
 933                    "\n", // context ellipsis
 934                    // diagnostic group 2
 935                    "\n", // primary message
 936                    "\n", // padding
 937                    "fn main() {\n",
 938                    "    let x = vec![];\n",
 939                    "\n", // supporting diagnostic
 940                    "    let y = vec![];\n",
 941                    "    a(x);\n",
 942                    "\n", // supporting diagnostic
 943                    "    b(y);\n",
 944                    "\n", // context ellipsis
 945                    "    c(y);\n",
 946                    "    d(x);\n",
 947                    "\n", // supporting diagnostic
 948                    "}"
 949                )
 950            );
 951
 952            // Cursor is at the first diagnostic
 953            view.editor.update(cx, |editor, cx| {
 954                assert_eq!(
 955                    editor.selections.display_ranges(cx),
 956                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
 957                );
 958            });
 959        });
 960
 961        // Diagnostics are added for another earlier path.
 962        project.update(cx, |project, cx| {
 963            project.disk_based_diagnostics_started(0, cx);
 964            project
 965                .update_diagnostic_entries(
 966                    0,
 967                    PathBuf::from("/test/consts.rs"),
 968                    None,
 969                    vec![DiagnosticEntry {
 970                        range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
 971                        diagnostic: Diagnostic {
 972                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
 973                            severity: DiagnosticSeverity::ERROR,
 974                            is_primary: true,
 975                            is_disk_based: true,
 976                            group_id: 0,
 977                            ..Default::default()
 978                        },
 979                    }],
 980                    cx,
 981                )
 982                .unwrap();
 983            project.disk_based_diagnostics_finished(0, cx);
 984        });
 985
 986        view.next_notification(cx).await;
 987        view.update(cx, |view, cx| {
 988            assert_eq!(
 989                editor_blocks(&view.editor, cx),
 990                [
 991                    (0, "path header block".into()),
 992                    (2, "diagnostic header".into()),
 993                    (7, "path header block".into()),
 994                    (9, "diagnostic header".into()),
 995                    (22, "collapsed context".into()),
 996                    (23, "diagnostic header".into()),
 997                    (32, "collapsed context".into()),
 998                ]
 999            );
1000            assert_eq!(
1001                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1002                concat!(
1003                    //
1004                    // consts.rs
1005                    //
1006                    "\n", // filename
1007                    "\n", // padding
1008                    // diagnostic group 1
1009                    "\n", // primary message
1010                    "\n", // padding
1011                    "const a: i32 = 'a';\n",
1012                    "\n", // supporting diagnostic
1013                    "const b: i32 = c;\n",
1014                    //
1015                    // main.rs
1016                    //
1017                    "\n", // filename
1018                    "\n", // padding
1019                    // diagnostic group 1
1020                    "\n", // primary message
1021                    "\n", // padding
1022                    "    let x = vec![];\n",
1023                    "    let y = vec![];\n",
1024                    "\n", // supporting diagnostic
1025                    "    a(x);\n",
1026                    "    b(y);\n",
1027                    "\n", // supporting diagnostic
1028                    "    // comment 1\n",
1029                    "    // comment 2\n",
1030                    "    c(y);\n",
1031                    "\n", // supporting diagnostic
1032                    "    d(x);\n",
1033                    "\n", // collapsed context
1034                    // diagnostic group 2
1035                    "\n", // primary message
1036                    "\n", // filename
1037                    "fn main() {\n",
1038                    "    let x = vec![];\n",
1039                    "\n", // supporting diagnostic
1040                    "    let y = vec![];\n",
1041                    "    a(x);\n",
1042                    "\n", // supporting diagnostic
1043                    "    b(y);\n",
1044                    "\n", // context ellipsis
1045                    "    c(y);\n",
1046                    "    d(x);\n",
1047                    "\n", // supporting diagnostic
1048                    "}"
1049                )
1050            );
1051
1052            // Cursor keeps its position.
1053            view.editor.update(cx, |editor, cx| {
1054                assert_eq!(
1055                    editor.selections.display_ranges(cx),
1056                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1057                );
1058            });
1059        });
1060
1061        // Diagnostics are added to the first path
1062        project.update(cx, |project, cx| {
1063            project.disk_based_diagnostics_started(0, cx);
1064            project
1065                .update_diagnostic_entries(
1066                    0,
1067                    PathBuf::from("/test/consts.rs"),
1068                    None,
1069                    vec![
1070                        DiagnosticEntry {
1071                            range: Unclipped(PointUtf16::new(0, 15))
1072                                ..Unclipped(PointUtf16::new(0, 15)),
1073                            diagnostic: Diagnostic {
1074                                message: "mismatched types\nexpected `usize`, found `char`"
1075                                    .to_string(),
1076                                severity: DiagnosticSeverity::ERROR,
1077                                is_primary: true,
1078                                is_disk_based: true,
1079                                group_id: 0,
1080                                ..Default::default()
1081                            },
1082                        },
1083                        DiagnosticEntry {
1084                            range: Unclipped(PointUtf16::new(1, 15))
1085                                ..Unclipped(PointUtf16::new(1, 15)),
1086                            diagnostic: Diagnostic {
1087                                message: "unresolved name `c`".to_string(),
1088                                severity: DiagnosticSeverity::ERROR,
1089                                is_primary: true,
1090                                is_disk_based: true,
1091                                group_id: 1,
1092                                ..Default::default()
1093                            },
1094                        },
1095                    ],
1096                    cx,
1097                )
1098                .unwrap();
1099            project.disk_based_diagnostics_finished(0, cx);
1100        });
1101
1102        view.next_notification(cx).await;
1103        view.update(cx, |view, cx| {
1104            assert_eq!(
1105                editor_blocks(&view.editor, cx),
1106                [
1107                    (0, "path header block".into()),
1108                    (2, "diagnostic header".into()),
1109                    (7, "collapsed context".into()),
1110                    (8, "diagnostic header".into()),
1111                    (13, "path header block".into()),
1112                    (15, "diagnostic header".into()),
1113                    (28, "collapsed context".into()),
1114                    (29, "diagnostic header".into()),
1115                    (38, "collapsed context".into()),
1116                ]
1117            );
1118            assert_eq!(
1119                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1120                concat!(
1121                    //
1122                    // consts.rs
1123                    //
1124                    "\n", // filename
1125                    "\n", // padding
1126                    // diagnostic group 1
1127                    "\n", // primary message
1128                    "\n", // padding
1129                    "const a: i32 = 'a';\n",
1130                    "\n", // supporting diagnostic
1131                    "const b: i32 = c;\n",
1132                    "\n", // context ellipsis
1133                    // diagnostic group 2
1134                    "\n", // primary message
1135                    "\n", // padding
1136                    "const a: i32 = 'a';\n",
1137                    "const b: i32 = c;\n",
1138                    "\n", // supporting diagnostic
1139                    //
1140                    // main.rs
1141                    //
1142                    "\n", // filename
1143                    "\n", // padding
1144                    // diagnostic group 1
1145                    "\n", // primary message
1146                    "\n", // padding
1147                    "    let x = vec![];\n",
1148                    "    let y = vec![];\n",
1149                    "\n", // supporting diagnostic
1150                    "    a(x);\n",
1151                    "    b(y);\n",
1152                    "\n", // supporting diagnostic
1153                    "    // comment 1\n",
1154                    "    // comment 2\n",
1155                    "    c(y);\n",
1156                    "\n", // supporting diagnostic
1157                    "    d(x);\n",
1158                    "\n", // context ellipsis
1159                    // diagnostic group 2
1160                    "\n", // primary message
1161                    "\n", // filename
1162                    "fn main() {\n",
1163                    "    let x = vec![];\n",
1164                    "\n", // supporting diagnostic
1165                    "    let y = vec![];\n",
1166                    "    a(x);\n",
1167                    "\n", // supporting diagnostic
1168                    "    b(y);\n",
1169                    "\n", // context ellipsis
1170                    "    c(y);\n",
1171                    "    d(x);\n",
1172                    "\n", // supporting diagnostic
1173                    "}"
1174                )
1175            );
1176        });
1177    }
1178
1179    fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut AppContext) -> Vec<(u32, String)> {
1180        let mut presenter = cx.build_presenter(editor.id(), 0., Default::default());
1181        let mut cx = presenter.build_layout_context(Default::default(), false, cx);
1182        cx.render(editor, |editor, cx| {
1183            let snapshot = editor.snapshot(cx);
1184            snapshot
1185                .blocks_in_range(0..snapshot.max_point().row())
1186                .filter_map(|(row, block)| {
1187                    let name = match block {
1188                        TransformBlock::Custom(block) => block
1189                            .render(&mut BlockContext {
1190                                cx,
1191                                anchor_x: 0.,
1192                                scroll_x: 0.,
1193                                gutter_padding: 0.,
1194                                gutter_width: 0.,
1195                                line_height: 0.,
1196                                em_width: 0.,
1197                            })
1198                            .name()?
1199                            .to_string(),
1200                        TransformBlock::ExcerptHeader {
1201                            starts_new_buffer, ..
1202                        } => {
1203                            if *starts_new_buffer {
1204                                "path header block".to_string()
1205                            } else {
1206                                "collapsed context".to_string()
1207                            }
1208                        }
1209                    };
1210
1211                    Some((row, name))
1212                })
1213                .collect()
1214        })
1215    }
1216}