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