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