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