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 Rope::from_str(
174 &format_commit(&commit, stash.is_some()),
175 cx.background_executor(),
176 ),
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![0..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.0
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_str(&text, cx.background_executor());
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 Rope::from_str(old_text.as_deref().unwrap_or(""), cx.background_executor()),
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 writeln!(
422 &mut result,
423 "Date: {}",
424 time_format::format_local_timestamp(
425 time::OffsetDateTime::from_unix_timestamp(commit.commit_timestamp).unwrap(),
426 time::OffsetDateTime::now_utc(),
427 time_format::TimestampFormat::MediumAbsolute,
428 ),
429 )
430 .unwrap();
431 result.push('\n');
432 for line in commit.message.split('\n') {
433 if line.is_empty() {
434 result.push('\n');
435 } else {
436 writeln!(&mut result, " {}", line).unwrap();
437 }
438 }
439 if result.ends_with("\n\n") {
440 result.pop();
441 }
442 result
443}
444
445impl EventEmitter<EditorEvent> for CommitView {}
446
447impl Focusable for CommitView {
448 fn focus_handle(&self, cx: &App) -> FocusHandle {
449 self.editor.focus_handle(cx)
450 }
451}
452
453impl Item for CommitView {
454 type Event = EditorEvent;
455
456 fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
457 Some(Icon::new(IconName::GitBranch).color(Color::Muted))
458 }
459
460 fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
461 Label::new(self.tab_content_text(params.detail.unwrap_or_default(), cx))
462 .color(if params.selected {
463 Color::Default
464 } else {
465 Color::Muted
466 })
467 .into_any_element()
468 }
469
470 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
471 let short_sha = self.commit.sha.get(0..7).unwrap_or(&*self.commit.sha);
472 let subject = truncate_and_trailoff(self.commit.message.split('\n').next().unwrap(), 20);
473 format!("{short_sha} - {subject}").into()
474 }
475
476 fn tab_tooltip_text(&self, _: &App) -> Option<ui::SharedString> {
477 let short_sha = self.commit.sha.get(0..16).unwrap_or(&*self.commit.sha);
478 let subject = self.commit.message.split('\n').next().unwrap();
479 Some(format!("{short_sha} - {subject}").into())
480 }
481
482 fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
483 Editor::to_item_events(event, f)
484 }
485
486 fn telemetry_event_text(&self) -> Option<&'static str> {
487 Some("Commit View Opened")
488 }
489
490 fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
491 self.editor
492 .update(cx, |editor, cx| editor.deactivated(window, cx));
493 }
494
495 fn act_as_type<'a>(
496 &'a self,
497 type_id: TypeId,
498 self_handle: &'a Entity<Self>,
499 _: &'a App,
500 ) -> Option<AnyView> {
501 if type_id == TypeId::of::<Self>() {
502 Some(self_handle.to_any())
503 } else if type_id == TypeId::of::<Editor>() {
504 Some(self.editor.to_any())
505 } else {
506 None
507 }
508 }
509
510 fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
511 Some(Box::new(self.editor.clone()))
512 }
513
514 fn for_each_project_item(
515 &self,
516 cx: &App,
517 f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
518 ) {
519 self.editor.for_each_project_item(cx, f)
520 }
521
522 fn set_nav_history(
523 &mut self,
524 nav_history: ItemNavHistory,
525 _: &mut Window,
526 cx: &mut Context<Self>,
527 ) {
528 self.editor.update(cx, |editor, _| {
529 editor.set_nav_history(Some(nav_history));
530 });
531 }
532
533 fn navigate(
534 &mut self,
535 data: Box<dyn Any>,
536 window: &mut Window,
537 cx: &mut Context<Self>,
538 ) -> bool {
539 self.editor
540 .update(cx, |editor, cx| editor.navigate(data, window, cx))
541 }
542
543 fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
544 ToolbarItemLocation::PrimaryLeft
545 }
546
547 fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
548 self.editor.breadcrumbs(theme, cx)
549 }
550
551 fn added_to_workspace(
552 &mut self,
553 workspace: &mut Workspace,
554 window: &mut Window,
555 cx: &mut Context<Self>,
556 ) {
557 self.editor.update(cx, |editor, cx| {
558 editor.added_to_workspace(workspace, window, cx)
559 });
560 }
561
562 fn can_split(&self) -> bool {
563 true
564 }
565
566 fn clone_on_split(
567 &self,
568 _workspace_id: Option<workspace::WorkspaceId>,
569 window: &mut Window,
570 cx: &mut Context<Self>,
571 ) -> Task<Option<Entity<Self>>>
572 where
573 Self: Sized,
574 {
575 Task::ready(Some(cx.new(|cx| {
576 let editor = cx.new(|cx| {
577 self.editor
578 .update(cx, |editor, cx| editor.clone(window, cx))
579 });
580 let multibuffer = editor.read(cx).buffer().clone();
581 Self {
582 editor,
583 multibuffer,
584 commit: self.commit.clone(),
585 stash: self.stash,
586 }
587 })))
588 }
589}
590
591impl Render for CommitView {
592 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
593 let is_stash = self.stash.is_some();
594 div()
595 .key_context(if is_stash { "StashDiff" } else { "CommitDiff" })
596 .bg(cx.theme().colors().editor_background)
597 .flex()
598 .items_center()
599 .justify_center()
600 .size_full()
601 .child(self.editor.clone())
602 }
603}
604
605pub struct CommitViewToolbar {
606 commit_view: Option<WeakEntity<CommitView>>,
607 workspace: WeakEntity<Workspace>,
608}
609
610impl CommitViewToolbar {
611 pub fn new(workspace: &Workspace, _: &mut Context<Self>) -> Self {
612 Self {
613 commit_view: None,
614 workspace: workspace.weak_handle(),
615 }
616 }
617
618 fn commit_view(&self, _: &App) -> Option<Entity<CommitView>> {
619 self.commit_view.as_ref()?.upgrade()
620 }
621
622 async fn close_commit_view(
623 commit_view: Entity<CommitView>,
624 workspace: WeakEntity<Workspace>,
625 cx: &mut AsyncWindowContext,
626 ) -> anyhow::Result<()> {
627 workspace
628 .update_in(cx, |workspace, window, cx| {
629 let active_pane = workspace.active_pane();
630 let commit_view_id = commit_view.entity_id();
631 active_pane.update(cx, |pane, cx| {
632 pane.close_item_by_id(commit_view_id, SaveIntent::Skip, window, cx)
633 })
634 })?
635 .await?;
636 anyhow::Ok(())
637 }
638
639 fn apply_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
640 self.stash_action(
641 "Apply",
642 window,
643 cx,
644 async move |repository, sha, stash, commit_view, workspace, cx| {
645 let result = repository.update(cx, |repo, cx| {
646 if !stash_matches_index(&sha, stash, repo) {
647 return Err(anyhow::anyhow!("Stash has changed, not applying"));
648 }
649 Ok(repo.stash_apply(Some(stash), cx))
650 })?;
651
652 match result {
653 Ok(task) => task.await?,
654 Err(err) => {
655 Self::close_commit_view(commit_view, workspace, cx).await?;
656 return Err(err);
657 }
658 };
659 Self::close_commit_view(commit_view, workspace, cx).await?;
660 anyhow::Ok(())
661 },
662 );
663 }
664
665 fn pop_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
666 self.stash_action(
667 "Pop",
668 window,
669 cx,
670 async move |repository, sha, stash, commit_view, workspace, cx| {
671 let result = repository.update(cx, |repo, cx| {
672 if !stash_matches_index(&sha, stash, repo) {
673 return Err(anyhow::anyhow!("Stash has changed, pop aborted"));
674 }
675 Ok(repo.stash_pop(Some(stash), cx))
676 })?;
677
678 match result {
679 Ok(task) => task.await?,
680 Err(err) => {
681 Self::close_commit_view(commit_view, workspace, cx).await?;
682 return Err(err);
683 }
684 };
685 Self::close_commit_view(commit_view, workspace, cx).await?;
686 anyhow::Ok(())
687 },
688 );
689 }
690
691 fn remove_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
692 self.stash_action(
693 "Drop",
694 window,
695 cx,
696 async move |repository, sha, stash, commit_view, workspace, cx| {
697 let result = repository.update(cx, |repo, cx| {
698 if !stash_matches_index(&sha, stash, repo) {
699 return Err(anyhow::anyhow!("Stash has changed, drop aborted"));
700 }
701 Ok(repo.stash_drop(Some(stash), cx))
702 })?;
703
704 match result {
705 Ok(task) => task.await??,
706 Err(err) => {
707 Self::close_commit_view(commit_view, workspace, cx).await?;
708 return Err(err);
709 }
710 };
711 Self::close_commit_view(commit_view, workspace, cx).await?;
712 anyhow::Ok(())
713 },
714 );
715 }
716
717 fn stash_action<AsyncFn>(
718 &mut self,
719 str_action: &str,
720 window: &mut Window,
721 cx: &mut Context<Self>,
722 callback: AsyncFn,
723 ) where
724 AsyncFn: AsyncFnOnce(
725 Entity<Repository>,
726 &SharedString,
727 usize,
728 Entity<CommitView>,
729 WeakEntity<Workspace>,
730 &mut AsyncWindowContext,
731 ) -> anyhow::Result<()>
732 + 'static,
733 {
734 let Some(commit_view) = self.commit_view(cx) else {
735 return;
736 };
737 let Some(stash) = commit_view.read(cx).stash else {
738 return;
739 };
740 let sha = commit_view.read(cx).commit.sha.clone();
741 let answer = window.prompt(
742 PromptLevel::Info,
743 &format!("{} stash@{{{}}}?", str_action, stash),
744 None,
745 &[str_action, "Cancel"],
746 cx,
747 );
748
749 let workspace = self.workspace.clone();
750 cx.spawn_in(window, async move |_, cx| {
751 if answer.await != Ok(0) {
752 return anyhow::Ok(());
753 }
754 let repo = workspace.update(cx, |workspace, cx| {
755 workspace
756 .panel::<GitPanel>(cx)
757 .and_then(|p| p.read(cx).active_repository.clone())
758 })?;
759
760 let Some(repo) = repo else {
761 return Ok(());
762 };
763 callback(repo, &sha, stash, commit_view, workspace, cx).await?;
764 anyhow::Ok(())
765 })
766 .detach_and_notify_err(window, cx);
767 }
768}
769
770impl EventEmitter<ToolbarItemEvent> for CommitViewToolbar {}
771
772impl ToolbarItemView for CommitViewToolbar {
773 fn set_active_pane_item(
774 &mut self,
775 active_pane_item: Option<&dyn ItemHandle>,
776 _: &mut Window,
777 cx: &mut Context<Self>,
778 ) -> ToolbarItemLocation {
779 if let Some(entity) = active_pane_item.and_then(|i| i.act_as::<CommitView>(cx))
780 && entity.read(cx).stash.is_some()
781 {
782 self.commit_view = Some(entity.downgrade());
783 return ToolbarItemLocation::PrimaryRight;
784 }
785 ToolbarItemLocation::Hidden
786 }
787
788 fn pane_focus_update(
789 &mut self,
790 _pane_focused: bool,
791 _window: &mut Window,
792 _cx: &mut Context<Self>,
793 ) {
794 }
795}
796
797impl Render for CommitViewToolbar {
798 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
799 let Some(commit_view) = self.commit_view(cx) else {
800 return div();
801 };
802
803 let is_stash = commit_view.read(cx).stash.is_some();
804 if !is_stash {
805 return div();
806 }
807
808 let focus_handle = commit_view.focus_handle(cx);
809
810 h_group_xl().my_neg_1().py_1().items_center().child(
811 h_group_sm()
812 .child(
813 Button::new("apply-stash", "Apply")
814 .tooltip(Tooltip::for_action_title_in(
815 "Apply current stash",
816 &ApplyCurrentStash,
817 &focus_handle,
818 ))
819 .on_click(cx.listener(|this, _, window, cx| this.apply_stash(window, cx))),
820 )
821 .child(
822 Button::new("pop-stash", "Pop")
823 .tooltip(Tooltip::for_action_title_in(
824 "Pop current stash",
825 &PopCurrentStash,
826 &focus_handle,
827 ))
828 .on_click(cx.listener(|this, _, window, cx| this.pop_stash(window, cx))),
829 )
830 .child(
831 Button::new("remove-stash", "Remove")
832 .icon(IconName::Trash)
833 .tooltip(Tooltip::for_action_title_in(
834 "Remove current stash",
835 &DropCurrentStash,
836 &focus_handle,
837 ))
838 .on_click(cx.listener(|this, _, window, cx| this.remove_stash(window, cx))),
839 ),
840 )
841 }
842}
843
844fn register_workspace_action<A: Action>(
845 workspace: &mut Workspace,
846 callback: fn(&mut CommitViewToolbar, &A, &mut Window, &mut Context<CommitViewToolbar>),
847) {
848 workspace.register_action(move |workspace, action: &A, window, cx| {
849 if workspace.has_active_modal(window, cx) {
850 cx.propagate();
851 return;
852 }
853
854 workspace.active_pane().update(cx, |pane, cx| {
855 pane.toolbar().update(cx, move |workspace, cx| {
856 if let Some(toolbar) = workspace.item_of_type::<CommitViewToolbar>() {
857 toolbar.update(cx, move |toolbar, cx| {
858 callback(toolbar, action, window, cx);
859 cx.notify();
860 });
861 }
862 });
863 })
864 });
865}
866
867fn stash_matches_index(sha: &str, index: usize, repo: &mut Repository) -> bool {
868 match repo
869 .cached_stash()
870 .entries
871 .iter()
872 .find(|entry| entry.index == index)
873 {
874 Some(entry) => entry.oid.to_string() == sha,
875 None => false,
876 }
877}