diagnostics.rs

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