diagnostics.rs

   1pub mod items;
   2
   3use anyhow::Result;
   4use collections::{BTreeMap, HashSet};
   5use editor::{
   6    diagnostic_block_renderer,
   7    display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
   8    highlight_diagnostic_message,
   9    scroll::autoscroll::Autoscroll,
  10    Editor, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
  11};
  12use gpui::{
  13    actions, elements::*, fonts::TextStyle, impl_internal_actions, serde_json, AnyViewHandle,
  14    AppContext, Entity, ModelHandle, RenderContext, Task, View, ViewContext, ViewHandle,
  15    WeakViewHandle,
  16};
  17use language::{
  18    Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
  19    SelectionGoal,
  20};
  21use project::{DiagnosticSummary, Project, ProjectPath};
  22use serde_json::json;
  23use settings::Settings;
  24use smallvec::SmallVec;
  25use std::{
  26    any::{Any, TypeId},
  27    borrow::Cow,
  28    cmp::Ordering,
  29    ops::Range,
  30    path::PathBuf,
  31    sync::Arc,
  32};
  33use util::TryFutureExt;
  34use workspace::{
  35    item::{Item, ItemEvent, ItemHandle},
  36    ItemNavHistory, Pane, Workspace,
  37};
  38
  39actions!(diagnostics, [Deploy]);
  40
  41impl_internal_actions!(diagnostics, [Jump]);
  42
  43const CONTEXT_LINE_COUNT: u32 = 1;
  44
  45pub fn init(cx: &mut AppContext) {
  46    cx.add_action(ProjectDiagnosticsEditor::deploy);
  47    items::init(cx);
  48}
  49
  50type Event = editor::Event;
  51
  52struct ProjectDiagnosticsEditor {
  53    project: ModelHandle<Project>,
  54    workspace: WeakViewHandle<Workspace>,
  55    editor: ViewHandle<Editor>,
  56    summary: DiagnosticSummary,
  57    excerpts: ModelHandle<MultiBuffer>,
  58    path_states: Vec<PathState>,
  59    paths_to_update: BTreeMap<ProjectPath, usize>,
  60}
  61
  62struct PathState {
  63    path: ProjectPath,
  64    diagnostic_groups: Vec<DiagnosticGroupState>,
  65}
  66
  67#[derive(Clone, Debug, PartialEq)]
  68struct Jump {
  69    path: ProjectPath,
  70    position: Point,
  71    anchor: Anchor,
  72}
  73
  74struct DiagnosticGroupState {
  75    primary_diagnostic: DiagnosticEntry<language::Anchor>,
  76    primary_excerpt_ix: usize,
  77    excerpts: Vec<ExcerptId>,
  78    blocks: HashSet<BlockId>,
  79    block_count: usize,
  80}
  81
  82impl Entity for ProjectDiagnosticsEditor {
  83    type Event = Event;
  84}
  85
  86impl View for ProjectDiagnosticsEditor {
  87    fn ui_name() -> &'static str {
  88        "ProjectDiagnosticsEditor"
  89    }
  90
  91    fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
  92        if self.path_states.is_empty() {
  93            let theme = &cx.global::<Settings>().theme.project_diagnostics;
  94            Label::new("No problems in workspace", theme.empty_message.clone())
  95                .aligned()
  96                .contained()
  97                .with_style(theme.container)
  98                .boxed()
  99        } else {
 100            ChildView::new(&self.editor, cx).boxed()
 101        }
 102    }
 103
 104    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
 105        if cx.is_self_focused() && !self.path_states.is_empty() {
 106            cx.focus(&self.editor);
 107        }
 108    }
 109
 110    fn debug_json(&self, cx: &AppContext) -> serde_json::Value {
 111        let project = self.project.read(cx);
 112        json!({
 113            "project": json!({
 114                "language_servers": project.language_server_statuses().collect::<Vec<_>>(),
 115                "summary": project.diagnostic_summary(cx),
 116            }),
 117            "summary": self.summary,
 118            "paths_to_update": self.paths_to_update.iter().map(|(path, server_id)|
 119                (path.path.to_string_lossy(), server_id)
 120            ).collect::<Vec<_>>(),
 121            "paths_states": self.path_states.iter().map(|state|
 122                json!({
 123                    "path": state.path.path.to_string_lossy(),
 124                    "groups": state.diagnostic_groups.iter().map(|group|
 125                        json!({
 126                            "block_count": group.blocks.len(),
 127                            "excerpt_count": group.excerpts.len(),
 128                        })
 129                    ).collect::<Vec<_>>(),
 130                })
 131            ).collect::<Vec<_>>(),
 132        })
 133    }
 134}
 135
 136impl ProjectDiagnosticsEditor {
 137    fn new(
 138        project_handle: ModelHandle<Project>,
 139        workspace: WeakViewHandle<Workspace>,
 140        cx: &mut ViewContext<Self>,
 141    ) -> Self {
 142        cx.subscribe(&project_handle, |this, _, event, cx| match event {
 143            project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
 144                this.update_excerpts(Some(*language_server_id), cx);
 145                this.update_title(cx);
 146            }
 147            project::Event::DiagnosticsUpdated {
 148                language_server_id,
 149                path,
 150            } => {
 151                this.paths_to_update
 152                    .insert(path.clone(), *language_server_id);
 153            }
 154            _ => {}
 155        })
 156        .detach();
 157
 158        let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
 159        let editor = cx.add_view(|cx| {
 160            let mut editor =
 161                Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);
 162            editor.set_vertical_scroll_margin(5, cx);
 163            editor
 164        });
 165        cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
 166            .detach();
 167
 168        let project = project_handle.read(cx);
 169        let paths_to_update = project
 170            .diagnostic_summaries(cx)
 171            .map(|e| (e.0, e.1.language_server_id))
 172            .collect();
 173        let summary = project.diagnostic_summary(cx);
 174        let mut this = Self {
 175            project: project_handle,
 176            summary,
 177            workspace,
 178            excerpts,
 179            editor,
 180            path_states: Default::default(),
 181            paths_to_update,
 182        };
 183        this.update_excerpts(None, cx);
 184        this
 185    }
 186
 187    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
 188        if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
 189            workspace.activate_item(&existing, cx);
 190        } else {
 191            let workspace_handle = cx.weak_handle();
 192            let diagnostics = cx.add_view(|cx| {
 193                ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
 194            });
 195            workspace.add_item(Box::new(diagnostics), cx);
 196        }
 197    }
 198
 199    fn update_excerpts(&mut self, language_server_id: Option<usize>, cx: &mut ViewContext<Self>) {
 200        let mut paths = Vec::new();
 201        self.paths_to_update.retain(|path, server_id| {
 202            if language_server_id
 203                .map_or(true, |language_server_id| language_server_id == *server_id)
 204            {
 205                paths.push(path.clone());
 206                false
 207            } else {
 208                true
 209            }
 210        });
 211        let project = self.project.clone();
 212        cx.spawn(|this, mut cx| {
 213            async move {
 214                for path in paths {
 215                    let buffer = project
 216                        .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
 217                        .await?;
 218                    this.update(&mut cx, |this, cx| this.populate_excerpts(path, buffer, cx))
 219                }
 220                Result::<_, anyhow::Error>::Ok(())
 221            }
 222            .log_err()
 223        })
 224        .detach();
 225    }
 226
 227    fn populate_excerpts(
 228        &mut self,
 229        path: ProjectPath,
 230        buffer: ModelHandle<Buffer>,
 231        cx: &mut ViewContext<Self>,
 232    ) {
 233        let was_empty = self.path_states.is_empty();
 234        let snapshot = buffer.read(cx).snapshot();
 235        let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
 236            Ok(ix) => ix,
 237            Err(ix) => {
 238                self.path_states.insert(
 239                    ix,
 240                    PathState {
 241                        path: path.clone(),
 242                        diagnostic_groups: Default::default(),
 243                    },
 244                );
 245                ix
 246            }
 247        };
 248
 249        let mut prev_excerpt_id = if path_ix > 0 {
 250            let prev_path_last_group = &self.path_states[path_ix - 1]
 251                .diagnostic_groups
 252                .last()
 253                .unwrap();
 254            prev_path_last_group.excerpts.last().unwrap().clone()
 255        } else {
 256            ExcerptId::min()
 257        };
 258
 259        let path_state = &mut self.path_states[path_ix];
 260        let mut groups_to_add = Vec::new();
 261        let mut group_ixs_to_remove = Vec::new();
 262        let mut blocks_to_add = Vec::new();
 263        let mut blocks_to_remove = HashSet::default();
 264        let mut first_excerpt_id = None;
 265        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
 266            let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
 267            let mut new_groups = snapshot
 268                .diagnostic_groups()
 269                .into_iter()
 270                .filter(|group| {
 271                    group.entries[group.primary_ix].diagnostic.severity
 272                        <= DiagnosticSeverity::WARNING
 273                })
 274                .peekable();
 275            loop {
 276                let mut to_insert = None;
 277                let mut to_remove = None;
 278                let mut to_keep = None;
 279                match (old_groups.peek(), new_groups.peek()) {
 280                    (None, None) => break,
 281                    (None, Some(_)) => to_insert = new_groups.next(),
 282                    (Some(_), None) => to_remove = old_groups.next(),
 283                    (Some((_, old_group)), Some(new_group)) => {
 284                        let old_primary = &old_group.primary_diagnostic;
 285                        let new_primary = &new_group.entries[new_group.primary_ix];
 286                        match compare_diagnostics(old_primary, new_primary, &snapshot) {
 287                            Ordering::Less => to_remove = old_groups.next(),
 288                            Ordering::Equal => {
 289                                to_keep = old_groups.next();
 290                                new_groups.next();
 291                            }
 292                            Ordering::Greater => to_insert = new_groups.next(),
 293                        }
 294                    }
 295                }
 296
 297                if let Some(group) = to_insert {
 298                    let mut group_state = DiagnosticGroupState {
 299                        primary_diagnostic: group.entries[group.primary_ix].clone(),
 300                        primary_excerpt_ix: 0,
 301                        excerpts: Default::default(),
 302                        blocks: Default::default(),
 303                        block_count: 0,
 304                    };
 305                    let mut pending_range: Option<(Range<Point>, usize)> = None;
 306                    let mut is_first_excerpt_for_group = true;
 307                    for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
 308                        let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
 309                        if let Some((range, start_ix)) = &mut pending_range {
 310                            if let Some(entry) = resolved_entry.as_ref() {
 311                                if entry.range.start.row
 312                                    <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
 313                                {
 314                                    range.end = range.end.max(entry.range.end);
 315                                    continue;
 316                                }
 317                            }
 318
 319                            let excerpt_start =
 320                                Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
 321                            let excerpt_end = snapshot.clip_point(
 322                                Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
 323                                Bias::Left,
 324                            );
 325                            let excerpt_id = excerpts
 326                                .insert_excerpts_after(
 327                                    prev_excerpt_id,
 328                                    buffer.clone(),
 329                                    [ExcerptRange {
 330                                        context: excerpt_start..excerpt_end,
 331                                        primary: Some(range.clone()),
 332                                    }],
 333                                    excerpts_cx,
 334                                )
 335                                .pop()
 336                                .unwrap();
 337
 338                            prev_excerpt_id = excerpt_id.clone();
 339                            first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
 340                            group_state.excerpts.push(excerpt_id.clone());
 341                            let header_position = (excerpt_id.clone(), language::Anchor::MIN);
 342
 343                            if is_first_excerpt_for_group {
 344                                is_first_excerpt_for_group = false;
 345                                let mut primary =
 346                                    group.entries[group.primary_ix].diagnostic.clone();
 347                                primary.message =
 348                                    primary.message.split('\n').next().unwrap().to_string();
 349                                group_state.block_count += 1;
 350                                blocks_to_add.push(BlockProperties {
 351                                    position: header_position,
 352                                    height: 2,
 353                                    style: BlockStyle::Sticky,
 354                                    render: diagnostic_header_renderer(primary),
 355                                    disposition: BlockDisposition::Above,
 356                                });
 357                            }
 358
 359                            for entry in &group.entries[*start_ix..ix] {
 360                                let mut diagnostic = entry.diagnostic.clone();
 361                                if diagnostic.is_primary {
 362                                    group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
 363                                    diagnostic.message =
 364                                        entry.diagnostic.message.split('\n').skip(1).collect();
 365                                }
 366
 367                                if !diagnostic.message.is_empty() {
 368                                    group_state.block_count += 1;
 369                                    blocks_to_add.push(BlockProperties {
 370                                        position: (excerpt_id.clone(), entry.range.start),
 371                                        height: diagnostic.message.matches('\n').count() as u8 + 1,
 372                                        style: BlockStyle::Fixed,
 373                                        render: diagnostic_block_renderer(diagnostic, true),
 374                                        disposition: BlockDisposition::Below,
 375                                    });
 376                                }
 377                            }
 378
 379                            pending_range.take();
 380                        }
 381
 382                        if let Some(entry) = resolved_entry {
 383                            pending_range = Some((entry.range.clone(), ix));
 384                        }
 385                    }
 386
 387                    groups_to_add.push(group_state);
 388                } else if let Some((group_ix, group_state)) = to_remove {
 389                    excerpts.remove_excerpts(group_state.excerpts.iter().copied(), excerpts_cx);
 390                    group_ixs_to_remove.push(group_ix);
 391                    blocks_to_remove.extend(group_state.blocks.iter().copied());
 392                } else if let Some((_, group)) = to_keep {
 393                    prev_excerpt_id = group.excerpts.last().unwrap().clone();
 394                    first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
 395                }
 396            }
 397
 398            excerpts.snapshot(excerpts_cx)
 399        });
 400
 401        self.editor.update(cx, |editor, cx| {
 402            editor.remove_blocks(blocks_to_remove, cx);
 403            let block_ids = editor.insert_blocks(
 404                blocks_to_add.into_iter().map(|block| {
 405                    let (excerpt_id, text_anchor) = block.position;
 406                    BlockProperties {
 407                        position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
 408                        height: block.height,
 409                        style: block.style,
 410                        render: block.render,
 411                        disposition: block.disposition,
 412                    }
 413                }),
 414                cx,
 415            );
 416
 417            let mut block_ids = block_ids.into_iter();
 418            for group_state in &mut groups_to_add {
 419                group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
 420            }
 421        });
 422
 423        for ix in group_ixs_to_remove.into_iter().rev() {
 424            path_state.diagnostic_groups.remove(ix);
 425        }
 426        path_state.diagnostic_groups.extend(groups_to_add);
 427        path_state.diagnostic_groups.sort_unstable_by(|a, b| {
 428            let range_a = &a.primary_diagnostic.range;
 429            let range_b = &b.primary_diagnostic.range;
 430            range_a
 431                .start
 432                .cmp(&range_b.start, &snapshot)
 433                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot))
 434        });
 435
 436        if path_state.diagnostic_groups.is_empty() {
 437            self.path_states.remove(path_ix);
 438        }
 439
 440        self.editor.update(cx, |editor, cx| {
 441            let groups;
 442            let mut selections;
 443            let new_excerpt_ids_by_selection_id;
 444            if was_empty {
 445                groups = self.path_states.first()?.diagnostic_groups.as_slice();
 446                new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
 447                selections = vec![Selection {
 448                    id: 0,
 449                    start: 0,
 450                    end: 0,
 451                    reversed: false,
 452                    goal: SelectionGoal::None,
 453                }];
 454            } else {
 455                groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
 456                new_excerpt_ids_by_selection_id =
 457                    editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
 458                selections = editor.selections.all::<usize>(cx);
 459            }
 460
 461            // If any selection has lost its position, move it to start of the next primary diagnostic.
 462            let snapshot = editor.snapshot(cx);
 463            for selection in &mut selections {
 464                if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
 465                    let group_ix = match groups.binary_search_by(|probe| {
 466                        probe
 467                            .excerpts
 468                            .last()
 469                            .unwrap()
 470                            .cmp(new_excerpt_id, &snapshot.buffer_snapshot)
 471                    }) {
 472                        Ok(ix) | Err(ix) => ix,
 473                    };
 474                    if let Some(group) = groups.get(group_ix) {
 475                        let offset = excerpts_snapshot
 476                            .anchor_in_excerpt(
 477                                group.excerpts[group.primary_excerpt_ix].clone(),
 478                                group.primary_diagnostic.range.start,
 479                            )
 480                            .to_offset(&excerpts_snapshot);
 481                        selection.start = offset;
 482                        selection.end = offset;
 483                    }
 484                }
 485            }
 486            editor.change_selections(None, cx, |s| {
 487                s.select(selections);
 488            });
 489            Some(())
 490        });
 491
 492        if self.path_states.is_empty() {
 493            if self.editor.is_focused(cx) {
 494                cx.focus_self();
 495            }
 496        } else if cx.handle().is_focused(cx) {
 497            cx.focus(&self.editor);
 498        }
 499        cx.notify();
 500    }
 501
 502    fn update_title(&mut self, cx: &mut ViewContext<Self>) {
 503        self.summary = self.project.read(cx).diagnostic_summary(cx);
 504        cx.emit(Event::TitleChanged);
 505    }
 506}
 507
 508impl Item for ProjectDiagnosticsEditor {
 509    fn tab_content(
 510        &self,
 511        _detail: Option<usize>,
 512        style: &theme::Tab,
 513        cx: &AppContext,
 514    ) -> ElementBox {
 515        render_summary(
 516            &self.summary,
 517            &style.label.text,
 518            &cx.global::<Settings>().theme.project_diagnostics,
 519        )
 520    }
 521
 522    fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
 523        self.editor.for_each_project_item(cx, f)
 524    }
 525
 526    fn is_singleton(&self, _: &AppContext) -> bool {
 527        false
 528    }
 529
 530    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
 531        self.editor
 532            .update(cx, |editor, cx| editor.navigate(data, cx))
 533    }
 534
 535    fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
 536        Some("Project Diagnostics".into())
 537    }
 538
 539    fn is_dirty(&self, cx: &AppContext) -> bool {
 540        self.excerpts.read(cx).is_dirty(cx)
 541    }
 542
 543    fn has_conflict(&self, cx: &AppContext) -> bool {
 544        self.excerpts.read(cx).has_conflict(cx)
 545    }
 546
 547    fn can_save(&self, _: &AppContext) -> bool {
 548        true
 549    }
 550
 551    fn save(
 552        &mut self,
 553        project: ModelHandle<Project>,
 554        cx: &mut ViewContext<Self>,
 555    ) -> Task<Result<()>> {
 556        self.editor.save(project, cx)
 557    }
 558
 559    fn reload(
 560        &mut self,
 561        project: ModelHandle<Project>,
 562        cx: &mut ViewContext<Self>,
 563    ) -> Task<Result<()>> {
 564        self.editor.reload(project, cx)
 565    }
 566
 567    fn save_as(
 568        &mut self,
 569        _: ModelHandle<Project>,
 570        _: PathBuf,
 571        _: &mut ViewContext<Self>,
 572    ) -> Task<Result<()>> {
 573        unreachable!()
 574    }
 575
 576    fn git_diff_recalc(
 577        &mut self,
 578        project: ModelHandle<Project>,
 579        cx: &mut ViewContext<Self>,
 580    ) -> Task<Result<()>> {
 581        self.editor
 582            .update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
 583    }
 584
 585    fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
 586        Editor::to_item_events(event)
 587    }
 588
 589    fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
 590        self.editor.update(cx, |editor, _| {
 591            editor.set_nav_history(Some(nav_history));
 592        });
 593    }
 594
 595    fn clone_on_split(
 596        &self,
 597        _workspace_id: workspace::WorkspaceId,
 598        cx: &mut ViewContext<Self>,
 599    ) -> Option<Self>
 600    where
 601        Self: Sized,
 602    {
 603        Some(ProjectDiagnosticsEditor::new(
 604            self.project.clone(),
 605            self.workspace.clone(),
 606            cx,
 607        ))
 608    }
 609
 610    fn act_as_type<'a>(
 611        &'a self,
 612        type_id: TypeId,
 613        self_handle: &'a ViewHandle<Self>,
 614        _: &'a AppContext,
 615    ) -> Option<&AnyViewHandle> {
 616        if type_id == TypeId::of::<Self>() {
 617            Some(self_handle)
 618        } else if type_id == TypeId::of::<Editor>() {
 619            Some(&self.editor)
 620        } else {
 621            None
 622        }
 623    }
 624
 625    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 626        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
 627    }
 628
 629    fn serialized_item_kind() -> Option<&'static str> {
 630        Some("diagnostics")
 631    }
 632
 633    fn deserialize(
 634        project: ModelHandle<Project>,
 635        workspace: WeakViewHandle<Workspace>,
 636        _workspace_id: workspace::WorkspaceId,
 637        _item_id: workspace::ItemId,
 638        cx: &mut ViewContext<Pane>,
 639    ) -> Task<Result<ViewHandle<Self>>> {
 640        Task::ready(Ok(cx.add_view(|cx| Self::new(project, workspace, cx))))
 641    }
 642}
 643
 644fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
 645    let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
 646    Arc::new(move |cx| {
 647        let settings = cx.global::<Settings>();
 648        let theme = &settings.theme.editor;
 649        let style = theme.diagnostic_header.clone();
 650        let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
 651        let icon_width = cx.em_width * style.icon_width_factor;
 652        let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
 653            Svg::new("icons/circle_x_mark_12.svg")
 654                .with_color(theme.error_diagnostic.message.text.color)
 655        } else {
 656            Svg::new("icons/triangle_exclamation_12.svg")
 657                .with_color(theme.warning_diagnostic.message.text.color)
 658        };
 659
 660        Flex::row()
 661            .with_child(
 662                icon.constrained()
 663                    .with_width(icon_width)
 664                    .aligned()
 665                    .contained()
 666                    .boxed(),
 667            )
 668            .with_child(
 669                Label::new(
 670                    message.clone(),
 671                    style.message.label.clone().with_font_size(font_size),
 672                )
 673                .with_highlights(highlights.clone())
 674                .contained()
 675                .with_style(style.message.container)
 676                .with_margin_left(cx.gutter_padding)
 677                .aligned()
 678                .boxed(),
 679            )
 680            .with_children(diagnostic.code.clone().map(|code| {
 681                Label::new(code, style.code.text.clone().with_font_size(font_size))
 682                    .contained()
 683                    .with_style(style.code.container)
 684                    .aligned()
 685                    .boxed()
 686            }))
 687            .contained()
 688            .with_style(style.container)
 689            .with_padding_left(cx.gutter_padding)
 690            .with_padding_right(cx.gutter_padding)
 691            .expanded()
 692            .named("diagnostic header")
 693    })
 694}
 695
 696pub(crate) fn render_summary(
 697    summary: &DiagnosticSummary,
 698    text_style: &TextStyle,
 699    theme: &theme::ProjectDiagnostics,
 700) -> ElementBox {
 701    if summary.error_count == 0 && summary.warning_count == 0 {
 702        Label::new("No problems", text_style.clone()).boxed()
 703    } else {
 704        let icon_width = theme.tab_icon_width;
 705        let icon_spacing = theme.tab_icon_spacing;
 706        let summary_spacing = theme.tab_summary_spacing;
 707        Flex::row()
 708            .with_children([
 709                Svg::new("icons/circle_x_mark_12.svg")
 710                    .with_color(text_style.color)
 711                    .constrained()
 712                    .with_width(icon_width)
 713                    .aligned()
 714                    .contained()
 715                    .with_margin_right(icon_spacing)
 716                    .named("no-icon"),
 717                Label::new(
 718                    summary.error_count.to_string(),
 719                    LabelStyle {
 720                        text: text_style.clone(),
 721                        highlight_text: None,
 722                    },
 723                )
 724                .aligned()
 725                .boxed(),
 726                Svg::new("icons/triangle_exclamation_12.svg")
 727                    .with_color(text_style.color)
 728                    .constrained()
 729                    .with_width(icon_width)
 730                    .aligned()
 731                    .contained()
 732                    .with_margin_left(summary_spacing)
 733                    .with_margin_right(icon_spacing)
 734                    .named("warn-icon"),
 735                Label::new(
 736                    summary.warning_count.to_string(),
 737                    LabelStyle {
 738                        text: text_style.clone(),
 739                        highlight_text: None,
 740                    },
 741                )
 742                .aligned()
 743                .boxed(),
 744            ])
 745            .boxed()
 746    }
 747}
 748
 749fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 750    lhs: &DiagnosticEntry<L>,
 751    rhs: &DiagnosticEntry<R>,
 752    snapshot: &language::BufferSnapshot,
 753) -> Ordering {
 754    lhs.range
 755        .start
 756        .to_offset(snapshot)
 757        .cmp(&rhs.range.start.to_offset(snapshot))
 758        .then_with(|| {
 759            lhs.range
 760                .end
 761                .to_offset(snapshot)
 762                .cmp(&rhs.range.end.to_offset(snapshot))
 763        })
 764        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 765}
 766
 767#[cfg(test)]
 768mod tests {
 769    use super::*;
 770    use editor::{
 771        display_map::{BlockContext, TransformBlock},
 772        DisplayPoint,
 773    };
 774    use gpui::TestAppContext;
 775    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
 776    use serde_json::json;
 777    use unindent::Unindent as _;
 778    use workspace::AppState;
 779
 780    #[gpui::test]
 781    async fn test_diagnostics(cx: &mut TestAppContext) {
 782        let app_state = cx.update(AppState::test);
 783        app_state
 784            .fs
 785            .as_fake()
 786            .insert_tree(
 787                "/test",
 788                json!({
 789                    "consts.rs": "
 790                        const a: i32 = 'a';
 791                        const b: i32 = c;
 792                    "
 793                    .unindent(),
 794
 795                    "main.rs": "
 796                        fn main() {
 797                            let x = vec![];
 798                            let y = vec![];
 799                            a(x);
 800                            b(y);
 801                            // comment 1
 802                            // comment 2
 803                            c(y);
 804                            d(x);
 805                        }
 806                    "
 807                    .unindent(),
 808                }),
 809            )
 810            .await;
 811
 812        let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
 813        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 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(editor: &ViewHandle<Editor>, cx: &mut AppContext) -> Vec<(u32, String)> {
1185        let mut presenter = cx.build_presenter(editor.id(), 0., Default::default());
1186        let mut cx = presenter.build_layout_context(Default::default(), false, cx);
1187        cx.render(editor, |editor, cx| {
1188            let snapshot = editor.snapshot(cx);
1189            snapshot
1190                .blocks_in_range(0..snapshot.max_point().row())
1191                .filter_map(|(row, block)| {
1192                    let name = match block {
1193                        TransformBlock::Custom(block) => block
1194                            .render(&mut BlockContext {
1195                                cx,
1196                                anchor_x: 0.,
1197                                scroll_x: 0.,
1198                                gutter_padding: 0.,
1199                                gutter_width: 0.,
1200                                line_height: 0.,
1201                                em_width: 0.,
1202                            })
1203                            .name()?
1204                            .to_string(),
1205                        TransformBlock::ExcerptHeader {
1206                            starts_new_buffer, ..
1207                        } => {
1208                            if *starts_new_buffer {
1209                                "path header block".to_string()
1210                            } else {
1211                                "collapsed context".to_string()
1212                            }
1213                        }
1214                    };
1215
1216                    Some((row, name))
1217                })
1218                .collect()
1219        })
1220    }
1221}