commit_view.rs

  1use anyhow::{Context as _, Result};
  2use buffer_diff::{BufferDiff, BufferDiffSnapshot};
  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, Context, Element, Entity,
 12    EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,
 13    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::{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 mut hunks = buffer_diff.read(cx).hunks(&snapshot, cx).peekable();
266                            if hunks.peek().is_none() {
267                                vec![language::Point::zero()..snapshot.max_point()]
268                            } else {
269                                hunks
270                                    .map(|hunk| hunk.buffer_range.to_point(&snapshot))
271                                    .collect::<Vec<_>>()
272                            }
273                        };
274
275                        let _is_newly_added = multibuffer.set_excerpts_for_path(
276                            PathKey::with_sort_prefix(FILE_NAMESPACE_SORT_PREFIX, path),
277                            buffer,
278                            excerpt_ranges,
279                            multibuffer_context_lines(cx),
280                            cx,
281                        );
282                        multibuffer.add_diff(buffer_diff, cx);
283                    });
284                })?;
285            }
286
287            anyhow::Ok(())
288        })
289        .detach();
290
291        let snapshot = repository.read(cx).snapshot();
292        let remote_url = snapshot
293            .remote_upstream_url
294            .as_ref()
295            .or(snapshot.remote_origin_url.as_ref());
296
297        let remote = remote_url.and_then(|url| {
298            let provider_registry = GitHostingProviderRegistry::default_global(cx);
299            parse_git_remote_url(provider_registry, url).map(|(host, parsed)| GitRemote {
300                host,
301                owner: parsed.owner.into(),
302                repo: parsed.repo.into(),
303            })
304        });
305
306        Self {
307            commit,
308            editor,
309            multibuffer,
310            stash,
311            repository,
312            remote,
313        }
314    }
315
316    fn render_commit_avatar(
317        &self,
318        sha: &SharedString,
319        size: impl Into<gpui::AbsoluteLength>,
320        window: &mut Window,
321        cx: &mut App,
322    ) -> AnyElement {
323        let size = size.into();
324        let avatar = CommitAvatar::new(sha, self.remote.as_ref());
325
326        v_flex()
327            .w(size)
328            .h(size)
329            .border_1()
330            .border_color(cx.theme().colors().border)
331            .rounded_full()
332            .justify_center()
333            .items_center()
334            .child(
335                avatar
336                    .avatar(window, cx)
337                    .map(|a| a.size(size).into_any_element())
338                    .unwrap_or_else(|| {
339                        Icon::new(IconName::Person)
340                            .color(Color::Muted)
341                            .size(IconSize::Medium)
342                            .into_any_element()
343                    }),
344            )
345            .into_any()
346    }
347
348    fn calculate_changed_lines(&self, cx: &App) -> (u32, u32) {
349        let snapshot = self.multibuffer.read(cx).snapshot(cx);
350        let mut total_additions = 0u32;
351        let mut total_deletions = 0u32;
352
353        let mut seen_buffers = std::collections::HashSet::new();
354        for (_, buffer, _) in snapshot.excerpts() {
355            let buffer_id = buffer.remote_id();
356            if !seen_buffers.insert(buffer_id) {
357                continue;
358            }
359
360            let Some(diff) = snapshot.diff_for_buffer_id(buffer_id) else {
361                continue;
362            };
363
364            let base_text = diff.base_text();
365
366            for hunk in diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, buffer) {
367                let added_rows = hunk.range.end.row.saturating_sub(hunk.range.start.row);
368                total_additions += added_rows;
369
370                let base_start = base_text
371                    .offset_to_point(hunk.diff_base_byte_range.start)
372                    .row;
373                let base_end = base_text.offset_to_point(hunk.diff_base_byte_range.end).row;
374                let deleted_rows = base_end.saturating_sub(base_start);
375
376                total_deletions += deleted_rows;
377            }
378        }
379
380        (total_additions, total_deletions)
381    }
382
383    fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
384        let commit = &self.commit;
385        let author_name = commit.author_name.clone();
386        let commit_date = time::OffsetDateTime::from_unix_timestamp(commit.commit_timestamp)
387            .unwrap_or_else(|_| time::OffsetDateTime::now_utc());
388        let local_offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
389        let date_string = time_format::format_localized_timestamp(
390            commit_date,
391            time::OffsetDateTime::now_utc(),
392            local_offset,
393            time_format::TimestampFormat::MediumAbsolute,
394        );
395
396        let remote_info = self.remote.as_ref().map(|remote| {
397            let provider = remote.host.name();
398            let parsed_remote = ParsedGitRemote {
399                owner: remote.owner.as_ref().into(),
400                repo: remote.repo.as_ref().into(),
401            };
402            let params = BuildCommitPermalinkParams { sha: &commit.sha };
403            let url = remote
404                .host
405                .build_commit_permalink(&parsed_remote, params)
406                .to_string();
407            (provider, url)
408        });
409
410        let (additions, deletions) = self.calculate_changed_lines(cx);
411
412        let commit_diff_stat = if additions > 0 || deletions > 0 {
413            Some(DiffStat::new(
414                "commit-diff-stat",
415                additions as usize,
416                deletions as usize,
417            ))
418        } else {
419            None
420        };
421
422        let gutter_width = self.editor.update(cx, |editor, cx| {
423            let snapshot = editor.snapshot(window, cx);
424            let style = editor.style(cx);
425            let font_id = window.text_system().resolve_font(&style.text.font());
426            let font_size = style.text.font_size.to_pixels(window.rem_size());
427            snapshot
428                .gutter_dimensions(font_id, font_size, style, window, cx)
429                .full_width()
430        });
431
432        h_flex()
433            .border_b_1()
434            .border_color(cx.theme().colors().border_variant)
435            .w_full()
436            .child(
437                h_flex()
438                    .w(gutter_width)
439                    .justify_center()
440                    .child(self.render_commit_avatar(&commit.sha, rems_from_px(48.), window, cx)),
441            )
442            .child(
443                h_flex()
444                    .py_4()
445                    .pl_1()
446                    .pr_4()
447                    .w_full()
448                    .items_start()
449                    .justify_between()
450                    .flex_wrap()
451                    .child(
452                        v_flex()
453                            .child(
454                                h_flex()
455                                    .gap_1()
456                                    .child(Label::new(author_name).color(Color::Default))
457                                    .child(
458                                        Label::new(format!("Commit:{}", commit.sha))
459                                            .color(Color::Muted)
460                                            .size(LabelSize::Small)
461                                            .truncate()
462                                            .buffer_font(cx),
463                                    ),
464                            )
465                            .child(
466                                h_flex()
467                                    .gap_1p5()
468                                    .child(
469                                        Label::new(date_string)
470                                            .color(Color::Muted)
471                                            .size(LabelSize::Small),
472                                    )
473                                    .child(
474                                        Label::new("")
475                                            .color(Color::Ignored)
476                                            .size(LabelSize::Small),
477                                    )
478                                    .children(commit_diff_stat),
479                            ),
480                    )
481                    .children(remote_info.map(|(provider_name, url)| {
482                        let icon = match provider_name.as_str() {
483                            "GitHub" => IconName::Github,
484                            _ => IconName::Link,
485                        };
486
487                        Button::new("view_on_provider", format!("View on {}", provider_name))
488                            .icon(icon)
489                            .icon_color(Color::Muted)
490                            .icon_size(IconSize::Small)
491                            .icon_position(IconPosition::Start)
492                            .on_click(move |_, _, cx| cx.open_url(&url))
493                    })),
494            )
495    }
496
497    fn apply_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
498        Self::stash_action(
499            workspace,
500            "Apply",
501            window,
502            cx,
503            async move |repository, sha, stash, commit_view, workspace, cx| {
504                let result = repository.update(cx, |repo, cx| {
505                    if !stash_matches_index(&sha, stash, repo) {
506                        return Err(anyhow::anyhow!("Stash has changed, not applying"));
507                    }
508                    Ok(repo.stash_apply(Some(stash), cx))
509                })?;
510
511                match result {
512                    Ok(task) => task.await?,
513                    Err(err) => {
514                        Self::close_commit_view(commit_view, workspace, cx).await?;
515                        return Err(err);
516                    }
517                };
518                Self::close_commit_view(commit_view, workspace, cx).await?;
519                anyhow::Ok(())
520            },
521        );
522    }
523
524    fn pop_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
525        Self::stash_action(
526            workspace,
527            "Pop",
528            window,
529            cx,
530            async move |repository, sha, stash, commit_view, workspace, cx| {
531                let result = repository.update(cx, |repo, cx| {
532                    if !stash_matches_index(&sha, stash, repo) {
533                        return Err(anyhow::anyhow!("Stash has changed, pop aborted"));
534                    }
535                    Ok(repo.stash_pop(Some(stash), cx))
536                })?;
537
538                match result {
539                    Ok(task) => task.await?,
540                    Err(err) => {
541                        Self::close_commit_view(commit_view, workspace, cx).await?;
542                        return Err(err);
543                    }
544                };
545                Self::close_commit_view(commit_view, workspace, cx).await?;
546                anyhow::Ok(())
547            },
548        );
549    }
550
551    fn remove_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
552        Self::stash_action(
553            workspace,
554            "Drop",
555            window,
556            cx,
557            async move |repository, sha, stash, commit_view, workspace, cx| {
558                let result = repository.update(cx, |repo, cx| {
559                    if !stash_matches_index(&sha, stash, repo) {
560                        return Err(anyhow::anyhow!("Stash has changed, drop aborted"));
561                    }
562                    Ok(repo.stash_drop(Some(stash), cx))
563                })?;
564
565                match result {
566                    Ok(task) => task.await??,
567                    Err(err) => {
568                        Self::close_commit_view(commit_view, workspace, cx).await?;
569                        return Err(err);
570                    }
571                };
572                Self::close_commit_view(commit_view, workspace, cx).await?;
573                anyhow::Ok(())
574            },
575        );
576    }
577
578    fn stash_action<AsyncFn>(
579        workspace: &mut Workspace,
580        str_action: &str,
581        window: &mut Window,
582        cx: &mut App,
583        callback: AsyncFn,
584    ) where
585        AsyncFn: AsyncFnOnce(
586                Entity<Repository>,
587                &SharedString,
588                usize,
589                Entity<CommitView>,
590                WeakEntity<Workspace>,
591                &mut AsyncWindowContext,
592            ) -> anyhow::Result<()>
593            + 'static,
594    {
595        let Some(commit_view) = workspace.active_item_as::<CommitView>(cx) else {
596            return;
597        };
598        let Some(stash) = commit_view.read(cx).stash else {
599            return;
600        };
601        let sha = commit_view.read(cx).commit.sha.clone();
602        let answer = window.prompt(
603            PromptLevel::Info,
604            &format!("{} stash@{{{}}}?", str_action, stash),
605            None,
606            &[str_action, "Cancel"],
607            cx,
608        );
609
610        let workspace_weak = workspace.weak_handle();
611        let commit_view_entity = commit_view;
612
613        window
614            .spawn(cx, async move |cx| {
615                if answer.await != Ok(0) {
616                    return anyhow::Ok(());
617                }
618
619                let Some(workspace) = workspace_weak.upgrade() else {
620                    return Ok(());
621                };
622
623                let repo = workspace.update(cx, |workspace, cx| {
624                    workspace
625                        .panel::<GitPanel>(cx)
626                        .and_then(|p| p.read(cx).active_repository.clone())
627                })?;
628
629                let Some(repo) = repo else {
630                    return Ok(());
631                };
632
633                callback(repo, &sha, stash, commit_view_entity, workspace_weak, cx).await?;
634                anyhow::Ok(())
635            })
636            .detach_and_notify_err(window, cx);
637    }
638
639    async fn close_commit_view(
640        commit_view: Entity<CommitView>,
641        workspace: WeakEntity<Workspace>,
642        cx: &mut AsyncWindowContext,
643    ) -> anyhow::Result<()> {
644        workspace
645            .update_in(cx, |workspace, window, cx| {
646                let active_pane = workspace.active_pane();
647                let commit_view_id = commit_view.entity_id();
648                active_pane.update(cx, |pane, cx| {
649                    pane.close_item_by_id(commit_view_id, SaveIntent::Skip, window, cx)
650                })
651            })?
652            .await?;
653        anyhow::Ok(())
654    }
655}
656
657impl language::File for GitBlob {
658    fn as_local(&self) -> Option<&dyn language::LocalFile> {
659        None
660    }
661
662    fn disk_state(&self) -> DiskState {
663        DiskState::Historic {
664            was_deleted: self.is_deleted,
665        }
666    }
667
668    fn path_style(&self, _: &App) -> PathStyle {
669        PathStyle::local()
670    }
671
672    fn path(&self) -> &Arc<RelPath> {
673        self.path.as_ref()
674    }
675
676    fn full_path(&self, _: &App) -> PathBuf {
677        self.path.as_std_path().to_path_buf()
678    }
679
680    fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
681        self.display_name.as_ref()
682    }
683
684    fn worktree_id(&self, _: &App) -> WorktreeId {
685        self.worktree_id
686    }
687
688    fn to_proto(&self, _cx: &App) -> language::proto::File {
689        unimplemented!()
690    }
691
692    fn is_private(&self) -> bool {
693        false
694    }
695}
696
697async fn build_buffer(
698    mut text: String,
699    blob: Arc<dyn File>,
700    language_registry: &Arc<language::LanguageRegistry>,
701    cx: &mut AsyncApp,
702) -> Result<Entity<Buffer>> {
703    let line_ending = LineEnding::detect(&text);
704    LineEnding::normalize(&mut text);
705    let text = Rope::from(text);
706    let language = cx.update(|cx| language_registry.language_for_file(&blob, Some(&text), cx))?;
707    let language = if let Some(language) = language {
708        language_registry
709            .load_language(&language)
710            .await
711            .ok()
712            .and_then(|e| e.log_err())
713    } else {
714        None
715    };
716    let buffer = cx.new(|cx| {
717        let buffer = TextBuffer::new_normalized(
718            ReplicaId::LOCAL,
719            cx.entity_id().as_non_zero_u64().into(),
720            line_ending,
721            text,
722        );
723        let mut buffer = Buffer::build(buffer, Some(blob), Capability::ReadWrite);
724        buffer.set_language_async(language, cx);
725        buffer
726    })?;
727    Ok(buffer)
728}
729
730async fn build_buffer_diff(
731    mut old_text: Option<String>,
732    buffer: &Entity<Buffer>,
733    language_registry: &Arc<LanguageRegistry>,
734    cx: &mut AsyncApp,
735) -> Result<Entity<BufferDiff>> {
736    if let Some(old_text) = &mut old_text {
737        LineEnding::normalize(old_text);
738    }
739
740    let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
741
742    let base_buffer = cx
743        .update(|cx| {
744            Buffer::build_snapshot(
745                old_text.as_deref().unwrap_or("").into(),
746                buffer.language().cloned(),
747                Some(language_registry.clone()),
748                cx,
749            )
750        })?
751        .await;
752
753    let diff_snapshot = cx
754        .update(|cx| {
755            BufferDiffSnapshot::new_with_base_buffer(
756                buffer.text.clone(),
757                old_text.map(Arc::new),
758                base_buffer,
759                cx,
760            )
761        })?
762        .await;
763
764    cx.new(|cx| {
765        let mut diff = BufferDiff::new(&buffer.text, cx);
766        diff.set_snapshot(diff_snapshot, &buffer.text, cx);
767        diff
768    })
769}
770
771impl EventEmitter<EditorEvent> for CommitView {}
772
773impl Focusable for CommitView {
774    fn focus_handle(&self, cx: &App) -> FocusHandle {
775        self.editor.focus_handle(cx)
776    }
777}
778
779impl Item for CommitView {
780    type Event = EditorEvent;
781
782    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
783        Some(Icon::new(IconName::GitBranch).color(Color::Muted))
784    }
785
786    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
787        Label::new(self.tab_content_text(params.detail.unwrap_or_default(), cx))
788            .color(if params.selected {
789                Color::Default
790            } else {
791                Color::Muted
792            })
793            .into_any_element()
794    }
795
796    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
797        let short_sha = self.commit.sha.get(0..7).unwrap_or(&*self.commit.sha);
798        let subject = truncate_and_trailoff(self.commit.message.split('\n').next().unwrap(), 20);
799        format!("{short_sha}{subject}").into()
800    }
801
802    fn tab_tooltip_content(&self, _: &App) -> Option<TabTooltipContent> {
803        let short_sha = self.commit.sha.get(0..16).unwrap_or(&*self.commit.sha);
804        let subject = self.commit.message.split('\n').next().unwrap();
805
806        Some(TabTooltipContent::Custom(Box::new(Tooltip::element({
807            let subject = subject.to_string();
808            let short_sha = short_sha.to_string();
809
810            move |_, _| {
811                v_flex()
812                    .child(Label::new(subject.clone()))
813                    .child(
814                        Label::new(short_sha.clone())
815                            .color(Color::Muted)
816                            .size(LabelSize::Small),
817                    )
818                    .into_any_element()
819            }
820        }))))
821    }
822
823    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
824        Editor::to_item_events(event, f)
825    }
826
827    fn telemetry_event_text(&self) -> Option<&'static str> {
828        Some("Commit View Opened")
829    }
830
831    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
832        self.editor
833            .update(cx, |editor, cx| editor.deactivated(window, cx));
834    }
835
836    fn act_as_type<'a>(
837        &'a self,
838        type_id: TypeId,
839        self_handle: &'a Entity<Self>,
840        _: &'a App,
841    ) -> Option<gpui::AnyEntity> {
842        if type_id == TypeId::of::<Self>() {
843            Some(self_handle.clone().into())
844        } else if type_id == TypeId::of::<Editor>() {
845            Some(self.editor.clone().into())
846        } else {
847            None
848        }
849    }
850
851    fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
852        Some(Box::new(self.editor.clone()))
853    }
854
855    fn for_each_project_item(
856        &self,
857        cx: &App,
858        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
859    ) {
860        self.editor.for_each_project_item(cx, f)
861    }
862
863    fn set_nav_history(
864        &mut self,
865        nav_history: ItemNavHistory,
866        _: &mut Window,
867        cx: &mut Context<Self>,
868    ) {
869        self.editor.update(cx, |editor, _| {
870            editor.set_nav_history(Some(nav_history));
871        });
872    }
873
874    fn navigate(
875        &mut self,
876        data: Box<dyn Any>,
877        window: &mut Window,
878        cx: &mut Context<Self>,
879    ) -> bool {
880        self.editor
881            .update(cx, |editor, cx| editor.navigate(data, window, cx))
882    }
883
884    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
885        ToolbarItemLocation::Hidden
886    }
887
888    fn breadcrumbs(&self, _theme: &theme::Theme, _cx: &App) -> Option<Vec<BreadcrumbText>> {
889        None
890    }
891
892    fn added_to_workspace(
893        &mut self,
894        workspace: &mut Workspace,
895        window: &mut Window,
896        cx: &mut Context<Self>,
897    ) {
898        self.editor.update(cx, |editor, cx| {
899            editor.added_to_workspace(workspace, window, cx)
900        });
901    }
902
903    fn can_split(&self) -> bool {
904        true
905    }
906
907    fn clone_on_split(
908        &self,
909        _workspace_id: Option<workspace::WorkspaceId>,
910        window: &mut Window,
911        cx: &mut Context<Self>,
912    ) -> Task<Option<Entity<Self>>>
913    where
914        Self: Sized,
915    {
916        Task::ready(Some(cx.new(|cx| {
917            let editor = cx.new(|cx| {
918                self.editor
919                    .update(cx, |editor, cx| editor.clone(window, cx))
920            });
921            let multibuffer = editor.read(cx).buffer().clone();
922            Self {
923                editor,
924                multibuffer,
925                commit: self.commit.clone(),
926                stash: self.stash,
927                repository: self.repository.clone(),
928                remote: self.remote.clone(),
929            }
930        })))
931    }
932}
933
934impl Render for CommitView {
935    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
936        let is_stash = self.stash.is_some();
937
938        v_flex()
939            .key_context(if is_stash { "StashDiff" } else { "CommitDiff" })
940            .size_full()
941            .bg(cx.theme().colors().editor_background)
942            .child(self.render_header(window, cx))
943            .when(!self.editor.read(cx).is_empty(cx), |this| {
944                this.child(div().flex_grow().child(self.editor.clone()))
945            })
946    }
947}
948
949pub struct CommitViewToolbar {
950    commit_view: Option<WeakEntity<CommitView>>,
951}
952
953impl CommitViewToolbar {
954    pub fn new() -> Self {
955        Self { commit_view: None }
956    }
957}
958
959impl EventEmitter<ToolbarItemEvent> for CommitViewToolbar {}
960
961impl Render for CommitViewToolbar {
962    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
963        div().hidden()
964    }
965}
966
967impl ToolbarItemView for CommitViewToolbar {
968    fn set_active_pane_item(
969        &mut self,
970        active_pane_item: Option<&dyn ItemHandle>,
971        _: &mut Window,
972        cx: &mut Context<Self>,
973    ) -> ToolbarItemLocation {
974        if let Some(entity) = active_pane_item.and_then(|i| i.act_as::<CommitView>(cx))
975            && entity.read(cx).stash.is_some()
976        {
977            self.commit_view = Some(entity.downgrade());
978            return ToolbarItemLocation::PrimaryRight;
979        }
980        ToolbarItemLocation::Hidden
981    }
982
983    fn pane_focus_update(
984        &mut self,
985        _pane_focused: bool,
986        _window: &mut Window,
987        _cx: &mut Context<Self>,
988    ) {
989    }
990}
991
992fn stash_matches_index(sha: &str, stash_index: usize, repo: &Repository) -> bool {
993    repo.stash_entries
994        .entries
995        .get(stash_index)
996        .map(|entry| entry.oid.to_string() == sha)
997        .unwrap_or(false)
998}