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