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