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