From 231a348e69e8b823936887a01232ba491e21a197 Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Fri, 8 Aug 2025 19:32:11 -0400 Subject: [PATCH 01/28] WIP --- crates/fs/src/fake_git_repo.rs | 4 +++ crates/git/src/git.rs | 1 + crates/git/src/repository.rs | 23 ++++++++++++++ crates/git/src/stash.rs | 56 +++++++++++++++++++++++++++++++++ crates/git_ui/src/git_panel.rs | 11 ++++++- crates/project/src/git_store.rs | 12 +++++++ 6 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 crates/git/src/stash.rs diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 8a67eddcd7..8726d8d3b7 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -320,6 +320,10 @@ impl GitRepository for FakeGitRepository { }) } + fn stash_entries(&self) -> BoxFuture<'_, Result> { + unimplemented!() + } + fn branches(&self) -> BoxFuture<'_, Result>> { self.with_state_async(false, move |state| { let current_branch = &state.current_branch_name; diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index e84014129c..21a31657ed 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -3,6 +3,7 @@ pub mod commit; mod hosting_provider; mod remote; pub mod repository; +pub mod stash; pub mod status; pub use crate::hosting_provider::*; diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index fd12dafa98..c3e2934f5c 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -1,4 +1,5 @@ use crate::commit::parse_git_diff_name_status; +use crate::stash::GitStash; use crate::status::{GitStatus, StatusCode}; use crate::{Oid, SHORT_SHA_LENGTH}; use anyhow::{Context as _, Result, anyhow, bail}; @@ -338,6 +339,8 @@ pub trait GitRepository: Send + Sync { fn status(&self, path_prefixes: &[RepoPath]) -> Task>; + fn stash_entries(&self) -> BoxFuture<'_, Result>; + fn branches(&self) -> BoxFuture<'_, Result>>; fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>>; @@ -974,6 +977,26 @@ impl GitRepository for RealGitRepository { }) } + fn stash_entries(&self) -> BoxFuture<'_, Result> { + let git_binary_path = self.git_binary_path.clone(); + let working_directory = self.working_directory(); + self.executor + .spawn(async move { + let output = new_std_command(&git_binary_path) + .current_dir(working_directory?) + .args(&["stash", "list", "--pretty=%gd:%H:%s"]) + .output()?; + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + stdout.parse() + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!("git status failed: {stderr}"); + } + }) + .boxed() + } + fn branches(&self) -> BoxFuture<'_, Result>> { let working_directory = self.working_directory(); let git_binary_path = self.git_binary_path.clone(); diff --git a/crates/git/src/stash.rs b/crates/git/src/stash.rs new file mode 100644 index 0000000000..090009712c --- /dev/null +++ b/crates/git/src/stash.rs @@ -0,0 +1,56 @@ +use crate::Oid; +use anyhow::Result; +use std::{str::FromStr, sync::Arc}; + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct StashEntry { + pub index: usize, + pub oid: Oid, + pub message: String, +} + +#[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] +pub struct GitStash { + pub entries: Arc<[StashEntry]>, +} + +impl FromStr for GitStash { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + // git stash list --pretty=%gd:%H:%s + let entries = s + .split('\n') + .filter_map(|entry| { + let mut parts = entry.splitn(3, ':'); + let raw_idx = parts.next().and_then(|i| { + let trimmed = i.trim(); + if trimmed.starts_with("stash@{") && trimmed.ends_with('}') { + trimmed + .strip_prefix("stash@{") + .and_then(|s| s.strip_suffix('}')) + } else { + None + } + }); + let raw_oid = parts.next(); + let message = parts.next(); + + if let (Some(raw_idx), Some(raw_oid), Some(message)) = (raw_idx, raw_oid, message) { + let index = raw_idx.parse::().ok()?; + let oid = Oid::from_str(raw_oid).ok()?; + let entry = StashEntry { + index, + oid, + message: message.to_string(), + }; + return Some(entry); + } + None + }) + .collect::>(); + Ok(Self { + entries: entries.clone(), + }) + } +} diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 4ecb4a8829..ce6129b8a1 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -24,6 +24,7 @@ use git::repository::{ PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus, get_git_committer, }; +use git::stash::GitStash; use git::status::StageStatus; use git::{Amend, Signoff, ToggleStaged, repository::RepoPath, status::FileStatus}; use git::{ @@ -119,6 +120,7 @@ struct GitMenuState { has_staged_changes: bool, has_unstaged_changes: bool, has_new_changes: bool, + has_stash_items: bool, } fn git_panel_context_menu( @@ -146,7 +148,7 @@ fn git_panel_context_menu( "Stash All", StashAll.boxed_clone(), ) - .action("Stash Pop", StashPop.boxed_clone()) + .action_disabled_when(!state.has_stash_items, "Stash Pop", StashPop.boxed_clone()) .separator() .action("Open Diff", project_diff::Diff.boxed_clone()) .separator() @@ -369,6 +371,7 @@ pub struct GitPanel { local_committer: Option, local_committer_task: Option>, bulk_staging: Option, + stash_entries: GitStash, _settings_subscription: Subscription, } @@ -558,6 +561,7 @@ impl GitPanel { horizontal_scrollbar, vertical_scrollbar, bulk_staging: None, + stash_entries: Default::default(), _settings_subscription, }; @@ -2685,6 +2689,8 @@ impl GitPanel { let repo = repo.read(cx); + self.stash_entries = repo.cached_stash(); + for entry in repo.cached_status() { let is_conflict = repo.had_conflict_on_last_merge_head_change(&entry.repo_path); let is_new = entry.status.is_created(); @@ -3053,6 +3059,7 @@ impl GitPanel { let has_staged_changes = self.has_staged_changes(); let has_unstaged_changes = self.has_unstaged_changes(); let has_new_changes = self.new_count > 0; + let has_stash_items = self.stash_entries.entries.len() > 0; PopoverMenu::new(id.into()) .trigger( @@ -3068,6 +3075,7 @@ impl GitPanel { has_staged_changes, has_unstaged_changes, has_new_changes, + has_stash_items, }, window, cx, @@ -4115,6 +4123,7 @@ impl GitPanel { has_staged_changes: self.has_staged_changes(), has_unstaged_changes: self.has_unstaged_changes(), has_new_changes: self.new_count > 0, + has_stash_items: self.stash_entries.entries.len() > 0, }, window, cx, diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 5cf298a8bf..6a86fc2a83 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -28,6 +28,7 @@ use git::{ GitRepository, GitRepositoryCheckpoint, PushOptions, Remote, RemoteCommandOutput, RepoPath, ResetMode, UpstreamTrackingStatus, }, + stash::GitStash, status::{ FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode, }, @@ -248,6 +249,7 @@ pub struct RepositorySnapshot { pub merge: MergeDetails, pub remote_origin_url: Option, pub remote_upstream_url: Option, + pub stash_entries: GitStash, } type JobId = u64; @@ -2744,6 +2746,7 @@ impl RepositorySnapshot { merge: Default::default(), remote_origin_url: None, remote_upstream_url: None, + stash_entries: Default::default(), } } @@ -3264,6 +3267,10 @@ impl Repository { self.snapshot.status() } + pub fn cached_stash(&self) -> GitStash { + self.snapshot.stash_entries.clone() + } + pub fn repo_path_to_project_path(&self, path: &RepoPath, cx: &App) -> Option { let git_store = self.git_store.upgrade()?; let worktree_store = git_store.read(cx).worktree_store.read(cx); @@ -4539,6 +4546,7 @@ impl Repository { updates_tx: Option>, cx: &mut Context, ) { + println!("paths changed with updates_tx: {:?}", updates_tx); self.paths_needing_status_update.extend(paths); let this = cx.weak_entity(); @@ -4561,6 +4569,7 @@ impl Repository { return Ok(()); } let statuses = backend.status(&paths).await?; + let stash_entries = backend.stash_entries().await?; let changed_path_statuses = cx .background_spawn(async move { @@ -4592,6 +4601,7 @@ impl Repository { .await; this.update(&mut cx, |this, cx| { + this.snapshot.stash_entries = stash_entries; if !changed_path_statuses.is_empty() { this.snapshot .statuses_by_path @@ -4852,6 +4862,7 @@ async fn compute_snapshot( let statuses = backend .status(std::slice::from_ref(&WORK_DIRECTORY_REPO_PATH)) .await?; + let stash_entries = backend.stash_entries().await?; let statuses_by_path = SumTree::from_iter( statuses .entries @@ -4902,6 +4913,7 @@ async fn compute_snapshot( merge: merge_details, remote_origin_url, remote_upstream_url, + stash_entries, }; Ok((snapshot, events)) From 3498ba894e3848346a3c2a47914c8aecc2a57305 Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Fri, 8 Aug 2025 20:53:50 -0400 Subject: [PATCH 02/28] Enable git stash on collab --- crates/collab/src/db/queries/projects.rs | 1 + crates/collab/src/db/queries/rooms.rs | 1 + crates/git/src/stash.rs | 6 ++++ crates/project/src/git_store.rs | 39 ++++++++++++++++++++++-- crates/proto/proto/git.proto | 7 +++++ 5 files changed, 52 insertions(+), 2 deletions(-) diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 393f2c80f8..9c57f9a2a5 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -993,6 +993,7 @@ impl Database { scan_id: db_repository_entry.scan_id as u64, is_last_update: true, merge_message: db_repository_entry.merge_message, + stash_entries: Vec::new(), }); } } diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 9e7cabf9b2..251e1c1034 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -794,6 +794,7 @@ impl Database { scan_id: db_repository.scan_id as u64, is_last_update: true, merge_message: db_repository.merge_message, + stash_entries: Vec::new(), }); } } diff --git a/crates/git/src/stash.rs b/crates/git/src/stash.rs index 090009712c..c97c13ad9f 100644 --- a/crates/git/src/stash.rs +++ b/crates/git/src/stash.rs @@ -14,6 +14,12 @@ pub struct GitStash { pub entries: Arc<[StashEntry]>, } +impl GitStash { + pub fn apply(&mut self, other: GitStash) { + self.entries = other.entries; + } +} + impl FromStr for GitStash { type Err = anyhow::Error; diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 6a86fc2a83..5b945aecfb 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -20,7 +20,7 @@ use futures::{ stream::FuturesOrdered, }; use git::{ - BuildPermalinkParams, GitHostingProviderRegistry, WORK_DIRECTORY_REPO_PATH, + BuildPermalinkParams, GitHostingProviderRegistry, Oid, WORK_DIRECTORY_REPO_PATH, blame::Blame, parse_git_remote_url, repository::{ @@ -28,7 +28,7 @@ use git::{ GitRepository, GitRepositoryCheckpoint, PushOptions, Remote, RemoteCommandOutput, RepoPath, ResetMode, UpstreamTrackingStatus, }, - stash::GitStash, + stash::{GitStash, StashEntry}, status::{ FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode, }, @@ -2773,6 +2773,12 @@ impl RepositorySnapshot { entry_ids: vec![self.id.to_proto()], scan_id: self.scan_id, is_last_update: true, + stash_entries: self + .stash_entries + .entries + .iter() + .map(|entry| stash_to_proto(entry)) + .collect(), } } @@ -2836,6 +2842,12 @@ impl RepositorySnapshot { entry_ids: vec![], scan_id: self.scan_id, is_last_update: true, + stash_entries: self + .stash_entries + .entries + .iter() + .map(|entry| stash_to_proto(entry)) + .collect(), } } @@ -2892,6 +2904,22 @@ impl RepositorySnapshot { } } +pub fn stash_to_proto(entry: &StashEntry) -> proto::StashEntry { + proto::StashEntry { + oid: entry.oid.as_bytes().to_vec(), + message: entry.message.clone(), + index: entry.index as i64, + } +} + +pub fn proto_to_stash(entry: &proto::StashEntry) -> Result { + Ok(StashEntry { + oid: Oid::from_bytes(&entry.oid)?, + message: entry.message.clone(), + index: entry.index as usize, + }) +} + impl MergeDetails { async fn load( backend: &Arc, @@ -4259,6 +4287,13 @@ impl Repository { self.snapshot.merge.conflicted_paths = conflicted_paths; self.snapshot.merge.message = update.merge_message.map(SharedString::from); + self.snapshot.stash_entries = GitStash { + entries: update + .stash_entries + .iter() + .filter_map(|entry| proto_to_stash(entry).ok()) + .collect(), + }; let edits = update .removed_statuses diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto index cfb0369875..a5bef1bc4a 100644 --- a/crates/proto/proto/git.proto +++ b/crates/proto/proto/git.proto @@ -122,6 +122,7 @@ message UpdateRepository { bool is_last_update = 10; optional GitCommitDetails head_commit_details = 11; optional string merge_message = 12; + repeated StashEntry stash_entries = 13; } message RemoveRepository { @@ -283,6 +284,12 @@ message StatusEntry { GitFileStatus status = 3; } +message StashEntry { + bytes oid = 1; + string message = 2; + int64 index = 3; +} + message Stage { uint64 project_id = 1; reserved 2; From 6a09300c639745015045c133fe91af6c67ecdbfb Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Sat, 9 Aug 2025 12:53:33 -0400 Subject: [PATCH 03/28] Add stash picker --- crates/collab/src/rpc.rs | 1 + crates/fs/src/fake_git_repo.rs | 14 +- crates/git/src/repository.rs | 53 ++++- crates/git_ui/src/git_panel.rs | 2 +- crates/git_ui/src/git_ui.rs | 2 + crates/git_ui/src/stash_picker.rs | 356 ++++++++++++++++++++++++++++++ crates/project/src/git_store.rs | 65 +++++- crates/proto/proto/git.proto | 7 + crates/proto/proto/zed.proto | 3 +- crates/proto/src/proto.rs | 3 + crates/zed_actions/src/lib.rs | 4 +- 11 files changed, 500 insertions(+), 10 deletions(-) create mode 100644 crates/git_ui/src/stash_picker.rs diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 73f327166a..014b02adf6 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -460,6 +460,7 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_read_only_project_request::) diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 8726d8d3b7..d4b371e770 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -416,7 +416,19 @@ impl GitRepository for FakeGitRepository { unimplemented!() } - fn stash_pop(&self, _env: Arc>) -> BoxFuture<'_, Result<()>> { + fn stash_pop( + &self, + _index: Option, + _env: Arc>, + ) -> BoxFuture<'_, Result<()>> { + unimplemented!() + } + + fn stash_drop( + &self, + _index: Option, + _env: Arc>, + ) -> BoxFuture<'_, Result<()>> { unimplemented!() } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index c3e2934f5c..98392fb2e6 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -402,7 +402,17 @@ pub trait GitRepository: Send + Sync { env: Arc>, ) -> BoxFuture<'_, Result<()>>; - fn stash_pop(&self, env: Arc>) -> BoxFuture<'_, Result<()>>; + fn stash_pop( + &self, + index: Option, + env: Arc>, + ) -> BoxFuture<'_, Result<()>>; + + fn stash_drop( + &self, + index: Option, + env: Arc>, + ) -> BoxFuture<'_, Result<()>>; fn push( &self, @@ -1250,14 +1260,22 @@ impl GitRepository for RealGitRepository { .boxed() } - fn stash_pop(&self, env: Arc>) -> BoxFuture<'_, Result<()>> { + fn stash_pop( + &self, + index: Option, + env: Arc>, + ) -> BoxFuture<'_, Result<()>> { let working_directory = self.working_directory(); self.executor .spawn(async move { let mut cmd = new_smol_command("git"); + let mut args = vec!["stash".to_string(), "pop".to_string()]; + if let Some(index) = index { + args.push(format!("stash@{{{}}}", index)); + } cmd.current_dir(&working_directory?) .envs(env.iter()) - .args(["stash", "pop"]); + .args(args); let output = cmd.output().await?; @@ -1271,6 +1289,35 @@ impl GitRepository for RealGitRepository { .boxed() } + fn stash_drop( + &self, + index: Option, + env: Arc>, + ) -> BoxFuture<'_, Result<()>> { + let working_directory = self.working_directory(); + self.executor + .spawn(async move { + let mut cmd = new_smol_command("git"); + let mut args = vec!["stash".to_string(), "drop".to_string()]; + if let Some(index) = index { + args.push(format!("stash@{{{}}}", index)); + } + cmd.current_dir(&working_directory?) + .envs(env.iter()) + .args(args); + + let output = cmd.output().await?; + + anyhow::ensure!( + output.status.success(), + "Failed to stash drop:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + Ok(()) + }) + .boxed() + } + fn commit( &self, message: SharedString, diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index ce6129b8a1..ff6921274f 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1426,7 +1426,7 @@ impl GitPanel { cx.spawn({ async move |this, cx| { let stash_task = active_repository - .update(cx, |repo, cx| repo.stash_pop(cx))? + .update(cx, |repo, cx| repo.stash_pop(None, cx))? .await; this.update(cx, |this, cx| { stash_task diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 5369b8b404..54b1f8ba11 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -36,6 +36,7 @@ pub mod picker_prompt; pub mod project_diff; pub(crate) mod remote_output; pub mod repository_selector; +pub mod stash_picker; pub mod text_diff_view; actions!( @@ -62,6 +63,7 @@ pub fn init(cx: &mut App) { git_panel::register(workspace); repository_selector::register(workspace); branch_picker::register(workspace); + stash_picker::register(workspace); let project = workspace.project().read(cx); if project.is_read_only(cx) { diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs new file mode 100644 index 0000000000..02c57fc619 --- /dev/null +++ b/crates/git_ui/src/stash_picker.rs @@ -0,0 +1,356 @@ +use fuzzy::StringMatchCandidate; + +use git::stash::StashEntry; +use gpui::{ + App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, + IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render, SharedString, Styled, + Subscription, Task, Window, rems, +}; +use picker::{Picker, PickerDelegate, PickerEditorPosition}; +use project::git_store::Repository; +use std::sync::Arc; +use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*}; +use util::ResultExt; +use workspace::notifications::DetachAndPromptErr; +use workspace::{ModalView, Workspace}; + +pub fn register(workspace: &mut Workspace) { + workspace.register_action(open); +} + +pub fn open( + workspace: &mut Workspace, + _: &zed_actions::git::Stash, + window: &mut Window, + cx: &mut Context, +) { + let repository = workspace.project().read(cx).active_repository(cx).clone(); + let style = StashListStyle::Modal; + workspace.toggle_modal(window, cx, |window, cx| { + StashList::new(repository, style, rems(34.), window, cx) + }) +} + +pub fn popover( + repository: Option>, + window: &mut Window, + cx: &mut App, +) -> Entity { + cx.new(|cx| { + let list = StashList::new(repository, StashListStyle::Popover, rems(20.), window, cx); + list.focus_handle(cx).focus(window); + list + }) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum StashListStyle { + Modal, + Popover, +} + +pub struct StashList { + width: Rems, + pub picker: Entity>, + _subscription: Subscription, +} + +impl StashList { + fn new( + repository: Option>, + style: StashListStyle, + width: Rems, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let stash_request = repository + .clone() + .map(|repository| repository.read_with(cx, |repo, _| repo.stash_entries.clone())); + + cx.spawn_in(window, async move |this, cx| { + let stash_entries = stash_request + .map(|git_stash| git_stash.entries.to_vec()) + .unwrap_or_default(); + + this.update_in(cx, |this, window, cx| { + this.picker.update(cx, |picker, cx| { + picker.delegate.all_stash_entries = Some(stash_entries); + picker.refresh(window, cx); + }) + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + + let delegate = StashListDelegate::new(repository.clone(), style); + let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); + + let _subscription = cx.subscribe(&picker, |_, _, _, cx| { + cx.emit(DismissEvent); + }); + + Self { + picker, + width, + _subscription, + } + } + + fn handle_modifiers_changed( + &mut self, + ev: &ModifiersChangedEvent, + _: &mut Window, + cx: &mut Context, + ) { + self.picker + .update(cx, |picker, _| picker.delegate.modifiers = ev.modifiers) + } +} + +impl ModalView for StashList {} +impl EventEmitter for StashList {} + +impl Focusable for StashList { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl Render for StashList { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .key_context("GitStashSelector") + .w(self.width) + .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) + .child(self.picker.clone()) + .on_mouse_down_out({ + cx.listener(move |this, _, window, cx| { + this.picker.update(cx, |this, cx| { + this.cancel(&Default::default(), window, cx); + }) + }) + }) + } +} + +#[derive(Debug, Clone)] +struct StashEntryMatch { + entry: StashEntry, + positions: Vec, +} + +pub struct StashListDelegate { + matches: Vec, + all_stash_entries: Option>, + repo: Option>, + style: StashListStyle, + selected_index: usize, + last_query: String, + modifiers: Modifiers, +} + +impl StashListDelegate { + fn new(repo: Option>, style: StashListStyle) -> Self { + Self { + matches: vec![], + repo, + style, + all_stash_entries: None, + selected_index: 0, + last_query: Default::default(), + modifiers: Default::default(), + } + } + + fn drop_stash(&self, stash_index: usize, window: &mut Window, cx: &mut Context>) { + let Some(repo) = self.repo.clone() else { + return; + }; + + cx.spawn(async move |_, cx| { + repo.update(cx, |repo, cx| repo.stash_drop(Some(stash_index), cx))? + .await?; + Ok(()) + }) + .detach_and_prompt_err("Failed to apply stash", window, cx, |e, _, _| { + Some(e.to_string()) + }); + cx.emit(DismissEvent); + } + + fn pop_stash(&self, stash_index: usize, window: &mut Window, cx: &mut Context>) { + let Some(repo) = self.repo.clone() else { + return; + }; + + cx.spawn(async move |_, cx| { + repo.update(cx, |repo, cx| repo.stash_pop(Some(stash_index), cx))? + .await?; + Ok(()) + }) + .detach_and_prompt_err("Failed to pop stash", window, cx, |e, _, _| { + Some(e.to_string()) + }); + cx.emit(DismissEvent); + } +} + +impl PickerDelegate for StashListDelegate { + type ListItem = ListItem; + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + "Select stash&".into() + } + + fn editor_position(&self) -> PickerEditorPosition { + match self.style { + StashListStyle::Modal => PickerEditorPosition::Start, + StashListStyle::Popover => PickerEditorPosition::End, + } + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + _: &mut Context>, + ) { + self.selected_index = ix; + } + + fn update_matches( + &mut self, + query: String, + window: &mut Window, + cx: &mut Context>, + ) -> Task<()> { + let Some(all_stash_entries) = self.all_stash_entries.clone() else { + return Task::ready(()); + }; + + cx.spawn_in(window, async move |picker, cx| { + let matches: Vec = if query.is_empty() { + all_stash_entries + .into_iter() + .map(|entry| StashEntryMatch { + entry, + positions: Vec::new(), + }) + .collect() + } else { + let candidates = all_stash_entries + .iter() + .enumerate() + .map(|(ix, entry)| StringMatchCandidate::new(ix, &entry.message)) + .collect::>(); + fuzzy::match_strings( + &candidates, + &query, + true, + true, + 10000, + &Default::default(), + cx.background_executor().clone(), + ) + .await + .into_iter() + .map(|candidate| StashEntryMatch { + entry: all_stash_entries[candidate.candidate_id].clone(), + positions: candidate.positions, + }) + .collect() + }; + + picker + .update(cx, |picker, _| { + let delegate = &mut picker.delegate; + delegate.matches = matches; + if delegate.matches.is_empty() { + delegate.selected_index = 0; + } else { + delegate.selected_index = + core::cmp::min(delegate.selected_index, delegate.matches.len() - 1); + } + delegate.last_query = query; + }) + .log_err(); + }) + } + + fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { + let Some(entry_match) = self.matches.get(self.selected_index()) else { + return; + }; + + let stash_index = entry_match.entry.index; + + if secondary { + // Secondary action: remove stash (remove from stash list) + self.drop_stash(stash_index, window, cx); + } else { + // Primary action: apply stash (keep in stash list) + self.pop_stash(stash_index, window, cx); + } + } + + fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { + cx.emit(DismissEvent); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _: &mut Window, + _: &mut Context>, + ) -> Option { + let entry_match = &self.matches[ix]; + + let stash_name = HighlightedLabel::new( + entry_match.entry.message.clone(), + entry_match.positions.clone(), + ) + .truncate() + .into_any_element(); + + let stash_index_label = Label::new(format!("stash@{{{}}}", entry_match.entry.index)) + .size(LabelSize::Small) + .color(Color::Muted); + + Some( + ListItem::new(SharedString::from(format!("stash-{ix}"))) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .toggle_state(selected) + .child( + v_flex() + .w_full() + .overflow_hidden() + .child( + h_flex() + .gap_6() + .justify_between() + .overflow_x_hidden() + .child(stash_name) + .child(stash_index_label.into_element()), + ) + .when(self.style == StashListStyle::Modal, |el| { + el.child(div().max_w_96()) + }), + ), + ) + } + + fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option { + Some("No stashes found".into()) + } +} diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 5b945aecfb..cd0f2b301e 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -427,6 +427,7 @@ impl GitStore { client.add_entity_request_handler(Self::handle_unstage); client.add_entity_request_handler(Self::handle_stash); client.add_entity_request_handler(Self::handle_stash_pop); + client.add_entity_request_handler(Self::handle_stash_drop); client.add_entity_request_handler(Self::handle_commit); client.add_entity_request_handler(Self::handle_reset); client.add_entity_request_handler(Self::handle_show); @@ -1780,10 +1781,29 @@ impl GitStore { ) -> Result { let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; + let stash_index = envelope.payload.stash_index.and_then(|i| Some(i as usize)); repository_handle .update(&mut cx, |repository_handle, cx| { - repository_handle.stash_pop(cx) + repository_handle.stash_pop(stash_index, cx) + })? + .await?; + + Ok(proto::Ack {}) + } + + async fn handle_stash_drop( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); + let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; + let stash_index = envelope.payload.stash_index.and_then(|i| Some(i as usize)); + + repository_handle + .update(&mut cx, |repository_handle, cx| { + repository_handle.stash_drop(stash_index, cx) })? .await?; @@ -3733,7 +3753,11 @@ impl Repository { }) } - pub fn stash_pop(&mut self, cx: &mut Context) -> Task> { + pub fn stash_pop( + &mut self, + index: Option, + cx: &mut Context, + ) -> Task> { let id = self.id; cx.spawn(async move |this, cx| { this.update(cx, |this, _| { @@ -3743,12 +3767,47 @@ impl Repository { backend, environment, .. - } => backend.stash_pop(environment).await, + } => backend.stash_pop(index, environment).await, RepositoryState::Remote { project_id, client } => { client .request(proto::StashPop { project_id: project_id.0, repository_id: id.to_proto(), + stash_index: index.and_then(|i| Some(i as u64)), + }) + .await + .context("sending stash pop request")?; + Ok(()) + } + } + }) + })? + .await??; + Ok(()) + }) + } + + pub fn stash_drop( + &mut self, + index: Option, + cx: &mut Context, + ) -> Task> { + let id = self.id; + cx.spawn(async move |this, cx| { + this.update(cx, |this, _| { + this.send_job(None, move |git_repo, _cx| async move { + match git_repo { + RepositoryState::Local { + backend, + environment, + .. + } => backend.stash_drop(index, environment).await, + RepositoryState::Remote { project_id, client } => { + client + .request(proto::StashDrop { + project_id: project_id.0, + repository_id: id.to_proto(), + stash_index: index.and_then(|i| Some(i as u64)), }) .await .context("sending stash pop request")?; diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto index a5bef1bc4a..4ea846db84 100644 --- a/crates/proto/proto/git.proto +++ b/crates/proto/proto/git.proto @@ -313,6 +313,13 @@ message Stash { message StashPop { uint64 project_id = 1; uint64 repository_id = 2; + optional uint64 stash_index = 3; +} + +message StashDrop { + uint64 project_id = 1; + uint64 repository_id = 2; + optional uint64 stash_index = 3; } message Commit { diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 70689bcd63..f2e70ce7d2 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -396,7 +396,8 @@ message Envelope { GitCloneResponse git_clone_response = 364; LspQuery lsp_query = 365; - LspQueryResponse lsp_query_response = 366; // current max + LspQueryResponse lsp_query_response = 366; + StashDrop stash_drop = 367; // current max } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index d38e54685f..5387b26a8d 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -257,6 +257,7 @@ messages!( (Unstage, Background), (Stash, Background), (StashPop, Background), + (StashDrop, Background), (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), (UpdateChannelBuffer, Foreground), @@ -417,6 +418,7 @@ request_messages!( (Unstage, Ack), (Stash, Ack), (StashPop, Ack), + (StashDrop, Ack), (UpdateBuffer, Ack), (UpdateParticipantLocation, Ack), (UpdateProject, Ack), @@ -570,6 +572,7 @@ entity_messages!( Unstage, Stash, StashPop, + StashDrop, UpdateBuffer, UpdateBufferFile, UpdateDiagnosticSummary, diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 9455369e9a..a4dbc6ec75 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -175,7 +175,9 @@ pub mod git { SelectRepo, /// Opens the git branch selector. #[action(deprecated_aliases = ["branches::OpenRecent"])] - Branch + Branch, + /// Opens the git stash selector. + Stash ] ); } From 21006953876f5b3d71e30c816d2720647d4ded32 Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Sun, 10 Aug 2025 11:04:11 -0400 Subject: [PATCH 04/28] Add keybind hints --- assets/keymaps/default-linux.json | 6 ++ assets/keymaps/default-macos.json | 7 ++ crates/git_ui/src/stash_picker.rs | 143 +++++++++++++++++++++--------- crates/project/src/git_store.rs | 1 + crates/zed_actions/src/lib.rs | 2 +- 5 files changed, 114 insertions(+), 45 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 955e68f5a9..591f135245 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1053,6 +1053,12 @@ "ctrl-backspace": "tab_switcher::CloseSelectedItem" } }, + { + "context": "StashList || (StashList > Picker > Editor)", + "bindings": { + "ctrl-shift-backspace": "stash_picker::DropStashItem" + } + }, { "context": "Terminal", "bindings": { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 8b18299a91..0b82742570 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1123,6 +1123,13 @@ "ctrl-backspace": "tab_switcher::CloseSelectedItem" } }, + { + "context": "StashList || (StashList > Picker > Editor)", + "use_key_equivalents": true, + "bindings": { + "ctrl-shift-backspace": "stash_picker::DropStashItem" + } + }, { "context": "Terminal", "use_key_equivalents": true, diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index 02c57fc619..e3f7fab7c9 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -2,25 +2,35 @@ use fuzzy::StringMatchCandidate; use git::stash::StashEntry; use gpui::{ - App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render, SharedString, Styled, - Subscription, Task, Window, rems, + Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, + InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render, + SharedString, Styled, Subscription, Task, Window, actions, rems, }; use picker::{Picker, PickerDelegate, PickerEditorPosition}; use project::git_store::Repository; use std::sync::Arc; -use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*}; +use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*}; use util::ResultExt; use workspace::notifications::DetachAndPromptErr; use workspace::{ModalView, Workspace}; +use crate::stash_picker; + +actions!( + stash_picker, + [ + /// Drop the selected stash entry. + DropStashItem, + ] +); + pub fn register(workspace: &mut Workspace) { workspace.register_action(open); } pub fn open( workspace: &mut Workspace, - _: &zed_actions::git::Stash, + _: &zed_actions::git::StashEntries, window: &mut Window, cx: &mut Context, ) { @@ -52,6 +62,7 @@ enum StashListStyle { pub struct StashList { width: Rems, pub picker: Entity>, + picker_focus_handle: FocusHandle, _subscription: Subscription, } @@ -83,8 +94,12 @@ impl StashList { }) .detach_and_log_err(cx); - let delegate = StashListDelegate::new(repository.clone(), style); + let delegate = StashListDelegate::new(repository.clone(), style, 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, _| { + picker.delegate.focus_handle = picker_focus_handle.clone(); + }); let _subscription = cx.subscribe(&picker, |_, _, _, cx| { cx.emit(DismissEvent); @@ -92,11 +107,26 @@ impl StashList { Self { picker, + picker_focus_handle, width, _subscription, } } + fn handle_drop_stash( + &mut self, + _: &DropStashItem, + window: &mut Window, + cx: &mut Context, + ) { + self.picker.update(cx, |picker, cx| { + picker + .delegate + .drop_stash_at(picker.delegate.selected_index(), window, cx); + }); + cx.notify(); + } + fn handle_modifiers_changed( &mut self, ev: &ModifiersChangedEvent, @@ -110,27 +140,20 @@ impl StashList { impl ModalView for StashList {} impl EventEmitter for StashList {} - impl Focusable for StashList { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.picker.focus_handle(cx) + fn focus_handle(&self, _: &App) -> FocusHandle { + self.picker_focus_handle.clone() } } impl Render for StashList { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() - .key_context("GitStashSelector") + .key_context("StashList") .w(self.width) .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) + .on_action(cx.listener(Self::handle_drop_stash)) .child(self.picker.clone()) - .on_mouse_down_out({ - cx.listener(move |this, _, window, cx| { - this.picker.update(cx, |this, cx| { - this.cancel(&Default::default(), window, cx); - }) - }) - }) } } @@ -148,10 +171,16 @@ pub struct StashListDelegate { selected_index: usize, last_query: String, modifiers: Modifiers, + focus_handle: FocusHandle, } impl StashListDelegate { - fn new(repo: Option>, style: StashListStyle) -> Self { + fn new( + repo: Option>, + style: StashListStyle, + _window: &mut Window, + cx: &mut Context, + ) -> Self { Self { matches: vec![], repo, @@ -160,10 +189,15 @@ impl StashListDelegate { selected_index: 0, last_query: Default::default(), modifiers: Default::default(), + focus_handle: cx.focus_handle(), } } - fn drop_stash(&self, stash_index: usize, window: &mut Window, cx: &mut Context>) { + fn drop_stash_at(&self, ix: usize, window: &mut Window, cx: &mut Context>) { + let Some(entry_match) = self.matches.get(ix) else { + return; + }; + let stash_index = entry_match.entry.index; let Some(repo) = self.repo.clone() else { return; }; @@ -176,7 +210,6 @@ impl StashListDelegate { .detach_and_prompt_err("Failed to apply stash", window, cx, |e, _, _| { Some(e.to_string()) }); - cx.emit(DismissEvent); } fn pop_stash(&self, stash_index: usize, window: &mut Window, cx: &mut Context>) { @@ -286,20 +319,12 @@ impl PickerDelegate for StashListDelegate { }) } - fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { + fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { let Some(entry_match) = self.matches.get(self.selected_index()) else { return; }; - let stash_index = entry_match.entry.index; - - if secondary { - // Secondary action: remove stash (remove from stash list) - self.drop_stash(stash_index, window, cx); - } else { - // Primary action: apply stash (keep in stash list) - self.pop_stash(stash_index, window, cx); - } + self.pop_stash(stash_index, window, cx); } fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { @@ -332,20 +357,14 @@ impl PickerDelegate for StashListDelegate { .spacing(ListItemSpacing::Sparse) .toggle_state(selected) .child( - v_flex() - .w_full() - .overflow_hidden() - .child( - h_flex() - .gap_6() - .justify_between() - .overflow_x_hidden() - .child(stash_name) - .child(stash_index_label.into_element()), - ) - .when(self.style == StashListStyle::Modal, |el| { - el.child(div().max_w_96()) - }), + v_flex().w_full().overflow_hidden().child( + h_flex() + .gap_6() + .justify_between() + .overflow_x_hidden() + .child(stash_name) + .child(stash_index_label.into_element()), + ), ), ) } @@ -353,4 +372,40 @@ impl PickerDelegate for StashListDelegate { fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option { Some("No stashes found".into()) } + + fn render_footer( + &self, + window: &mut Window, + cx: &mut Context>, + ) -> Option { + let focus_handle = self.focus_handle.clone(); + + Some( + h_flex() + .w_full() + .p_1p5() + .justify_between() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .child( + h_flex().gap_0p5().child( + Button::new("drop-stash-item", "Drop") + .key_binding( + KeyBinding::for_action_in( + &stash_picker::DropStashItem, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(|_, window, cx| { + window + .dispatch_action(stash_picker::DropStashItem.boxed_clone(), cx) + }), + ), + ) + .into_any(), + ) + } } diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index cd0f2b301e..2ad36af7ad 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -3817,6 +3817,7 @@ impl Repository { }) })? .await??; + this.update(cx, |_, cx| cx.notify())?; Ok(()) }) } diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index a4dbc6ec75..e0bca2a406 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -177,7 +177,7 @@ pub mod git { #[action(deprecated_aliases = ["branches::OpenRecent"])] Branch, /// Opens the git stash selector. - Stash + StashEntries ] ); } From a13e87b82fe439b1e91c3c5eaef483754675219d Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Sun, 10 Aug 2025 13:13:55 -0400 Subject: [PATCH 05/28] Change placeholder text and remove cx notify call --- crates/git_ui/src/stash_picker.rs | 2 +- crates/project/src/git_store.rs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index e3f7fab7c9..8170a440a2 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -233,7 +233,7 @@ impl PickerDelegate for StashListDelegate { type ListItem = ListItem; fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Select stash&".into() + "Select a stash…".into() } fn editor_position(&self) -> PickerEditorPosition { diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 2ad36af7ad..7da796f584 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -3817,7 +3817,6 @@ impl Repository { }) })? .await??; - this.update(cx, |_, cx| cx.notify())?; Ok(()) }) } @@ -4641,7 +4640,6 @@ impl Repository { updates_tx: Option>, cx: &mut Context, ) { - println!("paths changed with updates_tx: {:?}", updates_tx); self.paths_needing_status_update.extend(paths); let this = cx.weak_entity(); From a185e0dfaa1402ea79d32144088699a739e7963c Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Sun, 10 Aug 2025 15:34:45 -0400 Subject: [PATCH 06/28] Truncate commit message --- crates/git_ui/src/stash_picker.rs | 43 ++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index 8170a440a2..c08ac2a710 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -4,7 +4,7 @@ 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, Window, actions, px, rems, }; use picker::{Picker, PickerDelegate, PickerEditorPosition}; use project::git_store::Repository; @@ -94,7 +94,7 @@ impl StashList { }) .detach_and_log_err(cx); - let delegate = StashListDelegate::new(repository.clone(), style, window, cx); + let delegate = StashListDelegate::new(repository.clone(), style, width, 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, _| { @@ -171,6 +171,7 @@ pub struct StashListDelegate { selected_index: usize, last_query: String, modifiers: Modifiers, + max_width: Rems, focus_handle: FocusHandle, } @@ -178,6 +179,7 @@ impl StashListDelegate { fn new( repo: Option>, style: StashListStyle, + max_width: Rems, _window: &mut Window, cx: &mut Context, ) -> Self { @@ -189,6 +191,7 @@ impl StashListDelegate { selected_index: 0, last_query: Default::default(), modifiers: Default::default(), + max_width, focus_handle: cx.focus_handle(), } } @@ -335,17 +338,37 @@ impl PickerDelegate for StashListDelegate { &self, ix: usize, selected: bool, - _: &mut Window, - _: &mut Context>, + window: &mut Window, + cx: &mut Context>, ) -> Option { let entry_match = &self.matches[ix]; - let stash_name = HighlightedLabel::new( - entry_match.entry.message.clone(), - entry_match.positions.clone(), - ) - .truncate() - .into_any_element(); + let mut stash_message = entry_match.entry.message.clone(); + let mut positions = entry_match.positions.clone(); + + if stash_message.is_ascii() { + let max_width = self.max_width.to_pixels(window.rem_size()); + let normal_em = { + let style = window.text_style(); + let font_id = window.text_system().resolve_font(&style.font()); + let font_size = ui::TextSize::Default.rems(cx).to_pixels(window.rem_size()); + let normal = cx + .text_system() + .em_width(font_id, font_size) + .unwrap_or(px(16.)); + normal + }; + + let max_chars = ((max_width * 0.8) / normal_em) as usize; + + if stash_message.len() > max_chars && max_chars > 3 { + stash_message.truncate(max_chars - 3); + stash_message.push_str("…"); + positions.retain(|&pos| pos < max_chars - 3); + } + } + + let stash_name = HighlightedLabel::new(stash_message, positions).into_any_element(); let stash_index_label = Label::new(format!("stash@{{{}}}", entry_match.entry.index)) .size(LabelSize::Small) From b55c2d68efe265221c47ea7bf02c49f5ddb9d65e Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Sun, 10 Aug 2025 15:42:25 -0400 Subject: [PATCH 07/28] Fix clippy --- crates/project/src/git_store.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 7da796f584..d7aa77ee0d 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -1781,7 +1781,7 @@ impl GitStore { ) -> Result { let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let stash_index = envelope.payload.stash_index.and_then(|i| Some(i as usize)); + let stash_index = envelope.payload.stash_index.map(|i| i as usize); repository_handle .update(&mut cx, |repository_handle, cx| { @@ -1799,7 +1799,7 @@ impl GitStore { ) -> Result { let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let stash_index = envelope.payload.stash_index.and_then(|i| Some(i as usize)); + let stash_index = envelope.payload.stash_index.map(|i| i as usize); repository_handle .update(&mut cx, |repository_handle, cx| { @@ -2797,7 +2797,7 @@ impl RepositorySnapshot { .stash_entries .entries .iter() - .map(|entry| stash_to_proto(entry)) + .map(stash_to_proto) .collect(), } } @@ -2866,7 +2866,7 @@ impl RepositorySnapshot { .stash_entries .entries .iter() - .map(|entry| stash_to_proto(entry)) + .map(stash_to_proto) .collect(), } } @@ -3773,7 +3773,7 @@ impl Repository { .request(proto::StashPop { project_id: project_id.0, repository_id: id.to_proto(), - stash_index: index.and_then(|i| Some(i as u64)), + stash_index: index.map(|i| i as u64), }) .await .context("sending stash pop request")?; @@ -3807,7 +3807,7 @@ impl Repository { .request(proto::StashDrop { project_id: project_id.0, repository_id: id.to_proto(), - stash_index: index.and_then(|i| Some(i as u64)), + stash_index: index.map(|i| i as u64), }) .await .context("sending stash pop request")?; From 3bed7a4f0d86ce7102fd1f23732c07813b9d8a5a Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Sun, 10 Aug 2025 15:59:21 -0400 Subject: [PATCH 08/28] Add keymap hints for pop stash --- crates/git_ui/src/stash_picker.rs | 49 +++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index c08ac2a710..68fb952c9e 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -411,22 +411,41 @@ impl PickerDelegate for StashListDelegate { .border_t_1() .border_color(cx.theme().colors().border_variant) .child( - h_flex().gap_0p5().child( - Button::new("drop-stash-item", "Drop") - .key_binding( - KeyBinding::for_action_in( - &stash_picker::DropStashItem, - &focus_handle, - window, - cx, + h_flex() + .gap_0p5() + .child( + Button::new("drop-stash", "Drop") + .key_binding( + KeyBinding::for_action_in( + &stash_picker::DropStashItem, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(12.))), ) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(|_, window, cx| { - window - .dispatch_action(stash_picker::DropStashItem.boxed_clone(), cx) - }), - ), + .on_click(|_, window, cx| { + window.dispatch_action( + stash_picker::DropStashItem.boxed_clone(), + cx, + ) + }), + ) + .child( + Button::new("pop-stash", "Pop") + .key_binding( + KeyBinding::for_action_in( + &menu::Confirm, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(|_, window, cx| { + window.dispatch_action(menu::Confirm.boxed_clone(), cx) + }), + ), ) .into_any(), ) From 46ed4fad44e1d5737342c70599b7a59b0f6e8b14 Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Sun, 10 Aug 2025 23:24:40 -0400 Subject: [PATCH 09/28] Fix tests --- crates/fs/src/fake_git_repo.rs | 2 +- crates/zed/src/zed.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index d4b371e770..69686f8d48 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -321,7 +321,7 @@ impl GitRepository for FakeGitRepository { } fn stash_entries(&self) -> BoxFuture<'_, Result> { - unimplemented!() + async { Ok(git::stash::GitStash::default()) }.boxed() } fn branches(&self) -> BoxFuture<'_, Result>> { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 3b5f99f9bd..d28cba7133 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4481,6 +4481,7 @@ mod tests { "search", "settings_profile_selector", "snippets", + "stash_picker", "supermaven", "svg", "tab_switcher", From e570a99cfc47b0f6c9daab36adc4e32819261ba6 Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Wed, 13 Aug 2025 14:31:24 -0400 Subject: [PATCH 10/28] Notify on stash drop --- crates/git_ui/src/stash_picker.rs | 36 +++++++++++--- crates/project/src/git_store.rs | 80 +++++++++++++++++++++---------- 2 files changed, 85 insertions(+), 31 deletions(-) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index 68fb952c9e..1f64976b17 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -7,7 +7,7 @@ use gpui::{ SharedString, Styled, Subscription, Task, Window, actions, px, rems, }; use picker::{Picker, PickerDelegate, PickerEditorPosition}; -use project::git_store::Repository; +use project::git_store::{Repository, RepositoryEvent}; use std::sync::Arc; use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*}; use util::ResultExt; @@ -63,7 +63,7 @@ pub struct StashList { width: Rems, pub picker: Entity>, picker_focus_handle: FocusHandle, - _subscription: Subscription, + _subscriptions: Vec, } impl StashList { @@ -74,10 +74,34 @@ impl StashList { window: &mut Window, cx: &mut Context, ) -> Self { + let mut _subscriptions = Vec::new(); let stash_request = repository .clone() .map(|repository| repository.read_with(cx, |repo, _| repo.stash_entries.clone())); + if let Some(repo) = repository.clone() { + _subscriptions.push( + cx.subscribe_in(&repo, window, |this, _, event, window, cx| { + if matches!(event, RepositoryEvent::Updated { .. }) { + let stash_entries = this.picker.read_with(cx, |picker, cx| { + picker.delegate.repo.clone().map(|repo| { + repo.read(cx) + .snapshot() + .stash_entries + .entries + .to_vec() + .clone() + }) + }); + this.picker.update(cx, |this, cx| { + this.delegate.all_stash_entries = stash_entries; + this.refresh(window, cx); + }); + } + }), + ) + } + cx.spawn_in(window, async move |this, cx| { let stash_entries = stash_request .map(|git_stash| git_stash.entries.to_vec()) @@ -101,15 +125,15 @@ impl StashList { picker.delegate.focus_handle = picker_focus_handle.clone(); }); - let _subscription = cx.subscribe(&picker, |_, _, _, cx| { + _subscriptions.push(cx.subscribe(&picker, |_, _, _, cx| { cx.emit(DismissEvent); - }); + })); Self { picker, picker_focus_handle, width, - _subscription, + _subscriptions, } } @@ -207,7 +231,7 @@ impl StashListDelegate { cx.spawn(async move |_, cx| { repo.update(cx, |repo, cx| repo.stash_drop(Some(stash_index), cx))? - .await?; + .await??; Ok(()) }) .detach_and_prompt_err("Failed to apply stash", window, cx, |e, _, _| { diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index d7aa77ee0d..ddfb46bcc3 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -1805,7 +1805,7 @@ impl GitStore { .update(&mut cx, |repository_handle, cx| { repository_handle.stash_drop(stash_index, cx) })? - .await?; + .await??; Ok(proto::Ack {}) } @@ -3791,33 +3791,58 @@ impl Repository { &mut self, index: Option, cx: &mut Context, - ) -> Task> { + ) -> oneshot::Receiver> { let id = self.id; - cx.spawn(async move |this, cx| { - this.update(cx, |this, _| { - this.send_job(None, move |git_repo, _cx| async move { - match git_repo { - RepositoryState::Local { - backend, - environment, - .. - } => backend.stash_drop(index, environment).await, - RepositoryState::Remote { project_id, client } => { - client - .request(proto::StashDrop { - project_id: project_id.0, - repository_id: id.to_proto(), - stash_index: index.map(|i| i as u64), - }) - .await - .context("sending stash pop request")?; - Ok(()) + let updates_tx = self + .git_store() + .and_then(|git_store| match &git_store.read(cx).state { + GitStoreState::Local { downstream, .. } => downstream + .as_ref() + .map(|downstream| downstream.updates_tx.clone()), + _ => None, + }); + let this = cx.weak_entity(); + self.send_job(None, move |git_repo, mut cx| async move { + match git_repo { + RepositoryState::Local { + backend, + environment, + .. + } => { + let result = backend.stash_drop(index, environment).await; + if result.is_ok() { + if let Ok(stash_entries) = backend.stash_entries().await { + let snapshot = this.update(&mut cx, |this, cx| { + this.snapshot.stash_entries = stash_entries; + let snapshot = this.snapshot.clone(); + cx.emit(RepositoryEvent::Updated { + full_scan: false, + new_instance: false, + }); + snapshot + })?; + if let Some(updates_tx) = updates_tx { + updates_tx + .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot)) + .ok(); + } } } - }) - })? - .await??; - Ok(()) + + result + } + RepositoryState::Remote { project_id, client } => { + client + .request(proto::StashDrop { + project_id: project_id.0, + repository_id: id.to_proto(), + stash_index: index.map(|i| i as u64), + }) + .await + .context("sending stash pop request")?; + Ok(()) + } + } }) } @@ -4694,12 +4719,17 @@ impl Repository { .await; this.update(&mut cx, |this, cx| { + let needs_update = !changed_path_statuses.is_empty() + || this.snapshot.stash_entries != stash_entries; this.snapshot.stash_entries = stash_entries; if !changed_path_statuses.is_empty() { this.snapshot .statuses_by_path .edit(changed_path_statuses, &()); this.snapshot.scan_id += 1; + } + + if needs_update { if let Some(updates_tx) = updates_tx { updates_tx .unbounded_send(DownstreamUpdate::UpdateRepository( From d03557748b37906c1948674a666993e19d6bf62e Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Wed, 13 Aug 2025 15:06:48 -0400 Subject: [PATCH 11/28] Fix error message --- crates/git_ui/src/stash_picker.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index 1f64976b17..1462081aa1 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -234,7 +234,7 @@ impl StashListDelegate { .await??; Ok(()) }) - .detach_and_prompt_err("Failed to apply stash", window, cx, |e, _, _| { + .detach_and_prompt_err("Failed to drop stash", window, cx, |e, _, _| { Some(e.to_string()) }); } From 4f11b9ef5643d594bbd79dc1b1633c4b0003482a Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Wed, 13 Aug 2025 19:38:19 -0400 Subject: [PATCH 12/28] Improve stash picker ui --- crates/git/src/repository.rs | 2 +- crates/git/src/stash.rs | 35 ++++++++++++++++++++++-- crates/git_ui/src/stash_picker.rs | 45 ++++++++++++++++++++++++++----- crates/project/src/git_store.rs | 6 ++++- crates/proto/proto/git.proto | 4 ++- 5 files changed, 80 insertions(+), 12 deletions(-) diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 98392fb2e6..7b71b5d6b1 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -994,7 +994,7 @@ impl GitRepository for RealGitRepository { .spawn(async move { let output = new_std_command(&git_binary_path) .current_dir(working_directory?) - .args(&["stash", "list", "--pretty=%gd:%H:%s"]) + .args(&["stash", "list", "--pretty=format:%gd%x00%H%x00%ct%x00%s"]) .output()?; if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); diff --git a/crates/git/src/stash.rs b/crates/git/src/stash.rs index c97c13ad9f..0f7edd116a 100644 --- a/crates/git/src/stash.rs +++ b/crates/git/src/stash.rs @@ -7,6 +7,8 @@ pub struct StashEntry { pub index: usize, pub oid: Oid, pub message: String, + pub branch: Option, + pub timestamp: i64, } #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] @@ -28,7 +30,7 @@ impl FromStr for GitStash { let entries = s .split('\n') .filter_map(|entry| { - let mut parts = entry.splitn(3, ':'); + let mut parts = entry.splitn(4, '\0'); let raw_idx = parts.next().and_then(|i| { let trimmed = i.trim(); if trimmed.starts_with("stash@{") && trimmed.ends_with('}') { @@ -40,15 +42,21 @@ impl FromStr for GitStash { } }); let raw_oid = parts.next(); + let raw_date = parts.next().and_then(|d| d.parse().ok()); let message = parts.next(); - if let (Some(raw_idx), Some(raw_oid), Some(message)) = (raw_idx, raw_oid, message) { + if let (Some(raw_idx), Some(raw_oid), Some(raw_date), Some(message)) = + (raw_idx, raw_oid, raw_date, message) + { + let (branch, message) = parse_stash_entry(message); let index = raw_idx.parse::().ok()?; let oid = Oid::from_str(raw_oid).ok()?; let entry = StashEntry { index, oid, message: message.to_string(), + branch: branch.map(Into::into), + timestamp: raw_date, }; return Some(entry); } @@ -60,3 +68,26 @@ impl FromStr for GitStash { }) } } + +fn parse_stash_entry(input: &str) -> (Option<&str>, &str) { + // Try to match "WIP on : " pattern + if let Some(stripped) = input.strip_prefix("WIP on ") { + if let Some(colon_pos) = stripped.find(": ") { + let branch = &stripped[..colon_pos]; + let message = &stripped[colon_pos + 2..]; + return (Some(branch), message); + } + } + + // Try to match "On : " pattern + if let Some(stripped) = input.strip_prefix("On ") { + if let Some(colon_pos) = stripped.find(": ") { + let branch = &stripped[..colon_pos]; + let message = &stripped[colon_pos + 2..]; + return (Some(branch), message); + } + } + + // Edge case: format doesn't match, return None for branch and full string as message + (None, input) +} diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index 1462081aa1..62873f8719 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -9,7 +9,9 @@ use gpui::{ use picker::{Picker, PickerDelegate, PickerEditorPosition}; use project::git_store::{Repository, RepositoryEvent}; use std::sync::Arc; -use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*}; +use time::OffsetDateTime; +use time_format::format_local_timestamp; +use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*}; use util::ResultExt; use workspace::notifications::DetachAndPromptErr; use workspace::{ModalView, Workspace}; @@ -220,6 +222,10 @@ impl StashListDelegate { } } + fn format_message(ix: usize, message: &String) -> String { + format!("#{}: {}", ix, message) + } + fn drop_stash_at(&self, ix: usize, window: &mut Window, cx: &mut Context>) { let Some(entry_match) = self.matches.get(ix) else { return; @@ -310,7 +316,12 @@ impl PickerDelegate for StashListDelegate { let candidates = all_stash_entries .iter() .enumerate() - .map(|(ix, entry)| StringMatchCandidate::new(ix, &entry.message)) + .map(|(ix, entry)| { + StringMatchCandidate::new( + ix, + &Self::format_message(entry.index, &entry.message), + ) + }) .collect::>(); fuzzy::match_strings( &candidates, @@ -367,7 +378,8 @@ impl PickerDelegate for StashListDelegate { ) -> Option { let entry_match = &self.matches[ix]; - let mut stash_message = entry_match.entry.message.clone(); + let mut stash_message = + Self::format_message(entry_match.entry.index, &entry_match.entry.message); let mut positions = entry_match.positions.clone(); if stash_message.is_ascii() { @@ -394,9 +406,27 @@ impl PickerDelegate for StashListDelegate { let stash_name = HighlightedLabel::new(stash_message, positions).into_any_element(); - let stash_index_label = Label::new(format!("stash@{{{}}}", entry_match.entry.index)) - .size(LabelSize::Small) - .color(Color::Muted); + let stash_index_label = Label::new( + entry_match + .entry + .branch + .clone() + .unwrap_or_default() + .to_string(), + ) + .size(LabelSize::Small) + .color(Color::Muted); + + let absolute_timestamp = format_local_timestamp( + OffsetDateTime::from_unix_timestamp(entry_match.entry.timestamp) + .unwrap_or(OffsetDateTime::now_utc()), + OffsetDateTime::now_utc(), + time_format::TimestampFormat::MediumAbsolute, + ); + let tooltip_text = format!( + "stash@{{{}}} created {}", + entry_match.entry.index, absolute_timestamp + ); Some( ListItem::new(SharedString::from(format!("stash-{ix}"))) @@ -412,7 +442,8 @@ impl PickerDelegate for StashListDelegate { .child(stash_name) .child(stash_index_label.into_element()), ), - ), + ) + .tooltip(Tooltip::text(tooltip_text)), ) } diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index ddfb46bcc3..ba24ff2c8c 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -2928,7 +2928,9 @@ pub fn stash_to_proto(entry: &StashEntry) -> proto::StashEntry { proto::StashEntry { oid: entry.oid.as_bytes().to_vec(), message: entry.message.clone(), - index: entry.index as i64, + branch: entry.branch.clone(), + index: entry.index as u64, + timestamp: entry.timestamp, } } @@ -2937,6 +2939,8 @@ pub fn proto_to_stash(entry: &proto::StashEntry) -> Result { oid: Oid::from_bytes(&entry.oid)?, message: entry.message.clone(), index: entry.index as usize, + branch: entry.branch.clone(), + timestamp: entry.timestamp, }) } diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto index 4ea846db84..ecc4f51212 100644 --- a/crates/proto/proto/git.proto +++ b/crates/proto/proto/git.proto @@ -287,7 +287,9 @@ message StatusEntry { message StashEntry { bytes oid = 1; string message = 2; - int64 index = 3; + optional string branch = 3; + uint64 index = 4; + int64 timestamp = 5; } message Stage { From ddf95cc2aeba9c6e213c97b1b1122cba4bd15bba Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Thu, 14 Aug 2025 00:57:06 -0400 Subject: [PATCH 13/28] Improve stash parser --- crates/git/src/stash.rs | Bin 3062 -> 7110 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/crates/git/src/stash.rs b/crates/git/src/stash.rs index 0f7edd116a5d1ef1ebd630810ae14483115bddaa..4000f01092c175ab288d2cf809b798ef278e8739 100644 GIT binary patch literal 7110 zcmXRdPE|-QN-RmWvhvSNvE~Bt6Z0xF^2@EPs-5%mN>VFIbQFS8i%WA#bQB;mwGd^+ zB`H=`)x{-6R#t9B`MJR*MLG(_m3hflR*prkyMnJmz-gx z;9pRZnV)9^k+ws~mt^Lq7MCRE7Fa1{nwjWu)#CJkOKMsoI5-IU#67bF>=`6~q~?_r zWu_KeDL57-+eCw9!CsBE16!GyTacrGMO#{)LSjKdPNjxgZfS`^acWMQjzWG(Mrx52 zT(u_D7Ep+R6zD;%Qm}=ngz>Gxp;rs?AlNo&)Y8pT!$YQ-f*nhLsh(BxthoSKtn2Q>-gsLV8lV!e{0 z%v=pkz0BhH)ZBuSN)2RJgA^5|mXsFdDfnk=fV5j#r9c8(LsJvtZjhQ*Fe$$R|Rr>v4X7vvLdioKxTmgKqDtJFEu`|G*?Ff#L`sA%u^`V z12Ky=H1$&RN^?^|xdp@7nQ3741ic5cCpkZ_Br~rx72zR}D3Yf@+}y;HNV z_+n6ohzB_W>^$@!11a~<)&K=TrKWRKy>>RKg82!X;%L0iF4MELEX6KQ~pOEHNiD1?CNfq|y?F#GK-Mh#bUSItn@Y=?Wzo zsR|H2SP?8>;dW-GDJVkZkrSMnnt~!Mh;XLEocwevtMbI6JVgy9x5Uhx)D(r1d`Nsi z+yRahm>Z!10<|kEKQm85Nk>5mIYep|Qgez^6|g%AH431402D)z5?4_J?j=w#f-t6| zK#2s^Js6rW+=rq9#kGhm2F;2{85v|hGytt&HtJ>OmE>z^BGOr{CfIOrIis(y4=U`7 zQxy^wiZk=lb5a!`!3RpypezQ8NJt{HQcwa*IaK@A#u(V-XQtT27}&rIXAlQk#M>!x zrR70l1U-jfDT_d%11@M`B@(DiC`c?SDYgQY1~#C=%TB=-l8cKAaxzQuG)#09)ME_P zHT9D7b8=FXOF%^m*i)HlAbU%S^>R}4G&B_yZ52$Af`?G_DY;e@q$Zc7rYM*w+ z=oY6IB!Wtn6o{!h3hDVJ;2Z|F3>4ZR40bHUMz}aE%AnZ^t6HA_ionUU_CoMm#9nq*jz@)YO3L07yp1Nb;cYQvjO-F7p-Ct83MjbU?P06obsv zw6}&N8IT+DGgB07LG`kg6}*Cg*ltLc?f#xFSZp0ti=P0M5tL-FuU|h@Gq0eu1T(XO z8+pk26x3+QO$DVXunuS~3k@?+i3(MqS6l+h(ea>UtpO`Xlr$A;Y7`VcMuiXjTr zYt@mfQi9pmGY?WPp@adrX0%dBg=J}Vu&hIMwO?(mx)MD3;mW9>m{PD+fI1GYT(7vK zD6=5GpeQvhvjWK{2nC6GDe)y4sd*YT#Wf1WQ1!*7X(06=k0GV-{OtJrqIgj41IpsC z8U?8WNv$X;N=z<6j#jWQ6pB*QQj1dal2ersISm|hAZH`o0?S&Ecq1xvVGGN=(%d9a zgBDt}z%A0mng*fCULi3rMFHjkNa9w=%!8!L%=|o1eOpkJTAZ3!!lkdT55fu_iFqkG zsgT5(mYsKy9>B0=029g7b4zq1jv$ zkjbr06FnP2#W6CH2DFz0uGMs)EptdCLth^n0QkLFkXTZZT9gNl zWKiQawL}5zV{pPONKFBiXpqbZ7J(;KXs{_E=PYFX$@w|?dGQ7L#UKN~S_@KB^wKi( zQZ$sT6i~E*3O7(~0*L_yTd+4W3kp(GqV+&kGQ^x%)MhkD12j}nbinOZ&{i1|-*lm4XW| z<>=(Jbd8eK;u6hRuH5_-1rW0sTET0~fiBBz51T`w)4GoAwSXF3HtE8z{npa+wSfHV)qhNs4MJ7tWi4m#+ zCPqY?pa5y{J5-z2Dkz}1!W7o{0mTT!8Cd<038{XSAcF@*MJPi8c=Um*GKcEC{F3-Y zP;WIUwFu-3P|uHKLkuu&hvr{GF$OJ1P~uEqUjbBHLMkh8!T|*Vs04#nEQopqNBa?0 zOXVhJ=2o}{qC!z>dTCB#5vMi1b4nm$#U&|LR@KENMOIdBMfthG zB}F<4#g%!@a4 z9k^(IW{Q=9e`X3oBsaCVI59odN+GzUC^Ih|A(2#+n3tSkrQlysl9``p1Ch2v$d_d1 zrWThZ<`!5fWSW`iaMj}UfJ;C}7c+mZy+dP>@rpp_W@(qEMWglcuAPUy_kpWCd5P3AF_jVju;2P^%Pd zAu3^fYjEh*f;>B8_4z1-0UmB25KdI|WE8u?bGiNwb5R1ag$VzCwCti9#_bdKGdqi%S%Abqk78 zOG+wjRnt?fR6VRzi^1lC6z8OtC?Gt55C_TW6&K`WmT0KQ<6g;VUW?FP%KC+Dk)a51zS@IF*8rYL`OkA#z0*Y5=tQDFbvXGlvp00 znNp!(s{q!lmzP>mqM@mmn3ob?l98IHQIm}kthL% zEDvJq6_*rc7Q`16rKV+8AX$iC6TfN5H!<(dhG5jqMm%@{f%g44>%23)Y)LBrJ^w|+=$`e&wCS;5O5s1G4=f=4gdjS9Ay z6%5F6=osu!NcDg&1>%E36eIXR5+Do>1}j)V>y_k#3dzj8bPY}1MnXc^N&!OafeJCt zypnt?tIWKTd_4M)YA#SnfTI|z(`vB?Yf)-RX;B^|)q#TuOR~nyau~U@7B!peCFkel zq$ZbGS=nGzb#}-lpMSOnsHQ+G2tW=2*OO3&UUE);UMh0%LG1%qWNCSz0vRnaX66-? zmLOG28qlgAR8QIIK&vZAir3dy2q~&mD9KmIO)N>yP*4i@3{c3=Q?P-=g`Jgx4Kz;e zloSdQOG;9U^1wk1&B5Rt3obhgQd2ZR1w6=ZXh{ewO`&EgAs5of`jhi>^7G;g@{2(R zzzow%%gjsBP_j}$(FRInpxgv;o`NmdGnoYisVUKVpn!*%6N?((APrDopy+_xtDvo5 zq^F0b3z|A0Wfa6dO++bxumO@nY9TEHjPUfQa#;H3p$5qC4M*3M^i+l9#Nt#dPzx&f70}{CAu%sSA+0ng2h^(0%u82DEQXav;DoCIQm>-` K&Lf&!wOjxyXiMh+ From b004c052c674a087e6ae26128b8e3ac1e14b7d1f Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Thu, 14 Aug 2025 16:09:30 -0400 Subject: [PATCH 14/28] Improve timestamp display format --- crates/git_ui/src/stash_picker.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index 62873f8719..fee4ebed5c 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -1,5 +1,6 @@ use fuzzy::StringMatchCandidate; +use chrono; use git::stash::StashEntry; use gpui::{ Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, @@ -9,8 +10,8 @@ use gpui::{ use picker::{Picker, PickerDelegate, PickerEditorPosition}; use project::git_store::{Repository, RepositoryEvent}; use std::sync::Arc; -use time::OffsetDateTime; -use time_format::format_local_timestamp; +use time::{OffsetDateTime, UtcOffset}; +use time_format; use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*}; use util::ResultExt; use workspace::notifications::DetachAndPromptErr; @@ -417,11 +418,17 @@ impl PickerDelegate for StashListDelegate { .size(LabelSize::Small) .color(Color::Muted); - let absolute_timestamp = format_local_timestamp( - OffsetDateTime::from_unix_timestamp(entry_match.entry.timestamp) - .unwrap_or(OffsetDateTime::now_utc()), + let timestamp = OffsetDateTime::from_unix_timestamp(entry_match.entry.timestamp) + .unwrap_or(OffsetDateTime::now_utc()); + let timezone = + UtcOffset::from_whole_seconds(chrono::Local::now().offset().local_minus_utc()) + .unwrap_or(UtcOffset::UTC); + + let absolute_timestamp = time_format::format_localized_timestamp( + timestamp, OffsetDateTime::now_utc(), - time_format::TimestampFormat::MediumAbsolute, + timezone, + time_format::TimestampFormat::EnhancedAbsolute, ); let tooltip_text = format!( "stash@{{{}}} created {}", From 5662056ea331fe33b138a258378fcb273b24665c Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Thu, 14 Aug 2025 16:57:42 -0400 Subject: [PATCH 15/28] Move timestamp format outside or render --- crates/git_ui/src/stash_picker.rs | 57 ++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index fee4ebed5c..aefcc969b5 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -188,6 +188,7 @@ impl Render for StashList { struct StashEntryMatch { entry: StashEntry, positions: Vec, + formatted_timestamp: String, } pub struct StashListDelegate { @@ -200,6 +201,7 @@ pub struct StashListDelegate { modifiers: Modifiers, max_width: Rems, focus_handle: FocusHandle, + timezone: UtcOffset, } impl StashListDelegate { @@ -210,6 +212,10 @@ impl StashListDelegate { _window: &mut Window, cx: &mut Context, ) -> Self { + let timezone = + UtcOffset::from_whole_seconds(chrono::Local::now().offset().local_minus_utc()) + .unwrap_or(UtcOffset::UTC); + Self { matches: vec![], repo, @@ -220,6 +226,7 @@ impl StashListDelegate { modifiers: Default::default(), max_width, focus_handle: cx.focus_handle(), + timezone, } } @@ -227,6 +234,17 @@ impl StashListDelegate { format!("#{}: {}", ix, message) } + fn format_timestamp(timestamp: i64, timezone: UtcOffset) -> String { + let timestamp = + OffsetDateTime::from_unix_timestamp(timestamp).unwrap_or(OffsetDateTime::now_utc()); + time_format::format_localized_timestamp( + timestamp, + OffsetDateTime::now_utc(), + timezone, + time_format::TimestampFormat::EnhancedAbsolute, + ) + } + fn drop_stash_at(&self, ix: usize, window: &mut Window, cx: &mut Context>) { let Some(entry_match) = self.matches.get(ix) else { return; @@ -304,13 +322,20 @@ impl PickerDelegate for StashListDelegate { return Task::ready(()); }; + let timezone = self.timezone; + cx.spawn_in(window, async move |picker, cx| { let matches: Vec = if query.is_empty() { all_stash_entries .into_iter() - .map(|entry| StashEntryMatch { - entry, - positions: Vec::new(), + .map(|entry| { + let formatted_timestamp = Self::format_timestamp(entry.timestamp, timezone); + + StashEntryMatch { + entry, + positions: Vec::new(), + formatted_timestamp, + } }) .collect() } else { @@ -335,9 +360,15 @@ impl PickerDelegate for StashListDelegate { ) .await .into_iter() - .map(|candidate| StashEntryMatch { - entry: all_stash_entries[candidate.candidate_id].clone(), - positions: candidate.positions, + .map(|candidate| { + let entry = all_stash_entries[candidate.candidate_id].clone(); + let formatted_timestamp = Self::format_timestamp(entry.timestamp, timezone); + + StashEntryMatch { + entry, + positions: candidate.positions, + formatted_timestamp, + } }) .collect() }; @@ -418,21 +449,9 @@ impl PickerDelegate for StashListDelegate { .size(LabelSize::Small) .color(Color::Muted); - let timestamp = OffsetDateTime::from_unix_timestamp(entry_match.entry.timestamp) - .unwrap_or(OffsetDateTime::now_utc()); - let timezone = - UtcOffset::from_whole_seconds(chrono::Local::now().offset().local_minus_utc()) - .unwrap_or(UtcOffset::UTC); - - let absolute_timestamp = time_format::format_localized_timestamp( - timestamp, - OffsetDateTime::now_utc(), - timezone, - time_format::TimestampFormat::EnhancedAbsolute, - ); let tooltip_text = format!( "stash@{{{}}} created {}", - entry_match.entry.index, absolute_timestamp + entry_match.entry.index, entry_match.formatted_timestamp ); Some( From 33fd16c749c15bc73059f0f1669e7eae9952877c Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Sun, 17 Aug 2025 19:09:16 -0400 Subject: [PATCH 16/28] Handle trim on non-ascii messages --- crates/git_ui/src/stash_picker.rs | 36 ++++++++++++++++--------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index aefcc969b5..7b6ef2a4e5 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -414,26 +414,28 @@ impl PickerDelegate for StashListDelegate { Self::format_message(entry_match.entry.index, &entry_match.entry.message); let mut positions = entry_match.positions.clone(); - if stash_message.is_ascii() { - let max_width = self.max_width.to_pixels(window.rem_size()); - let normal_em = { - let style = window.text_style(); - let font_id = window.text_system().resolve_font(&style.font()); - let font_size = ui::TextSize::Default.rems(cx).to_pixels(window.rem_size()); - let normal = cx - .text_system() - .em_width(font_id, font_size) - .unwrap_or(px(16.)); - normal - }; + let max_width = self.max_width.to_pixels(window.rem_size()); + let normal_em = { + let style = window.text_style(); + let font_id = window.text_system().resolve_font(&style.font()); + let font_size = ui::TextSize::Default.rems(cx).to_pixels(window.rem_size()); + let normal = cx + .text_system() + .em_width(font_id, font_size) + .unwrap_or(px(16.)); + normal + }; - let max_chars = ((max_width * 0.8) / normal_em) as usize; + let max_chars = ((max_width * 0.8) / normal_em) as usize; - if stash_message.len() > max_chars && max_chars > 3 { - stash_message.truncate(max_chars - 3); - stash_message.push_str("…"); - positions.retain(|&pos| pos < max_chars - 3); + if stash_message.len() > max_chars && max_chars > 1 { + let mut index = max_chars - 1; + while !stash_message.is_char_boundary(index) && index != 0 { + index -= 1; } + stash_message.truncate(index); + stash_message.push_str("…"); + positions.retain(|&pos| pos < index); } let stash_name = HighlightedLabel::new(stash_message, positions).into_any_element(); From 66c4e3fe3e58a2489f9db3bbc696eed83c9864c0 Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Sun, 17 Aug 2025 19:25:56 -0400 Subject: [PATCH 17/28] Trim branch for larger names --- crates/git_ui/src/stash_picker.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index 7b6ef2a4e5..11caedf27d 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -440,16 +440,20 @@ impl PickerDelegate for StashListDelegate { let stash_name = HighlightedLabel::new(stash_message, positions).into_any_element(); - let stash_index_label = Label::new( - entry_match - .entry - .branch - .clone() - .unwrap_or_default() - .to_string(), - ) - .size(LabelSize::Small) - .color(Color::Muted); + let mut branch_name = entry_match.entry.branch.clone().unwrap_or_default(); + let max_branch_chars = ((max_width * 0.2) / normal_em) as usize; + if branch_name.len() > max_branch_chars && max_branch_chars > 1 { + let mut index = max_branch_chars - 1; + while !branch_name.is_char_boundary(index) && index != 0 { + index -= 1; + } + branch_name.truncate(index); + branch_name.push_str("…"); + } + + let stash_index_label = Label::new(branch_name) + .size(LabelSize::Small) + .color(Color::Muted); let tooltip_text = format!( "stash@{{{}}} created {}", From 0725df87cd7b1790085793883b30e477bfb49814 Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Sun, 17 Aug 2025 20:37:38 -0400 Subject: [PATCH 18/28] Change ratio for trimmed matches --- crates/git_ui/src/stash_picker.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index 11caedf27d..52a5bbc7b7 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -426,7 +426,7 @@ impl PickerDelegate for StashListDelegate { normal }; - let max_chars = ((max_width * 0.8) / normal_em) as usize; + let max_chars = ((max_width * 0.7) / normal_em) as usize; if stash_message.len() > max_chars && max_chars > 1 { let mut index = max_chars - 1; @@ -441,7 +441,7 @@ impl PickerDelegate for StashListDelegate { let stash_name = HighlightedLabel::new(stash_message, positions).into_any_element(); let mut branch_name = entry_match.entry.branch.clone().unwrap_or_default(); - let max_branch_chars = ((max_width * 0.2) / normal_em) as usize; + let max_branch_chars = ((max_width * 0.3) / normal_em) as usize; if branch_name.len() > max_branch_chars && max_branch_chars > 1 { let mut index = max_branch_chars - 1; while !branch_name.is_char_boundary(index) && index != 0 { From 50f721ebd410576e543611eaf4b57477b6a7bd2a Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Mon, 18 Aug 2025 09:59:10 -0400 Subject: [PATCH 19/28] Remove popup code --- crates/git_ui/src/git_panel.rs | 1 + crates/git_ui/src/stash_picker.rs | 51 ++++++------------------------- 2 files changed, 10 insertions(+), 42 deletions(-) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index ff6921274f..37ec7efb89 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -149,6 +149,7 @@ fn git_panel_context_menu( StashAll.boxed_clone(), ) .action_disabled_when(!state.has_stash_items, "Stash Pop", StashPop.boxed_clone()) + .action("View Stash", zed_actions::git::StashEntries.boxed_clone()) .separator() .action("Open Diff", project_diff::Diff.boxed_clone()) .separator() diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index 52a5bbc7b7..c7a2919d1d 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -7,7 +7,7 @@ use gpui::{ InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement, Render, SharedString, Styled, Subscription, Task, Window, actions, px, rems, }; -use picker::{Picker, PickerDelegate, PickerEditorPosition}; +use picker::{Picker, PickerDelegate}; use project::git_store::{Repository, RepositoryEvent}; use std::sync::Arc; use time::{OffsetDateTime, UtcOffset}; @@ -38,30 +38,11 @@ pub fn open( cx: &mut Context, ) { let repository = workspace.project().read(cx).active_repository(cx).clone(); - let style = StashListStyle::Modal; workspace.toggle_modal(window, cx, |window, cx| { - StashList::new(repository, style, rems(34.), window, cx) + StashList::new(repository, rems(34.), window, cx) }) } -pub fn popover( - repository: Option>, - window: &mut Window, - cx: &mut App, -) -> Entity { - cx.new(|cx| { - let list = StashList::new(repository, StashListStyle::Popover, rems(20.), window, cx); - list.focus_handle(cx).focus(window); - list - }) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -enum StashListStyle { - Modal, - Popover, -} - pub struct StashList { width: Rems, pub picker: Entity>, @@ -72,7 +53,6 @@ pub struct StashList { impl StashList { fn new( repository: Option>, - style: StashListStyle, width: Rems, window: &mut Window, cx: &mut Context, @@ -80,21 +60,18 @@ impl StashList { let mut _subscriptions = Vec::new(); let stash_request = repository .clone() - .map(|repository| repository.read_with(cx, |repo, _| repo.stash_entries.clone())); + .map(|repository| repository.read_with(cx, |repo, _| repo.cached_stash())); if let Some(repo) = repository.clone() { _subscriptions.push( cx.subscribe_in(&repo, window, |this, _, event, window, cx| { if matches!(event, RepositoryEvent::Updated { .. }) { let stash_entries = this.picker.read_with(cx, |picker, cx| { - picker.delegate.repo.clone().map(|repo| { - repo.read(cx) - .snapshot() - .stash_entries - .entries - .to_vec() - .clone() - }) + picker + .delegate + .repo + .clone() + .map(|repo| repo.read(cx).cached_stash().entries.to_vec()) }); this.picker.update(cx, |this, cx| { this.delegate.all_stash_entries = stash_entries; @@ -121,7 +98,7 @@ impl StashList { }) .detach_and_log_err(cx); - let delegate = StashListDelegate::new(repository.clone(), style, width, window, cx); + let delegate = StashListDelegate::new(repository.clone(), width, 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, _| { @@ -195,7 +172,6 @@ pub struct StashListDelegate { matches: Vec, all_stash_entries: Option>, repo: Option>, - style: StashListStyle, selected_index: usize, last_query: String, modifiers: Modifiers, @@ -207,7 +183,6 @@ pub struct StashListDelegate { impl StashListDelegate { fn new( repo: Option>, - style: StashListStyle, max_width: Rems, _window: &mut Window, cx: &mut Context, @@ -219,7 +194,6 @@ impl StashListDelegate { Self { matches: vec![], repo, - style, all_stash_entries: None, selected_index: 0, last_query: Default::default(), @@ -288,13 +262,6 @@ impl PickerDelegate for StashListDelegate { "Select a stash…".into() } - fn editor_position(&self) -> PickerEditorPosition { - match self.style { - StashListStyle::Modal => PickerEditorPosition::Start, - StashListStyle::Popover => PickerEditorPosition::End, - } - } - fn match_count(&self) -> usize { self.matches.len() } From ec4c2f5fd03f9022d17f67abef7416bffb32bb59 Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Mon, 18 Aug 2025 09:59:18 -0400 Subject: [PATCH 20/28] Change action name --- crates/git_ui/src/git_panel.rs | 2 +- crates/git_ui/src/stash_picker.rs | 2 +- crates/zed_actions/src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 37ec7efb89..4eddb2a0d3 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -149,7 +149,7 @@ fn git_panel_context_menu( StashAll.boxed_clone(), ) .action_disabled_when(!state.has_stash_items, "Stash Pop", StashPop.boxed_clone()) - .action("View Stash", zed_actions::git::StashEntries.boxed_clone()) + .action("View Stash", zed_actions::git::ViewStash.boxed_clone()) .separator() .action("Open Diff", project_diff::Diff.boxed_clone()) .separator() diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index c7a2919d1d..603f7125df 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -33,7 +33,7 @@ pub fn register(workspace: &mut Workspace) { pub fn open( workspace: &mut Workspace, - _: &zed_actions::git::StashEntries, + _: &zed_actions::git::ViewStash, window: &mut Window, cx: &mut Context, ) { diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index e0bca2a406..c66fe93f8e 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -177,7 +177,7 @@ pub mod git { #[action(deprecated_aliases = ["branches::OpenRecent"])] Branch, /// Opens the git stash selector. - StashEntries + ViewStash ] ); } From ea7e0c47593101c97dea0682ae226349ddb8b5f7 Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Mon, 18 Aug 2025 23:10:53 -0400 Subject: [PATCH 21/28] Render branch on new line --- crates/git_ui/src/stash_picker.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index 603f7125df..f0093e2e63 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -393,7 +393,7 @@ impl PickerDelegate for StashListDelegate { normal }; - let max_chars = ((max_width * 0.7) / normal_em) as usize; + let max_chars = ((max_width * 0.9) / normal_em) as usize; if stash_message.len() > max_chars && max_chars > 1 { let mut index = max_chars - 1; @@ -408,7 +408,7 @@ impl PickerDelegate for StashListDelegate { let stash_name = HighlightedLabel::new(stash_message, positions).into_any_element(); let mut branch_name = entry_match.entry.branch.clone().unwrap_or_default(); - let max_branch_chars = ((max_width * 0.3) / normal_em) as usize; + let max_branch_chars = ((max_width * 0.9) / normal_em) as usize; if branch_name.len() > max_branch_chars && max_branch_chars > 1 { let mut index = max_branch_chars - 1; while !branch_name.is_char_boundary(index) && index != 0 { @@ -418,9 +418,7 @@ impl PickerDelegate for StashListDelegate { branch_name.push_str("…"); } - let stash_index_label = Label::new(branch_name) - .size(LabelSize::Small) - .color(Color::Muted); + let stash_index_label = Label::new(branch_name).color(Color::Muted); let tooltip_text = format!( "stash@{{{}}} created {}", @@ -433,14 +431,9 @@ impl PickerDelegate for StashListDelegate { .spacing(ListItemSpacing::Sparse) .toggle_state(selected) .child( - v_flex().w_full().overflow_hidden().child( - h_flex() - .gap_6() - .justify_between() - .overflow_x_hidden() - .child(stash_name) - .child(stash_index_label.into_element()), - ), + v_flex() + .child(stash_name) + .child(stash_index_label.into_element()), ) .tooltip(Tooltip::text(tooltip_text)), ) From 4a11d9d4c6044dd124cc77bb7b87e5f673c39f45 Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Mon, 18 Aug 2025 23:51:53 -0400 Subject: [PATCH 22/28] Add branch icon --- crates/git_ui/src/stash_picker.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index f0093e2e63..a064c81903 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -418,7 +418,14 @@ impl PickerDelegate for StashListDelegate { branch_name.push_str("…"); } - let stash_index_label = Label::new(branch_name).color(Color::Muted); + let branch_label = h_flex() + .gap_1() + .child( + Icon::new(IconName::GitBranch) + .color(Color::Muted) + .size(IconSize::Small), + ) + .child(Label::new(branch_name).color(Color::Muted)); let tooltip_text = format!( "stash@{{{}}} created {}", @@ -433,7 +440,7 @@ impl PickerDelegate for StashListDelegate { .child( v_flex() .child(stash_name) - .child(stash_index_label.into_element()), + .child(branch_label.into_element()), ) .tooltip(Tooltip::text(tooltip_text)), ) From 775425e8c7a7553b4748ce5a1b45ae1fbc6481ff Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Tue, 19 Aug 2025 08:37:59 -0400 Subject: [PATCH 23/28] Add stash apply action --- crates/fs/src/fake_git_repo.rs | 8 +++++ crates/git/src/git.rs | 2 ++ crates/git/src/repository.rs | 35 ++++++++++++++++++++ crates/git_ui/src/git_panel.rs | 27 ++++++++++++++-- crates/git_ui/src/git_ui.rs | 8 +++++ crates/git_ui/src/stash_picker.rs | 41 ++++++++++++++++++++++-- crates/project/src/git_store.rs | 53 +++++++++++++++++++++++++++++++ crates/proto/proto/git.proto | 6 ++++ crates/proto/proto/zed.proto | 3 +- crates/proto/src/proto.rs | 3 ++ 10 files changed, 180 insertions(+), 6 deletions(-) diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 69686f8d48..549c788dfa 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -424,6 +424,14 @@ impl GitRepository for FakeGitRepository { unimplemented!() } + fn stash_apply( + &self, + _index: Option, + _env: Arc>, + ) -> BoxFuture<'_, Result<()>> { + unimplemented!() + } + fn stash_drop( &self, _index: Option, diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index 21a31657ed..73d32ac9e4 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -60,6 +60,8 @@ actions!( StashAll, /// Pops the most recent stash. StashPop, + /// Apply the most recent stash. + StashApply, /// Restores all tracked files to their last committed state. RestoreTrackedFiles, /// Moves all untracked files to trash. diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 7b71b5d6b1..adc1d71c04 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -408,6 +408,12 @@ pub trait GitRepository: Send + Sync { env: Arc>, ) -> BoxFuture<'_, Result<()>>; + fn stash_apply( + &self, + index: Option, + env: Arc>, + ) -> BoxFuture<'_, Result<()>>; + fn stash_drop( &self, index: Option, @@ -1289,6 +1295,35 @@ impl GitRepository for RealGitRepository { .boxed() } + fn stash_apply( + &self, + index: Option, + env: Arc>, + ) -> BoxFuture<'_, Result<()>> { + let working_directory = self.working_directory(); + self.executor + .spawn(async move { + let mut cmd = new_smol_command("git"); + let mut args = vec!["stash".to_string(), "apply".to_string()]; + if let Some(index) = index { + args.push(format!("stash@{{{}}}", index)); + } + cmd.current_dir(&working_directory?) + .envs(env.iter()) + .args(args); + + let output = cmd.output().await?; + + anyhow::ensure!( + output.status.success(), + "Failed to apply stash:\n{}", + String::from_utf8_lossy(&output.stderr) + ); + Ok(()) + }) + .boxed() + } + fn stash_drop( &self, index: Option, diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 4eddb2a0d3..6988a4bb6b 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -28,8 +28,8 @@ use git::stash::GitStash; use git::status::StageStatus; use git::{Amend, Signoff, ToggleStaged, repository::RepoPath, status::FileStatus}; use git::{ - ExpandCommitEditor, RestoreTrackedFiles, StageAll, StashAll, StashPop, TrashUntrackedFiles, - UnstageAll, + ExpandCommitEditor, RestoreTrackedFiles, StageAll, StashAll, StashApply, StashPop, + TrashUntrackedFiles, UnstageAll, }; use gpui::{ Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner, @@ -1442,6 +1442,29 @@ impl GitPanel { .detach(); } + pub fn stash_apply(&mut self, _: &StashApply, _window: &mut Window, cx: &mut Context) { + let Some(active_repository) = self.active_repository.clone() else { + return; + }; + + cx.spawn({ + async move |this, cx| { + let stash_task = active_repository + .update(cx, |repo, cx| repo.stash_apply(None, cx))? + .await; + this.update(cx, |this, cx| { + stash_task + .map_err(|e| { + this.show_error_toast("stash apply", e, cx); + }) + .ok(); + cx.notify(); + }) + } + }) + .detach(); + } + pub fn stash_all(&mut self, _: &StashAll, _window: &mut Window, cx: &mut Context) { let Some(active_repository) = self.active_repository.clone() else { return; diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 54b1f8ba11..b2070fba25 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -135,6 +135,14 @@ pub fn init(cx: &mut App) { panel.stash_pop(action, window, cx); }); }); + workspace.register_action(|workspace, action: &git::StashApply, window, cx| { + let Some(panel) = workspace.panel::(cx) else { + return; + }; + panel.update(cx, |panel, cx| { + panel.stash_apply(action, window, cx); + }); + }); workspace.register_action(|workspace, action: &git::StageAll, window, cx| { let Some(panel) = workspace.panel::(cx) else { return; diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index a064c81903..f424cca47b 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -253,6 +253,22 @@ impl StashListDelegate { }); cx.emit(DismissEvent); } + + fn apply_stash(&self, stash_index: usize, window: &mut Window, cx: &mut Context>) { + let Some(repo) = self.repo.clone() else { + return; + }; + + cx.spawn(async move |_, cx| { + repo.update(cx, |repo, cx| repo.stash_apply(Some(stash_index), cx))? + .await?; + Ok(()) + }) + .detach_and_prompt_err("Failed to apply stash", window, cx, |e, _, _| { + Some(e.to_string()) + }); + cx.emit(DismissEvent); + } } impl PickerDelegate for StashListDelegate { @@ -356,12 +372,16 @@ impl PickerDelegate for StashListDelegate { }) } - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { + fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { let Some(entry_match) = self.matches.get(self.selected_index()) else { return; }; let stash_index = entry_match.entry.index; - self.pop_stash(stash_index, window, cx); + if secondary { + self.pop_stash(stash_index, window, cx); + } else { + self.apply_stash(stash_index, window, cx); + } } fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { @@ -486,7 +506,7 @@ impl PickerDelegate for StashListDelegate { }), ) .child( - Button::new("pop-stash", "Pop") + Button::new("apply-stash", "Apply") .key_binding( KeyBinding::for_action_in( &menu::Confirm, @@ -499,6 +519,21 @@ impl PickerDelegate for StashListDelegate { .on_click(|_, window, cx| { window.dispatch_action(menu::Confirm.boxed_clone(), cx) }), + ) + .child( + Button::new("pop-stash", "Pop") + .key_binding( + KeyBinding::for_action_in( + &menu::SecondaryConfirm, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(|_, window, cx| { + window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx) + }), ), ) .into_any(), diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index ba24ff2c8c..2ee96c5ca3 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -427,6 +427,7 @@ impl GitStore { client.add_entity_request_handler(Self::handle_unstage); client.add_entity_request_handler(Self::handle_stash); client.add_entity_request_handler(Self::handle_stash_pop); + client.add_entity_request_handler(Self::handle_stash_apply); client.add_entity_request_handler(Self::handle_stash_drop); client.add_entity_request_handler(Self::handle_commit); client.add_entity_request_handler(Self::handle_reset); @@ -1792,6 +1793,24 @@ impl GitStore { Ok(proto::Ack {}) } + async fn handle_stash_apply( + this: Entity, + envelope: TypedEnvelope, + mut cx: AsyncApp, + ) -> Result { + let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); + let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; + let stash_index = envelope.payload.stash_index.map(|i| i as usize); + + repository_handle + .update(&mut cx, |repository_handle, cx| { + repository_handle.stash_apply(stash_index, cx) + })? + .await?; + + Ok(proto::Ack {}) + } + async fn handle_stash_drop( this: Entity, envelope: TypedEnvelope, @@ -3791,6 +3810,40 @@ impl Repository { }) } + pub fn stash_apply( + &mut self, + index: Option, + cx: &mut Context, + ) -> Task> { + let id = self.id; + cx.spawn(async move |this, cx| { + this.update(cx, |this, _| { + this.send_job(None, move |git_repo, _cx| async move { + match git_repo { + RepositoryState::Local { + backend, + environment, + .. + } => backend.stash_apply(index, environment).await, + RepositoryState::Remote { project_id, client } => { + client + .request(proto::StashApply { + project_id: project_id.0, + repository_id: id.to_proto(), + stash_index: index.map(|i| i as u64), + }) + .await + .context("sending stash apply request")?; + Ok(()) + } + } + }) + })? + .await??; + Ok(()) + }) + } + pub fn stash_drop( &mut self, index: Option, diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto index ecc4f51212..278d85897a 100644 --- a/crates/proto/proto/git.proto +++ b/crates/proto/proto/git.proto @@ -318,6 +318,12 @@ message StashPop { optional uint64 stash_index = 3; } +message StashApply { + uint64 project_id = 1; + uint64 repository_id = 2; + optional uint64 stash_index = 3; +} + message StashDrop { uint64 project_id = 1; uint64 repository_id = 2; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index f2e70ce7d2..b1e86a12e4 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -397,7 +397,8 @@ message Envelope { LspQuery lsp_query = 365; LspQueryResponse lsp_query_response = 366; - StashDrop stash_drop = 367; // current max + StashDrop stash_drop = 367; + StashApply stash_apply = 368; // current max } reserved 87 to 88; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 5387b26a8d..c93fbb03c3 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -257,6 +257,7 @@ messages!( (Unstage, Background), (Stash, Background), (StashPop, Background), + (StashApply, Background), (StashDrop, Background), (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), @@ -418,6 +419,7 @@ request_messages!( (Unstage, Ack), (Stash, Ack), (StashPop, Ack), + (StashApply, Ack), (StashDrop, Ack), (UpdateBuffer, Ack), (UpdateParticipantLocation, Ack), @@ -572,6 +574,7 @@ entity_messages!( Unstage, Stash, StashPop, + StashApply, StashDrop, UpdateBuffer, UpdateBufferFile, From d500dc086f2a0d04601a70c0db59f698c1bece97 Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Tue, 19 Aug 2025 13:57:50 -0400 Subject: [PATCH 24/28] Use util truncate function --- crates/git/src/stash.rs | Bin 7110 -> 7054 bytes crates/git_ui/src/stash_picker.rs | 54 +++++++++++++++--------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/crates/git/src/stash.rs b/crates/git/src/stash.rs index 4000f01092c175ab288d2cf809b798ef278e8739..93e8aba6b9503ab509803caf76c9f38aee4ade90 100644 GIT binary patch delta 96 zcmX?R-e5T$5+;nNGgQXEd3e-+HnezsBSm{#;_!Zsr!; G%>@8jiWgV_ delta 116 zcmeA(KW4rmijP;Jno9u+GSepC;MZb@icIDdFr92AU^Y2NzFmn;z`| diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index f424cca47b..bd5529a078 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -401,8 +401,7 @@ impl PickerDelegate for StashListDelegate { Self::format_message(entry_match.entry.index, &entry_match.entry.message); let mut positions = entry_match.positions.clone(); - let max_width = self.max_width.to_pixels(window.rem_size()); - let normal_em = { + let max_chars_msg = { let style = window.text_style(); let font_id = window.text_system().resolve_font(&style.font()); let font_size = ui::TextSize::Default.rems(cx).to_pixels(window.rem_size()); @@ -410,34 +409,31 @@ impl PickerDelegate for StashListDelegate { .text_system() .em_width(font_id, font_size) .unwrap_or(px(16.)); - normal + let max_width = (self.max_width - rems(2.)).to_pixels(window.rem_size()); + (max_width / normal) as usize }; - - let max_chars = ((max_width * 0.9) / normal_em) as usize; - - if stash_message.len() > max_chars && max_chars > 1 { - let mut index = max_chars - 1; - while !stash_message.is_char_boundary(index) && index != 0 { - index -= 1; - } - stash_message.truncate(index); - stash_message.push_str("…"); - positions.retain(|&pos| pos < index); + if max_chars_msg > 5 { + stash_message = util::truncate_and_trailoff(&stash_message, max_chars_msg); + positions.retain(|&pos| pos < max_chars_msg); } + let stash_label = HighlightedLabel::new(stash_message, positions).into_any_element(); - let stash_name = HighlightedLabel::new(stash_message, positions).into_any_element(); - + let max_chars_branch = { + let style = window.text_style(); + let font_id = window.text_system().resolve_font(&style.font()); + let font_size = ui::TextSize::Small.rems(cx).to_pixels(window.rem_size()); + let small = cx + .text_system() + .em_width(font_id, font_size) + .unwrap_or(px(10.)); + let max_width = + (self.max_width - rems(3.) - IconSize::Small.rems()).to_pixels(window.rem_size()); + (max_width / small) as usize + }; let mut branch_name = entry_match.entry.branch.clone().unwrap_or_default(); - let max_branch_chars = ((max_width * 0.9) / normal_em) as usize; - if branch_name.len() > max_branch_chars && max_branch_chars > 1 { - let mut index = max_branch_chars - 1; - while !branch_name.is_char_boundary(index) && index != 0 { - index -= 1; - } - branch_name.truncate(index); - branch_name.push_str("…"); + if max_chars_branch > 5 { + branch_name = util::truncate_and_trailoff(&branch_name, max_chars_branch); } - let branch_label = h_flex() .gap_1() .child( @@ -445,7 +441,11 @@ impl PickerDelegate for StashListDelegate { .color(Color::Muted) .size(IconSize::Small), ) - .child(Label::new(branch_name).color(Color::Muted)); + .child( + Label::new(branch_name) + .color(Color::Muted) + .size(LabelSize::Small), + ); let tooltip_text = format!( "stash@{{{}}} created {}", @@ -459,7 +459,7 @@ impl PickerDelegate for StashListDelegate { .toggle_state(selected) .child( v_flex() - .child(stash_name) + .child(stash_label) .child(branch_label.into_element()), ) .tooltip(Tooltip::text(tooltip_text)), From de260fde993755a65f8e2b38d49eca519f9019af Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Tue, 19 Aug 2025 15:00:46 -0400 Subject: [PATCH 25/28] Fix clippy errors --- crates/project/src/git_store.rs | 46 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 2ee96c5ca3..6122514b7c 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -3867,22 +3867,22 @@ impl Repository { .. } => { let result = backend.stash_drop(index, environment).await; - if result.is_ok() { - if let Ok(stash_entries) = backend.stash_entries().await { - let snapshot = this.update(&mut cx, |this, cx| { - this.snapshot.stash_entries = stash_entries; - let snapshot = this.snapshot.clone(); - cx.emit(RepositoryEvent::Updated { - full_scan: false, - new_instance: false, - }); - snapshot - })?; - if let Some(updates_tx) = updates_tx { - updates_tx - .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot)) - .ok(); - } + if result.is_ok() + && let Ok(stash_entries) = backend.stash_entries().await + { + let snapshot = this.update(&mut cx, |this, cx| { + this.snapshot.stash_entries = stash_entries; + let snapshot = this.snapshot.clone(); + cx.emit(RepositoryEvent::Updated { + full_scan: false, + new_instance: false, + }); + snapshot + })?; + if let Some(updates_tx) = updates_tx { + updates_tx + .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot)) + .ok(); } } @@ -4786,14 +4786,12 @@ impl Repository { this.snapshot.scan_id += 1; } - if needs_update { - if let Some(updates_tx) = updates_tx { - updates_tx - .unbounded_send(DownstreamUpdate::UpdateRepository( - this.snapshot.clone(), - )) - .ok(); - } + if needs_update && let Some(updates_tx) = updates_tx { + updates_tx + .unbounded_send(DownstreamUpdate::UpdateRepository( + this.snapshot.clone(), + )) + .ok(); } cx.emit(RepositoryEvent::Updated { full_scan: false, From 637027f1856090e65b3ab9d90482178f972c6491 Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Thu, 21 Aug 2025 11:39:51 -0400 Subject: [PATCH 26/28] Remove trim --- crates/git_ui/src/stash_picker.rs | 49 +++++-------------------------- 1 file changed, 7 insertions(+), 42 deletions(-) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index bd5529a078..0ebf7e8b05 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -5,7 +5,7 @@ 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, px, rems, + SharedString, Styled, Subscription, Task, Window, actions, rems, }; use picker::{Picker, PickerDelegate}; use project::git_store::{Repository, RepositoryEvent}; @@ -98,7 +98,7 @@ impl StashList { }) .detach_and_log_err(cx); - let delegate = StashListDelegate::new(repository.clone(), width, window, cx); + let delegate = StashListDelegate::new(repository.clone(), 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, _| { @@ -175,7 +175,6 @@ pub struct StashListDelegate { selected_index: usize, last_query: String, modifiers: Modifiers, - max_width: Rems, focus_handle: FocusHandle, timezone: UtcOffset, } @@ -183,7 +182,6 @@ pub struct StashListDelegate { impl StashListDelegate { fn new( repo: Option>, - max_width: Rems, _window: &mut Window, cx: &mut Context, ) -> Self { @@ -198,7 +196,6 @@ impl StashListDelegate { selected_index: 0, last_query: Default::default(), modifiers: Default::default(), - max_width, focus_handle: cx.focus_handle(), timezone, } @@ -392,48 +389,16 @@ impl PickerDelegate for StashListDelegate { &self, ix: usize, selected: bool, - window: &mut Window, - cx: &mut Context>, + _window: &mut Window, + _cx: &mut Context>, ) -> Option { let entry_match = &self.matches[ix]; - let mut stash_message = + let stash_message = Self::format_message(entry_match.entry.index, &entry_match.entry.message); - let mut positions = entry_match.positions.clone(); - - let max_chars_msg = { - let style = window.text_style(); - let font_id = window.text_system().resolve_font(&style.font()); - let font_size = ui::TextSize::Default.rems(cx).to_pixels(window.rem_size()); - let normal = cx - .text_system() - .em_width(font_id, font_size) - .unwrap_or(px(16.)); - let max_width = (self.max_width - rems(2.)).to_pixels(window.rem_size()); - (max_width / normal) as usize - }; - if max_chars_msg > 5 { - stash_message = util::truncate_and_trailoff(&stash_message, max_chars_msg); - positions.retain(|&pos| pos < max_chars_msg); - } + let positions = entry_match.positions.clone(); let stash_label = HighlightedLabel::new(stash_message, positions).into_any_element(); - - let max_chars_branch = { - let style = window.text_style(); - let font_id = window.text_system().resolve_font(&style.font()); - let font_size = ui::TextSize::Small.rems(cx).to_pixels(window.rem_size()); - let small = cx - .text_system() - .em_width(font_id, font_size) - .unwrap_or(px(10.)); - let max_width = - (self.max_width - rems(3.) - IconSize::Small.rems()).to_pixels(window.rem_size()); - (max_width / small) as usize - }; - let mut branch_name = entry_match.entry.branch.clone().unwrap_or_default(); - if max_chars_branch > 5 { - branch_name = util::truncate_and_trailoff(&branch_name, max_chars_branch); - } + let branch_name = entry_match.entry.branch.clone().unwrap_or_default(); let branch_label = h_flex() .gap_1() .child( From edb866102107928f1668dd5d4a3c57ab4982963c Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Thu, 21 Aug 2025 11:55:04 -0400 Subject: [PATCH 27/28] Fix clippy errors --- crates/git_ui/src/stash_picker.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index 0ebf7e8b05..5f6c493b1b 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -37,7 +37,7 @@ pub fn open( window: &mut Window, cx: &mut Context, ) { - let repository = workspace.project().read(cx).active_repository(cx).clone(); + let repository = workspace.project().read(cx).active_repository(cx); workspace.toggle_modal(window, cx, |window, cx| { StashList::new(repository, rems(34.), window, cx) }) @@ -98,7 +98,7 @@ impl StashList { }) .detach_and_log_err(cx); - let delegate = StashListDelegate::new(repository.clone(), window, cx); + let delegate = StashListDelegate::new(repository, 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, _| { From 0354564c0e0028f1d9846d3809fb989e97fcddf9 Mon Sep 17 00:00:00 2001 From: Alvaro Parker Date: Thu, 21 Aug 2025 16:03:40 -0400 Subject: [PATCH 28/28] Change keybind hints order --- crates/git_ui/src/stash_picker.rs | 36 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/git_ui/src/stash_picker.rs b/crates/git_ui/src/stash_picker.rs index 5f6c493b1b..60e58b1928 100644 --- a/crates/git_ui/src/stash_picker.rs +++ b/crates/git_ui/src/stash_picker.rs @@ -452,24 +452,6 @@ impl PickerDelegate for StashListDelegate { .child( h_flex() .gap_0p5() - .child( - Button::new("drop-stash", "Drop") - .key_binding( - KeyBinding::for_action_in( - &stash_picker::DropStashItem, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(|_, window, cx| { - window.dispatch_action( - stash_picker::DropStashItem.boxed_clone(), - cx, - ) - }), - ) .child( Button::new("apply-stash", "Apply") .key_binding( @@ -499,6 +481,24 @@ impl PickerDelegate for StashListDelegate { .on_click(|_, window, cx| { window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx) }), + ) + .child( + Button::new("drop-stash", "Drop") + .key_binding( + KeyBinding::for_action_in( + &stash_picker::DropStashItem, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(|_, window, cx| { + window.dispatch_action( + stash_picker::DropStashItem.boxed_clone(), + cx, + ) + }), ), ) .into_any(),