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 serde_json::json;
 684    use std::sync::Arc;
 685    use unindent::Unindent as _;
 686    use workspace::WorkspaceParams;
 687
 688    #[gpui::test]
 689    async fn test_diagnostics(mut cx: TestAppContext) {
 690        let params = cx.update(WorkspaceParams::test);
 691        let project = params.project.clone();
 692        let workspace = cx.add_view(0, |cx| Workspace::new(&params, cx));
 693
 694        params
 695            .fs
 696            .as_fake()
 697            .insert_tree(
 698                "/test",
 699                json!({
 700                    "consts.rs": "
 701                    const a: i32 = 'a';
 702                    const b: i32 = c;
 703                "
 704                    .unindent(),
 705
 706                    "main.rs": "
 707                    fn main() {
 708                        let x = vec![];
 709                        let y = vec![];
 710                        a(x);
 711                        b(y);
 712                        // comment 1
 713                        // comment 2
 714                        c(y);
 715                        d(x);
 716                    }
 717                "
 718                    .unindent(),
 719                }),
 720            )
 721            .await;
 722
 723        let worktree = project
 724            .update(&mut cx, |project, cx| {
 725                project.add_local_worktree("/test", cx)
 726            })
 727            .await
 728            .unwrap();
 729        let worktree_id = worktree.read_with(&cx, |tree, _| tree.id());
 730
 731        // Create some diagnostics
 732        worktree.update(&mut cx, |worktree, cx| {
 733            worktree
 734                .as_local_mut()
 735                .unwrap()
 736                .update_diagnostic_entries(
 737                    Arc::from("/test/main.rs".as_ref()),
 738                    None,
 739                    vec![
 740                        DiagnosticEntry {
 741                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
 742                            diagnostic: Diagnostic {
 743                                message:
 744                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 745                                        .to_string(),
 746                                severity: DiagnosticSeverity::INFORMATION,
 747                                is_primary: false,
 748                                is_disk_based: true,
 749                                group_id: 1,
 750                                ..Default::default()
 751                            },
 752                        },
 753                        DiagnosticEntry {
 754                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
 755                            diagnostic: Diagnostic {
 756                                message:
 757                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 758                                        .to_string(),
 759                                severity: DiagnosticSeverity::INFORMATION,
 760                                is_primary: false,
 761                                is_disk_based: true,
 762                                group_id: 0,
 763                                ..Default::default()
 764                            },
 765                        },
 766                        DiagnosticEntry {
 767                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
 768                            diagnostic: Diagnostic {
 769                                message: "value moved here".to_string(),
 770                                severity: DiagnosticSeverity::INFORMATION,
 771                                is_primary: false,
 772                                is_disk_based: true,
 773                                group_id: 1,
 774                                ..Default::default()
 775                            },
 776                        },
 777                        DiagnosticEntry {
 778                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
 779                            diagnostic: Diagnostic {
 780                                message: "value moved here".to_string(),
 781                                severity: DiagnosticSeverity::INFORMATION,
 782                                is_primary: false,
 783                                is_disk_based: true,
 784                                group_id: 0,
 785                                ..Default::default()
 786                            },
 787                        },
 788                        DiagnosticEntry {
 789                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
 790                            diagnostic: Diagnostic {
 791                                message: "use of moved value\nvalue used here after move".to_string(),
 792                                severity: DiagnosticSeverity::ERROR,
 793                                is_primary: true,
 794                                is_disk_based: true,
 795                                group_id: 0,
 796                                ..Default::default()
 797                            },
 798                        },
 799                        DiagnosticEntry {
 800                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
 801                            diagnostic: Diagnostic {
 802                                message: "use of moved value\nvalue used here after move".to_string(),
 803                                severity: DiagnosticSeverity::ERROR,
 804                                is_primary: true,
 805                                is_disk_based: true,
 806                                group_id: 1,
 807                                ..Default::default()
 808                            },
 809                        },
 810                    ],
 811                    cx,
 812                )
 813                .unwrap();
 814        });
 815
 816        // Open the project diagnostics view while there are already diagnostics.
 817        let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
 818        let view = cx.add_view(0, |cx| {
 819            ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
 820        });
 821
 822        view.next_notification(&cx).await;
 823        view.update(&mut cx, |view, cx| {
 824            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
 825
 826            assert_eq!(
 827                editor_blocks(&editor, cx),
 828                [
 829                    (0, "path header block".into()),
 830                    (2, "diagnostic header".into()),
 831                    (15, "diagnostic header".into()),
 832                    (24, "collapsed context".into()),
 833                ]
 834            );
 835            assert_eq!(
 836                editor.text(),
 837                concat!(
 838                    //
 839                    // main.rs
 840                    //
 841                    "\n", // filename
 842                    "\n", // padding
 843                    // diagnostic group 1
 844                    "\n", // primary message
 845                    "\n", // padding
 846                    "    let x = vec![];\n",
 847                    "    let y = vec![];\n",
 848                    "\n", // supporting diagnostic
 849                    "    a(x);\n",
 850                    "    b(y);\n",
 851                    "\n", // supporting diagnostic
 852                    "    // comment 1\n",
 853                    "    // comment 2\n",
 854                    "    c(y);\n",
 855                    "\n", // supporting diagnostic
 856                    "    d(x);\n",
 857                    // diagnostic group 2
 858                    "\n", // primary message
 859                    "\n", // padding
 860                    "fn main() {\n",
 861                    "    let x = vec![];\n",
 862                    "\n", // supporting diagnostic
 863                    "    let y = vec![];\n",
 864                    "    a(x);\n",
 865                    "\n", // supporting diagnostic
 866                    "    b(y);\n",
 867                    "\n", // context ellipsis
 868                    "    c(y);\n",
 869                    "    d(x);\n",
 870                    "\n", // supporting diagnostic
 871                    "}"
 872                )
 873            );
 874
 875            // Cursor is at the first diagnostic
 876            view.editor.update(cx, |editor, cx| {
 877                assert_eq!(
 878                    editor.selected_display_ranges(cx),
 879                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
 880                );
 881            });
 882        });
 883
 884        // Diagnostics are added for another earlier path.
 885        worktree.update(&mut cx, |worktree, cx| {
 886            worktree
 887                .as_local_mut()
 888                .unwrap()
 889                .update_diagnostic_entries(
 890                    Arc::from("/test/consts.rs".as_ref()),
 891                    None,
 892                    vec![DiagnosticEntry {
 893                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
 894                        diagnostic: Diagnostic {
 895                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
 896                            severity: DiagnosticSeverity::ERROR,
 897                            is_primary: true,
 898                            is_disk_based: true,
 899                            group_id: 0,
 900                            ..Default::default()
 901                        },
 902                    }],
 903                    cx,
 904                )
 905                .unwrap();
 906        });
 907        project.update(&mut cx, |_, cx| {
 908            cx.emit(project::Event::DiagnosticsUpdated(ProjectPath {
 909                worktree_id,
 910                path: Arc::from("/test/consts.rs".as_ref()),
 911            }));
 912            cx.emit(project::Event::DiskBasedDiagnosticsUpdated { worktree_id });
 913        });
 914
 915        view.next_notification(&cx).await;
 916        view.update(&mut cx, |view, cx| {
 917            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
 918
 919            assert_eq!(
 920                editor_blocks(&editor, cx),
 921                [
 922                    (0, "path header block".into()),
 923                    (2, "diagnostic header".into()),
 924                    (7, "path header block".into()),
 925                    (9, "diagnostic header".into()),
 926                    (22, "diagnostic header".into()),
 927                    (31, "collapsed context".into()),
 928                ]
 929            );
 930            assert_eq!(
 931                editor.text(),
 932                concat!(
 933                    //
 934                    // consts.rs
 935                    //
 936                    "\n", // filename
 937                    "\n", // padding
 938                    // diagnostic group 1
 939                    "\n", // primary message
 940                    "\n", // padding
 941                    "const a: i32 = 'a';\n",
 942                    "\n", // supporting diagnostic
 943                    "const b: i32 = c;\n",
 944                    //
 945                    // main.rs
 946                    //
 947                    "\n", // filename
 948                    "\n", // padding
 949                    // diagnostic group 1
 950                    "\n", // primary message
 951                    "\n", // padding
 952                    "    let x = vec![];\n",
 953                    "    let y = vec![];\n",
 954                    "\n", // supporting diagnostic
 955                    "    a(x);\n",
 956                    "    b(y);\n",
 957                    "\n", // supporting diagnostic
 958                    "    // comment 1\n",
 959                    "    // comment 2\n",
 960                    "    c(y);\n",
 961                    "\n", // supporting diagnostic
 962                    "    d(x);\n",
 963                    // diagnostic group 2
 964                    "\n", // primary message
 965                    "\n", // filename
 966                    "fn main() {\n",
 967                    "    let x = vec![];\n",
 968                    "\n", // supporting diagnostic
 969                    "    let y = vec![];\n",
 970                    "    a(x);\n",
 971                    "\n", // supporting diagnostic
 972                    "    b(y);\n",
 973                    "\n", // context ellipsis
 974                    "    c(y);\n",
 975                    "    d(x);\n",
 976                    "\n", // supporting diagnostic
 977                    "}"
 978                )
 979            );
 980
 981            // Cursor keeps its position.
 982            view.editor.update(cx, |editor, cx| {
 983                assert_eq!(
 984                    editor.selected_display_ranges(cx),
 985                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
 986                );
 987            });
 988        });
 989
 990        // Diagnostics are added to the first path
 991        worktree.update(&mut cx, |worktree, cx| {
 992            worktree
 993                .as_local_mut()
 994                .unwrap()
 995                .update_diagnostic_entries(
 996                    Arc::from("/test/consts.rs".as_ref()),
 997                    None,
 998                    vec![
 999                        DiagnosticEntry {
1000                            range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1001                            diagnostic: Diagnostic {
1002                                message: "mismatched types\nexpected `usize`, found `char`"
1003                                    .to_string(),
1004                                severity: DiagnosticSeverity::ERROR,
1005                                is_primary: true,
1006                                is_disk_based: true,
1007                                group_id: 0,
1008                                ..Default::default()
1009                            },
1010                        },
1011                        DiagnosticEntry {
1012                            range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1013                            diagnostic: Diagnostic {
1014                                message: "unresolved name `c`".to_string(),
1015                                severity: DiagnosticSeverity::ERROR,
1016                                is_primary: true,
1017                                is_disk_based: true,
1018                                group_id: 1,
1019                                ..Default::default()
1020                            },
1021                        },
1022                    ],
1023                    cx,
1024                )
1025                .unwrap();
1026        });
1027        project.update(&mut cx, |_, cx| {
1028            cx.emit(project::Event::DiagnosticsUpdated(ProjectPath {
1029                worktree_id,
1030                path: Arc::from("/test/consts.rs".as_ref()),
1031            }));
1032            cx.emit(project::Event::DiskBasedDiagnosticsUpdated { worktree_id });
1033        });
1034
1035        view.next_notification(&cx).await;
1036        view.update(&mut cx, |view, cx| {
1037            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1038
1039            assert_eq!(
1040                editor_blocks(&editor, cx),
1041                [
1042                    (0, "path header block".into()),
1043                    (2, "diagnostic header".into()),
1044                    (7, "diagnostic header".into()),
1045                    (12, "path header block".into()),
1046                    (14, "diagnostic header".into()),
1047                    (27, "diagnostic header".into()),
1048                    (36, "collapsed context".into()),
1049                ]
1050            );
1051            assert_eq!(
1052                editor.text(),
1053                concat!(
1054                    //
1055                    // consts.rs
1056                    //
1057                    "\n", // filename
1058                    "\n", // padding
1059                    // diagnostic group 1
1060                    "\n", // primary message
1061                    "\n", // padding
1062                    "const a: i32 = 'a';\n",
1063                    "\n", // supporting diagnostic
1064                    "const b: i32 = c;\n",
1065                    // diagnostic group 2
1066                    "\n", // primary message
1067                    "\n", // padding
1068                    "const a: i32 = 'a';\n",
1069                    "const b: i32 = c;\n",
1070                    "\n", // supporting diagnostic
1071                    //
1072                    // main.rs
1073                    //
1074                    "\n", // filename
1075                    "\n", // padding
1076                    // diagnostic group 1
1077                    "\n", // primary message
1078                    "\n", // padding
1079                    "    let x = vec![];\n",
1080                    "    let y = vec![];\n",
1081                    "\n", // supporting diagnostic
1082                    "    a(x);\n",
1083                    "    b(y);\n",
1084                    "\n", // supporting diagnostic
1085                    "    // comment 1\n",
1086                    "    // comment 2\n",
1087                    "    c(y);\n",
1088                    "\n", // supporting diagnostic
1089                    "    d(x);\n",
1090                    // diagnostic group 2
1091                    "\n", // primary message
1092                    "\n", // filename
1093                    "fn main() {\n",
1094                    "    let x = vec![];\n",
1095                    "\n", // supporting diagnostic
1096                    "    let y = vec![];\n",
1097                    "    a(x);\n",
1098                    "\n", // supporting diagnostic
1099                    "    b(y);\n",
1100                    "\n", // context ellipsis
1101                    "    c(y);\n",
1102                    "    d(x);\n",
1103                    "\n", // supporting diagnostic
1104                    "}"
1105                )
1106            );
1107        });
1108    }
1109
1110    fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1111        editor
1112            .blocks_in_range(0..editor.max_point().row())
1113            .filter_map(|(row, block)| {
1114                block
1115                    .render(&BlockContext {
1116                        cx,
1117                        anchor_x: 0.,
1118                        line_number_x: 0.,
1119                    })
1120                    .name()
1121                    .map(|s| (row, s.to_string()))
1122            })
1123            .collect()
1124    }
1125}