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, cx).boxed()
  99        }
 100    }
 101
 102    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
 103        if cx.is_self_focused() && !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.clone()))
 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),
 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().copied(), 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            let snapshot = editor.snapshot(cx);
 461            for selection in &mut selections {
 462                if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
 463                    let group_ix = match groups.binary_search_by(|probe| {
 464                        probe
 465                            .excerpts
 466                            .last()
 467                            .unwrap()
 468                            .cmp(new_excerpt_id, &snapshot.buffer_snapshot)
 469                    }) {
 470                        Ok(ix) | Err(ix) => ix,
 471                    };
 472                    if let Some(group) = groups.get(group_ix) {
 473                        let offset = excerpts_snapshot
 474                            .anchor_in_excerpt(
 475                                group.excerpts[group.primary_excerpt_ix].clone(),
 476                                group.primary_diagnostic.range.start,
 477                            )
 478                            .to_offset(&excerpts_snapshot);
 479                        selection.start = offset;
 480                        selection.end = offset;
 481                    }
 482                }
 483            }
 484            editor.change_selections(None, cx, |s| {
 485                s.select(selections);
 486            });
 487            Some(())
 488        });
 489
 490        if self.path_states.is_empty() {
 491            if self.editor.is_focused(cx) {
 492                cx.focus_self();
 493            }
 494        } else if cx.handle().is_focused(cx) {
 495            cx.focus(&self.editor);
 496        }
 497        cx.notify();
 498    }
 499
 500    fn update_title(&mut self, cx: &mut ViewContext<Self>) {
 501        self.summary = self.project.read(cx).diagnostic_summary(cx);
 502        cx.emit(Event::TitleChanged);
 503    }
 504}
 505
 506impl workspace::Item for ProjectDiagnosticsEditor {
 507    fn tab_content(
 508        &self,
 509        _detail: Option<usize>,
 510        style: &theme::Tab,
 511        cx: &AppContext,
 512    ) -> ElementBox {
 513        render_summary(
 514            &self.summary,
 515            &style.label.text,
 516            &cx.global::<Settings>().theme.project_diagnostics,
 517        )
 518    }
 519
 520    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
 521        None
 522    }
 523
 524    fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
 525        self.editor.project_entry_ids(cx)
 526    }
 527
 528    fn is_singleton(&self, _: &AppContext) -> bool {
 529        false
 530    }
 531
 532    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
 533        self.editor
 534            .update(cx, |editor, cx| editor.navigate(data, cx))
 535    }
 536
 537    fn is_dirty(&self, cx: &AppContext) -> bool {
 538        self.excerpts.read(cx).is_dirty(cx)
 539    }
 540
 541    fn has_conflict(&self, cx: &AppContext) -> bool {
 542        self.excerpts.read(cx).has_conflict(cx)
 543    }
 544
 545    fn can_save(&self, _: &AppContext) -> bool {
 546        true
 547    }
 548
 549    fn save(
 550        &mut self,
 551        project: ModelHandle<Project>,
 552        cx: &mut ViewContext<Self>,
 553    ) -> Task<Result<()>> {
 554        self.editor.save(project, cx)
 555    }
 556
 557    fn reload(
 558        &mut self,
 559        project: ModelHandle<Project>,
 560        cx: &mut ViewContext<Self>,
 561    ) -> Task<Result<()>> {
 562        self.editor.reload(project, cx)
 563    }
 564
 565    fn save_as(
 566        &mut self,
 567        _: ModelHandle<Project>,
 568        _: PathBuf,
 569        _: &mut ViewContext<Self>,
 570    ) -> Task<Result<()>> {
 571        unreachable!()
 572    }
 573
 574    fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
 575        Editor::to_item_events(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/circle_x_mark_12.svg")
 625                .with_color(theme.error_diagnostic.message.text.color)
 626        } else {
 627            Svg::new("icons/triangle_exclamation_12.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/circle_x_mark_12.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/triangle_exclamation_12.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, Unclipped};
 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) =
 785            cx.add_window(|cx| Workspace::new(project.clone(), |_, _| unimplemented!(), cx));
 786
 787        // Create some diagnostics
 788        project.update(cx, |project, cx| {
 789            project
 790                .update_diagnostic_entries(
 791                    0,
 792                    PathBuf::from("/test/main.rs"),
 793                    None,
 794                    vec![
 795                        DiagnosticEntry {
 796                            range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
 797                            diagnostic: Diagnostic {
 798                                message:
 799                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 800                                        .to_string(),
 801                                severity: DiagnosticSeverity::INFORMATION,
 802                                is_primary: false,
 803                                is_disk_based: true,
 804                                group_id: 1,
 805                                ..Default::default()
 806                            },
 807                        },
 808                        DiagnosticEntry {
 809                            range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
 810                            diagnostic: Diagnostic {
 811                                message:
 812                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 813                                        .to_string(),
 814                                severity: DiagnosticSeverity::INFORMATION,
 815                                is_primary: false,
 816                                is_disk_based: true,
 817                                group_id: 0,
 818                                ..Default::default()
 819                            },
 820                        },
 821                        DiagnosticEntry {
 822                            range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
 823                            diagnostic: Diagnostic {
 824                                message: "value moved here".to_string(),
 825                                severity: DiagnosticSeverity::INFORMATION,
 826                                is_primary: false,
 827                                is_disk_based: true,
 828                                group_id: 1,
 829                                ..Default::default()
 830                            },
 831                        },
 832                        DiagnosticEntry {
 833                            range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
 834                            diagnostic: Diagnostic {
 835                                message: "value moved here".to_string(),
 836                                severity: DiagnosticSeverity::INFORMATION,
 837                                is_primary: false,
 838                                is_disk_based: true,
 839                                group_id: 0,
 840                                ..Default::default()
 841                            },
 842                        },
 843                        DiagnosticEntry {
 844                            range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
 845                            diagnostic: Diagnostic {
 846                                message: "use of moved value\nvalue used here after move".to_string(),
 847                                severity: DiagnosticSeverity::ERROR,
 848                                is_primary: true,
 849                                is_disk_based: true,
 850                                group_id: 0,
 851                                ..Default::default()
 852                            },
 853                        },
 854                        DiagnosticEntry {
 855                            range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
 856                            diagnostic: Diagnostic {
 857                                message: "use of moved value\nvalue used here after move".to_string(),
 858                                severity: DiagnosticSeverity::ERROR,
 859                                is_primary: true,
 860                                is_disk_based: true,
 861                                group_id: 1,
 862                                ..Default::default()
 863                            },
 864                        },
 865                    ],
 866                    cx,
 867                )
 868                .unwrap();
 869        });
 870
 871        // Open the project diagnostics view while there are already diagnostics.
 872        let view = cx.add_view(&workspace, |cx| {
 873            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
 874        });
 875
 876        view.next_notification(cx).await;
 877        view.update(cx, |view, cx| {
 878            assert_eq!(
 879                editor_blocks(&view.editor, cx),
 880                [
 881                    (0, "path header block".into()),
 882                    (2, "diagnostic header".into()),
 883                    (15, "collapsed context".into()),
 884                    (16, "diagnostic header".into()),
 885                    (25, "collapsed context".into()),
 886                ]
 887            );
 888            assert_eq!(
 889                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
 890                concat!(
 891                    //
 892                    // main.rs
 893                    //
 894                    "\n", // filename
 895                    "\n", // padding
 896                    // diagnostic group 1
 897                    "\n", // primary message
 898                    "\n", // padding
 899                    "    let x = vec![];\n",
 900                    "    let y = vec![];\n",
 901                    "\n", // supporting diagnostic
 902                    "    a(x);\n",
 903                    "    b(y);\n",
 904                    "\n", // supporting diagnostic
 905                    "    // comment 1\n",
 906                    "    // comment 2\n",
 907                    "    c(y);\n",
 908                    "\n", // supporting diagnostic
 909                    "    d(x);\n",
 910                    "\n", // context ellipsis
 911                    // diagnostic group 2
 912                    "\n", // primary message
 913                    "\n", // padding
 914                    "fn main() {\n",
 915                    "    let x = vec![];\n",
 916                    "\n", // supporting diagnostic
 917                    "    let y = vec![];\n",
 918                    "    a(x);\n",
 919                    "\n", // supporting diagnostic
 920                    "    b(y);\n",
 921                    "\n", // context ellipsis
 922                    "    c(y);\n",
 923                    "    d(x);\n",
 924                    "\n", // supporting diagnostic
 925                    "}"
 926                )
 927            );
 928
 929            // Cursor is at the first diagnostic
 930            view.editor.update(cx, |editor, cx| {
 931                assert_eq!(
 932                    editor.selections.display_ranges(cx),
 933                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
 934                );
 935            });
 936        });
 937
 938        // Diagnostics are added for another earlier path.
 939        project.update(cx, |project, cx| {
 940            project.disk_based_diagnostics_started(0, cx);
 941            project
 942                .update_diagnostic_entries(
 943                    0,
 944                    PathBuf::from("/test/consts.rs"),
 945                    None,
 946                    vec![DiagnosticEntry {
 947                        range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
 948                        diagnostic: Diagnostic {
 949                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
 950                            severity: DiagnosticSeverity::ERROR,
 951                            is_primary: true,
 952                            is_disk_based: true,
 953                            group_id: 0,
 954                            ..Default::default()
 955                        },
 956                    }],
 957                    cx,
 958                )
 959                .unwrap();
 960            project.disk_based_diagnostics_finished(0, cx);
 961        });
 962
 963        view.next_notification(cx).await;
 964        view.update(cx, |view, cx| {
 965            assert_eq!(
 966                editor_blocks(&view.editor, cx),
 967                [
 968                    (0, "path header block".into()),
 969                    (2, "diagnostic header".into()),
 970                    (7, "path header block".into()),
 971                    (9, "diagnostic header".into()),
 972                    (22, "collapsed context".into()),
 973                    (23, "diagnostic header".into()),
 974                    (32, "collapsed context".into()),
 975                ]
 976            );
 977            assert_eq!(
 978                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
 979                concat!(
 980                    //
 981                    // consts.rs
 982                    //
 983                    "\n", // filename
 984                    "\n", // padding
 985                    // diagnostic group 1
 986                    "\n", // primary message
 987                    "\n", // padding
 988                    "const a: i32 = 'a';\n",
 989                    "\n", // supporting diagnostic
 990                    "const b: i32 = c;\n",
 991                    //
 992                    // main.rs
 993                    //
 994                    "\n", // filename
 995                    "\n", // padding
 996                    // diagnostic group 1
 997                    "\n", // primary message
 998                    "\n", // padding
 999                    "    let x = vec![];\n",
1000                    "    let y = vec![];\n",
1001                    "\n", // supporting diagnostic
1002                    "    a(x);\n",
1003                    "    b(y);\n",
1004                    "\n", // supporting diagnostic
1005                    "    // comment 1\n",
1006                    "    // comment 2\n",
1007                    "    c(y);\n",
1008                    "\n", // supporting diagnostic
1009                    "    d(x);\n",
1010                    "\n", // collapsed context
1011                    // diagnostic group 2
1012                    "\n", // primary message
1013                    "\n", // filename
1014                    "fn main() {\n",
1015                    "    let x = vec![];\n",
1016                    "\n", // supporting diagnostic
1017                    "    let y = vec![];\n",
1018                    "    a(x);\n",
1019                    "\n", // supporting diagnostic
1020                    "    b(y);\n",
1021                    "\n", // context ellipsis
1022                    "    c(y);\n",
1023                    "    d(x);\n",
1024                    "\n", // supporting diagnostic
1025                    "}"
1026                )
1027            );
1028
1029            // Cursor keeps its position.
1030            view.editor.update(cx, |editor, cx| {
1031                assert_eq!(
1032                    editor.selections.display_ranges(cx),
1033                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1034                );
1035            });
1036        });
1037
1038        // Diagnostics are added to the first path
1039        project.update(cx, |project, cx| {
1040            project.disk_based_diagnostics_started(0, cx);
1041            project
1042                .update_diagnostic_entries(
1043                    0,
1044                    PathBuf::from("/test/consts.rs"),
1045                    None,
1046                    vec![
1047                        DiagnosticEntry {
1048                            range: Unclipped(PointUtf16::new(0, 15))
1049                                ..Unclipped(PointUtf16::new(0, 15)),
1050                            diagnostic: Diagnostic {
1051                                message: "mismatched types\nexpected `usize`, found `char`"
1052                                    .to_string(),
1053                                severity: DiagnosticSeverity::ERROR,
1054                                is_primary: true,
1055                                is_disk_based: true,
1056                                group_id: 0,
1057                                ..Default::default()
1058                            },
1059                        },
1060                        DiagnosticEntry {
1061                            range: Unclipped(PointUtf16::new(1, 15))
1062                                ..Unclipped(PointUtf16::new(1, 15)),
1063                            diagnostic: Diagnostic {
1064                                message: "unresolved name `c`".to_string(),
1065                                severity: DiagnosticSeverity::ERROR,
1066                                is_primary: true,
1067                                is_disk_based: true,
1068                                group_id: 1,
1069                                ..Default::default()
1070                            },
1071                        },
1072                    ],
1073                    cx,
1074                )
1075                .unwrap();
1076            project.disk_based_diagnostics_finished(0, cx);
1077        });
1078
1079        view.next_notification(cx).await;
1080        view.update(cx, |view, cx| {
1081            assert_eq!(
1082                editor_blocks(&view.editor, cx),
1083                [
1084                    (0, "path header block".into()),
1085                    (2, "diagnostic header".into()),
1086                    (7, "collapsed context".into()),
1087                    (8, "diagnostic header".into()),
1088                    (13, "path header block".into()),
1089                    (15, "diagnostic header".into()),
1090                    (28, "collapsed context".into()),
1091                    (29, "diagnostic header".into()),
1092                    (38, "collapsed context".into()),
1093                ]
1094            );
1095            assert_eq!(
1096                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1097                concat!(
1098                    //
1099                    // consts.rs
1100                    //
1101                    "\n", // filename
1102                    "\n", // padding
1103                    // diagnostic group 1
1104                    "\n", // primary message
1105                    "\n", // padding
1106                    "const a: i32 = 'a';\n",
1107                    "\n", // supporting diagnostic
1108                    "const b: i32 = c;\n",
1109                    "\n", // context ellipsis
1110                    // diagnostic group 2
1111                    "\n", // primary message
1112                    "\n", // padding
1113                    "const a: i32 = 'a';\n",
1114                    "const b: i32 = c;\n",
1115                    "\n", // supporting diagnostic
1116                    //
1117                    // main.rs
1118                    //
1119                    "\n", // filename
1120                    "\n", // padding
1121                    // diagnostic group 1
1122                    "\n", // primary message
1123                    "\n", // padding
1124                    "    let x = vec![];\n",
1125                    "    let y = vec![];\n",
1126                    "\n", // supporting diagnostic
1127                    "    a(x);\n",
1128                    "    b(y);\n",
1129                    "\n", // supporting diagnostic
1130                    "    // comment 1\n",
1131                    "    // comment 2\n",
1132                    "    c(y);\n",
1133                    "\n", // supporting diagnostic
1134                    "    d(x);\n",
1135                    "\n", // context ellipsis
1136                    // diagnostic group 2
1137                    "\n", // primary message
1138                    "\n", // filename
1139                    "fn main() {\n",
1140                    "    let x = vec![];\n",
1141                    "\n", // supporting diagnostic
1142                    "    let y = vec![];\n",
1143                    "    a(x);\n",
1144                    "\n", // supporting diagnostic
1145                    "    b(y);\n",
1146                    "\n", // context ellipsis
1147                    "    c(y);\n",
1148                    "    d(x);\n",
1149                    "\n", // supporting diagnostic
1150                    "}"
1151                )
1152            );
1153        });
1154    }
1155
1156    fn editor_blocks(
1157        editor: &ViewHandle<Editor>,
1158        cx: &mut MutableAppContext,
1159    ) -> Vec<(u32, String)> {
1160        let mut presenter = cx.build_presenter(editor.id(), 0., Default::default());
1161        let mut cx = presenter.build_layout_context(Default::default(), false, cx);
1162        cx.render(editor, |editor, cx| {
1163            let snapshot = editor.snapshot(cx);
1164            snapshot
1165                .blocks_in_range(0..snapshot.max_point().row())
1166                .filter_map(|(row, block)| {
1167                    let name = match block {
1168                        TransformBlock::Custom(block) => block
1169                            .render(&mut BlockContext {
1170                                cx,
1171                                anchor_x: 0.,
1172                                scroll_x: 0.,
1173                                gutter_padding: 0.,
1174                                gutter_width: 0.,
1175                                line_height: 0.,
1176                                em_width: 0.,
1177                            })
1178                            .name()?
1179                            .to_string(),
1180                        TransformBlock::ExcerptHeader {
1181                            starts_new_buffer, ..
1182                        } => {
1183                            if *starts_new_buffer {
1184                                "path header block".to_string()
1185                            } else {
1186                                "collapsed context".to_string()
1187                            }
1188                        }
1189                    };
1190
1191                    Some((row, name))
1192                })
1193                .collect()
1194        })
1195    }
1196}