commit_view.rs

   1use anyhow::{Context as _, Result};
   2use buffer_diff::BufferDiff;
   3use editor::display_map::{BlockPlacement, BlockProperties, BlockStyle};
   4use editor::{Editor, EditorEvent, ExcerptRange, MultiBuffer, multibuffer_context_lines};
   5use git::repository::{CommitDetails, CommitDiff, RepoPath};
   6use git::{
   7    BuildCommitPermalinkParams, GitHostingProviderRegistry, GitRemote, ParsedGitRemote,
   8    parse_git_remote_url,
   9};
  10use gpui::{
  11    AnyElement, App, AppContext as _, AsyncApp, AsyncWindowContext, ClipboardItem, Context,
  12    Element, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement,
  13    ParentElement, PromptLevel, Render, Styled, Task, WeakEntity, Window, actions,
  14};
  15use language::{
  16    Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,
  17    Point, ReplicaId, Rope, TextBuffer,
  18};
  19use multi_buffer::PathKey;
  20use project::{Project, WorktreeId, git_store::Repository};
  21use std::{
  22    any::{Any, TypeId},
  23    path::PathBuf,
  24    sync::Arc,
  25};
  26use theme::ActiveTheme;
  27use ui::{ButtonLike, DiffStat, Tooltip, prelude::*};
  28use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};
  29use workspace::item::TabTooltipContent;
  30use workspace::{
  31    Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
  32    Workspace,
  33    item::{BreadcrumbText, ItemEvent, TabContentParams},
  34    notifications::NotifyTaskExt,
  35    pane::SaveIntent,
  36    searchable::SearchableItemHandle,
  37};
  38
  39use crate::commit_tooltip::CommitAvatar;
  40use crate::git_panel::GitPanel;
  41
  42actions!(git, [ApplyCurrentStash, PopCurrentStash, DropCurrentStash,]);
  43
  44pub fn init(cx: &mut App) {
  45    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
  46        workspace.register_action(|workspace, _: &ApplyCurrentStash, window, cx| {
  47            CommitView::apply_stash(workspace, window, cx);
  48        });
  49        workspace.register_action(|workspace, _: &DropCurrentStash, window, cx| {
  50            CommitView::remove_stash(workspace, window, cx);
  51        });
  52        workspace.register_action(|workspace, _: &PopCurrentStash, window, cx| {
  53            CommitView::pop_stash(workspace, window, cx);
  54        });
  55    })
  56    .detach();
  57}
  58
  59pub struct CommitView {
  60    commit: CommitDetails,
  61    editor: Entity<Editor>,
  62    stash: Option<usize>,
  63    multibuffer: Entity<MultiBuffer>,
  64    repository: Entity<Repository>,
  65    remote: Option<GitRemote>,
  66}
  67
  68struct GitBlob {
  69    path: RepoPath,
  70    worktree_id: WorktreeId,
  71    is_deleted: bool,
  72    display_name: String,
  73}
  74
  75const COMMIT_MESSAGE_SORT_PREFIX: u64 = 0;
  76const FILE_NAMESPACE_SORT_PREFIX: u64 = 1;
  77
  78impl CommitView {
  79    pub fn open(
  80        commit_sha: String,
  81        repo: WeakEntity<Repository>,
  82        workspace: WeakEntity<Workspace>,
  83        stash: Option<usize>,
  84        file_filter: Option<RepoPath>,
  85        window: &mut Window,
  86        cx: &mut App,
  87    ) {
  88        let commit_diff = repo
  89            .update(cx, |repo, _| repo.load_commit_diff(commit_sha.clone()))
  90            .ok();
  91        let commit_details = repo
  92            .update(cx, |repo, _| repo.show(commit_sha.clone()))
  93            .ok();
  94
  95        window
  96            .spawn(cx, async move |cx| {
  97                let (commit_diff, commit_details) = futures::join!(commit_diff?, commit_details?);
  98                let mut commit_diff = commit_diff.log_err()?.log_err()?;
  99                let commit_details = commit_details.log_err()?.log_err()?;
 100
 101                // Filter to specific file if requested
 102                if let Some(ref filter_path) = file_filter {
 103                    commit_diff.files.retain(|f| &f.path == filter_path);
 104                }
 105
 106                let repo = repo.upgrade()?;
 107
 108                workspace
 109                    .update_in(cx, |workspace, window, cx| {
 110                        let project = workspace.project();
 111                        let commit_view = cx.new(|cx| {
 112                            CommitView::new(
 113                                commit_details,
 114                                commit_diff,
 115                                repo,
 116                                project.clone(),
 117                                stash,
 118                                window,
 119                                cx,
 120                            )
 121                        });
 122
 123                        let pane = workspace.active_pane();
 124                        pane.update(cx, |pane, cx| {
 125                            let ix = pane.items().position(|item| {
 126                                let commit_view = item.downcast::<CommitView>();
 127                                commit_view
 128                                    .is_some_and(|view| view.read(cx).commit.sha == commit_sha)
 129                            });
 130                            if let Some(ix) = ix {
 131                                pane.activate_item(ix, true, true, window, cx);
 132                            } else {
 133                                pane.add_item(Box::new(commit_view), true, true, None, window, cx);
 134                            }
 135                        })
 136                    })
 137                    .log_err()
 138            })
 139            .detach();
 140    }
 141
 142    fn new(
 143        commit: CommitDetails,
 144        commit_diff: CommitDiff,
 145        repository: Entity<Repository>,
 146        project: Entity<Project>,
 147        stash: Option<usize>,
 148        window: &mut Window,
 149        cx: &mut Context<Self>,
 150    ) -> Self {
 151        let language_registry = project.read(cx).languages().clone();
 152        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadOnly));
 153
 154        let message_buffer = cx.new(|cx| {
 155            let mut buffer = Buffer::local(commit.message.clone(), cx);
 156            buffer.set_capability(Capability::ReadOnly, cx);
 157            buffer
 158        });
 159
 160        multibuffer.update(cx, |multibuffer, cx| {
 161            let snapshot = message_buffer.read(cx).snapshot();
 162            let full_range = Point::zero()..snapshot.max_point();
 163            let range = ExcerptRange {
 164                context: full_range.clone(),
 165                primary: full_range,
 166            };
 167            multibuffer.set_excerpt_ranges_for_path(
 168                PathKey::with_sort_prefix(
 169                    COMMIT_MESSAGE_SORT_PREFIX,
 170                    RelPath::unix("commit message").unwrap().into(),
 171                ),
 172                message_buffer.clone(),
 173                &snapshot,
 174                vec![range],
 175                cx,
 176            )
 177        });
 178
 179        let editor = cx.new(|cx| {
 180            let mut editor =
 181                Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
 182
 183            editor.disable_inline_diagnostics();
 184            editor.set_show_breakpoints(false, cx);
 185            editor.set_expand_all_diff_hunks(cx);
 186            editor.disable_header_for_buffer(message_buffer.read(cx).remote_id(), cx);
 187            editor.disable_indent_guides_for_buffer(message_buffer.read(cx).remote_id(), cx);
 188
 189            editor.insert_blocks(
 190                [BlockProperties {
 191                    placement: BlockPlacement::Above(editor::Anchor::min()),
 192                    height: Some(1),
 193                    style: BlockStyle::Sticky,
 194                    render: Arc::new(|_| gpui::Empty.into_any_element()),
 195                    priority: 0,
 196                }]
 197                .into_iter()
 198                .chain(
 199                    editor
 200                        .buffer()
 201                        .read(cx)
 202                        .buffer_anchor_to_anchor(&message_buffer, Anchor::MAX, cx)
 203                        .map(|anchor| BlockProperties {
 204                            placement: BlockPlacement::Below(anchor),
 205                            height: Some(1),
 206                            style: BlockStyle::Sticky,
 207                            render: Arc::new(|_| gpui::Empty.into_any_element()),
 208                            priority: 0,
 209                        }),
 210                ),
 211                None,
 212                cx,
 213            );
 214
 215            editor
 216        });
 217
 218        let commit_sha = Arc::<str>::from(commit.sha.as_ref());
 219
 220        let first_worktree_id = project
 221            .read(cx)
 222            .worktrees(cx)
 223            .next()
 224            .map(|worktree| worktree.read(cx).id());
 225
 226        let repository_clone = repository.clone();
 227
 228        cx.spawn(async move |this, cx| {
 229            for file in commit_diff.files {
 230                let is_deleted = file.new_text.is_none();
 231                let new_text = file.new_text.unwrap_or_default();
 232                let old_text = file.old_text;
 233                let worktree_id = repository_clone
 234                    .update(cx, |repository, cx| {
 235                        repository
 236                            .repo_path_to_project_path(&file.path, cx)
 237                            .map(|path| path.worktree_id)
 238                            .or(first_worktree_id)
 239                    })?
 240                    .context("project has no worktrees")?;
 241                let short_sha = commit_sha.get(0..7).unwrap_or(&commit_sha);
 242                let file_name = file
 243                    .path
 244                    .file_name()
 245                    .map(|name| name.to_string())
 246                    .unwrap_or_else(|| file.path.display(PathStyle::local()).to_string());
 247                let display_name = format!("{short_sha} - {file_name}");
 248
 249                let file = Arc::new(GitBlob {
 250                    path: file.path.clone(),
 251                    is_deleted,
 252                    worktree_id,
 253                    display_name,
 254                }) as Arc<dyn language::File>;
 255
 256                let buffer = build_buffer(new_text, file, &language_registry, cx).await?;
 257                let buffer_diff =
 258                    build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;
 259
 260                this.update(cx, |this, cx| {
 261                    this.multibuffer.update(cx, |multibuffer, cx| {
 262                        let snapshot = buffer.read(cx).snapshot();
 263                        let path = snapshot.file().unwrap().path().clone();
 264                        let excerpt_ranges = {
 265                            let diff_snapshot = buffer_diff.read(cx).snapshot(cx);
 266                            let mut hunks = diff_snapshot.hunks(&snapshot).peekable();
 267                            if hunks.peek().is_none() {
 268                                vec![language::Point::zero()..snapshot.max_point()]
 269                            } else {
 270                                hunks
 271                                    .map(|hunk| hunk.buffer_range.to_point(&snapshot))
 272                                    .collect::<Vec<_>>()
 273                            }
 274                        };
 275
 276                        let _is_newly_added = multibuffer.set_excerpts_for_path(
 277                            PathKey::with_sort_prefix(FILE_NAMESPACE_SORT_PREFIX, path),
 278                            buffer,
 279                            excerpt_ranges,
 280                            multibuffer_context_lines(cx),
 281                            cx,
 282                        );
 283                        multibuffer.add_diff(buffer_diff, cx);
 284                    });
 285                })?;
 286            }
 287
 288            anyhow::Ok(())
 289        })
 290        .detach();
 291
 292        let snapshot = repository.read(cx).snapshot();
 293        let remote_url = snapshot
 294            .remote_upstream_url
 295            .as_ref()
 296            .or(snapshot.remote_origin_url.as_ref());
 297
 298        let remote = remote_url.and_then(|url| {
 299            let provider_registry = GitHostingProviderRegistry::default_global(cx);
 300            parse_git_remote_url(provider_registry, url).map(|(host, parsed)| GitRemote {
 301                host,
 302                owner: parsed.owner.into(),
 303                repo: parsed.repo.into(),
 304            })
 305        });
 306
 307        Self {
 308            commit,
 309            editor,
 310            multibuffer,
 311            stash,
 312            repository,
 313            remote,
 314        }
 315    }
 316
 317    fn render_commit_avatar(
 318        &self,
 319        sha: &SharedString,
 320        size: impl Into<gpui::AbsoluteLength>,
 321        window: &mut Window,
 322        cx: &mut App,
 323    ) -> AnyElement {
 324        let size = size.into();
 325        let avatar = CommitAvatar::new(sha, self.remote.as_ref());
 326
 327        v_flex()
 328            .w(size)
 329            .h(size)
 330            .border_1()
 331            .border_color(cx.theme().colors().border)
 332            .rounded_full()
 333            .justify_center()
 334            .items_center()
 335            .child(
 336                avatar
 337                    .avatar(window, cx)
 338                    .map(|a| a.size(size).into_any_element())
 339                    .unwrap_or_else(|| {
 340                        Icon::new(IconName::Person)
 341                            .color(Color::Muted)
 342                            .size(IconSize::Medium)
 343                            .into_any_element()
 344                    }),
 345            )
 346            .into_any()
 347    }
 348
 349    fn calculate_changed_lines(&self, cx: &App) -> (u32, u32) {
 350        let snapshot = self.multibuffer.read(cx).snapshot(cx);
 351        let mut total_additions = 0u32;
 352        let mut total_deletions = 0u32;
 353
 354        let mut seen_buffers = std::collections::HashSet::new();
 355        for (_, buffer, _) in snapshot.excerpts() {
 356            let buffer_id = buffer.remote_id();
 357            if !seen_buffers.insert(buffer_id) {
 358                continue;
 359            }
 360
 361            let Some(diff) = snapshot.diff_for_buffer_id(buffer_id) else {
 362                continue;
 363            };
 364
 365            let base_text = diff.base_text();
 366
 367            for hunk in diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer) {
 368                let added_rows = hunk.range.end.row.saturating_sub(hunk.range.start.row);
 369                total_additions += added_rows;
 370
 371                let base_start = base_text
 372                    .offset_to_point(hunk.diff_base_byte_range.start)
 373                    .row;
 374                let base_end = base_text.offset_to_point(hunk.diff_base_byte_range.end).row;
 375                let deleted_rows = base_end.saturating_sub(base_start);
 376
 377                total_deletions += deleted_rows;
 378            }
 379        }
 380
 381        (total_additions, total_deletions)
 382    }
 383
 384    fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 385        let commit = &self.commit;
 386        let author_name = commit.author_name.clone();
 387        let commit_sha = commit.sha.clone();
 388        let commit_date = time::OffsetDateTime::from_unix_timestamp(commit.commit_timestamp)
 389            .unwrap_or_else(|_| time::OffsetDateTime::now_utc());
 390        let local_offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
 391        let date_string = time_format::format_localized_timestamp(
 392            commit_date,
 393            time::OffsetDateTime::now_utc(),
 394            local_offset,
 395            time_format::TimestampFormat::MediumAbsolute,
 396        );
 397
 398        let remote_info = self.remote.as_ref().map(|remote| {
 399            let provider = remote.host.name();
 400            let parsed_remote = ParsedGitRemote {
 401                owner: remote.owner.as_ref().into(),
 402                repo: remote.repo.as_ref().into(),
 403            };
 404            let params = BuildCommitPermalinkParams { sha: &commit.sha };
 405            let url = remote
 406                .host
 407                .build_commit_permalink(&parsed_remote, params)
 408                .to_string();
 409            (provider, url)
 410        });
 411
 412        let (additions, deletions) = self.calculate_changed_lines(cx);
 413
 414        let commit_diff_stat = if additions > 0 || deletions > 0 {
 415            Some(DiffStat::new(
 416                "commit-diff-stat",
 417                additions as usize,
 418                deletions as usize,
 419            ))
 420        } else {
 421            None
 422        };
 423
 424        let gutter_width = self.editor.update(cx, |editor, cx| {
 425            let snapshot = editor.snapshot(window, cx);
 426            let style = editor.style(cx);
 427            let font_id = window.text_system().resolve_font(&style.text.font());
 428            let font_size = style.text.font_size.to_pixels(window.rem_size());
 429            snapshot
 430                .gutter_dimensions(font_id, font_size, style, window, cx)
 431                .full_width()
 432        });
 433
 434        let clipboard_has_link = cx
 435            .read_from_clipboard()
 436            .and_then(|entry| entry.text())
 437            .map_or(false, |clipboard_text| {
 438                clipboard_text.trim() == commit_sha.as_ref()
 439            });
 440
 441        let (copy_icon, copy_icon_color) = if clipboard_has_link {
 442            (IconName::Check, Color::Success)
 443        } else {
 444            (IconName::Copy, Color::Muted)
 445        };
 446
 447        h_flex()
 448            .border_b_1()
 449            .border_color(cx.theme().colors().border_variant)
 450            .w_full()
 451            .child(
 452                h_flex()
 453                    .w(gutter_width)
 454                    .justify_center()
 455                    .child(self.render_commit_avatar(&commit.sha, rems_from_px(48.), window, cx)),
 456            )
 457            .child(
 458                h_flex()
 459                    .py_4()
 460                    .pl_1()
 461                    .pr_4()
 462                    .w_full()
 463                    .items_start()
 464                    .justify_between()
 465                    .flex_wrap()
 466                    .child(
 467                        v_flex()
 468                            .child(
 469                                h_flex()
 470                                    .gap_1()
 471                                    .child(Label::new(author_name).color(Color::Default))
 472                                    .child({
 473                                        ButtonLike::new("sha")
 474                                            .child(
 475                                                h_flex()
 476                                                    .group("sha_btn")
 477                                                    .size_full()
 478                                                    .max_w_32()
 479                                                    .gap_0p5()
 480                                                    .child(
 481                                                        Label::new(commit_sha.clone())
 482                                                            .color(Color::Muted)
 483                                                            .size(LabelSize::Small)
 484                                                            .truncate()
 485                                                            .buffer_font(cx),
 486                                                    )
 487                                                    .child(
 488                                                        div().visible_on_hover("sha_btn").child(
 489                                                            Icon::new(copy_icon)
 490                                                                .color(copy_icon_color)
 491                                                                .size(IconSize::Small),
 492                                                        ),
 493                                                    ),
 494                                            )
 495                                            .tooltip({
 496                                                let commit_sha = commit_sha.clone();
 497                                                move |_, cx| {
 498                                                    Tooltip::with_meta(
 499                                                        "Copy Commit SHA",
 500                                                        None,
 501                                                        commit_sha.clone(),
 502                                                        cx,
 503                                                    )
 504                                                }
 505                                            })
 506                                            .on_click(move |_, _, cx| {
 507                                                cx.stop_propagation();
 508                                                cx.write_to_clipboard(ClipboardItem::new_string(
 509                                                    commit_sha.to_string(),
 510                                                ));
 511                                            })
 512                                    }),
 513                            )
 514                            .child(
 515                                h_flex()
 516                                    .gap_1p5()
 517                                    .child(
 518                                        Label::new(date_string)
 519                                            .color(Color::Muted)
 520                                            .size(LabelSize::Small),
 521                                    )
 522                                    .child(
 523                                        Label::new("")
 524                                            .color(Color::Ignored)
 525                                            .size(LabelSize::Small),
 526                                    )
 527                                    .children(commit_diff_stat),
 528                            ),
 529                    )
 530                    .children(remote_info.map(|(provider_name, url)| {
 531                        let icon = match provider_name.as_str() {
 532                            "GitHub" => IconName::Github,
 533                            _ => IconName::Link,
 534                        };
 535
 536                        Button::new("view_on_provider", format!("View on {}", provider_name))
 537                            .icon(icon)
 538                            .icon_color(Color::Muted)
 539                            .icon_size(IconSize::Small)
 540                            .icon_position(IconPosition::Start)
 541                            .on_click(move |_, _, cx| cx.open_url(&url))
 542                    })),
 543            )
 544    }
 545
 546    fn apply_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
 547        Self::stash_action(
 548            workspace,
 549            "Apply",
 550            window,
 551            cx,
 552            async move |repository, sha, stash, commit_view, workspace, cx| {
 553                let result = repository.update(cx, |repo, cx| {
 554                    if !stash_matches_index(&sha, stash, repo) {
 555                        return Err(anyhow::anyhow!("Stash has changed, not applying"));
 556                    }
 557                    Ok(repo.stash_apply(Some(stash), cx))
 558                })?;
 559
 560                match result {
 561                    Ok(task) => task.await?,
 562                    Err(err) => {
 563                        Self::close_commit_view(commit_view, workspace, cx).await?;
 564                        return Err(err);
 565                    }
 566                };
 567                Self::close_commit_view(commit_view, workspace, cx).await?;
 568                anyhow::Ok(())
 569            },
 570        );
 571    }
 572
 573    fn pop_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
 574        Self::stash_action(
 575            workspace,
 576            "Pop",
 577            window,
 578            cx,
 579            async move |repository, sha, stash, commit_view, workspace, cx| {
 580                let result = repository.update(cx, |repo, cx| {
 581                    if !stash_matches_index(&sha, stash, repo) {
 582                        return Err(anyhow::anyhow!("Stash has changed, pop aborted"));
 583                    }
 584                    Ok(repo.stash_pop(Some(stash), cx))
 585                })?;
 586
 587                match result {
 588                    Ok(task) => task.await?,
 589                    Err(err) => {
 590                        Self::close_commit_view(commit_view, workspace, cx).await?;
 591                        return Err(err);
 592                    }
 593                };
 594                Self::close_commit_view(commit_view, workspace, cx).await?;
 595                anyhow::Ok(())
 596            },
 597        );
 598    }
 599
 600    fn remove_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
 601        Self::stash_action(
 602            workspace,
 603            "Drop",
 604            window,
 605            cx,
 606            async move |repository, sha, stash, commit_view, workspace, cx| {
 607                let result = repository.update(cx, |repo, cx| {
 608                    if !stash_matches_index(&sha, stash, repo) {
 609                        return Err(anyhow::anyhow!("Stash has changed, drop aborted"));
 610                    }
 611                    Ok(repo.stash_drop(Some(stash), cx))
 612                })?;
 613
 614                match result {
 615                    Ok(task) => task.await??,
 616                    Err(err) => {
 617                        Self::close_commit_view(commit_view, workspace, cx).await?;
 618                        return Err(err);
 619                    }
 620                };
 621                Self::close_commit_view(commit_view, workspace, cx).await?;
 622                anyhow::Ok(())
 623            },
 624        );
 625    }
 626
 627    fn stash_action<AsyncFn>(
 628        workspace: &mut Workspace,
 629        str_action: &str,
 630        window: &mut Window,
 631        cx: &mut App,
 632        callback: AsyncFn,
 633    ) where
 634        AsyncFn: AsyncFnOnce(
 635                Entity<Repository>,
 636                &SharedString,
 637                usize,
 638                Entity<CommitView>,
 639                WeakEntity<Workspace>,
 640                &mut AsyncWindowContext,
 641            ) -> anyhow::Result<()>
 642            + 'static,
 643    {
 644        let Some(commit_view) = workspace.active_item_as::<CommitView>(cx) else {
 645            return;
 646        };
 647        let Some(stash) = commit_view.read(cx).stash else {
 648            return;
 649        };
 650        let sha = commit_view.read(cx).commit.sha.clone();
 651        let answer = window.prompt(
 652            PromptLevel::Info,
 653            &format!("{} stash@{{{}}}?", str_action, stash),
 654            None,
 655            &[str_action, "Cancel"],
 656            cx,
 657        );
 658
 659        let workspace_weak = workspace.weak_handle();
 660        let commit_view_entity = commit_view;
 661
 662        window
 663            .spawn(cx, async move |cx| {
 664                if answer.await != Ok(0) {
 665                    return anyhow::Ok(());
 666                }
 667
 668                let Some(workspace) = workspace_weak.upgrade() else {
 669                    return Ok(());
 670                };
 671
 672                let repo = workspace.update(cx, |workspace, cx| {
 673                    workspace
 674                        .panel::<GitPanel>(cx)
 675                        .and_then(|p| p.read(cx).active_repository.clone())
 676                })?;
 677
 678                let Some(repo) = repo else {
 679                    return Ok(());
 680                };
 681
 682                callback(repo, &sha, stash, commit_view_entity, workspace_weak, cx).await?;
 683                anyhow::Ok(())
 684            })
 685            .detach_and_notify_err(window, cx);
 686    }
 687
 688    async fn close_commit_view(
 689        commit_view: Entity<CommitView>,
 690        workspace: WeakEntity<Workspace>,
 691        cx: &mut AsyncWindowContext,
 692    ) -> anyhow::Result<()> {
 693        workspace
 694            .update_in(cx, |workspace, window, cx| {
 695                let active_pane = workspace.active_pane();
 696                let commit_view_id = commit_view.entity_id();
 697                active_pane.update(cx, |pane, cx| {
 698                    pane.close_item_by_id(commit_view_id, SaveIntent::Skip, window, cx)
 699                })
 700            })?
 701            .await?;
 702        anyhow::Ok(())
 703    }
 704}
 705
 706impl language::File for GitBlob {
 707    fn as_local(&self) -> Option<&dyn language::LocalFile> {
 708        None
 709    }
 710
 711    fn disk_state(&self) -> DiskState {
 712        DiskState::Historic {
 713            was_deleted: self.is_deleted,
 714        }
 715    }
 716
 717    fn path_style(&self, _: &App) -> PathStyle {
 718        PathStyle::local()
 719    }
 720
 721    fn path(&self) -> &Arc<RelPath> {
 722        self.path.as_ref()
 723    }
 724
 725    fn full_path(&self, _: &App) -> PathBuf {
 726        self.path.as_std_path().to_path_buf()
 727    }
 728
 729    fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
 730        self.display_name.as_ref()
 731    }
 732
 733    fn worktree_id(&self, _: &App) -> WorktreeId {
 734        self.worktree_id
 735    }
 736
 737    fn to_proto(&self, _cx: &App) -> language::proto::File {
 738        unimplemented!()
 739    }
 740
 741    fn is_private(&self) -> bool {
 742        false
 743    }
 744}
 745
 746async fn build_buffer(
 747    mut text: String,
 748    blob: Arc<dyn File>,
 749    language_registry: &Arc<language::LanguageRegistry>,
 750    cx: &mut AsyncApp,
 751) -> Result<Entity<Buffer>> {
 752    let line_ending = LineEnding::detect(&text);
 753    LineEnding::normalize(&mut text);
 754    let text = Rope::from(text);
 755    let language = cx.update(|cx| language_registry.language_for_file(&blob, Some(&text), cx))?;
 756    let language = if let Some(language) = language {
 757        language_registry
 758            .load_language(&language)
 759            .await
 760            .ok()
 761            .and_then(|e| e.log_err())
 762    } else {
 763        None
 764    };
 765    let buffer = cx.new(|cx| {
 766        let buffer = TextBuffer::new_normalized(
 767            ReplicaId::LOCAL,
 768            cx.entity_id().as_non_zero_u64().into(),
 769            line_ending,
 770            text,
 771        );
 772        let mut buffer = Buffer::build(buffer, Some(blob), Capability::ReadWrite);
 773        buffer.set_language_async(language, cx);
 774        buffer
 775    })?;
 776    Ok(buffer)
 777}
 778
 779async fn build_buffer_diff(
 780    mut old_text: Option<String>,
 781    buffer: &Entity<Buffer>,
 782    language_registry: &Arc<LanguageRegistry>,
 783    cx: &mut AsyncApp,
 784) -> Result<Entity<BufferDiff>> {
 785    if let Some(old_text) = &mut old_text {
 786        LineEnding::normalize(old_text);
 787    }
 788
 789    let language = cx.update(|cx| buffer.read(cx).language().cloned())?;
 790    let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
 791
 792    let diff = cx.new(|cx| BufferDiff::new(&buffer.text, cx))?;
 793
 794    let update = diff
 795        .update(cx, |diff, cx| {
 796            diff.update_diff(
 797                buffer.text.clone(),
 798                old_text.map(|old_text| Arc::from(old_text.as_str())),
 799                true,
 800                language.clone(),
 801                cx,
 802            )
 803        })?
 804        .await;
 805
 806    diff.update(cx, |diff, cx| {
 807        diff.language_changed(language, Some(language_registry.clone()), cx);
 808        diff.set_snapshot(update, &buffer.text, cx)
 809    })
 810    .ok();
 811
 812    Ok(diff)
 813}
 814
 815impl EventEmitter<EditorEvent> for CommitView {}
 816
 817impl Focusable for CommitView {
 818    fn focus_handle(&self, cx: &App) -> FocusHandle {
 819        self.editor.focus_handle(cx)
 820    }
 821}
 822
 823impl Item for CommitView {
 824    type Event = EditorEvent;
 825
 826    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
 827        Some(Icon::new(IconName::GitBranch).color(Color::Muted))
 828    }
 829
 830    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
 831        Label::new(self.tab_content_text(params.detail.unwrap_or_default(), cx))
 832            .color(if params.selected {
 833                Color::Default
 834            } else {
 835                Color::Muted
 836            })
 837            .into_any_element()
 838    }
 839
 840    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
 841        let short_sha = self.commit.sha.get(0..7).unwrap_or(&*self.commit.sha);
 842        let subject = truncate_and_trailoff(self.commit.message.split('\n').next().unwrap(), 20);
 843        format!("{short_sha}{subject}").into()
 844    }
 845
 846    fn tab_tooltip_content(&self, _: &App) -> Option<TabTooltipContent> {
 847        let short_sha = self.commit.sha.get(0..16).unwrap_or(&*self.commit.sha);
 848        let subject = self.commit.message.split('\n').next().unwrap();
 849
 850        Some(TabTooltipContent::Custom(Box::new(Tooltip::element({
 851            let subject = subject.to_string();
 852            let short_sha = short_sha.to_string();
 853
 854            move |_, _| {
 855                v_flex()
 856                    .child(Label::new(subject.clone()))
 857                    .child(
 858                        Label::new(short_sha.clone())
 859                            .color(Color::Muted)
 860                            .size(LabelSize::Small),
 861                    )
 862                    .into_any_element()
 863            }
 864        }))))
 865    }
 866
 867    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
 868        Editor::to_item_events(event, f)
 869    }
 870
 871    fn telemetry_event_text(&self) -> Option<&'static str> {
 872        Some("Commit View Opened")
 873    }
 874
 875    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 876        self.editor
 877            .update(cx, |editor, cx| editor.deactivated(window, cx));
 878    }
 879
 880    fn act_as_type<'a>(
 881        &'a self,
 882        type_id: TypeId,
 883        self_handle: &'a Entity<Self>,
 884        _: &'a App,
 885    ) -> Option<gpui::AnyEntity> {
 886        if type_id == TypeId::of::<Self>() {
 887            Some(self_handle.clone().into())
 888        } else if type_id == TypeId::of::<Editor>() {
 889            Some(self.editor.clone().into())
 890        } else {
 891            None
 892        }
 893    }
 894
 895    fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
 896        Some(Box::new(self.editor.clone()))
 897    }
 898
 899    fn for_each_project_item(
 900        &self,
 901        cx: &App,
 902        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
 903    ) {
 904        self.editor.for_each_project_item(cx, f)
 905    }
 906
 907    fn set_nav_history(
 908        &mut self,
 909        nav_history: ItemNavHistory,
 910        _: &mut Window,
 911        cx: &mut Context<Self>,
 912    ) {
 913        self.editor.update(cx, |editor, _| {
 914            editor.set_nav_history(Some(nav_history));
 915        });
 916    }
 917
 918    fn navigate(
 919        &mut self,
 920        data: Box<dyn Any>,
 921        window: &mut Window,
 922        cx: &mut Context<Self>,
 923    ) -> bool {
 924        self.editor
 925            .update(cx, |editor, cx| editor.navigate(data, window, cx))
 926    }
 927
 928    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
 929        ToolbarItemLocation::Hidden
 930    }
 931
 932    fn breadcrumbs(&self, _theme: &theme::Theme, _cx: &App) -> Option<Vec<BreadcrumbText>> {
 933        None
 934    }
 935
 936    fn added_to_workspace(
 937        &mut self,
 938        workspace: &mut Workspace,
 939        window: &mut Window,
 940        cx: &mut Context<Self>,
 941    ) {
 942        self.editor.update(cx, |editor, cx| {
 943            editor.added_to_workspace(workspace, window, cx)
 944        });
 945    }
 946
 947    fn can_split(&self) -> bool {
 948        true
 949    }
 950
 951    fn clone_on_split(
 952        &self,
 953        _workspace_id: Option<workspace::WorkspaceId>,
 954        window: &mut Window,
 955        cx: &mut Context<Self>,
 956    ) -> Task<Option<Entity<Self>>>
 957    where
 958        Self: Sized,
 959    {
 960        Task::ready(Some(cx.new(|cx| {
 961            let editor = cx.new(|cx| {
 962                self.editor
 963                    .update(cx, |editor, cx| editor.clone(window, cx))
 964            });
 965            let multibuffer = editor.read(cx).buffer().clone();
 966            Self {
 967                editor,
 968                multibuffer,
 969                commit: self.commit.clone(),
 970                stash: self.stash,
 971                repository: self.repository.clone(),
 972                remote: self.remote.clone(),
 973            }
 974        })))
 975    }
 976}
 977
 978impl Render for CommitView {
 979    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 980        let is_stash = self.stash.is_some();
 981
 982        v_flex()
 983            .key_context(if is_stash { "StashDiff" } else { "CommitDiff" })
 984            .size_full()
 985            .bg(cx.theme().colors().editor_background)
 986            .child(self.render_header(window, cx))
 987            .when(!self.editor.read(cx).is_empty(cx), |this| {
 988                this.child(div().flex_grow().child(self.editor.clone()))
 989            })
 990    }
 991}
 992
 993pub struct CommitViewToolbar {
 994    commit_view: Option<WeakEntity<CommitView>>,
 995}
 996
 997impl CommitViewToolbar {
 998    pub fn new() -> Self {
 999        Self { commit_view: None }
1000    }
1001}
1002
1003impl EventEmitter<ToolbarItemEvent> for CommitViewToolbar {}
1004
1005impl Render for CommitViewToolbar {
1006    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1007        div().hidden()
1008    }
1009}
1010
1011impl ToolbarItemView for CommitViewToolbar {
1012    fn set_active_pane_item(
1013        &mut self,
1014        active_pane_item: Option<&dyn ItemHandle>,
1015        _: &mut Window,
1016        cx: &mut Context<Self>,
1017    ) -> ToolbarItemLocation {
1018        if let Some(entity) = active_pane_item.and_then(|i| i.act_as::<CommitView>(cx))
1019            && entity.read(cx).stash.is_some()
1020        {
1021            self.commit_view = Some(entity.downgrade());
1022            return ToolbarItemLocation::PrimaryRight;
1023        }
1024        ToolbarItemLocation::Hidden
1025    }
1026
1027    fn pane_focus_update(
1028        &mut self,
1029        _pane_focused: bool,
1030        _window: &mut Window,
1031        _cx: &mut Context<Self>,
1032    ) {
1033    }
1034}
1035
1036fn stash_matches_index(sha: &str, stash_index: usize, repo: &Repository) -> bool {
1037    repo.stash_entries
1038        .entries
1039        .get(stash_index)
1040        .map(|entry| entry.oid.to_string() == sha)
1041        .unwrap_or(false)
1042}