diagnostics.rs

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