diagnostics.rs

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