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