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