Improve stash picker ui

This commit is contained in:
Alvaro Parker 2025-08-13 19:38:19 -04:00
parent d03557748b
commit 4f11b9ef56
No known key found for this signature in database
5 changed files with 80 additions and 12 deletions

View file

@ -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);

View file

@ -7,6 +7,8 @@ pub struct StashEntry {
pub index: usize,
pub oid: Oid,
pub message: String,
pub branch: Option<String>,
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::<usize>().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 <branch>: <message>" 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 <branch>: <message>" 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)
}

View file

@ -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<Picker<Self>>) {
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::<Vec<StringMatchCandidate>>();
fuzzy::match_strings(
&candidates,
@ -367,7 +378,8 @@ impl PickerDelegate for StashListDelegate {
) -> Option<Self::ListItem> {
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,10 +406,28 @@ 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))
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}")))
.inset(true)
@ -412,7 +442,8 @@ impl PickerDelegate for StashListDelegate {
.child(stash_name)
.child(stash_index_label.into_element()),
),
),
)
.tooltip(Tooltip::text(tooltip_text)),
)
}

View file

@ -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<StashEntry> {
oid: Oid::from_bytes(&entry.oid)?,
message: entry.message.clone(),
index: entry.index as usize,
branch: entry.branch.clone(),
timestamp: entry.timestamp,
})
}

View file

@ -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 {