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