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