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