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(&self, cx: &mut ViewContext<Self>) -> Option<Self>
 588    where
 589        Self: Sized,
 590    {
 591        Some(ProjectDiagnosticsEditor::new(
 592            self.project.clone(),
 593            self.workspace.clone(),
 594            cx,
 595        ))
 596    }
 597
 598    fn act_as_type(
 599        &self,
 600        type_id: TypeId,
 601        self_handle: &ViewHandle<Self>,
 602        _: &AppContext,
 603    ) -> Option<AnyViewHandle> {
 604        if type_id == TypeId::of::<Self>() {
 605            Some(self_handle.into())
 606        } else if type_id == TypeId::of::<Editor>() {
 607            Some((&self.editor).into())
 608        } else {
 609            None
 610        }
 611    }
 612
 613    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 614        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
 615    }
 616
 617    fn serialized_item_kind() -> Option<&'static str> {
 618        Some("diagnostics")
 619    }
 620
 621    fn deserialize(
 622        project: ModelHandle<Project>,
 623        workspace: WeakViewHandle<Workspace>,
 624        _workspace_id: workspace::WorkspaceId,
 625        _item_id: workspace::ItemId,
 626        cx: &mut ViewContext<Pane>,
 627    ) -> Task<Result<ViewHandle<Self>>> {
 628        Task::ready(Ok(cx.add_view(|cx| Self::new(project, workspace, cx))))
 629    }
 630}
 631
 632fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
 633    let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
 634    Arc::new(move |cx| {
 635        let settings = cx.global::<Settings>();
 636        let theme = &settings.theme.editor;
 637        let style = theme.diagnostic_header.clone();
 638        let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
 639        let icon_width = cx.em_width * style.icon_width_factor;
 640        let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
 641            Svg::new("icons/circle_x_mark_12.svg")
 642                .with_color(theme.error_diagnostic.message.text.color)
 643        } else {
 644            Svg::new("icons/triangle_exclamation_12.svg")
 645                .with_color(theme.warning_diagnostic.message.text.color)
 646        };
 647
 648        Flex::row()
 649            .with_child(
 650                icon.constrained()
 651                    .with_width(icon_width)
 652                    .aligned()
 653                    .contained()
 654                    .boxed(),
 655            )
 656            .with_child(
 657                Label::new(
 658                    message.clone(),
 659                    style.message.label.clone().with_font_size(font_size),
 660                )
 661                .with_highlights(highlights.clone())
 662                .contained()
 663                .with_style(style.message.container)
 664                .with_margin_left(cx.gutter_padding)
 665                .aligned()
 666                .boxed(),
 667            )
 668            .with_children(diagnostic.code.clone().map(|code| {
 669                Label::new(code, style.code.text.clone().with_font_size(font_size))
 670                    .contained()
 671                    .with_style(style.code.container)
 672                    .aligned()
 673                    .boxed()
 674            }))
 675            .contained()
 676            .with_style(style.container)
 677            .with_padding_left(cx.gutter_padding)
 678            .with_padding_right(cx.gutter_padding)
 679            .expanded()
 680            .named("diagnostic header")
 681    })
 682}
 683
 684pub(crate) fn render_summary(
 685    summary: &DiagnosticSummary,
 686    text_style: &TextStyle,
 687    theme: &theme::ProjectDiagnostics,
 688) -> ElementBox {
 689    if summary.error_count == 0 && summary.warning_count == 0 {
 690        Label::new("No problems".to_string(), text_style.clone()).boxed()
 691    } else {
 692        let icon_width = theme.tab_icon_width;
 693        let icon_spacing = theme.tab_icon_spacing;
 694        let summary_spacing = theme.tab_summary_spacing;
 695        Flex::row()
 696            .with_children([
 697                Svg::new("icons/circle_x_mark_12.svg")
 698                    .with_color(text_style.color)
 699                    .constrained()
 700                    .with_width(icon_width)
 701                    .aligned()
 702                    .contained()
 703                    .with_margin_right(icon_spacing)
 704                    .named("no-icon"),
 705                Label::new(
 706                    summary.error_count.to_string(),
 707                    LabelStyle {
 708                        text: text_style.clone(),
 709                        highlight_text: None,
 710                    },
 711                )
 712                .aligned()
 713                .boxed(),
 714                Svg::new("icons/triangle_exclamation_12.svg")
 715                    .with_color(text_style.color)
 716                    .constrained()
 717                    .with_width(icon_width)
 718                    .aligned()
 719                    .contained()
 720                    .with_margin_left(summary_spacing)
 721                    .with_margin_right(icon_spacing)
 722                    .named("warn-icon"),
 723                Label::new(
 724                    summary.warning_count.to_string(),
 725                    LabelStyle {
 726                        text: text_style.clone(),
 727                        highlight_text: None,
 728                    },
 729                )
 730                .aligned()
 731                .boxed(),
 732            ])
 733            .boxed()
 734    }
 735}
 736
 737fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 738    lhs: &DiagnosticEntry<L>,
 739    rhs: &DiagnosticEntry<R>,
 740    snapshot: &language::BufferSnapshot,
 741) -> Ordering {
 742    lhs.range
 743        .start
 744        .to_offset(snapshot)
 745        .cmp(&rhs.range.start.to_offset(snapshot))
 746        .then_with(|| {
 747            lhs.range
 748                .end
 749                .to_offset(snapshot)
 750                .cmp(&rhs.range.end.to_offset(snapshot))
 751        })
 752        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 753}
 754
 755#[cfg(test)]
 756mod tests {
 757    use super::*;
 758    use editor::{
 759        display_map::{BlockContext, TransformBlock},
 760        DisplayPoint,
 761    };
 762    use gpui::TestAppContext;
 763    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
 764    use serde_json::json;
 765    use unindent::Unindent as _;
 766    use workspace::AppState;
 767
 768    #[gpui::test]
 769    async fn test_diagnostics(cx: &mut TestAppContext) {
 770        let app_state = cx.update(AppState::test);
 771        app_state
 772            .fs
 773            .as_fake()
 774            .insert_tree(
 775                "/test",
 776                json!({
 777                    "consts.rs": "
 778                        const a: i32 = 'a';
 779                        const b: i32 = c;
 780                    "
 781                    .unindent(),
 782
 783                    "main.rs": "
 784                        fn main() {
 785                            let x = vec![];
 786                            let y = vec![];
 787                            a(x);
 788                            b(y);
 789                            // comment 1
 790                            // comment 2
 791                            c(y);
 792                            d(x);
 793                        }
 794                    "
 795                    .unindent(),
 796                }),
 797            )
 798            .await;
 799
 800        let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
 801        let (_, workspace) = cx.add_window(|cx| {
 802            Workspace::new(
 803                Default::default(),
 804                project.clone(),
 805                |_, _| unimplemented!(),
 806                cx,
 807            )
 808        });
 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(
1180        editor: &ViewHandle<Editor>,
1181        cx: &mut MutableAppContext,
1182    ) -> Vec<(u32, String)> {
1183        let mut presenter = cx.build_presenter(editor.id(), 0., Default::default());
1184        let mut cx = presenter.build_layout_context(Default::default(), false, cx);
1185        cx.render(editor, |editor, cx| {
1186            let snapshot = editor.snapshot(cx);
1187            snapshot
1188                .blocks_in_range(0..snapshot.max_point().row())
1189                .filter_map(|(row, block)| {
1190                    let name = match block {
1191                        TransformBlock::Custom(block) => block
1192                            .render(&mut BlockContext {
1193                                cx,
1194                                anchor_x: 0.,
1195                                scroll_x: 0.,
1196                                gutter_padding: 0.,
1197                                gutter_width: 0.,
1198                                line_height: 0.,
1199                                em_width: 0.,
1200                            })
1201                            .name()?
1202                            .to_string(),
1203                        TransformBlock::ExcerptHeader {
1204                            starts_new_buffer, ..
1205                        } => {
1206                            if *starts_new_buffer {
1207                                "path header block".to_string()
1208                            } else {
1209                                "collapsed context".to_string()
1210                            }
1211                        }
1212                    };
1213
1214                    Some((row, name))
1215                })
1216                .collect()
1217        })
1218    }
1219}