From c5799cc7e47b33b90866aa514697858d9a453fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 7 Oct 2026 03:57:21 -0400 Subject: [PATCH] =?UTF-8?q?Git=20=C2=B7=20StashList:=20words=20in=20i18n,?= =?UTF-8?q?=20rows=20pick,=20only=20handled=20actions=20drawn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/git/picking.rs | 34 +++++++++++++++++++++- src/git/tags.rs | 71 +++++++++++++++++++++++++++++++-------------- src/i18n/builtin.rs | 8 +++++ 3 files changed, 91 insertions(+), 22 deletions(-) diff --git a/src/git/picking.rs b/src/git/picking.rs index ddd71fd..fe1f90e 100644 --- a/src/git/picking.rs +++ b/src/git/picking.rs @@ -8,7 +8,8 @@ use gpui::{ }; use super::{ - Blame, BlameView, Branch, BranchList, Commit, CommitList, FileHistory, GitTag, TagList, + Blame, BlameView, Branch, BranchList, Commit, CommitList, FileHistory, GitTag, Stash, + StashList, TagList, }; use crate::{forms, primitives::FocusNext, theme::Theme}; @@ -89,6 +90,24 @@ impl Render for Lists { tagged.borrow_mut().picked.push(name.to_string()) })) } + "stashes" => { + let stash = |message: &str| Stash { + message: message.to_string().into(), + branch: "main".into(), + when: "today".into(), + }; + let acted = self.heard.clone(); + frame + .child(StashList::new("bare", [stash("unhandled")])) + .child( + StashList::new("stashes", [stash("one"), stash("two")]) + .selected(1) + .on_pick(move |ix, _, _| pick.borrow_mut().picked.push(format!("{ix}"))) + .on_action(move |ix, what, _, _| { + acted.borrow_mut().picked.push(format!("{what:?} {ix}")) + }), + ) + } _ => { let blame = |commit: &str| Blame { commit: commit.into(), @@ -219,3 +238,16 @@ fn branches_and_tags_pick_from_the_keyboard(cx: &mut TestAppContext) { "row actions answer: {picked:?}" ); } + +#[gpui::test] +fn stashes_pick_and_act_from_the_keyboard(cx: &mut TestAppContext) { + let (heard, cx) = shown(cx, 0, "stashes"); + for _ in 0..4 { + press_first(cx); + } + assert_eq!( + heard.borrow().picked, + ["0", "Apply 0", "Pop 0", "Drop 0"], + "a list with no owner draws no stops" + ); +} diff --git a/src/git/tags.rs b/src/git/tags.rs index f65597f..a0488e9 100644 --- a/src/git/tags.rs +++ b/src/git/tags.rs @@ -7,11 +7,13 @@ use gpui::{ use super::refs::{OnName, Pick, Run, action, listed}; use crate::{ + i18n, primitives::IconName, theme::{ActiveTheme, TextSize}, }; type OnStash = Rc; +type OnAt = Rc; /// A tag, its message if annotated, its commit, when. #[derive(Clone, Debug, PartialEq)] @@ -120,6 +122,8 @@ pub struct StashList { id: ElementId, stashes: Vec, on_action: Option, + selected: Option, + on_pick: Option, } impl StashList { @@ -128,9 +132,23 @@ impl StashList { id: id.into(), stashes: stashes.into_iter().collect(), on_action: None, + selected: None, + on_pick: None, } } + /// The stash shown as picked. + pub fn selected(mut self, ix: usize) -> Self { + self.selected = Some(ix); + self + } + + /// A press on a row picks its stash. + pub fn on_pick(mut self, handler: impl Fn(usize, &mut Window, &mut App) + 'static) -> Self { + self.on_pick = Some(Rc::new(handler)); + self + } + pub fn on_action( mut self, handler: impl Fn(usize, StashAction, &mut Window, &mut App) + 'static, @@ -149,35 +167,46 @@ impl RenderOnce for StashList { .iter() .enumerate() .map(|(ix, stash)| { - let act = |key: &str, icon, words: &'static str, what: StashAction| { - let run = self.on_action.clone().map(|on_action| { - Rc::new(move |window: &mut Window, cx: &mut App| { - on_action(ix, what, window, cx) - }) as Run - }); - action( - (self.id.clone(), format!("{key}-{ix}")).into(), - icon, - words, - run, - ) - }; + let name = SharedString::from(format!("stash@{{{ix}}}")); + // Only actions the owner handles are drawn. + let actions: Vec = [ + ("apply", IconName::Download, StashAction::Apply), + ("pop", IconName::ArrowUp, StashAction::Pop), + ("drop", IconName::Trash2, StashAction::Drop), + ] + .into_iter() + .filter_map(|(key, icon, what)| { + let on_action = self.on_action.clone()?; + let run = Rc::new(move |window: &mut Window, cx: &mut App| { + on_action(ix, what, window, cx) + }) as Run; + let words = i18n::text(cx, &format!("git.stash.{key}"), &[]); + let id = (self.id.clone(), format!("{key}-{ix}")).into(); + Some(action(id, icon, words, Some(run))) + }) + .collect(); + let detail = i18n::text( + cx, + "git.stash.on", + &[("message", &stash.message), ("branch", &stash.branch)], + ); + let pick = self.on_pick.clone().map(|pick| Pick { + spoken: format!("{name} {detail}").into(), + selected: self.selected == Some(ix), + run: Rc::new(move |window: &mut Window, cx: &mut App| pick(ix, window, cx)), + }); listed( (self.id.clone(), format!("stash-{ix}")).into(), IconName::Archive, div() .font_family(theme.mono_family.clone()) .text_color(colors.fg) - .child(format!("stash@{{{ix}}}")) + .child(name) .into_any_element(), - format!("{} · on {}", stash.message, stash.branch).into(), + detail, vec![stash.when.clone()], - vec![ - act("apply", IconName::Download, "Apply", StashAction::Apply), - act("pop", IconName::ArrowUp, "Pop", StashAction::Pop), - act("drop", IconName::Trash2, "Drop", StashAction::Drop), - ], - None, + actions, + pick, cx, ) }) diff --git a/src/i18n/builtin.rs b/src/i18n/builtin.rs index cc61ae4..887841f 100644 --- a/src/i18n/builtin.rs +++ b/src/i18n/builtin.rs @@ -96,6 +96,10 @@ const EN: &[(&str, &str)] = &[ ("git.merge.accept_current", "Accept current"), ("git.merge.accept_incoming", "Accept incoming"), ("git.merge.accept_both", "Accept both"), + ("git.stash.apply", "Apply"), + ("git.stash.pop", "Pop"), + ("git.stash.drop", "Drop"), + ("git.stash.on", "{message} · on {branch}"), ]; const ZH_CN: &[(&str, &str)] = &[ @@ -190,6 +194,10 @@ const ZH_CN: &[(&str, &str)] = &[ ("git.merge.accept_current", "接受当前"), ("git.merge.accept_incoming", "接受传入"), ("git.merge.accept_both", "接受两者"), + ("git.stash.apply", "应用"), + ("git.stash.pop", "弹出"), + ("git.stash.drop", "删除"), + ("git.stash.on", "{message} · 在 {branch}"), ]; /// Ely's message for `key`; English if unshipped, fails if unknown.