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, ExcerptProperties, 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, 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.insert_excerpt_after(
 337                                &prev_excerpt_id,
 338                                ExcerptProperties {
 339                                    buffer: &buffer,
 340                                    range: excerpt_start..excerpt_end,
 341                                },
 342                                excerpts_cx,
 343                            );
 344
 345                            prev_excerpt_id = excerpt_id.clone();
 346                            first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
 347                            group_state.excerpts.push(excerpt_id.clone());
 348                            let header_position = (excerpt_id.clone(), language::Anchor::min());
 349
 350                            if is_first_excerpt_for_group {
 351                                is_first_excerpt_for_group = false;
 352                                let mut primary =
 353                                    group.entries[group.primary_ix].diagnostic.clone();
 354                                primary.message =
 355                                    primary.message.split('\n').next().unwrap().to_string();
 356                                group_state.block_count += 1;
 357                                blocks_to_add.push(BlockProperties {
 358                                    position: header_position,
 359                                    height: 2,
 360                                    render: diagnostic_header_renderer(
 361                                        primary,
 362                                        self.build_settings.clone(),
 363                                    ),
 364                                    disposition: BlockDisposition::Above,
 365                                });
 366                            }
 367
 368                            for entry in &group.entries[*start_ix..ix] {
 369                                let mut diagnostic = entry.diagnostic.clone();
 370                                if diagnostic.is_primary {
 371                                    group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
 372                                    diagnostic.message =
 373                                        entry.diagnostic.message.split('\n').skip(1).collect();
 374                                }
 375
 376                                if !diagnostic.message.is_empty() {
 377                                    group_state.block_count += 1;
 378                                    blocks_to_add.push(BlockProperties {
 379                                        position: (excerpt_id.clone(), entry.range.start.clone()),
 380                                        height: diagnostic.message.matches('\n').count() as u8 + 1,
 381                                        render: diagnostic_block_renderer(
 382                                            diagnostic,
 383                                            true,
 384                                            self.build_settings.clone(),
 385                                        ),
 386                                        disposition: BlockDisposition::Below,
 387                                    });
 388                                }
 389                            }
 390
 391                            pending_range.take();
 392                        }
 393
 394                        if let Some(entry) = resolved_entry {
 395                            pending_range = Some((entry.range.clone(), ix));
 396                        }
 397                    }
 398
 399                    groups_to_add.push(group_state);
 400                } else if let Some((group_ix, group_state)) = to_remove {
 401                    excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
 402                    group_ixs_to_remove.push(group_ix);
 403                    blocks_to_remove.extend(group_state.blocks.iter().copied());
 404                } else if let Some((_, group)) = to_keep {
 405                    prev_excerpt_id = group.excerpts.last().unwrap().clone();
 406                    first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
 407                }
 408            }
 409
 410            excerpts.snapshot(excerpts_cx)
 411        });
 412
 413        self.editor.update(cx, |editor, cx| {
 414            editor.remove_blocks(blocks_to_remove, cx);
 415            let block_ids = editor.insert_blocks(
 416                blocks_to_add.into_iter().map(|block| {
 417                    let (excerpt_id, text_anchor) = block.position;
 418                    BlockProperties {
 419                        position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
 420                        height: block.height,
 421                        render: block.render,
 422                        disposition: block.disposition,
 423                    }
 424                }),
 425                cx,
 426            );
 427
 428            let mut block_ids = block_ids.into_iter();
 429            for group_state in &mut groups_to_add {
 430                group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
 431            }
 432        });
 433
 434        for ix in group_ixs_to_remove.into_iter().rev() {
 435            path_state.diagnostic_groups.remove(ix);
 436        }
 437        path_state.diagnostic_groups.extend(groups_to_add);
 438        path_state.diagnostic_groups.sort_unstable_by(|a, b| {
 439            let range_a = &a.primary_diagnostic.range;
 440            let range_b = &b.primary_diagnostic.range;
 441            range_a
 442                .start
 443                .cmp(&range_b.start, &snapshot)
 444                .unwrap()
 445                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
 446        });
 447
 448        if path_state.diagnostic_groups.is_empty() {
 449            self.path_states.remove(path_ix);
 450        }
 451
 452        self.editor.update(cx, |editor, cx| {
 453            let groups;
 454            let mut selections;
 455            let new_excerpt_ids_by_selection_id;
 456            if was_empty {
 457                groups = self.path_states.first()?.diagnostic_groups.as_slice();
 458                new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
 459                selections = vec![Selection {
 460                    id: 0,
 461                    start: 0,
 462                    end: 0,
 463                    reversed: false,
 464                    goal: SelectionGoal::None,
 465                }];
 466            } else {
 467                groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
 468                new_excerpt_ids_by_selection_id = editor.refresh_selections(cx);
 469                selections = editor.local_selections::<usize>(cx);
 470            }
 471
 472            // If any selection has lost its position, move it to start of the next primary diagnostic.
 473            for selection in &mut selections {
 474                if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
 475                    let group_ix = match groups.binary_search_by(|probe| {
 476                        probe.excerpts.last().unwrap().cmp(&new_excerpt_id)
 477                    }) {
 478                        Ok(ix) | Err(ix) => ix,
 479                    };
 480                    if let Some(group) = groups.get(group_ix) {
 481                        let offset = excerpts_snapshot
 482                            .anchor_in_excerpt(
 483                                group.excerpts[group.primary_excerpt_ix].clone(),
 484                                group.primary_diagnostic.range.start.clone(),
 485                            )
 486                            .to_offset(&excerpts_snapshot);
 487                        selection.start = offset;
 488                        selection.end = offset;
 489                    }
 490                }
 491            }
 492            editor.update_selections(selections, None, cx);
 493            Some(())
 494        });
 495
 496        if self.path_states.is_empty() {
 497            if self.editor.is_focused(cx) {
 498                cx.focus_self();
 499            }
 500        } else {
 501            if cx.handle().is_focused(cx) {
 502                cx.focus(&self.editor);
 503            }
 504        }
 505        cx.notify();
 506    }
 507
 508    fn update_title(&mut self, cx: &mut ViewContext<Self>) {
 509        self.summary = self.model.read(cx).project.read(cx).diagnostic_summary(cx);
 510        cx.emit(Event::TitleChanged);
 511    }
 512}
 513
 514impl workspace::Item for ProjectDiagnostics {
 515    type View = ProjectDiagnosticsEditor;
 516
 517    fn build_view(
 518        handle: ModelHandle<Self>,
 519        workspace: &Workspace,
 520        nav_history: ItemNavHistory,
 521        cx: &mut ViewContext<Self::View>,
 522    ) -> Self::View {
 523        let diagnostics = ProjectDiagnosticsEditor::new(
 524            handle,
 525            workspace.weak_handle(),
 526            workspace.settings(),
 527            cx,
 528        );
 529        diagnostics
 530            .editor
 531            .update(cx, |editor, _| editor.set_nav_history(Some(nav_history)));
 532        diagnostics
 533    }
 534
 535    fn project_path(&self) -> Option<project::ProjectPath> {
 536        None
 537    }
 538}
 539
 540impl workspace::ItemView for ProjectDiagnosticsEditor {
 541    fn item_id(&self, _: &AppContext) -> usize {
 542        self.model.id()
 543    }
 544
 545    fn tab_content(&self, style: &theme::Tab, _: &AppContext) -> ElementBox {
 546        render_summary(
 547            &self.summary,
 548            &style.label.text,
 549            &self.settings.borrow().theme.project_diagnostics,
 550        )
 551    }
 552
 553    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
 554        None
 555    }
 556
 557    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
 558        self.editor
 559            .update(cx, |editor, cx| editor.navigate(data, cx));
 560    }
 561
 562    fn is_dirty(&self, cx: &AppContext) -> bool {
 563        self.excerpts.read(cx).read(cx).is_dirty()
 564    }
 565
 566    fn has_conflict(&self, cx: &AppContext) -> bool {
 567        self.excerpts.read(cx).read(cx).has_conflict()
 568    }
 569
 570    fn can_save(&self, _: &AppContext) -> bool {
 571        true
 572    }
 573
 574    fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
 575        self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
 576    }
 577
 578    fn can_save_as(&self, _: &AppContext) -> bool {
 579        false
 580    }
 581
 582    fn save_as(
 583        &mut self,
 584        _: ModelHandle<Project>,
 585        _: PathBuf,
 586        _: &mut ViewContext<Self>,
 587    ) -> Task<Result<()>> {
 588        unreachable!()
 589    }
 590
 591    fn should_activate_item_on_event(event: &Self::Event) -> bool {
 592        Editor::should_activate_item_on_event(event)
 593    }
 594
 595    fn should_update_tab_on_event(event: &Event) -> bool {
 596        matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
 597    }
 598
 599    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
 600    where
 601        Self: Sized,
 602    {
 603        let diagnostics = ProjectDiagnosticsEditor::new(
 604            self.model.clone(),
 605            self.workspace.clone(),
 606            self.settings.clone(),
 607            cx,
 608        );
 609        diagnostics.editor.update(cx, |editor, cx| {
 610            let nav_history = self
 611                .editor
 612                .read(cx)
 613                .nav_history()
 614                .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle()));
 615            editor.set_nav_history(nav_history);
 616        });
 617        Some(diagnostics)
 618    }
 619
 620    fn act_as_type(
 621        &self,
 622        type_id: TypeId,
 623        self_handle: &ViewHandle<Self>,
 624        _: &AppContext,
 625    ) -> Option<AnyViewHandle> {
 626        if type_id == TypeId::of::<Self>() {
 627            Some(self_handle.into())
 628        } else if type_id == TypeId::of::<Editor>() {
 629            Some((&self.editor).into())
 630        } else {
 631            None
 632        }
 633    }
 634
 635    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 636        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
 637    }
 638}
 639
 640fn diagnostic_header_renderer(
 641    diagnostic: Diagnostic,
 642    build_settings: BuildSettings,
 643) -> RenderBlock {
 644    let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
 645    Arc::new(move |cx| {
 646        let settings = build_settings(cx);
 647        let style = &settings.style.diagnostic_header;
 648        let font_size = (style.text_scale_factor * settings.style.text.font_size).round();
 649        let icon_width = cx.em_width * style.icon_width_factor;
 650        let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
 651            Svg::new("icons/diagnostic-error-10.svg")
 652                .with_color(settings.style.error_diagnostic.message.text.color)
 653        } else {
 654            Svg::new("icons/diagnostic-warning-10.svg")
 655                .with_color(settings.style.warning_diagnostic.message.text.color)
 656        };
 657
 658        Flex::row()
 659            .with_child(
 660                icon.constrained()
 661                    .with_width(icon_width)
 662                    .aligned()
 663                    .contained()
 664                    .boxed(),
 665            )
 666            .with_child(
 667                Label::new(
 668                    message.clone(),
 669                    style.message.label.clone().with_font_size(font_size),
 670                )
 671                .with_highlights(highlights.clone())
 672                .contained()
 673                .with_style(style.message.container)
 674                .with_margin_left(cx.gutter_padding)
 675                .aligned()
 676                .boxed(),
 677            )
 678            .with_children(diagnostic.code.clone().map(|code| {
 679                Label::new(code, style.code.text.clone().with_font_size(font_size))
 680                    .contained()
 681                    .with_style(style.code.container)
 682                    .aligned()
 683                    .boxed()
 684            }))
 685            .contained()
 686            .with_style(style.container)
 687            .with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width)
 688            .expanded()
 689            .named("diagnostic header")
 690    })
 691}
 692
 693pub(crate) fn render_summary(
 694    summary: &DiagnosticSummary,
 695    text_style: &TextStyle,
 696    theme: &theme::ProjectDiagnostics,
 697) -> ElementBox {
 698    if summary.error_count == 0 && summary.warning_count == 0 {
 699        Label::new("No problems".to_string(), text_style.clone()).boxed()
 700    } else {
 701        let icon_width = theme.tab_icon_width;
 702        let icon_spacing = theme.tab_icon_spacing;
 703        let summary_spacing = theme.tab_summary_spacing;
 704        Flex::row()
 705            .with_children([
 706                Svg::new("icons/diagnostic-summary-error.svg")
 707                    .with_color(text_style.color)
 708                    .constrained()
 709                    .with_width(icon_width)
 710                    .aligned()
 711                    .contained()
 712                    .with_margin_right(icon_spacing)
 713                    .named("no-icon"),
 714                Label::new(
 715                    summary.error_count.to_string(),
 716                    LabelStyle {
 717                        text: text_style.clone(),
 718                        highlight_text: None,
 719                    },
 720                )
 721                .aligned()
 722                .boxed(),
 723                Svg::new("icons/diagnostic-summary-warning.svg")
 724                    .with_color(text_style.color)
 725                    .constrained()
 726                    .with_width(icon_width)
 727                    .aligned()
 728                    .contained()
 729                    .with_margin_left(summary_spacing)
 730                    .with_margin_right(icon_spacing)
 731                    .named("warn-icon"),
 732                Label::new(
 733                    summary.warning_count.to_string(),
 734                    LabelStyle {
 735                        text: text_style.clone(),
 736                        highlight_text: None,
 737                    },
 738                )
 739                .aligned()
 740                .boxed(),
 741            ])
 742            .boxed()
 743    }
 744}
 745
 746fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 747    lhs: &DiagnosticEntry<L>,
 748    rhs: &DiagnosticEntry<R>,
 749    snapshot: &language::BufferSnapshot,
 750) -> Ordering {
 751    lhs.range
 752        .start
 753        .to_offset(&snapshot)
 754        .cmp(&rhs.range.start.to_offset(snapshot))
 755        .then_with(|| {
 756            lhs.range
 757                .end
 758                .to_offset(&snapshot)
 759                .cmp(&rhs.range.end.to_offset(snapshot))
 760        })
 761        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 762}
 763
 764#[cfg(test)]
 765mod tests {
 766    use super::*;
 767    use editor::{
 768        display_map::{BlockContext, TransformBlock},
 769        DisplayPoint, EditorSnapshot,
 770    };
 771    use gpui::TestAppContext;
 772    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
 773    use serde_json::json;
 774    use unindent::Unindent as _;
 775    use workspace::WorkspaceParams;
 776
 777    #[gpui::test]
 778    async fn test_diagnostics(mut cx: TestAppContext) {
 779        let params = cx.update(WorkspaceParams::test);
 780        let project = params.project.clone();
 781        let workspace = cx.add_view(0, |cx| Workspace::new(&params, cx));
 782
 783        params
 784            .fs
 785            .as_fake()
 786            .insert_tree(
 787                "/test",
 788                json!({
 789                    "consts.rs": "
 790                    const a: i32 = 'a';
 791                    const b: i32 = c;
 792                "
 793                    .unindent(),
 794
 795                    "main.rs": "
 796                    fn main() {
 797                        let x = vec![];
 798                        let y = vec![];
 799                        a(x);
 800                        b(y);
 801                        // comment 1
 802                        // comment 2
 803                        c(y);
 804                        d(x);
 805                    }
 806                "
 807                    .unindent(),
 808                }),
 809            )
 810            .await;
 811
 812        project
 813            .update(&mut cx, |project, cx| {
 814                project.find_or_create_local_worktree("/test", false, cx)
 815            })
 816            .await
 817            .unwrap();
 818
 819        // Create some diagnostics
 820        project.update(&mut cx, |project, cx| {
 821            project
 822                .update_diagnostic_entries(
 823                    PathBuf::from("/test/main.rs"),
 824                    None,
 825                    vec![
 826                        DiagnosticEntry {
 827                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
 828                            diagnostic: Diagnostic {
 829                                message:
 830                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 831                                        .to_string(),
 832                                severity: DiagnosticSeverity::INFORMATION,
 833                                is_primary: false,
 834                                is_disk_based: true,
 835                                group_id: 1,
 836                                ..Default::default()
 837                            },
 838                        },
 839                        DiagnosticEntry {
 840                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
 841                            diagnostic: Diagnostic {
 842                                message:
 843                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 844                                        .to_string(),
 845                                severity: DiagnosticSeverity::INFORMATION,
 846                                is_primary: false,
 847                                is_disk_based: true,
 848                                group_id: 0,
 849                                ..Default::default()
 850                            },
 851                        },
 852                        DiagnosticEntry {
 853                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
 854                            diagnostic: Diagnostic {
 855                                message: "value moved here".to_string(),
 856                                severity: DiagnosticSeverity::INFORMATION,
 857                                is_primary: false,
 858                                is_disk_based: true,
 859                                group_id: 1,
 860                                ..Default::default()
 861                            },
 862                        },
 863                        DiagnosticEntry {
 864                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
 865                            diagnostic: Diagnostic {
 866                                message: "value moved here".to_string(),
 867                                severity: DiagnosticSeverity::INFORMATION,
 868                                is_primary: false,
 869                                is_disk_based: true,
 870                                group_id: 0,
 871                                ..Default::default()
 872                            },
 873                        },
 874                        DiagnosticEntry {
 875                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
 876                            diagnostic: Diagnostic {
 877                                message: "use of moved value\nvalue used here after move".to_string(),
 878                                severity: DiagnosticSeverity::ERROR,
 879                                is_primary: true,
 880                                is_disk_based: true,
 881                                group_id: 0,
 882                                ..Default::default()
 883                            },
 884                        },
 885                        DiagnosticEntry {
 886                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
 887                            diagnostic: Diagnostic {
 888                                message: "use of moved value\nvalue used here after move".to_string(),
 889                                severity: DiagnosticSeverity::ERROR,
 890                                is_primary: true,
 891                                is_disk_based: true,
 892                                group_id: 1,
 893                                ..Default::default()
 894                            },
 895                        },
 896                    ],
 897                    cx,
 898                )
 899                .unwrap();
 900        });
 901
 902        // Open the project diagnostics view while there are already diagnostics.
 903        let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
 904        let view = cx.add_view(0, |cx| {
 905            ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
 906        });
 907
 908        view.next_notification(&cx).await;
 909        view.update(&mut cx, |view, cx| {
 910            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
 911
 912            assert_eq!(
 913                editor_blocks(&editor, cx),
 914                [
 915                    (0, "path header block".into()),
 916                    (2, "diagnostic header".into()),
 917                    (15, "collapsed context".into()),
 918                    (16, "diagnostic header".into()),
 919                    (25, "collapsed context".into()),
 920                ]
 921            );
 922            assert_eq!(
 923                editor.text(),
 924                concat!(
 925                    //
 926                    // main.rs
 927                    //
 928                    "\n", // filename
 929                    "\n", // padding
 930                    // diagnostic group 1
 931                    "\n", // primary message
 932                    "\n", // padding
 933                    "    let x = vec![];\n",
 934                    "    let y = vec![];\n",
 935                    "\n", // supporting diagnostic
 936                    "    a(x);\n",
 937                    "    b(y);\n",
 938                    "\n", // supporting diagnostic
 939                    "    // comment 1\n",
 940                    "    // comment 2\n",
 941                    "    c(y);\n",
 942                    "\n", // supporting diagnostic
 943                    "    d(x);\n",
 944                    "\n", // context ellipsis
 945                    // diagnostic group 2
 946                    "\n", // primary message
 947                    "\n", // padding
 948                    "fn main() {\n",
 949                    "    let x = vec![];\n",
 950                    "\n", // supporting diagnostic
 951                    "    let y = vec![];\n",
 952                    "    a(x);\n",
 953                    "\n", // supporting diagnostic
 954                    "    b(y);\n",
 955                    "\n", // context ellipsis
 956                    "    c(y);\n",
 957                    "    d(x);\n",
 958                    "\n", // supporting diagnostic
 959                    "}"
 960                )
 961            );
 962
 963            // Cursor is at the first diagnostic
 964            view.editor.update(cx, |editor, cx| {
 965                assert_eq!(
 966                    editor.selected_display_ranges(cx),
 967                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
 968                );
 969            });
 970        });
 971
 972        // Diagnostics are added for another earlier path.
 973        project.update(&mut cx, |project, cx| {
 974            project.disk_based_diagnostics_started(cx);
 975            project
 976                .update_diagnostic_entries(
 977                    PathBuf::from("/test/consts.rs"),
 978                    None,
 979                    vec![DiagnosticEntry {
 980                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
 981                        diagnostic: Diagnostic {
 982                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
 983                            severity: DiagnosticSeverity::ERROR,
 984                            is_primary: true,
 985                            is_disk_based: true,
 986                            group_id: 0,
 987                            ..Default::default()
 988                        },
 989                    }],
 990                    cx,
 991                )
 992                .unwrap();
 993            project.disk_based_diagnostics_finished(cx);
 994        });
 995
 996        view.next_notification(&cx).await;
 997        view.update(&mut cx, |view, cx| {
 998            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
 999
1000            assert_eq!(
1001                editor_blocks(&editor, cx),
1002                [
1003                    (0, "path header block".into()),
1004                    (2, "diagnostic header".into()),
1005                    (7, "path header block".into()),
1006                    (9, "diagnostic header".into()),
1007                    (22, "collapsed context".into()),
1008                    (23, "diagnostic header".into()),
1009                    (32, "collapsed context".into()),
1010                ]
1011            );
1012            assert_eq!(
1013                editor.text(),
1014                concat!(
1015                    //
1016                    // consts.rs
1017                    //
1018                    "\n", // filename
1019                    "\n", // padding
1020                    // diagnostic group 1
1021                    "\n", // primary message
1022                    "\n", // padding
1023                    "const a: i32 = 'a';\n",
1024                    "\n", // supporting diagnostic
1025                    "const b: i32 = c;\n",
1026                    //
1027                    // main.rs
1028                    //
1029                    "\n", // filename
1030                    "\n", // padding
1031                    // diagnostic group 1
1032                    "\n", // primary message
1033                    "\n", // padding
1034                    "    let x = vec![];\n",
1035                    "    let y = vec![];\n",
1036                    "\n", // supporting diagnostic
1037                    "    a(x);\n",
1038                    "    b(y);\n",
1039                    "\n", // supporting diagnostic
1040                    "    // comment 1\n",
1041                    "    // comment 2\n",
1042                    "    c(y);\n",
1043                    "\n", // supporting diagnostic
1044                    "    d(x);\n",
1045                    "\n", // collapsed context
1046                    // diagnostic group 2
1047                    "\n", // primary message
1048                    "\n", // filename
1049                    "fn main() {\n",
1050                    "    let x = vec![];\n",
1051                    "\n", // supporting diagnostic
1052                    "    let y = vec![];\n",
1053                    "    a(x);\n",
1054                    "\n", // supporting diagnostic
1055                    "    b(y);\n",
1056                    "\n", // context ellipsis
1057                    "    c(y);\n",
1058                    "    d(x);\n",
1059                    "\n", // supporting diagnostic
1060                    "}"
1061                )
1062            );
1063
1064            // Cursor keeps its position.
1065            view.editor.update(cx, |editor, cx| {
1066                assert_eq!(
1067                    editor.selected_display_ranges(cx),
1068                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1069                );
1070            });
1071        });
1072
1073        // Diagnostics are added to the first path
1074        project.update(&mut cx, |project, cx| {
1075            project.disk_based_diagnostics_started(cx);
1076            project
1077                .update_diagnostic_entries(
1078                    PathBuf::from("/test/consts.rs"),
1079                    None,
1080                    vec![
1081                        DiagnosticEntry {
1082                            range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1083                            diagnostic: Diagnostic {
1084                                message: "mismatched types\nexpected `usize`, found `char`"
1085                                    .to_string(),
1086                                severity: DiagnosticSeverity::ERROR,
1087                                is_primary: true,
1088                                is_disk_based: true,
1089                                group_id: 0,
1090                                ..Default::default()
1091                            },
1092                        },
1093                        DiagnosticEntry {
1094                            range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1095                            diagnostic: Diagnostic {
1096                                message: "unresolved name `c`".to_string(),
1097                                severity: DiagnosticSeverity::ERROR,
1098                                is_primary: true,
1099                                is_disk_based: true,
1100                                group_id: 1,
1101                                ..Default::default()
1102                            },
1103                        },
1104                    ],
1105                    cx,
1106                )
1107                .unwrap();
1108            project.disk_based_diagnostics_finished(cx);
1109        });
1110
1111        view.next_notification(&cx).await;
1112        view.update(&mut cx, |view, cx| {
1113            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1114
1115            assert_eq!(
1116                editor_blocks(&editor, cx),
1117                [
1118                    (0, "path header block".into()),
1119                    (2, "diagnostic header".into()),
1120                    (7, "collapsed context".into()),
1121                    (8, "diagnostic header".into()),
1122                    (13, "path header block".into()),
1123                    (15, "diagnostic header".into()),
1124                    (28, "collapsed context".into()),
1125                    (29, "diagnostic header".into()),
1126                    (38, "collapsed context".into()),
1127                ]
1128            );
1129            assert_eq!(
1130                editor.text(),
1131                concat!(
1132                    //
1133                    // consts.rs
1134                    //
1135                    "\n", // filename
1136                    "\n", // padding
1137                    // diagnostic group 1
1138                    "\n", // primary message
1139                    "\n", // padding
1140                    "const a: i32 = 'a';\n",
1141                    "\n", // supporting diagnostic
1142                    "const b: i32 = c;\n",
1143                    "\n", // context ellipsis
1144                    // diagnostic group 2
1145                    "\n", // primary message
1146                    "\n", // padding
1147                    "const a: i32 = 'a';\n",
1148                    "const b: i32 = c;\n",
1149                    "\n", // supporting diagnostic
1150                    //
1151                    // main.rs
1152                    //
1153                    "\n", // filename
1154                    "\n", // padding
1155                    // diagnostic group 1
1156                    "\n", // primary message
1157                    "\n", // padding
1158                    "    let x = vec![];\n",
1159                    "    let y = vec![];\n",
1160                    "\n", // supporting diagnostic
1161                    "    a(x);\n",
1162                    "    b(y);\n",
1163                    "\n", // supporting diagnostic
1164                    "    // comment 1\n",
1165                    "    // comment 2\n",
1166                    "    c(y);\n",
1167                    "\n", // supporting diagnostic
1168                    "    d(x);\n",
1169                    "\n", // context ellipsis
1170                    // diagnostic group 2
1171                    "\n", // primary message
1172                    "\n", // filename
1173                    "fn main() {\n",
1174                    "    let x = vec![];\n",
1175                    "\n", // supporting diagnostic
1176                    "    let y = vec![];\n",
1177                    "    a(x);\n",
1178                    "\n", // supporting diagnostic
1179                    "    b(y);\n",
1180                    "\n", // context ellipsis
1181                    "    c(y);\n",
1182                    "    d(x);\n",
1183                    "\n", // supporting diagnostic
1184                    "}"
1185                )
1186            );
1187        });
1188    }
1189
1190    fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1191        editor
1192            .blocks_in_range(0..editor.max_point().row())
1193            .filter_map(|(row, block)| {
1194                let name = match block {
1195                    TransformBlock::Custom(block) => block
1196                        .render(&BlockContext {
1197                            cx,
1198                            anchor_x: 0.,
1199                            scroll_x: 0.,
1200                            gutter_padding: 0.,
1201                            gutter_width: 0.,
1202                            line_height: 0.,
1203                            em_width: 0.,
1204                        })
1205                        .name()?
1206                        .to_string(),
1207                    TransformBlock::ExcerptHeader {
1208                        starts_new_buffer, ..
1209                    } => {
1210                        if *starts_new_buffer {
1211                            "path header block".to_string()
1212                        } else {
1213                            "collapsed context".to_string()
1214                        }
1215                    }
1216                };
1217
1218                Some((row, name))
1219            })
1220            .collect()
1221    }
1222}