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