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