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