diagnostics.rs

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