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