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