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