diagnostics.rs

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