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))
 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) = cx.add_window(|cx| {
 785            Workspace::new(
 786                Default::default(),
 787                project.clone(),
 788                |_, _| unimplemented!(),
 789                cx,
 790            )
 791        });
 792
 793        // Create some diagnostics
 794        project.update(cx, |project, cx| {
 795            project
 796                .update_diagnostic_entries(
 797                    0,
 798                    PathBuf::from("/test/main.rs"),
 799                    None,
 800                    vec![
 801                        DiagnosticEntry {
 802                            range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)),
 803                            diagnostic: Diagnostic {
 804                                message:
 805                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 806                                        .to_string(),
 807                                severity: DiagnosticSeverity::INFORMATION,
 808                                is_primary: false,
 809                                is_disk_based: true,
 810                                group_id: 1,
 811                                ..Default::default()
 812                            },
 813                        },
 814                        DiagnosticEntry {
 815                            range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)),
 816                            diagnostic: Diagnostic {
 817                                message:
 818                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 819                                        .to_string(),
 820                                severity: DiagnosticSeverity::INFORMATION,
 821                                is_primary: false,
 822                                is_disk_based: true,
 823                                group_id: 0,
 824                                ..Default::default()
 825                            },
 826                        },
 827                        DiagnosticEntry {
 828                            range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)),
 829                            diagnostic: Diagnostic {
 830                                message: "value moved here".to_string(),
 831                                severity: DiagnosticSeverity::INFORMATION,
 832                                is_primary: false,
 833                                is_disk_based: true,
 834                                group_id: 1,
 835                                ..Default::default()
 836                            },
 837                        },
 838                        DiagnosticEntry {
 839                            range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)),
 840                            diagnostic: Diagnostic {
 841                                message: "value moved here".to_string(),
 842                                severity: DiagnosticSeverity::INFORMATION,
 843                                is_primary: false,
 844                                is_disk_based: true,
 845                                group_id: 0,
 846                                ..Default::default()
 847                            },
 848                        },
 849                        DiagnosticEntry {
 850                            range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)),
 851                            diagnostic: Diagnostic {
 852                                message: "use of moved value\nvalue used here after move".to_string(),
 853                                severity: DiagnosticSeverity::ERROR,
 854                                is_primary: true,
 855                                is_disk_based: true,
 856                                group_id: 0,
 857                                ..Default::default()
 858                            },
 859                        },
 860                        DiagnosticEntry {
 861                            range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)),
 862                            diagnostic: Diagnostic {
 863                                message: "use of moved value\nvalue used here after move".to_string(),
 864                                severity: DiagnosticSeverity::ERROR,
 865                                is_primary: true,
 866                                is_disk_based: true,
 867                                group_id: 1,
 868                                ..Default::default()
 869                            },
 870                        },
 871                    ],
 872                    cx,
 873                )
 874                .unwrap();
 875        });
 876
 877        // Open the project diagnostics view while there are already diagnostics.
 878        let view = cx.add_view(&workspace, |cx| {
 879            ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
 880        });
 881
 882        view.next_notification(cx).await;
 883        view.update(cx, |view, cx| {
 884            assert_eq!(
 885                editor_blocks(&view.editor, cx),
 886                [
 887                    (0, "path header block".into()),
 888                    (2, "diagnostic header".into()),
 889                    (15, "collapsed context".into()),
 890                    (16, "diagnostic header".into()),
 891                    (25, "collapsed context".into()),
 892                ]
 893            );
 894            assert_eq!(
 895                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
 896                concat!(
 897                    //
 898                    // main.rs
 899                    //
 900                    "\n", // filename
 901                    "\n", // padding
 902                    // diagnostic group 1
 903                    "\n", // primary message
 904                    "\n", // padding
 905                    "    let x = vec![];\n",
 906                    "    let y = vec![];\n",
 907                    "\n", // supporting diagnostic
 908                    "    a(x);\n",
 909                    "    b(y);\n",
 910                    "\n", // supporting diagnostic
 911                    "    // comment 1\n",
 912                    "    // comment 2\n",
 913                    "    c(y);\n",
 914                    "\n", // supporting diagnostic
 915                    "    d(x);\n",
 916                    "\n", // context ellipsis
 917                    // diagnostic group 2
 918                    "\n", // primary message
 919                    "\n", // padding
 920                    "fn main() {\n",
 921                    "    let x = vec![];\n",
 922                    "\n", // supporting diagnostic
 923                    "    let y = vec![];\n",
 924                    "    a(x);\n",
 925                    "\n", // supporting diagnostic
 926                    "    b(y);\n",
 927                    "\n", // context ellipsis
 928                    "    c(y);\n",
 929                    "    d(x);\n",
 930                    "\n", // supporting diagnostic
 931                    "}"
 932                )
 933            );
 934
 935            // Cursor is at the first diagnostic
 936            view.editor.update(cx, |editor, cx| {
 937                assert_eq!(
 938                    editor.selections.display_ranges(cx),
 939                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
 940                );
 941            });
 942        });
 943
 944        // Diagnostics are added for another earlier path.
 945        project.update(cx, |project, cx| {
 946            project.disk_based_diagnostics_started(0, cx);
 947            project
 948                .update_diagnostic_entries(
 949                    0,
 950                    PathBuf::from("/test/consts.rs"),
 951                    None,
 952                    vec![DiagnosticEntry {
 953                        range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)),
 954                        diagnostic: Diagnostic {
 955                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
 956                            severity: DiagnosticSeverity::ERROR,
 957                            is_primary: true,
 958                            is_disk_based: true,
 959                            group_id: 0,
 960                            ..Default::default()
 961                        },
 962                    }],
 963                    cx,
 964                )
 965                .unwrap();
 966            project.disk_based_diagnostics_finished(0, cx);
 967        });
 968
 969        view.next_notification(cx).await;
 970        view.update(cx, |view, cx| {
 971            assert_eq!(
 972                editor_blocks(&view.editor, cx),
 973                [
 974                    (0, "path header block".into()),
 975                    (2, "diagnostic header".into()),
 976                    (7, "path header block".into()),
 977                    (9, "diagnostic header".into()),
 978                    (22, "collapsed context".into()),
 979                    (23, "diagnostic header".into()),
 980                    (32, "collapsed context".into()),
 981                ]
 982            );
 983            assert_eq!(
 984                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
 985                concat!(
 986                    //
 987                    // consts.rs
 988                    //
 989                    "\n", // filename
 990                    "\n", // padding
 991                    // diagnostic group 1
 992                    "\n", // primary message
 993                    "\n", // padding
 994                    "const a: i32 = 'a';\n",
 995                    "\n", // supporting diagnostic
 996                    "const b: i32 = c;\n",
 997                    //
 998                    // main.rs
 999                    //
1000                    "\n", // filename
1001                    "\n", // padding
1002                    // diagnostic group 1
1003                    "\n", // primary message
1004                    "\n", // padding
1005                    "    let x = vec![];\n",
1006                    "    let y = vec![];\n",
1007                    "\n", // supporting diagnostic
1008                    "    a(x);\n",
1009                    "    b(y);\n",
1010                    "\n", // supporting diagnostic
1011                    "    // comment 1\n",
1012                    "    // comment 2\n",
1013                    "    c(y);\n",
1014                    "\n", // supporting diagnostic
1015                    "    d(x);\n",
1016                    "\n", // collapsed context
1017                    // diagnostic group 2
1018                    "\n", // primary message
1019                    "\n", // filename
1020                    "fn main() {\n",
1021                    "    let x = vec![];\n",
1022                    "\n", // supporting diagnostic
1023                    "    let y = vec![];\n",
1024                    "    a(x);\n",
1025                    "\n", // supporting diagnostic
1026                    "    b(y);\n",
1027                    "\n", // context ellipsis
1028                    "    c(y);\n",
1029                    "    d(x);\n",
1030                    "\n", // supporting diagnostic
1031                    "}"
1032                )
1033            );
1034
1035            // Cursor keeps its position.
1036            view.editor.update(cx, |editor, cx| {
1037                assert_eq!(
1038                    editor.selections.display_ranges(cx),
1039                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1040                );
1041            });
1042        });
1043
1044        // Diagnostics are added to the first path
1045        project.update(cx, |project, cx| {
1046            project.disk_based_diagnostics_started(0, cx);
1047            project
1048                .update_diagnostic_entries(
1049                    0,
1050                    PathBuf::from("/test/consts.rs"),
1051                    None,
1052                    vec![
1053                        DiagnosticEntry {
1054                            range: Unclipped(PointUtf16::new(0, 15))
1055                                ..Unclipped(PointUtf16::new(0, 15)),
1056                            diagnostic: Diagnostic {
1057                                message: "mismatched types\nexpected `usize`, found `char`"
1058                                    .to_string(),
1059                                severity: DiagnosticSeverity::ERROR,
1060                                is_primary: true,
1061                                is_disk_based: true,
1062                                group_id: 0,
1063                                ..Default::default()
1064                            },
1065                        },
1066                        DiagnosticEntry {
1067                            range: Unclipped(PointUtf16::new(1, 15))
1068                                ..Unclipped(PointUtf16::new(1, 15)),
1069                            diagnostic: Diagnostic {
1070                                message: "unresolved name `c`".to_string(),
1071                                severity: DiagnosticSeverity::ERROR,
1072                                is_primary: true,
1073                                is_disk_based: true,
1074                                group_id: 1,
1075                                ..Default::default()
1076                            },
1077                        },
1078                    ],
1079                    cx,
1080                )
1081                .unwrap();
1082            project.disk_based_diagnostics_finished(0, cx);
1083        });
1084
1085        view.next_notification(cx).await;
1086        view.update(cx, |view, cx| {
1087            assert_eq!(
1088                editor_blocks(&view.editor, cx),
1089                [
1090                    (0, "path header block".into()),
1091                    (2, "diagnostic header".into()),
1092                    (7, "collapsed context".into()),
1093                    (8, "diagnostic header".into()),
1094                    (13, "path header block".into()),
1095                    (15, "diagnostic header".into()),
1096                    (28, "collapsed context".into()),
1097                    (29, "diagnostic header".into()),
1098                    (38, "collapsed context".into()),
1099                ]
1100            );
1101            assert_eq!(
1102                view.editor.update(cx, |editor, cx| editor.display_text(cx)),
1103                concat!(
1104                    //
1105                    // consts.rs
1106                    //
1107                    "\n", // filename
1108                    "\n", // padding
1109                    // diagnostic group 1
1110                    "\n", // primary message
1111                    "\n", // padding
1112                    "const a: i32 = 'a';\n",
1113                    "\n", // supporting diagnostic
1114                    "const b: i32 = c;\n",
1115                    "\n", // context ellipsis
1116                    // diagnostic group 2
1117                    "\n", // primary message
1118                    "\n", // padding
1119                    "const a: i32 = 'a';\n",
1120                    "const b: i32 = c;\n",
1121                    "\n", // supporting diagnostic
1122                    //
1123                    // main.rs
1124                    //
1125                    "\n", // filename
1126                    "\n", // padding
1127                    // diagnostic group 1
1128                    "\n", // primary message
1129                    "\n", // padding
1130                    "    let x = vec![];\n",
1131                    "    let y = vec![];\n",
1132                    "\n", // supporting diagnostic
1133                    "    a(x);\n",
1134                    "    b(y);\n",
1135                    "\n", // supporting diagnostic
1136                    "    // comment 1\n",
1137                    "    // comment 2\n",
1138                    "    c(y);\n",
1139                    "\n", // supporting diagnostic
1140                    "    d(x);\n",
1141                    "\n", // context ellipsis
1142                    // diagnostic group 2
1143                    "\n", // primary message
1144                    "\n", // filename
1145                    "fn main() {\n",
1146                    "    let x = vec![];\n",
1147                    "\n", // supporting diagnostic
1148                    "    let y = vec![];\n",
1149                    "    a(x);\n",
1150                    "\n", // supporting diagnostic
1151                    "    b(y);\n",
1152                    "\n", // context ellipsis
1153                    "    c(y);\n",
1154                    "    d(x);\n",
1155                    "\n", // supporting diagnostic
1156                    "}"
1157                )
1158            );
1159        });
1160    }
1161
1162    fn editor_blocks(
1163        editor: &ViewHandle<Editor>,
1164        cx: &mut MutableAppContext,
1165    ) -> Vec<(u32, String)> {
1166        let mut presenter = cx.build_presenter(editor.id(), 0., Default::default());
1167        let mut cx = presenter.build_layout_context(Default::default(), false, cx);
1168        cx.render(editor, |editor, cx| {
1169            let snapshot = editor.snapshot(cx);
1170            snapshot
1171                .blocks_in_range(0..snapshot.max_point().row())
1172                .filter_map(|(row, block)| {
1173                    let name = match block {
1174                        TransformBlock::Custom(block) => block
1175                            .render(&mut BlockContext {
1176                                cx,
1177                                anchor_x: 0.,
1178                                scroll_x: 0.,
1179                                gutter_padding: 0.,
1180                                gutter_width: 0.,
1181                                line_height: 0.,
1182                                em_width: 0.,
1183                            })
1184                            .name()?
1185                            .to_string(),
1186                        TransformBlock::ExcerptHeader {
1187                            starts_new_buffer, ..
1188                        } => {
1189                            if *starts_new_buffer {
1190                                "path header block".to_string()
1191                            } else {
1192                                "collapsed context".to_string()
1193                            }
1194                        }
1195                    };
1196
1197                    Some((row, name))
1198                })
1199                .collect()
1200        })
1201    }
1202}