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::{DiagnosticSummary, 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    summary: DiagnosticSummary,
  61    excerpts: ModelHandle<MultiBuffer>,
  62    path_states: Vec<PathState>,
  63    paths_to_update: BTreeSet<ProjectPath>,
  64    build_settings: BuildSettings,
  65    settings: watch::Receiver<workspace::Settings>,
  66}
  67
  68struct PathState {
  69    path: ProjectPath,
  70    header: Option<BlockId>,
  71    diagnostic_groups: Vec<DiagnosticGroupState>,
  72}
  73
  74struct DiagnosticGroupState {
  75    primary_diagnostic: DiagnosticEntry<language::Anchor>,
  76    primary_excerpt_ix: usize,
  77    excerpts: Vec<ExcerptId>,
  78    blocks: HashSet<BlockId>,
  79    block_count: usize,
  80}
  81
  82impl ProjectDiagnostics {
  83    fn new(project: ModelHandle<Project>) -> Self {
  84        Self { project }
  85    }
  86}
  87
  88impl Entity for ProjectDiagnostics {
  89    type Event = ();
  90}
  91
  92impl Entity for ProjectDiagnosticsEditor {
  93    type Event = Event;
  94}
  95
  96impl View for ProjectDiagnosticsEditor {
  97    fn ui_name() -> &'static str {
  98        "ProjectDiagnosticsEditor"
  99    }
 100
 101    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
 102        if self.path_states.is_empty() {
 103            let theme = &self.settings.borrow().theme.project_diagnostics;
 104            Label::new(
 105                "No problems detected in the project".to_string(),
 106                theme.empty_message.clone(),
 107            )
 108            .aligned()
 109            .contained()
 110            .with_style(theme.container)
 111            .boxed()
 112        } else {
 113            ChildView::new(self.editor.id()).boxed()
 114        }
 115    }
 116
 117    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
 118        if !self.path_states.is_empty() {
 119            cx.focus(&self.editor);
 120        }
 121    }
 122}
 123
 124impl ProjectDiagnosticsEditor {
 125    fn new(
 126        model: ModelHandle<ProjectDiagnostics>,
 127        workspace: WeakViewHandle<Workspace>,
 128        settings: watch::Receiver<workspace::Settings>,
 129        cx: &mut ViewContext<Self>,
 130    ) -> Self {
 131        let project = model.read(cx).project.clone();
 132        cx.subscribe(&project, |this, _, event, cx| match event {
 133            project::Event::DiskBasedDiagnosticsFinished => {
 134                let paths = mem::take(&mut this.paths_to_update);
 135                this.update_excerpts(paths, cx);
 136                cx.emit(Event::TitleChanged)
 137            }
 138            project::Event::DiagnosticsUpdated(path) => {
 139                this.paths_to_update.insert(path.clone());
 140            }
 141            _ => {}
 142        })
 143        .detach();
 144
 145        let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
 146        let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
 147        let editor =
 148            cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
 149        cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
 150            .detach();
 151
 152        let project = project.read(cx);
 153        let paths_to_update = project.diagnostic_summaries(cx).map(|e| e.0).collect();
 154        let this = Self {
 155            model,
 156            summary: project.diagnostic_summary(cx),
 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 tab_content(&self, style: &theme::Tab, _: &AppContext) -> ElementBox {
 560        let theme = &self.settings.borrow().theme.project_diagnostics;
 561        let icon_width = theme.tab_icon_width;
 562        let icon_spacing = theme.tab_icon_spacing;
 563        let summary_spacing = theme.tab_summary_spacing;
 564        Flex::row()
 565            .with_children([
 566                Svg::new("icons/no.svg")
 567                    .with_color(style.label.text.color)
 568                    .constrained()
 569                    .with_width(icon_width)
 570                    .aligned()
 571                    .contained()
 572                    .with_margin_right(icon_spacing)
 573                    .named("no-icon"),
 574                Label::new(self.summary.error_count.to_string(), style.label.clone())
 575                    .aligned()
 576                    .boxed(),
 577                Svg::new("icons/warning.svg")
 578                    .with_color(style.label.text.color)
 579                    .constrained()
 580                    .with_width(icon_width)
 581                    .aligned()
 582                    .contained()
 583                    .with_margin_left(summary_spacing)
 584                    .with_margin_right(icon_spacing)
 585                    .named("warn-icon"),
 586                Label::new(self.summary.warning_count.to_string(), style.label.clone())
 587                    .aligned()
 588                    .boxed(),
 589            ])
 590            .boxed()
 591    }
 592
 593    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
 594        None
 595    }
 596
 597    fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) {
 598        self.editor
 599            .update(cx, |editor, cx| editor.navigate(data, cx));
 600    }
 601
 602    fn is_dirty(&self, cx: &AppContext) -> bool {
 603        self.excerpts.read(cx).read(cx).is_dirty()
 604    }
 605
 606    fn has_conflict(&self, cx: &AppContext) -> bool {
 607        self.excerpts.read(cx).read(cx).has_conflict()
 608    }
 609
 610    fn can_save(&self, _: &AppContext) -> bool {
 611        true
 612    }
 613
 614    fn save(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
 615        self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
 616    }
 617
 618    fn can_save_as(&self, _: &AppContext) -> bool {
 619        false
 620    }
 621
 622    fn save_as(
 623        &mut self,
 624        _: ModelHandle<Project>,
 625        _: PathBuf,
 626        _: &mut ViewContext<Self>,
 627    ) -> Task<Result<()>> {
 628        unreachable!()
 629    }
 630
 631    fn should_activate_item_on_event(event: &Self::Event) -> bool {
 632        Editor::should_activate_item_on_event(event)
 633    }
 634
 635    fn should_update_tab_on_event(event: &Event) -> bool {
 636        matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
 637    }
 638
 639    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
 640    where
 641        Self: Sized,
 642    {
 643        let diagnostics = ProjectDiagnosticsEditor::new(
 644            self.model.clone(),
 645            self.workspace.clone(),
 646            self.settings.clone(),
 647            cx,
 648        );
 649        diagnostics.editor.update(cx, |editor, cx| {
 650            let nav_history = self
 651                .editor
 652                .read(cx)
 653                .nav_history()
 654                .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle()));
 655            editor.set_nav_history(nav_history);
 656        });
 657        Some(diagnostics)
 658    }
 659
 660    fn act_as_type(
 661        &self,
 662        type_id: TypeId,
 663        self_handle: &ViewHandle<Self>,
 664        _: &AppContext,
 665    ) -> Option<AnyViewHandle> {
 666        if type_id == TypeId::of::<Self>() {
 667            Some(self_handle.into())
 668        } else if type_id == TypeId::of::<Editor>() {
 669            Some((&self.editor).into())
 670        } else {
 671            None
 672        }
 673    }
 674
 675    fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
 676        self.editor.update(cx, |editor, cx| editor.deactivated(cx));
 677    }
 678}
 679
 680fn path_header_renderer(buffer: ModelHandle<Buffer>, build_settings: BuildSettings) -> RenderBlock {
 681    Arc::new(move |cx| {
 682        let settings = build_settings(cx);
 683        let style = settings.style.diagnostic_path_header;
 684        let font_size = (style.text_scale_factor * settings.style.text.font_size).round();
 685
 686        let mut filename = None;
 687        let mut path = None;
 688        if let Some(file) = buffer.read(&**cx).file() {
 689            filename = file
 690                .path()
 691                .file_name()
 692                .map(|f| f.to_string_lossy().to_string());
 693            path = file
 694                .path()
 695                .parent()
 696                .map(|p| p.to_string_lossy().to_string() + "/");
 697        }
 698
 699        Flex::row()
 700            .with_child(
 701                Label::new(
 702                    filename.unwrap_or_else(|| "untitled".to_string()),
 703                    style.filename.text.clone().with_font_size(font_size),
 704                )
 705                .contained()
 706                .with_style(style.filename.container)
 707                .boxed(),
 708            )
 709            .with_children(path.map(|path| {
 710                Label::new(path, style.path.text.clone().with_font_size(font_size))
 711                    .contained()
 712                    .with_style(style.path.container)
 713                    .boxed()
 714            }))
 715            .aligned()
 716            .left()
 717            .contained()
 718            .with_style(style.container)
 719            .with_padding_left(cx.gutter_padding)
 720            .expanded()
 721            .named("path header block")
 722    })
 723}
 724
 725fn diagnostic_header_renderer(
 726    diagnostic: Diagnostic,
 727    build_settings: BuildSettings,
 728) -> RenderBlock {
 729    let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
 730    Arc::new(move |cx| {
 731        let settings = build_settings(cx);
 732        let style = &settings.style.diagnostic_header;
 733        let font_size = (style.text_scale_factor * settings.style.text.font_size).round();
 734        let icon_width = cx.em_width * style.icon_width_factor;
 735        let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
 736            Svg::new("icons/diagnostic-error-10.svg")
 737                .with_color(settings.style.error_diagnostic.message.text.color)
 738        } else {
 739            Svg::new("icons/diagnostic-warning-10.svg")
 740                .with_color(settings.style.warning_diagnostic.message.text.color)
 741        };
 742
 743        Flex::row()
 744            .with_child(
 745                icon.constrained()
 746                    .with_width(icon_width)
 747                    .aligned()
 748                    .contained()
 749                    .boxed(),
 750            )
 751            .with_child(
 752                Label::new(
 753                    message.clone(),
 754                    style.message.label.clone().with_font_size(font_size),
 755                )
 756                .with_highlights(highlights.clone())
 757                .contained()
 758                .with_style(style.message.container)
 759                .with_margin_left(cx.gutter_padding)
 760                .aligned()
 761                .boxed(),
 762            )
 763            .with_children(diagnostic.code.clone().map(|code| {
 764                Label::new(code, style.code.text.clone().with_font_size(font_size))
 765                    .contained()
 766                    .with_style(style.code.container)
 767                    .aligned()
 768                    .boxed()
 769            }))
 770            .contained()
 771            .with_style(style.container)
 772            .with_padding_left(cx.gutter_width - cx.gutter_padding - icon_width)
 773            .expanded()
 774            .named("diagnostic header")
 775    })
 776}
 777
 778fn context_header_renderer(build_settings: BuildSettings) -> RenderBlock {
 779    Arc::new(move |cx| {
 780        let settings = build_settings(cx);
 781        let text_style = settings.style.text.clone();
 782        Label::new("".to_string(), text_style)
 783            .contained()
 784            .with_padding_left(cx.gutter_padding)
 785            .named("collapsed context")
 786    })
 787}
 788
 789fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 790    lhs: &DiagnosticEntry<L>,
 791    rhs: &DiagnosticEntry<R>,
 792    snapshot: &language::BufferSnapshot,
 793) -> Ordering {
 794    lhs.range
 795        .start
 796        .to_offset(&snapshot)
 797        .cmp(&rhs.range.start.to_offset(snapshot))
 798        .then_with(|| {
 799            lhs.range
 800                .end
 801                .to_offset(&snapshot)
 802                .cmp(&rhs.range.end.to_offset(snapshot))
 803        })
 804        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 805}
 806
 807#[cfg(test)]
 808mod tests {
 809    use super::*;
 810    use editor::{display_map::BlockContext, DisplayPoint, EditorSnapshot};
 811    use gpui::TestAppContext;
 812    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
 813    use serde_json::json;
 814    use unindent::Unindent as _;
 815    use workspace::WorkspaceParams;
 816
 817    #[gpui::test]
 818    async fn test_diagnostics(mut cx: TestAppContext) {
 819        let params = cx.update(WorkspaceParams::test);
 820        let project = params.project.clone();
 821        let workspace = cx.add_view(0, |cx| Workspace::new(&params, cx));
 822
 823        params
 824            .fs
 825            .as_fake()
 826            .insert_tree(
 827                "/test",
 828                json!({
 829                    "consts.rs": "
 830                    const a: i32 = 'a';
 831                    const b: i32 = c;
 832                "
 833                    .unindent(),
 834
 835                    "main.rs": "
 836                    fn main() {
 837                        let x = vec![];
 838                        let y = vec![];
 839                        a(x);
 840                        b(y);
 841                        // comment 1
 842                        // comment 2
 843                        c(y);
 844                        d(x);
 845                    }
 846                "
 847                    .unindent(),
 848                }),
 849            )
 850            .await;
 851
 852        project
 853            .update(&mut cx, |project, cx| {
 854                project.find_or_create_local_worktree("/test", false, cx)
 855            })
 856            .await
 857            .unwrap();
 858
 859        // Create some diagnostics
 860        project.update(&mut cx, |project, cx| {
 861            project
 862                .update_diagnostic_entries(
 863                    PathBuf::from("/test/main.rs"),
 864                    None,
 865                    vec![
 866                        DiagnosticEntry {
 867                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
 868                            diagnostic: Diagnostic {
 869                                message:
 870                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 871                                        .to_string(),
 872                                severity: DiagnosticSeverity::INFORMATION,
 873                                is_primary: false,
 874                                is_disk_based: true,
 875                                group_id: 1,
 876                                ..Default::default()
 877                            },
 878                        },
 879                        DiagnosticEntry {
 880                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
 881                            diagnostic: Diagnostic {
 882                                message:
 883                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 884                                        .to_string(),
 885                                severity: DiagnosticSeverity::INFORMATION,
 886                                is_primary: false,
 887                                is_disk_based: true,
 888                                group_id: 0,
 889                                ..Default::default()
 890                            },
 891                        },
 892                        DiagnosticEntry {
 893                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
 894                            diagnostic: Diagnostic {
 895                                message: "value moved here".to_string(),
 896                                severity: DiagnosticSeverity::INFORMATION,
 897                                is_primary: false,
 898                                is_disk_based: true,
 899                                group_id: 1,
 900                                ..Default::default()
 901                            },
 902                        },
 903                        DiagnosticEntry {
 904                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
 905                            diagnostic: Diagnostic {
 906                                message: "value moved here".to_string(),
 907                                severity: DiagnosticSeverity::INFORMATION,
 908                                is_primary: false,
 909                                is_disk_based: true,
 910                                group_id: 0,
 911                                ..Default::default()
 912                            },
 913                        },
 914                        DiagnosticEntry {
 915                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
 916                            diagnostic: Diagnostic {
 917                                message: "use of moved value\nvalue used here after move".to_string(),
 918                                severity: DiagnosticSeverity::ERROR,
 919                                is_primary: true,
 920                                is_disk_based: true,
 921                                group_id: 0,
 922                                ..Default::default()
 923                            },
 924                        },
 925                        DiagnosticEntry {
 926                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
 927                            diagnostic: Diagnostic {
 928                                message: "use of moved value\nvalue used here after move".to_string(),
 929                                severity: DiagnosticSeverity::ERROR,
 930                                is_primary: true,
 931                                is_disk_based: true,
 932                                group_id: 1,
 933                                ..Default::default()
 934                            },
 935                        },
 936                    ],
 937                    cx,
 938                )
 939                .unwrap();
 940        });
 941
 942        // Open the project diagnostics view while there are already diagnostics.
 943        let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
 944        let view = cx.add_view(0, |cx| {
 945            ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
 946        });
 947
 948        view.next_notification(&cx).await;
 949        view.update(&mut cx, |view, cx| {
 950            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
 951
 952            assert_eq!(
 953                editor_blocks(&editor, cx),
 954                [
 955                    (0, "path header block".into()),
 956                    (2, "diagnostic header".into()),
 957                    (15, "diagnostic header".into()),
 958                    (24, "collapsed context".into()),
 959                ]
 960            );
 961            assert_eq!(
 962                editor.text(),
 963                concat!(
 964                    //
 965                    // main.rs
 966                    //
 967                    "\n", // filename
 968                    "\n", // padding
 969                    // diagnostic group 1
 970                    "\n", // primary message
 971                    "\n", // padding
 972                    "    let x = vec![];\n",
 973                    "    let y = vec![];\n",
 974                    "\n", // supporting diagnostic
 975                    "    a(x);\n",
 976                    "    b(y);\n",
 977                    "\n", // supporting diagnostic
 978                    "    // comment 1\n",
 979                    "    // comment 2\n",
 980                    "    c(y);\n",
 981                    "\n", // supporting diagnostic
 982                    "    d(x);\n",
 983                    // diagnostic group 2
 984                    "\n", // primary message
 985                    "\n", // padding
 986                    "fn main() {\n",
 987                    "    let x = vec![];\n",
 988                    "\n", // supporting diagnostic
 989                    "    let y = vec![];\n",
 990                    "    a(x);\n",
 991                    "\n", // supporting diagnostic
 992                    "    b(y);\n",
 993                    "\n", // context ellipsis
 994                    "    c(y);\n",
 995                    "    d(x);\n",
 996                    "\n", // supporting diagnostic
 997                    "}"
 998                )
 999            );
1000
1001            // Cursor is at the first diagnostic
1002            view.editor.update(cx, |editor, cx| {
1003                assert_eq!(
1004                    editor.selected_display_ranges(cx),
1005                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
1006                );
1007            });
1008        });
1009
1010        // Diagnostics are added for another earlier path.
1011        project.update(&mut cx, |project, cx| {
1012            project.disk_based_diagnostics_started(cx);
1013            project
1014                .update_diagnostic_entries(
1015                    PathBuf::from("/test/consts.rs"),
1016                    None,
1017                    vec![DiagnosticEntry {
1018                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1019                        diagnostic: Diagnostic {
1020                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1021                            severity: DiagnosticSeverity::ERROR,
1022                            is_primary: true,
1023                            is_disk_based: true,
1024                            group_id: 0,
1025                            ..Default::default()
1026                        },
1027                    }],
1028                    cx,
1029                )
1030                .unwrap();
1031            project.disk_based_diagnostics_finished(cx);
1032        });
1033
1034        view.next_notification(&cx).await;
1035        view.update(&mut cx, |view, cx| {
1036            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1037
1038            assert_eq!(
1039                editor_blocks(&editor, cx),
1040                [
1041                    (0, "path header block".into()),
1042                    (2, "diagnostic header".into()),
1043                    (7, "path header block".into()),
1044                    (9, "diagnostic header".into()),
1045                    (22, "diagnostic header".into()),
1046                    (31, "collapsed context".into()),
1047                ]
1048            );
1049            assert_eq!(
1050                editor.text(),
1051                concat!(
1052                    //
1053                    // consts.rs
1054                    //
1055                    "\n", // filename
1056                    "\n", // padding
1057                    // diagnostic group 1
1058                    "\n", // primary message
1059                    "\n", // padding
1060                    "const a: i32 = 'a';\n",
1061                    "\n", // supporting diagnostic
1062                    "const b: i32 = c;\n",
1063                    //
1064                    // main.rs
1065                    //
1066                    "\n", // filename
1067                    "\n", // padding
1068                    // diagnostic group 1
1069                    "\n", // primary message
1070                    "\n", // padding
1071                    "    let x = vec![];\n",
1072                    "    let y = vec![];\n",
1073                    "\n", // supporting diagnostic
1074                    "    a(x);\n",
1075                    "    b(y);\n",
1076                    "\n", // supporting diagnostic
1077                    "    // comment 1\n",
1078                    "    // comment 2\n",
1079                    "    c(y);\n",
1080                    "\n", // supporting diagnostic
1081                    "    d(x);\n",
1082                    // diagnostic group 2
1083                    "\n", // primary message
1084                    "\n", // filename
1085                    "fn main() {\n",
1086                    "    let x = vec![];\n",
1087                    "\n", // supporting diagnostic
1088                    "    let y = vec![];\n",
1089                    "    a(x);\n",
1090                    "\n", // supporting diagnostic
1091                    "    b(y);\n",
1092                    "\n", // context ellipsis
1093                    "    c(y);\n",
1094                    "    d(x);\n",
1095                    "\n", // supporting diagnostic
1096                    "}"
1097                )
1098            );
1099
1100            // Cursor keeps its position.
1101            view.editor.update(cx, |editor, cx| {
1102                assert_eq!(
1103                    editor.selected_display_ranges(cx),
1104                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1105                );
1106            });
1107        });
1108
1109        // Diagnostics are added to the first path
1110        project.update(&mut cx, |project, cx| {
1111            project.disk_based_diagnostics_started(cx);
1112            project
1113                .update_diagnostic_entries(
1114                    PathBuf::from("/test/consts.rs"),
1115                    None,
1116                    vec![
1117                        DiagnosticEntry {
1118                            range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1119                            diagnostic: Diagnostic {
1120                                message: "mismatched types\nexpected `usize`, found `char`"
1121                                    .to_string(),
1122                                severity: DiagnosticSeverity::ERROR,
1123                                is_primary: true,
1124                                is_disk_based: true,
1125                                group_id: 0,
1126                                ..Default::default()
1127                            },
1128                        },
1129                        DiagnosticEntry {
1130                            range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1131                            diagnostic: Diagnostic {
1132                                message: "unresolved name `c`".to_string(),
1133                                severity: DiagnosticSeverity::ERROR,
1134                                is_primary: true,
1135                                is_disk_based: true,
1136                                group_id: 1,
1137                                ..Default::default()
1138                            },
1139                        },
1140                    ],
1141                    cx,
1142                )
1143                .unwrap();
1144            project.disk_based_diagnostics_finished(cx);
1145        });
1146
1147        view.next_notification(&cx).await;
1148        view.update(&mut cx, |view, cx| {
1149            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1150
1151            assert_eq!(
1152                editor_blocks(&editor, cx),
1153                [
1154                    (0, "path header block".into()),
1155                    (2, "diagnostic header".into()),
1156                    (7, "diagnostic header".into()),
1157                    (12, "path header block".into()),
1158                    (14, "diagnostic header".into()),
1159                    (27, "diagnostic header".into()),
1160                    (36, "collapsed context".into()),
1161                ]
1162            );
1163            assert_eq!(
1164                editor.text(),
1165                concat!(
1166                    //
1167                    // consts.rs
1168                    //
1169                    "\n", // filename
1170                    "\n", // padding
1171                    // diagnostic group 1
1172                    "\n", // primary message
1173                    "\n", // padding
1174                    "const a: i32 = 'a';\n",
1175                    "\n", // supporting diagnostic
1176                    "const b: i32 = c;\n",
1177                    // diagnostic group 2
1178                    "\n", // primary message
1179                    "\n", // padding
1180                    "const a: i32 = 'a';\n",
1181                    "const b: i32 = c;\n",
1182                    "\n", // supporting diagnostic
1183                    //
1184                    // main.rs
1185                    //
1186                    "\n", // filename
1187                    "\n", // padding
1188                    // diagnostic group 1
1189                    "\n", // primary message
1190                    "\n", // padding
1191                    "    let x = vec![];\n",
1192                    "    let y = vec![];\n",
1193                    "\n", // supporting diagnostic
1194                    "    a(x);\n",
1195                    "    b(y);\n",
1196                    "\n", // supporting diagnostic
1197                    "    // comment 1\n",
1198                    "    // comment 2\n",
1199                    "    c(y);\n",
1200                    "\n", // supporting diagnostic
1201                    "    d(x);\n",
1202                    // diagnostic group 2
1203                    "\n", // primary message
1204                    "\n", // filename
1205                    "fn main() {\n",
1206                    "    let x = vec![];\n",
1207                    "\n", // supporting diagnostic
1208                    "    let y = vec![];\n",
1209                    "    a(x);\n",
1210                    "\n", // supporting diagnostic
1211                    "    b(y);\n",
1212                    "\n", // context ellipsis
1213                    "    c(y);\n",
1214                    "    d(x);\n",
1215                    "\n", // supporting diagnostic
1216                    "}"
1217                )
1218            );
1219        });
1220    }
1221
1222    fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1223        editor
1224            .blocks_in_range(0..editor.max_point().row())
1225            .filter_map(|(row, block)| {
1226                block
1227                    .render(&BlockContext {
1228                        cx,
1229                        anchor_x: 0.,
1230                        gutter_padding: 0.,
1231                        gutter_width: 0.,
1232                        em_width: 0.,
1233                    })
1234                    .name()
1235                    .map(|s| (row, s.to_string()))
1236            })
1237            .collect()
1238    }
1239}