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