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(&self, cx: &mut ViewContext<Self>) -> Option<Self>
 602    where
 603        Self: Sized,
 604    {
 605        let diagnostics = ProjectDiagnosticsEditor::new(
 606            self.model.clone(),
 607            self.workspace.clone(),
 608            self.settings.clone(),
 609            cx,
 610        );
 611        diagnostics.editor.update(cx, |editor, cx| {
 612            let nav_history = self
 613                .editor
 614                .read(cx)
 615                .nav_history()
 616                .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle()));
 617            editor.set_nav_history(nav_history);
 618        });
 619        Some(diagnostics)
 620    }
 621
 622    fn act_as_type(
 623        &self,
 624        type_id: TypeId,
 625        self_handle: &ViewHandle<Self>,
 626        _: &AppContext,
 627    ) -> Option<AnyViewHandle> {
 628        if type_id == TypeId::of::<Self>() {
 629            Some(self_handle.into())
 630        } else if type_id == TypeId::of::<Editor>() {
 631            Some((&self.editor).into())
 632        } else {
 633            None
 634        }
 635    }
 636
 637    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 638        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
 639    }
 640}
 641
 642fn diagnostic_header_renderer(
 643    diagnostic: Diagnostic,
 644    settings: watch::Receiver<workspace::Settings>,
 645) -> RenderBlock {
 646    let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
 647    Arc::new(move |cx| {
 648        let settings = settings.borrow();
 649        let theme = &settings.theme.editor;
 650        let style = &theme.diagnostic_header;
 651        let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
 652        let icon_width = cx.em_width * style.icon_width_factor;
 653        let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
 654            Svg::new("icons/diagnostic-error-10.svg")
 655                .with_color(theme.error_diagnostic.message.text.color)
 656        } else {
 657            Svg::new("icons/diagnostic-warning-10.svg")
 658                .with_color(theme.warning_diagnostic.message.text.color)
 659        };
 660
 661        Flex::row()
 662            .with_child(
 663                icon.constrained()
 664                    .with_width(icon_width)
 665                    .aligned()
 666                    .contained()
 667                    .boxed(),
 668            )
 669            .with_child(
 670                Label::new(
 671                    message.clone(),
 672                    style.message.label.clone().with_font_size(font_size),
 673                )
 674                .with_highlights(highlights.clone())
 675                .contained()
 676                .with_style(style.message.container)
 677                .with_margin_left(cx.gutter_padding)
 678                .aligned()
 679                .boxed(),
 680            )
 681            .with_children(diagnostic.code.clone().map(|code| {
 682                Label::new(code, style.code.text.clone().with_font_size(font_size))
 683                    .contained()
 684                    .with_style(style.code.container)
 685                    .aligned()
 686                    .boxed()
 687            }))
 688            .contained()
 689            .with_style(style.container)
 690            .with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width)
 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".to_string(), 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/diagnostic-summary-error.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/diagnostic-summary-warning.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, EditorSnapshot,
 773    };
 774    use gpui::TestAppContext;
 775    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
 776    use serde_json::json;
 777    use unindent::Unindent as _;
 778    use workspace::WorkspaceParams;
 779
 780    #[gpui::test]
 781    async fn test_diagnostics(mut cx: TestAppContext) {
 782        let params = cx.update(WorkspaceParams::test);
 783        let project = params.project.clone();
 784        let workspace = cx.add_view(0, |cx| Workspace::new(&params, cx));
 785
 786        params
 787            .fs
 788            .as_fake()
 789            .insert_tree(
 790                "/test",
 791                json!({
 792                    "consts.rs": "
 793                    const a: i32 = 'a';
 794                    const b: i32 = c;
 795                "
 796                    .unindent(),
 797
 798                    "main.rs": "
 799                    fn main() {
 800                        let x = vec![];
 801                        let y = vec![];
 802                        a(x);
 803                        b(y);
 804                        // comment 1
 805                        // comment 2
 806                        c(y);
 807                        d(x);
 808                    }
 809                "
 810                    .unindent(),
 811                }),
 812            )
 813            .await;
 814
 815        project
 816            .update(&mut cx, |project, cx| {
 817                project.find_or_create_local_worktree("/test", false, cx)
 818            })
 819            .await
 820            .unwrap();
 821
 822        // Create some diagnostics
 823        project.update(&mut cx, |project, cx| {
 824            project
 825                .update_diagnostic_entries(
 826                    PathBuf::from("/test/main.rs"),
 827                    None,
 828                    vec![
 829                        DiagnosticEntry {
 830                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
 831                            diagnostic: Diagnostic {
 832                                message:
 833                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 834                                        .to_string(),
 835                                severity: DiagnosticSeverity::INFORMATION,
 836                                is_primary: false,
 837                                is_disk_based: true,
 838                                group_id: 1,
 839                                ..Default::default()
 840                            },
 841                        },
 842                        DiagnosticEntry {
 843                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
 844                            diagnostic: Diagnostic {
 845                                message:
 846                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 847                                        .to_string(),
 848                                severity: DiagnosticSeverity::INFORMATION,
 849                                is_primary: false,
 850                                is_disk_based: true,
 851                                group_id: 0,
 852                                ..Default::default()
 853                            },
 854                        },
 855                        DiagnosticEntry {
 856                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
 857                            diagnostic: Diagnostic {
 858                                message: "value moved here".to_string(),
 859                                severity: DiagnosticSeverity::INFORMATION,
 860                                is_primary: false,
 861                                is_disk_based: true,
 862                                group_id: 1,
 863                                ..Default::default()
 864                            },
 865                        },
 866                        DiagnosticEntry {
 867                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
 868                            diagnostic: Diagnostic {
 869                                message: "value moved here".to_string(),
 870                                severity: DiagnosticSeverity::INFORMATION,
 871                                is_primary: false,
 872                                is_disk_based: true,
 873                                group_id: 0,
 874                                ..Default::default()
 875                            },
 876                        },
 877                        DiagnosticEntry {
 878                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
 879                            diagnostic: Diagnostic {
 880                                message: "use of moved value\nvalue used here after move".to_string(),
 881                                severity: DiagnosticSeverity::ERROR,
 882                                is_primary: true,
 883                                is_disk_based: true,
 884                                group_id: 0,
 885                                ..Default::default()
 886                            },
 887                        },
 888                        DiagnosticEntry {
 889                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
 890                            diagnostic: Diagnostic {
 891                                message: "use of moved value\nvalue used here after move".to_string(),
 892                                severity: DiagnosticSeverity::ERROR,
 893                                is_primary: true,
 894                                is_disk_based: true,
 895                                group_id: 1,
 896                                ..Default::default()
 897                            },
 898                        },
 899                    ],
 900                    cx,
 901                )
 902                .unwrap();
 903        });
 904
 905        // Open the project diagnostics view while there are already diagnostics.
 906        let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
 907        let view = cx.add_view(0, |cx| {
 908            ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
 909        });
 910
 911        view.next_notification(&cx).await;
 912        view.update(&mut cx, |view, cx| {
 913            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
 914
 915            assert_eq!(
 916                editor_blocks(&editor, cx),
 917                [
 918                    (0, "path header block".into()),
 919                    (2, "diagnostic header".into()),
 920                    (15, "collapsed context".into()),
 921                    (16, "diagnostic header".into()),
 922                    (25, "collapsed context".into()),
 923                ]
 924            );
 925            assert_eq!(
 926                editor.text(),
 927                concat!(
 928                    //
 929                    // main.rs
 930                    //
 931                    "\n", // filename
 932                    "\n", // padding
 933                    // diagnostic group 1
 934                    "\n", // primary message
 935                    "\n", // padding
 936                    "    let x = vec![];\n",
 937                    "    let y = vec![];\n",
 938                    "\n", // supporting diagnostic
 939                    "    a(x);\n",
 940                    "    b(y);\n",
 941                    "\n", // supporting diagnostic
 942                    "    // comment 1\n",
 943                    "    // comment 2\n",
 944                    "    c(y);\n",
 945                    "\n", // supporting diagnostic
 946                    "    d(x);\n",
 947                    "\n", // context ellipsis
 948                    // diagnostic group 2
 949                    "\n", // primary message
 950                    "\n", // padding
 951                    "fn main() {\n",
 952                    "    let x = vec![];\n",
 953                    "\n", // supporting diagnostic
 954                    "    let y = vec![];\n",
 955                    "    a(x);\n",
 956                    "\n", // supporting diagnostic
 957                    "    b(y);\n",
 958                    "\n", // context ellipsis
 959                    "    c(y);\n",
 960                    "    d(x);\n",
 961                    "\n", // supporting diagnostic
 962                    "}"
 963                )
 964            );
 965
 966            // Cursor is at the first diagnostic
 967            view.editor.update(cx, |editor, cx| {
 968                assert_eq!(
 969                    editor.selected_display_ranges(cx),
 970                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
 971                );
 972            });
 973        });
 974
 975        // Diagnostics are added for another earlier path.
 976        project.update(&mut cx, |project, cx| {
 977            project.disk_based_diagnostics_started(cx);
 978            project
 979                .update_diagnostic_entries(
 980                    PathBuf::from("/test/consts.rs"),
 981                    None,
 982                    vec![DiagnosticEntry {
 983                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
 984                        diagnostic: Diagnostic {
 985                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
 986                            severity: DiagnosticSeverity::ERROR,
 987                            is_primary: true,
 988                            is_disk_based: true,
 989                            group_id: 0,
 990                            ..Default::default()
 991                        },
 992                    }],
 993                    cx,
 994                )
 995                .unwrap();
 996            project.disk_based_diagnostics_finished(cx);
 997        });
 998
 999        view.next_notification(&cx).await;
1000        view.update(&mut cx, |view, cx| {
1001            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1002
1003            assert_eq!(
1004                editor_blocks(&editor, cx),
1005                [
1006                    (0, "path header block".into()),
1007                    (2, "diagnostic header".into()),
1008                    (7, "path header block".into()),
1009                    (9, "diagnostic header".into()),
1010                    (22, "collapsed context".into()),
1011                    (23, "diagnostic header".into()),
1012                    (32, "collapsed context".into()),
1013                ]
1014            );
1015            assert_eq!(
1016                editor.text(),
1017                concat!(
1018                    //
1019                    // consts.rs
1020                    //
1021                    "\n", // filename
1022                    "\n", // padding
1023                    // diagnostic group 1
1024                    "\n", // primary message
1025                    "\n", // padding
1026                    "const a: i32 = 'a';\n",
1027                    "\n", // supporting diagnostic
1028                    "const b: i32 = c;\n",
1029                    //
1030                    // main.rs
1031                    //
1032                    "\n", // filename
1033                    "\n", // padding
1034                    // diagnostic group 1
1035                    "\n", // primary message
1036                    "\n", // padding
1037                    "    let x = vec![];\n",
1038                    "    let y = vec![];\n",
1039                    "\n", // supporting diagnostic
1040                    "    a(x);\n",
1041                    "    b(y);\n",
1042                    "\n", // supporting diagnostic
1043                    "    // comment 1\n",
1044                    "    // comment 2\n",
1045                    "    c(y);\n",
1046                    "\n", // supporting diagnostic
1047                    "    d(x);\n",
1048                    "\n", // collapsed context
1049                    // diagnostic group 2
1050                    "\n", // primary message
1051                    "\n", // filename
1052                    "fn main() {\n",
1053                    "    let x = vec![];\n",
1054                    "\n", // supporting diagnostic
1055                    "    let y = vec![];\n",
1056                    "    a(x);\n",
1057                    "\n", // supporting diagnostic
1058                    "    b(y);\n",
1059                    "\n", // context ellipsis
1060                    "    c(y);\n",
1061                    "    d(x);\n",
1062                    "\n", // supporting diagnostic
1063                    "}"
1064                )
1065            );
1066
1067            // Cursor keeps its position.
1068            view.editor.update(cx, |editor, cx| {
1069                assert_eq!(
1070                    editor.selected_display_ranges(cx),
1071                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1072                );
1073            });
1074        });
1075
1076        // Diagnostics are added to the first path
1077        project.update(&mut cx, |project, cx| {
1078            project.disk_based_diagnostics_started(cx);
1079            project
1080                .update_diagnostic_entries(
1081                    PathBuf::from("/test/consts.rs"),
1082                    None,
1083                    vec![
1084                        DiagnosticEntry {
1085                            range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1086                            diagnostic: Diagnostic {
1087                                message: "mismatched types\nexpected `usize`, found `char`"
1088                                    .to_string(),
1089                                severity: DiagnosticSeverity::ERROR,
1090                                is_primary: true,
1091                                is_disk_based: true,
1092                                group_id: 0,
1093                                ..Default::default()
1094                            },
1095                        },
1096                        DiagnosticEntry {
1097                            range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1098                            diagnostic: Diagnostic {
1099                                message: "unresolved name `c`".to_string(),
1100                                severity: DiagnosticSeverity::ERROR,
1101                                is_primary: true,
1102                                is_disk_based: true,
1103                                group_id: 1,
1104                                ..Default::default()
1105                            },
1106                        },
1107                    ],
1108                    cx,
1109                )
1110                .unwrap();
1111            project.disk_based_diagnostics_finished(cx);
1112        });
1113
1114        view.next_notification(&cx).await;
1115        view.update(&mut cx, |view, cx| {
1116            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1117
1118            assert_eq!(
1119                editor_blocks(&editor, cx),
1120                [
1121                    (0, "path header block".into()),
1122                    (2, "diagnostic header".into()),
1123                    (7, "collapsed context".into()),
1124                    (8, "diagnostic header".into()),
1125                    (13, "path header block".into()),
1126                    (15, "diagnostic header".into()),
1127                    (28, "collapsed context".into()),
1128                    (29, "diagnostic header".into()),
1129                    (38, "collapsed context".into()),
1130                ]
1131            );
1132            assert_eq!(
1133                editor.text(),
1134                concat!(
1135                    //
1136                    // consts.rs
1137                    //
1138                    "\n", // filename
1139                    "\n", // padding
1140                    // diagnostic group 1
1141                    "\n", // primary message
1142                    "\n", // padding
1143                    "const a: i32 = 'a';\n",
1144                    "\n", // supporting diagnostic
1145                    "const b: i32 = c;\n",
1146                    "\n", // context ellipsis
1147                    // diagnostic group 2
1148                    "\n", // primary message
1149                    "\n", // padding
1150                    "const a: i32 = 'a';\n",
1151                    "const b: i32 = c;\n",
1152                    "\n", // supporting diagnostic
1153                    //
1154                    // main.rs
1155                    //
1156                    "\n", // filename
1157                    "\n", // padding
1158                    // diagnostic group 1
1159                    "\n", // primary message
1160                    "\n", // padding
1161                    "    let x = vec![];\n",
1162                    "    let y = vec![];\n",
1163                    "\n", // supporting diagnostic
1164                    "    a(x);\n",
1165                    "    b(y);\n",
1166                    "\n", // supporting diagnostic
1167                    "    // comment 1\n",
1168                    "    // comment 2\n",
1169                    "    c(y);\n",
1170                    "\n", // supporting diagnostic
1171                    "    d(x);\n",
1172                    "\n", // context ellipsis
1173                    // diagnostic group 2
1174                    "\n", // primary message
1175                    "\n", // filename
1176                    "fn main() {\n",
1177                    "    let x = vec![];\n",
1178                    "\n", // supporting diagnostic
1179                    "    let y = vec![];\n",
1180                    "    a(x);\n",
1181                    "\n", // supporting diagnostic
1182                    "    b(y);\n",
1183                    "\n", // context ellipsis
1184                    "    c(y);\n",
1185                    "    d(x);\n",
1186                    "\n", // supporting diagnostic
1187                    "}"
1188                )
1189            );
1190        });
1191    }
1192
1193    fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1194        editor
1195            .blocks_in_range(0..editor.max_point().row())
1196            .filter_map(|(row, block)| {
1197                let name = match block {
1198                    TransformBlock::Custom(block) => block
1199                        .render(&BlockContext {
1200                            cx,
1201                            anchor_x: 0.,
1202                            scroll_x: 0.,
1203                            gutter_padding: 0.,
1204                            gutter_width: 0.,
1205                            line_height: 0.,
1206                            em_width: 0.,
1207                        })
1208                        .name()?
1209                        .to_string(),
1210                    TransformBlock::ExcerptHeader {
1211                        starts_new_buffer, ..
1212                    } => {
1213                        if *starts_new_buffer {
1214                            "path header block".to_string()
1215                        } else {
1216                            "collapsed context".to_string()
1217                        }
1218                    }
1219                };
1220
1221                Some((row, name))
1222            })
1223            .collect()
1224    }
1225}