@@ -1,10 +1,11 @@
use anyhow::{Context as _, Result};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects, multibuffer_context_lines};
-use git::repository::{CommitDetails, CommitDiff, CommitSummary, RepoPath};
+use git::repository::{CommitDetails, CommitDiff, RepoPath};
use gpui::{
- AnyElement, AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter,
- FocusHandle, Focusable, IntoElement, Render, WeakEntity, Window,
+ Action, AnyElement, AnyView, App, AppContext as _, AsyncApp, AsyncWindowContext, Context,
+ Entity, EventEmitter, FocusHandle, Focusable, IntoElement, PromptLevel, Render, WeakEntity,
+ Window, actions,
};
use language::{
Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,
@@ -18,17 +19,42 @@ use std::{
path::PathBuf,
sync::Arc,
};
-use ui::{Color, Icon, IconName, Label, LabelCommon as _, SharedString};
+use ui::{
+ Button, Color, Icon, IconName, Label, LabelCommon as _, SharedString, Tooltip, prelude::*,
+};
use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};
use workspace::{
- Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,
+ Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
+ Workspace,
item::{BreadcrumbText, ItemEvent, TabContentParams},
+ notifications::NotifyTaskExt,
+ pane::SaveIntent,
searchable::SearchableItemHandle,
};
+use crate::git_panel::GitPanel;
+
+actions!(git, [ApplyCurrentStash, PopCurrentStash, DropCurrentStash,]);
+
+pub fn init(cx: &mut App) {
+ cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
+ register_workspace_action(workspace, |toolbar, _: &ApplyCurrentStash, window, cx| {
+ toolbar.apply_stash(window, cx);
+ });
+ register_workspace_action(workspace, |toolbar, _: &DropCurrentStash, window, cx| {
+ toolbar.remove_stash(window, cx);
+ });
+ register_workspace_action(workspace, |toolbar, _: &PopCurrentStash, window, cx| {
+ toolbar.pop_stash(window, cx);
+ });
+ })
+ .detach();
+}
+
pub struct CommitView {
commit: CommitDetails,
editor: Entity<Editor>,
+ stash: Option<usize>,
multibuffer: Entity<MultiBuffer>,
}
@@ -48,17 +74,18 @@ const FILE_NAMESPACE_SORT_PREFIX: u64 = 1;
impl CommitView {
pub fn open(
- commit: CommitSummary,
+ commit_sha: String,
repo: WeakEntity<Repository>,
workspace: WeakEntity<Workspace>,
+ stash: Option<usize>,
window: &mut Window,
cx: &mut App,
) {
let commit_diff = repo
- .update(cx, |repo, _| repo.load_commit_diff(commit.sha.to_string()))
+ .update(cx, |repo, _| repo.load_commit_diff(commit_sha.clone()))
.ok();
let commit_details = repo
- .update(cx, |repo, _| repo.show(commit.sha.to_string()))
+ .update(cx, |repo, _| repo.show(commit_sha.clone()))
.ok();
window
@@ -77,6 +104,7 @@ impl CommitView {
commit_diff,
repo,
project.clone(),
+ stash,
window,
cx,
)
@@ -87,7 +115,7 @@ impl CommitView {
let ix = pane.items().position(|item| {
let commit_view = item.downcast::<CommitView>();
commit_view
- .is_some_and(|view| view.read(cx).commit.sha == commit.sha)
+ .is_some_and(|view| view.read(cx).commit.sha == commit_sha)
});
if let Some(ix) = ix {
pane.activate_item(ix, true, true, window, cx);
@@ -106,6 +134,7 @@ impl CommitView {
commit_diff: CommitDiff,
repository: Entity<Repository>,
project: Entity<Project>,
+ stash: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -127,10 +156,13 @@ impl CommitView {
let mut metadata_buffer_id = None;
if let Some(worktree_id) = first_worktree_id {
+ let title = if let Some(stash) = stash {
+ format!("stash@{{{}}}", stash)
+ } else {
+ format!("commit {}", commit.sha)
+ };
let file = Arc::new(CommitMetadataFile {
- title: RelPath::unix(&format!("commit {}", commit.sha))
- .unwrap()
- .into(),
+ title: RelPath::unix(&title).unwrap().into(),
worktree_id,
});
let buffer = cx.new(|cx| {
@@ -138,7 +170,7 @@ impl CommitView {
ReplicaId::LOCAL,
cx.entity_id().as_non_zero_u64().into(),
LineEnding::default(),
- format_commit(&commit).into(),
+ format_commit(&commit, stash.is_some()).into(),
);
metadata_buffer_id = Some(buffer.remote_id());
Buffer::build(buffer, Some(file.clone()), Capability::ReadWrite)
@@ -211,6 +243,7 @@ impl CommitView {
commit,
editor,
multibuffer,
+ stash,
}
}
}
@@ -369,9 +402,13 @@ async fn build_buffer_diff(
})
}
-fn format_commit(commit: &CommitDetails) -> String {
+fn format_commit(commit: &CommitDetails, is_stash: bool) -> String {
let mut result = String::new();
- writeln!(&mut result, "commit {}", commit.sha).unwrap();
+ if is_stash {
+ writeln!(&mut result, "stash commit {}", commit.sha).unwrap();
+ } else {
+ writeln!(&mut result, "commit {}", commit.sha).unwrap();
+ }
writeln!(
&mut result,
"Author: {} <{}>",
@@ -538,13 +575,296 @@ impl Item for CommitView {
editor,
multibuffer,
commit: self.commit.clone(),
+ stash: self.stash,
}
}))
}
}
impl Render for CommitView {
- fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
- self.editor.clone()
+ fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let is_stash = self.stash.is_some();
+ div()
+ .key_context(if is_stash { "StashDiff" } else { "CommitDiff" })
+ .bg(cx.theme().colors().editor_background)
+ .flex()
+ .items_center()
+ .justify_center()
+ .size_full()
+ .child(self.editor.clone())
+ }
+}
+
+pub struct CommitViewToolbar {
+ commit_view: Option<WeakEntity<CommitView>>,
+ workspace: WeakEntity<Workspace>,
+}
+
+impl CommitViewToolbar {
+ pub fn new(workspace: &Workspace, _: &mut Context<Self>) -> Self {
+ Self {
+ commit_view: None,
+ workspace: workspace.weak_handle(),
+ }
+ }
+
+ fn commit_view(&self, _: &App) -> Option<Entity<CommitView>> {
+ self.commit_view.as_ref()?.upgrade()
+ }
+
+ async fn close_commit_view(
+ commit_view: Entity<CommitView>,
+ workspace: WeakEntity<Workspace>,
+ cx: &mut AsyncWindowContext,
+ ) -> anyhow::Result<()> {
+ workspace
+ .update_in(cx, |workspace, window, cx| {
+ let active_pane = workspace.active_pane();
+ let commit_view_id = commit_view.entity_id();
+ active_pane.update(cx, |pane, cx| {
+ pane.close_item_by_id(commit_view_id, SaveIntent::Skip, window, cx)
+ })
+ })?
+ .await?;
+ anyhow::Ok(())
+ }
+
+ fn apply_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.stash_action(
+ "Apply",
+ window,
+ cx,
+ async move |repository, sha, stash, commit_view, workspace, cx| {
+ let result = repository.update(cx, |repo, cx| {
+ if !stash_matches_index(&sha, stash, repo) {
+ return Err(anyhow::anyhow!("Stash has changed, not applying"));
+ }
+ Ok(repo.stash_apply(Some(stash), cx))
+ })?;
+
+ match result {
+ Ok(task) => task.await?,
+ Err(err) => {
+ Self::close_commit_view(commit_view, workspace, cx).await?;
+ return Err(err);
+ }
+ };
+ Self::close_commit_view(commit_view, workspace, cx).await?;
+ anyhow::Ok(())
+ },
+ );
+ }
+
+ fn pop_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.stash_action(
+ "Pop",
+ window,
+ cx,
+ async move |repository, sha, stash, commit_view, workspace, cx| {
+ let result = repository.update(cx, |repo, cx| {
+ if !stash_matches_index(&sha, stash, repo) {
+ return Err(anyhow::anyhow!("Stash has changed, pop aborted"));
+ }
+ Ok(repo.stash_pop(Some(stash), cx))
+ })?;
+
+ match result {
+ Ok(task) => task.await?,
+ Err(err) => {
+ Self::close_commit_view(commit_view, workspace, cx).await?;
+ return Err(err);
+ }
+ };
+ Self::close_commit_view(commit_view, workspace, cx).await?;
+ anyhow::Ok(())
+ },
+ );
+ }
+
+ fn remove_stash(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.stash_action(
+ "Drop",
+ window,
+ cx,
+ async move |repository, sha, stash, commit_view, workspace, cx| {
+ let result = repository.update(cx, |repo, cx| {
+ if !stash_matches_index(&sha, stash, repo) {
+ return Err(anyhow::anyhow!("Stash has changed, drop aborted"));
+ }
+ Ok(repo.stash_drop(Some(stash), cx))
+ })?;
+
+ match result {
+ Ok(task) => task.await??,
+ Err(err) => {
+ Self::close_commit_view(commit_view, workspace, cx).await?;
+ return Err(err);
+ }
+ };
+ Self::close_commit_view(commit_view, workspace, cx).await?;
+ anyhow::Ok(())
+ },
+ );
+ }
+
+ fn stash_action<AsyncFn>(
+ &mut self,
+ str_action: &str,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ callback: AsyncFn,
+ ) where
+ AsyncFn: AsyncFnOnce(
+ Entity<Repository>,
+ &SharedString,
+ usize,
+ Entity<CommitView>,
+ WeakEntity<Workspace>,
+ &mut AsyncWindowContext,
+ ) -> anyhow::Result<()>
+ + 'static,
+ {
+ let Some(commit_view) = self.commit_view(cx) else {
+ return;
+ };
+ let Some(stash) = commit_view.read(cx).stash else {
+ return;
+ };
+ let sha = commit_view.read(cx).commit.sha.clone();
+ let answer = window.prompt(
+ PromptLevel::Info,
+ &format!("{} stash@{{{}}}?", str_action, stash),
+ None,
+ &[str_action, "Cancel"],
+ cx,
+ );
+
+ let workspace = self.workspace.clone();
+ cx.spawn_in(window, async move |_, cx| {
+ if answer.await != Ok(0) {
+ return anyhow::Ok(());
+ }
+ let repo = workspace.update(cx, |workspace, cx| {
+ workspace
+ .panel::<GitPanel>(cx)
+ .and_then(|p| p.read(cx).active_repository.clone())
+ })?;
+
+ let Some(repo) = repo else {
+ return Ok(());
+ };
+ callback(repo, &sha, stash, commit_view, workspace, cx).await?;
+ anyhow::Ok(())
+ })
+ .detach_and_notify_err(window, cx);
+ }
+}
+
+impl EventEmitter<ToolbarItemEvent> for CommitViewToolbar {}
+
+impl ToolbarItemView for CommitViewToolbar {
+ fn set_active_pane_item(
+ &mut self,
+ active_pane_item: Option<&dyn ItemHandle>,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> ToolbarItemLocation {
+ if let Some(entity) = active_pane_item.and_then(|i| i.act_as::<CommitView>(cx))
+ && entity.read(cx).stash.is_some()
+ {
+ self.commit_view = Some(entity.downgrade());
+ return ToolbarItemLocation::PrimaryRight;
+ }
+ ToolbarItemLocation::Hidden
+ }
+
+ fn pane_focus_update(
+ &mut self,
+ _pane_focused: bool,
+ _window: &mut Window,
+ _cx: &mut Context<Self>,
+ ) {
+ }
+}
+
+impl Render for CommitViewToolbar {
+ fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let Some(commit_view) = self.commit_view(cx) else {
+ return div();
+ };
+
+ let is_stash = commit_view.read(cx).stash.is_some();
+ if !is_stash {
+ return div();
+ }
+
+ let focus_handle = commit_view.focus_handle(cx);
+
+ h_group_xl().my_neg_1().py_1().items_center().child(
+ h_group_sm()
+ .child(
+ Button::new("apply-stash", "Apply")
+ .tooltip(Tooltip::for_action_title_in(
+ "Apply current stash",
+ &ApplyCurrentStash,
+ &focus_handle,
+ ))
+ .on_click(cx.listener(|this, _, window, cx| this.apply_stash(window, cx))),
+ )
+ .child(
+ Button::new("pop-stash", "Pop")
+ .tooltip(Tooltip::for_action_title_in(
+ "Pop current stash",
+ &PopCurrentStash,
+ &focus_handle,
+ ))
+ .on_click(cx.listener(|this, _, window, cx| this.pop_stash(window, cx))),
+ )
+ .child(
+ Button::new("remove-stash", "Remove")
+ .icon(IconName::Trash)
+ .tooltip(Tooltip::for_action_title_in(
+ "Remove current stash",
+ &DropCurrentStash,
+ &focus_handle,
+ ))
+ .on_click(cx.listener(|this, _, window, cx| this.remove_stash(window, cx))),
+ ),
+ )
+ }
+}
+
+fn register_workspace_action<A: Action>(
+ workspace: &mut Workspace,
+ callback: fn(&mut CommitViewToolbar, &A, &mut Window, &mut Context<CommitViewToolbar>),
+) {
+ workspace.register_action(move |workspace, action: &A, window, cx| {
+ if workspace.has_active_modal(window, cx) {
+ cx.propagate();
+ return;
+ }
+
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.toolbar().update(cx, move |workspace, cx| {
+ if let Some(toolbar) = workspace.item_of_type::<CommitViewToolbar>() {
+ toolbar.update(cx, move |toolbar, cx| {
+ callback(toolbar, action, window, cx);
+ cx.notify();
+ });
+ }
+ });
+ })
+ });
+}
+
+fn stash_matches_index(sha: &str, index: usize, repo: &mut Repository) -> bool {
+ match repo
+ .cached_stash()
+ .entries
+ .iter()
+ .find(|entry| entry.index == index)
+ {
+ Some(entry) => entry.oid.to_string() == sha,
+ None => false,
}
}
@@ -5,18 +5,21 @@ use git::stash::StashEntry;
use gpui::{
Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render,
- SharedString, Styled, Subscription, Task, Window, actions, rems,
+ SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, rems, svg,
};
use picker::{Picker, PickerDelegate};
use project::git_store::{Repository, RepositoryEvent};
use std::sync::Arc;
use time::{OffsetDateTime, UtcOffset};
use time_format;
-use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
+use ui::{
+ ButtonLike, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*,
+};
use util::ResultExt;
use workspace::notifications::DetachAndPromptErr;
use workspace::{ModalView, Workspace};
+use crate::commit_view::CommitView;
use crate::stash_picker;
actions!(
@@ -24,6 +27,8 @@ actions!(
[
/// Drop the selected stash entry.
DropStashItem,
+ /// Show the diff view of the selected stash entry.
+ ShowStashItem,
]
);
@@ -38,8 +43,9 @@ pub fn open(
cx: &mut Context<Workspace>,
) {
let repository = workspace.project().read(cx).active_repository(cx);
+ let weak_workspace = workspace.weak_handle();
workspace.toggle_modal(window, cx, |window, cx| {
- StashList::new(repository, rems(34.), window, cx)
+ StashList::new(repository, weak_workspace, rems(34.), window, cx)
})
}
@@ -53,6 +59,7 @@ pub struct StashList {
impl StashList {
fn new(
repository: Option<Entity<Repository>>,
+ workspace: WeakEntity<Workspace>,
width: Rems,
window: &mut Window,
cx: &mut Context<Self>,
@@ -98,7 +105,7 @@ impl StashList {
})
.detach_and_log_err(cx);
- let delegate = StashListDelegate::new(repository, window, cx);
+ let delegate = StashListDelegate::new(repository, workspace, window, cx);
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
let picker_focus_handle = picker.focus_handle(cx);
picker.update(cx, |picker, _| {
@@ -131,6 +138,20 @@ impl StashList {
cx.notify();
}
+ fn handle_show_stash(
+ &mut self,
+ _: &ShowStashItem,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.picker.update(cx, |picker, cx| {
+ picker
+ .delegate
+ .show_stash_at(picker.delegate.selected_index(), window, cx);
+ });
+ cx.notify();
+ }
+
fn handle_modifiers_changed(
&mut self,
ev: &ModifiersChangedEvent,
@@ -157,6 +178,7 @@ impl Render for StashList {
.w(self.width)
.on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
.on_action(cx.listener(Self::handle_drop_stash))
+ .on_action(cx.listener(Self::handle_show_stash))
.child(self.picker.clone())
}
}
@@ -172,6 +194,7 @@ pub struct StashListDelegate {
matches: Vec<StashEntryMatch>,
all_stash_entries: Option<Vec<StashEntry>>,
repo: Option<Entity<Repository>>,
+ workspace: WeakEntity<Workspace>,
selected_index: usize,
last_query: String,
modifiers: Modifiers,
@@ -182,6 +205,7 @@ pub struct StashListDelegate {
impl StashListDelegate {
fn new(
repo: Option<Entity<Repository>>,
+ workspace: WeakEntity<Workspace>,
_window: &mut Window,
cx: &mut Context<StashList>,
) -> Self {
@@ -192,6 +216,7 @@ impl StashListDelegate {
Self {
matches: vec![],
repo,
+ workspace,
all_stash_entries: None,
selected_index: 0,
last_query: Default::default(),
@@ -235,6 +260,25 @@ impl StashListDelegate {
});
}
+ fn show_stash_at(&self, ix: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+ let Some(entry_match) = self.matches.get(ix) else {
+ return;
+ };
+ let stash_sha = entry_match.entry.oid.to_string();
+ let stash_index = entry_match.entry.index;
+ let Some(repo) = self.repo.clone() else {
+ return;
+ };
+ CommitView::open(
+ stash_sha,
+ repo.downgrade(),
+ self.workspace.clone(),
+ Some(stash_index),
+ window,
+ cx,
+ );
+ }
+
fn pop_stash(&self, stash_index: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(repo) = self.repo.clone() else {
return;
@@ -390,7 +434,7 @@ impl PickerDelegate for StashListDelegate {
ix: usize,
selected: bool,
_window: &mut Window,
- _cx: &mut Context<Picker<Self>>,
+ cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let entry_match = &self.matches[ix];
@@ -432,11 +476,35 @@ impl PickerDelegate for StashListDelegate {
.size(LabelSize::Small),
);
+ let show_button = div()
+ .group("show-button-hover")
+ .child(
+ ButtonLike::new("show-button")
+ .child(
+ svg()
+ .size(IconSize::Medium.rems())
+ .flex_none()
+ .path(IconName::Eye.path())
+ .text_color(Color::Default.color(cx))
+ .group_hover("show-button-hover", |this| {
+ this.text_color(Color::Accent.color(cx))
+ })
+ .hover(|this| this.text_color(Color::Accent.color(cx))),
+ )
+ .tooltip(Tooltip::for_action_title("Show Stash", &ShowStashItem))
+ .on_click(cx.listener(move |picker, _, window, cx| {
+ cx.stop_propagation();
+ picker.delegate.show_stash_at(ix, window, cx);
+ })),
+ )
+ .into_any_element();
+
Some(
ListItem::new(SharedString::from(format!("stash-{ix}")))
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
+ .end_slot(show_button)
.child(
v_flex()
.w_full()