diagnostics.rs

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