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