1use anyhow::{Context as _, Result};
2use buffer_diff::{BufferDiff, BufferDiffSnapshot};
3use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects, multibuffer_context_lines};
4use git::repository::{CommitDetails, CommitDiff, RepoPath};
5use gpui::{
6 Action, AnyElement, AnyView, App, AppContext as _, AsyncApp, AsyncWindowContext, Context,
7 Entity, EventEmitter, FocusHandle, Focusable, IntoElement, PromptLevel, Render, Task,
8 WeakEntity, Window, actions,
9};
10use language::{
11 Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,
12 Point, ReplicaId, Rope, TextBuffer,
13};
14use multi_buffer::PathKey;
15use project::{Project, WorktreeId, git_store::Repository};
16use std::{
17 any::{Any, TypeId},
18 fmt::Write as _,
19 path::PathBuf,
20 sync::Arc,
21};
22use ui::{
23 Button, Color, Icon, IconName, Label, LabelCommon as _, SharedString, Tooltip, prelude::*,
24};
25use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};
26use workspace::{
27 Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
28 Workspace,
29 item::{BreadcrumbText, ItemEvent, TabContentParams},
30 notifications::NotifyTaskExt,
31 pane::SaveIntent,
32 searchable::SearchableItemHandle,
33};
34
35use crate::git_panel::GitPanel;
36
37actions!(git, [ApplyCurrentStash, PopCurrentStash, DropCurrentStash,]);
38
39pub fn init(cx: &mut App) {
40 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
41 register_workspace_action(workspace, |toolbar, _: &ApplyCurrentStash, window, cx| {
42 toolbar.apply_stash(window, cx);
43 });
44 register_workspace_action(workspace, |toolbar, _: &DropCurrentStash, window, cx| {
45 toolbar.remove_stash(window, cx);
46 });
47 register_workspace_action(workspace, |toolbar, _: &PopCurrentStash, window, cx| {
48 toolbar.pop_stash(window, cx);
49 });
50 })
51 .detach();
52}
53
54pub struct CommitView {
55 commit: CommitDetails,
56 editor: Entity<Editor>,
57 stash: Option<usize>,
58 multibuffer: Entity<MultiBuffer>,
59}
60
61struct GitBlob {
62 path: RepoPath,
63 worktree_id: WorktreeId,
64 is_deleted: bool,
65}
66
67struct CommitMetadataFile {
68 title: Arc<RelPath>,
69 worktree_id: WorktreeId,
70}
71
72const COMMIT_METADATA_SORT_PREFIX: u64 = 0;
73const FILE_NAMESPACE_SORT_PREFIX: u64 = 1;
74
75impl CommitView {
76 pub fn open(
77 commit_sha: String,
78 repo: WeakEntity<Repository>,
79 workspace: WeakEntity<Workspace>,
80 stash: Option<usize>,
81 file_filter: Option<RepoPath>,
82 window: &mut Window,
83 cx: &mut App,
84 ) {
85 let commit_diff = repo
86 .update(cx, |repo, _| repo.load_commit_diff(commit_sha.clone()))
87 .ok();
88 let commit_details = repo
89 .update(cx, |repo, _| repo.show(commit_sha.clone()))
90 .ok();
91
92 window
93 .spawn(cx, async move |cx| {
94 let (commit_diff, commit_details) = futures::join!(commit_diff?, commit_details?);
95 let mut commit_diff = commit_diff.log_err()?.log_err()?;
96 let commit_details = commit_details.log_err()?.log_err()?;
97
98 // Filter to specific file if requested
99 if let Some(ref filter_path) = file_filter {
100 commit_diff.files.retain(|f| &f.path == filter_path);
101 }
102
103 let repo = repo.upgrade()?;
104
105 workspace
106 .update_in(cx, |workspace, window, cx| {
107 let project = workspace.project();
108 let commit_view = cx.new(|cx| {
109 CommitView::new(
110 commit_details,
111 commit_diff,
112 repo,
113 project.clone(),
114 stash,
115 window,
116 cx,
117 )
118 });
119
120 let pane = workspace.active_pane();
121 pane.update(cx, |pane, cx| {
122 let ix = pane.items().position(|item| {
123 let commit_view = item.downcast::<CommitView>();
124 commit_view
125 .is_some_and(|view| view.read(cx).commit.sha == commit_sha)
126 });
127 if let Some(ix) = ix {
128 pane.activate_item(ix, true, true, window, cx);
129 } else {
130 pane.add_item(Box::new(commit_view), true, true, None, window, cx);
131 }
132 })
133 })
134 .log_err()
135 })
136 .detach();
137 }
138
139 fn new(
140 commit: CommitDetails,
141 commit_diff: CommitDiff,
142 repository: Entity<Repository>,
143 project: Entity<Project>,
144 stash: Option<usize>,
145 window: &mut Window,
146 cx: &mut Context<Self>,
147 ) -> Self {
148 let language_registry = project.read(cx).languages().clone();
149 let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadOnly));
150 let editor = cx.new(|cx| {
151 let mut editor =
152 Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
153 editor.disable_inline_diagnostics();
154 editor.set_expand_all_diff_hunks(cx);
155 editor
156 });
157
158 let first_worktree_id = project
159 .read(cx)
160 .worktrees(cx)
161 .next()
162 .map(|worktree| worktree.read(cx).id());
163
164 let mut metadata_buffer_id = None;
165 if let Some(worktree_id) = first_worktree_id {
166 let title = if let Some(stash) = stash {
167 format!("stash@{{{}}}", stash)
168 } else {
169 format!("commit {}", commit.sha)
170 };
171 let file = Arc::new(CommitMetadataFile {
172 title: RelPath::unix(&title).unwrap().into(),
173 worktree_id,
174 });
175 let buffer = cx.new(|cx| {
176 let buffer = TextBuffer::new_normalized(
177 ReplicaId::LOCAL,
178 cx.entity_id().as_non_zero_u64().into(),
179 LineEnding::default(),
180 format_commit(&commit, stash.is_some()).into(),
181 );
182 metadata_buffer_id = Some(buffer.remote_id());
183 Buffer::build(buffer, Some(file.clone()), Capability::ReadWrite)
184 });
185 multibuffer.update(cx, |multibuffer, cx| {
186 multibuffer.set_excerpts_for_path(
187 PathKey::with_sort_prefix(COMMIT_METADATA_SORT_PREFIX, file.title.clone()),
188 buffer.clone(),
189 vec![Point::zero()..buffer.read(cx).max_point()],
190 0,
191 cx,
192 );
193 });
194 editor.update(cx, |editor, cx| {
195 editor.disable_header_for_buffer(metadata_buffer_id.unwrap(), cx);
196 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
197 selections.select_ranges(vec![0..0]);
198 });
199 });
200 }
201
202 cx.spawn(async move |this, cx| {
203 for file in commit_diff.files {
204 let is_deleted = file.new_text.is_none();
205 let new_text = file.new_text.unwrap_or_default();
206 let old_text = file.old_text;
207 let worktree_id = repository
208 .update(cx, |repository, cx| {
209 repository
210 .repo_path_to_project_path(&file.path, cx)
211 .map(|path| path.worktree_id)
212 .or(first_worktree_id)
213 })?
214 .context("project has no worktrees")?;
215 let file = Arc::new(GitBlob {
216 path: file.path.clone(),
217 is_deleted,
218 worktree_id,
219 }) as Arc<dyn language::File>;
220
221 let buffer = build_buffer(new_text, file, &language_registry, cx).await?;
222 let buffer_diff =
223 build_buffer_diff(old_text, &buffer, &language_registry, cx).await?;
224
225 this.update(cx, |this, cx| {
226 this.multibuffer.update(cx, |multibuffer, cx| {
227 let snapshot = buffer.read(cx).snapshot();
228 let diff = buffer_diff.read(cx);
229 let diff_hunk_ranges = diff
230 .hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
231 .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
232 .collect::<Vec<_>>();
233 let path = snapshot.file().unwrap().path().clone();
234 let _is_newly_added = multibuffer.set_excerpts_for_path(
235 PathKey::with_sort_prefix(FILE_NAMESPACE_SORT_PREFIX, path),
236 buffer,
237 diff_hunk_ranges,
238 multibuffer_context_lines(cx),
239 cx,
240 );
241 multibuffer.add_diff(buffer_diff, cx);
242 });
243 })?;
244 }
245 anyhow::Ok(())
246 })
247 .detach();
248
249 Self {
250 commit,
251 editor,
252 multibuffer,
253 stash,
254 }
255 }
256}
257
258impl language::File for GitBlob {
259 fn as_local(&self) -> Option<&dyn language::LocalFile> {
260 None
261 }
262
263 fn disk_state(&self) -> DiskState {
264 if self.is_deleted {
265 DiskState::Deleted
266 } else {
267 DiskState::New
268 }
269 }
270
271 fn path_style(&self, _: &App) -> PathStyle {
272 PathStyle::Posix
273 }
274
275 fn path(&self) -> &Arc<RelPath> {
276 self.path.as_ref()
277 }
278
279 fn full_path(&self, _: &App) -> PathBuf {
280 self.path.as_std_path().to_path_buf()
281 }
282
283 fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
284 self.path.file_name().unwrap()
285 }
286
287 fn worktree_id(&self, _: &App) -> WorktreeId {
288 self.worktree_id
289 }
290
291 fn to_proto(&self, _cx: &App) -> language::proto::File {
292 unimplemented!()
293 }
294
295 fn is_private(&self) -> bool {
296 false
297 }
298}
299
300impl language::File for CommitMetadataFile {
301 fn as_local(&self) -> Option<&dyn language::LocalFile> {
302 None
303 }
304
305 fn disk_state(&self) -> DiskState {
306 DiskState::New
307 }
308
309 fn path_style(&self, _: &App) -> PathStyle {
310 PathStyle::Posix
311 }
312
313 fn path(&self) -> &Arc<RelPath> {
314 &self.title
315 }
316
317 fn full_path(&self, _: &App) -> PathBuf {
318 PathBuf::from(self.title.as_unix_str().to_owned())
319 }
320
321 fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
322 self.title.file_name().unwrap()
323 }
324
325 fn worktree_id(&self, _: &App) -> WorktreeId {
326 self.worktree_id
327 }
328
329 fn to_proto(&self, _: &App) -> language::proto::File {
330 unimplemented!()
331 }
332
333 fn is_private(&self) -> bool {
334 false
335 }
336}
337
338async fn build_buffer(
339 mut text: String,
340 blob: Arc<dyn File>,
341 language_registry: &Arc<language::LanguageRegistry>,
342 cx: &mut AsyncApp,
343) -> Result<Entity<Buffer>> {
344 let line_ending = LineEnding::detect(&text);
345 LineEnding::normalize(&mut text);
346 let text = Rope::from(text);
347 let language = cx.update(|cx| language_registry.language_for_file(&blob, Some(&text), cx))?;
348 let language = if let Some(language) = language {
349 language_registry
350 .load_language(&language)
351 .await
352 .ok()
353 .and_then(|e| e.log_err())
354 } else {
355 None
356 };
357 let buffer = cx.new(|cx| {
358 let buffer = TextBuffer::new_normalized(
359 ReplicaId::LOCAL,
360 cx.entity_id().as_non_zero_u64().into(),
361 line_ending,
362 text,
363 );
364 let mut buffer = Buffer::build(buffer, Some(blob), Capability::ReadWrite);
365 buffer.set_language(language, cx);
366 buffer
367 })?;
368 Ok(buffer)
369}
370
371async fn build_buffer_diff(
372 mut old_text: Option<String>,
373 buffer: &Entity<Buffer>,
374 language_registry: &Arc<LanguageRegistry>,
375 cx: &mut AsyncApp,
376) -> Result<Entity<BufferDiff>> {
377 if let Some(old_text) = &mut old_text {
378 LineEnding::normalize(old_text);
379 }
380
381 let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
382
383 let base_buffer = cx
384 .update(|cx| {
385 Buffer::build_snapshot(
386 old_text.as_deref().unwrap_or("").into(),
387 buffer.language().cloned(),
388 Some(language_registry.clone()),
389 cx,
390 )
391 })?
392 .await;
393
394 let diff_snapshot = cx
395 .update(|cx| {
396 BufferDiffSnapshot::new_with_base_buffer(
397 buffer.text.clone(),
398 old_text.map(Arc::new),
399 base_buffer,
400 cx,
401 )
402 })?
403 .await;
404
405 cx.new(|cx| {
406 let mut diff = BufferDiff::new(&buffer.text, cx);
407 diff.set_snapshot(diff_snapshot, &buffer.text, cx);
408 diff
409 })
410}
411
412fn format_commit(commit: &CommitDetails, is_stash: bool) -> String {
413 let mut result = String::new();
414 if is_stash {
415 writeln!(&mut result, "stash commit {}", commit.sha).unwrap();
416 } else {
417 writeln!(&mut result, "commit {}", commit.sha).unwrap();
418 }
419 writeln!(
420 &mut result,
421 "Author: {} <{}>",
422 commit.author_name, commit.author_email
423 )
424 .unwrap();
425 let local_offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
426 writeln!(
427 &mut result,
428 "Date: {}",
429 time_format::format_localized_timestamp(
430 time::OffsetDateTime::from_unix_timestamp(commit.commit_timestamp).unwrap(),
431 time::OffsetDateTime::now_utc(),
432 local_offset,
433 time_format::TimestampFormat::MediumAbsolute,
434 ),
435 )
436 .unwrap();
437 result.push('\n');
438 for line in commit.message.split('\n') {
439 if line.is_empty() {
440 result.push('\n');
441 } else {
442 writeln!(&mut result, " {}", line).unwrap();
443 }
444 }
445 if result.ends_with("\n\n") {
446 result.pop();
447 }
448 result
449}
450
451impl EventEmitter<EditorEvent> for CommitView {}
452
453impl Focusable for CommitView {
454 fn focus_handle(&self, cx: &App) -> FocusHandle {
455 self.editor.focus_handle(cx)
456 }
457}
458
459impl Item for CommitView {
460 type Event = EditorEvent;
461
462 fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
463 Some(Icon::new(IconName::GitBranch).color(Color::Muted))
464 }
465
466 fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
467 Label::new(self.tab_content_text(params.detail.unwrap_or_default(), cx))
468 .color(if params.selected {
469 Color::Default
470 } else {
471 Color::Muted
472 })
473 .into_any_element()
474 }
475
476 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
477 let short_sha = self.commit.sha.get(0..7).unwrap_or(&*self.commit.sha);
478 let subject = truncate_and_trailoff(self.commit.message.split('\n').next().unwrap(), 20);
479 format!("{short_sha} - {subject}").into()
480 }
481
482 fn tab_tooltip_text(&self, _: &App) -> Option<ui::SharedString> {
483 let short_sha = self.commit.sha.get(0..16).unwrap_or(&*self.commit.sha);
484 let subject = self.commit.message.split('\n').next().unwrap();
485 Some(format!("{short_sha} - {subject}").into())
486 }
487
488 fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
489 Editor::to_item_events(event, f)
490 }
491
492 fn telemetry_event_text(&self) -> Option<&'static str> {
493 Some("Commit View Opened")
494 }
495
496 fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
497 self.editor
498 .update(cx, |editor, cx| editor.deactivated(window, cx));
499 }
500
501 fn act_as_type<'a>(
502 &'a self,
503 type_id: TypeId,
504 self_handle: &'a Entity<Self>,
505 _: &'a App,
506 ) -> Option<AnyView> {
507 if type_id == TypeId::of::<Self>() {
508 Some(self_handle.to_any())
509 } else if type_id == TypeId::of::<Editor>() {
510 Some(self.editor.to_any())
511 } else {
512 None
513 }
514 }
515
516 fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
517 Some(Box::new(self.editor.clone()))
518 }
519
520 fn for_each_project_item(
521 &self,
522 cx: &App,
523 f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
524 ) {
525 self.editor.for_each_project_item(cx, f)
526 }
527
528 fn set_nav_history(
529 &mut self,
530 nav_history: ItemNavHistory,
531 _: &mut Window,
532 cx: &mut Context<Self>,
533 ) {
534 self.editor.update(cx, |editor, _| {
535 editor.set_nav_history(Some(nav_history));
536 });
537 }
538
539 fn navigate(
540 &mut self,
541 data: Box<dyn Any>,
542 window: &mut Window,
543 cx: &mut Context<Self>,
544 ) -> bool {
545 self.editor
546 .update(cx, |editor, cx| editor.navigate(data, window, cx))
547 }
548
549 fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
550 ToolbarItemLocation::PrimaryLeft
551 }
552
553 fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
554 self.editor.breadcrumbs(theme, cx)
555 }
556
557 fn added_to_workspace(
558 &mut self,
559 workspace: &mut Workspace,
560 window: &mut Window,
561 cx: &mut Context<Self>,
562 ) {
563 self.editor.update(cx, |editor, cx| {
564 editor.added_to_workspace(workspace, window, cx)
565 });
566 }
567
568 fn can_split(&self) -> bool {
569 true
570 }
571
572 fn clone_on_split(
573 &self,
574 _workspace_id: Option<workspace::WorkspaceId>,
575 window: &mut Window,
576 cx: &mut Context<Self>,
577 ) -> Task<Option<Entity<Self>>>
578 where
579 Self: Sized,
580 {
581 Task::ready(Some(cx.new(|cx| {
582 let editor = cx.new(|cx| {
583 self.editor
584 .update(cx, |editor, cx| editor.clone(window, cx))
585 });
586 let multibuffer = editor.read(cx).buffer().clone();
587 Self {
588 editor,
589 multibuffer,
590 commit: self.commit.clone(),
591 stash: self.stash,
592 }
593 })))
594 }
595}
596
597impl Render for CommitView {
598 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
599 let is_stash = self.stash.is_some();
600 div()
601 .key_context(if is_stash { "StashDiff" } else { "CommitDiff" })
602 .bg(cx.theme().colors().editor_background)
603 .flex()
604 .items_center()
605 .justify_center()
606 .size_full()
607 .child(self.editor.clone())
608 }
609}
610
611pub struct CommitViewToolbar {
612 commit_view: Option<WeakEntity<CommitView>>,
613 workspace: WeakEntity<Workspace>,
614}
615
616impl CommitViewToolbar {
617 pub fn new(workspace: &Workspace, _: &mut Context<Self>) -> Self {
618 Self {
619 commit_view: None,
620 workspace: workspace.weak_handle(),
621 }
622 }
623
624 fn commit_view(&self, _: &App) -> Option<Entity<CommitView>> {
625 self.commit_view.as_ref()?.upgrade()
626 }
627
628 async fn close_commit_view(
629 commit_view: Entity<CommitView>,
630 workspace: WeakEntity<Workspace>,
631 cx: &mut AsyncWindowContext,
632 ) -> anyhow::Result<()> {
633 workspace
634 .update_in(cx, |workspace, window, cx| {
635 let active_pane = workspace.active_pane();
636 let commit_view_id = commit_view.entity_id();
637 active_pane.update(cx, |pane, cx| {
638 pane.close_item_by_id(commit_view_id, SaveIntent::Skip, window, cx)
639 })
640 })?
641 .await?;
642 anyhow::Ok(())
643 }
644
645 fn apply_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
646 self.stash_action(
647 "Apply",
648 window,
649 cx,
650 async move |repository, sha, stash, commit_view, workspace, cx| {
651 let result = repository.update(cx, |repo, cx| {
652 if !stash_matches_index(&sha, stash, repo) {
653 return Err(anyhow::anyhow!("Stash has changed, not applying"));
654 }
655 Ok(repo.stash_apply(Some(stash), cx))
656 })?;
657
658 match result {
659 Ok(task) => task.await?,
660 Err(err) => {
661 Self::close_commit_view(commit_view, workspace, cx).await?;
662 return Err(err);
663 }
664 };
665 Self::close_commit_view(commit_view, workspace, cx).await?;
666 anyhow::Ok(())
667 },
668 );
669 }
670
671 fn pop_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
672 self.stash_action(
673 "Pop",
674 window,
675 cx,
676 async move |repository, sha, stash, commit_view, workspace, cx| {
677 let result = repository.update(cx, |repo, cx| {
678 if !stash_matches_index(&sha, stash, repo) {
679 return Err(anyhow::anyhow!("Stash has changed, pop aborted"));
680 }
681 Ok(repo.stash_pop(Some(stash), cx))
682 })?;
683
684 match result {
685 Ok(task) => task.await?,
686 Err(err) => {
687 Self::close_commit_view(commit_view, workspace, cx).await?;
688 return Err(err);
689 }
690 };
691 Self::close_commit_view(commit_view, workspace, cx).await?;
692 anyhow::Ok(())
693 },
694 );
695 }
696
697 fn remove_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
698 self.stash_action(
699 "Drop",
700 window,
701 cx,
702 async move |repository, sha, stash, commit_view, workspace, cx| {
703 let result = repository.update(cx, |repo, cx| {
704 if !stash_matches_index(&sha, stash, repo) {
705 return Err(anyhow::anyhow!("Stash has changed, drop aborted"));
706 }
707 Ok(repo.stash_drop(Some(stash), cx))
708 })?;
709
710 match result {
711 Ok(task) => task.await??,
712 Err(err) => {
713 Self::close_commit_view(commit_view, workspace, cx).await?;
714 return Err(err);
715 }
716 };
717 Self::close_commit_view(commit_view, workspace, cx).await?;
718 anyhow::Ok(())
719 },
720 );
721 }
722
723 fn stash_action<AsyncFn>(
724 &mut self,
725 str_action: &str,
726 window: &mut Window,
727 cx: &mut Context<Self>,
728 callback: AsyncFn,
729 ) where
730 AsyncFn: AsyncFnOnce(
731 Entity<Repository>,
732 &SharedString,
733 usize,
734 Entity<CommitView>,
735 WeakEntity<Workspace>,
736 &mut AsyncWindowContext,
737 ) -> anyhow::Result<()>
738 + 'static,
739 {
740 let Some(commit_view) = self.commit_view(cx) else {
741 return;
742 };
743 let Some(stash) = commit_view.read(cx).stash else {
744 return;
745 };
746 let sha = commit_view.read(cx).commit.sha.clone();
747 let answer = window.prompt(
748 PromptLevel::Info,
749 &format!("{} stash@{{{}}}?", str_action, stash),
750 None,
751 &[str_action, "Cancel"],
752 cx,
753 );
754
755 let workspace = self.workspace.clone();
756 cx.spawn_in(window, async move |_, cx| {
757 if answer.await != Ok(0) {
758 return anyhow::Ok(());
759 }
760 let repo = workspace.update(cx, |workspace, cx| {
761 workspace
762 .panel::<GitPanel>(cx)
763 .and_then(|p| p.read(cx).active_repository.clone())
764 })?;
765
766 let Some(repo) = repo else {
767 return Ok(());
768 };
769 callback(repo, &sha, stash, commit_view, workspace, cx).await?;
770 anyhow::Ok(())
771 })
772 .detach_and_notify_err(window, cx);
773 }
774}
775
776impl EventEmitter<ToolbarItemEvent> for CommitViewToolbar {}
777
778impl ToolbarItemView for CommitViewToolbar {
779 fn set_active_pane_item(
780 &mut self,
781 active_pane_item: Option<&dyn ItemHandle>,
782 _: &mut Window,
783 cx: &mut Context<Self>,
784 ) -> ToolbarItemLocation {
785 if let Some(entity) = active_pane_item.and_then(|i| i.act_as::<CommitView>(cx))
786 && entity.read(cx).stash.is_some()
787 {
788 self.commit_view = Some(entity.downgrade());
789 return ToolbarItemLocation::PrimaryRight;
790 }
791 ToolbarItemLocation::Hidden
792 }
793
794 fn pane_focus_update(
795 &mut self,
796 _pane_focused: bool,
797 _window: &mut Window,
798 _cx: &mut Context<Self>,
799 ) {
800 }
801}
802
803impl Render for CommitViewToolbar {
804 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
805 let Some(commit_view) = self.commit_view(cx) else {
806 return div();
807 };
808
809 let is_stash = commit_view.read(cx).stash.is_some();
810 if !is_stash {
811 return div();
812 }
813
814 let focus_handle = commit_view.focus_handle(cx);
815
816 h_group_xl().my_neg_1().py_1().items_center().child(
817 h_group_sm()
818 .child(
819 Button::new("apply-stash", "Apply")
820 .tooltip(Tooltip::for_action_title_in(
821 "Apply current stash",
822 &ApplyCurrentStash,
823 &focus_handle,
824 ))
825 .on_click(cx.listener(|this, _, window, cx| this.apply_stash(window, cx))),
826 )
827 .child(
828 Button::new("pop-stash", "Pop")
829 .tooltip(Tooltip::for_action_title_in(
830 "Pop current stash",
831 &PopCurrentStash,
832 &focus_handle,
833 ))
834 .on_click(cx.listener(|this, _, window, cx| this.pop_stash(window, cx))),
835 )
836 .child(
837 Button::new("remove-stash", "Remove")
838 .icon(IconName::Trash)
839 .tooltip(Tooltip::for_action_title_in(
840 "Remove current stash",
841 &DropCurrentStash,
842 &focus_handle,
843 ))
844 .on_click(cx.listener(|this, _, window, cx| this.remove_stash(window, cx))),
845 ),
846 )
847 }
848}
849
850fn register_workspace_action<A: Action>(
851 workspace: &mut Workspace,
852 callback: fn(&mut CommitViewToolbar, &A, &mut Window, &mut Context<CommitViewToolbar>),
853) {
854 workspace.register_action(move |workspace, action: &A, window, cx| {
855 if workspace.has_active_modal(window, cx) {
856 cx.propagate();
857 return;
858 }
859
860 workspace.active_pane().update(cx, |pane, cx| {
861 pane.toolbar().update(cx, move |workspace, cx| {
862 if let Some(toolbar) = workspace.item_of_type::<CommitViewToolbar>() {
863 toolbar.update(cx, move |toolbar, cx| {
864 callback(toolbar, action, window, cx);
865 cx.notify();
866 });
867 }
868 });
869 })
870 });
871}
872
873fn stash_matches_index(sha: &str, index: usize, repo: &mut Repository) -> bool {
874 match repo
875 .cached_stash()
876 .entries
877 .iter()
878 .find(|entry| entry.index == index)
879 {
880 Some(entry) => entry.oid.to_string() == sha,
881 None => false,
882 }
883}