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