Files
zed/crates/git_ui/src/worktree_names.rs
T
9daf886775 Move the worktree picker to the title bar + make it always visible (#54183)
This PR makes Zed only have one worktree picker, as opposed to a flavor
of it in the title bar and another in the agent panel. It then moves it
to the title bar, making it always present, so that its trigger is
separate from the branch picker (which now contains only two views:
branches and stashes). For the worktree picker, I'm mostly favoring the
behavior we've introduced in the agent-panel-flavored version.

It also updates the title bar settings migration to use the JSON
`migrate_settings` helper instead of a shallow Tree-sitter rewrite, so
old `show_branch_icon = true` values are promoted to
`show_branch_status_icon = true` across root, platform, release-channel,
and profile settings scopes.

- [x] Move worktree creation logic to the `git_ui` crate to make this
more generic and less agent-specific
- [x] Double-check the remote use case and ensure nothing broke there
- [x] Improve the UX for the detached HEAD state; better invite people
to create a branch
- [x] Migrate `show_branch_icon = true` to `show_branch_status_icon =
true` across nested settings scopes

Suggested .rules additions

When migrating renamed settings keys that can appear in platform
overrides, release-channel overrides, or profiles, prefer the JSON
`migrations::migrate_settings` helper over shallow Tree-sitter key
rewrites unless tests explicitly cover every nested scope that can
contain the key.

Release Notes:

- Improved migration of the title bar branch status icon setting.

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Zed Zippy <234243425+zed-zippy[bot]@users.noreply.github.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
2026-04-20 09:29:51 +00:00

172 lines
8.0 KiB
Rust

use collections::HashSet;
use rand::Rng;
const ADJECTIVES: &[&str] = &[
"able", "agate", "agile", "alpine", "amber", "ample", "aqua", "arctic", "arid", "astral",
"autumn", "avid", "azure", "balmy", "birch", "bold", "boreal", "brave", "breezy", "brief",
"bright", "brisk", "broad", "bronze", "calm", "cerith", "civil", "clean", "clear", "clever",
"cobalt", "cool", "copper", "coral", "cozy", "crisp", "cubic", "cyan", "deft", "dense", "dewy",
"direct", "dusky", "dusty", "eager", "early", "earnest", "elder", "elfin", "equal", "even",
"exact", "faint", "fair", "fast", "fawn", "ferny", "fiery", "fine", "firm", "fleet", "floral",
"focal", "fond", "frank", "fresh", "frosty", "full", "gentle", "gilded", "glacial", "glad",
"glossy", "golden", "grand", "green", "gusty", "hale", "happy", "hardy", "hazel", "hearty",
"hilly", "humble", "hushed", "icy", "ideal", "inner", "iron", "ivory", "jade", "jovial",
"keen", "kind", "lapis", "leafy", "level", "light", "lilac", "limber", "lively", "local",
"lofty", "lucid", "lunar", "major", "maple", "mellow", "merry", "mild", "milky", "misty",
"modal", "modest", "mossy", "muted", "native", "naval", "neat", "nimble", "noble", "north",
"novel", "oaken", "ochre", "olive", "onyx", "opal", "open", "optic", "outer", "owed", "ozone",
"pale", "pastel", "pearl", "pecan", "peppy", "pilot", "placid", "plain", "plum", "plush",
"poised", "polar", "polished", "poplar", "prime", "proof", "proud", "pure", "quartz", "quick",
"quiet", "rapid", "raspy", "ready", "regal", "rooted", "rosy", "round", "royal", "ruby",
"ruddy", "russet", "rustic", "sage", "salty", "sandy", "satin", "scenic", "sedge", "serene",
"sharp", "sheer", "silky", "silver", "sleek", "smart", "smooth", "snowy", "solar", "solid",
"south", "spry", "stark", "steady", "steel", "steep", "still", "stoic", "stony", "stout",
"sturdy", "suede", "sunny", "supple", "sure", "swift", "tall", "tawny", "teal", "terse",
"thick", "tidal", "tidy", "timber", "topaz", "total", "trim", "tropic", "true", "tulip",
"upper", "urban", "valid", "vast", "velvet", "verde", "vivid", "vocal", "warm", "waxen",
"west", "whole", "wide", "wild", "wise", "witty", "woven", "young", "zealous", "zephyr",
"zesty", "zinc",
];
const NOUNS: &[&str] = &[
"anchor", "anvil", "arbor", "arch", "arrow", "atlas", "badge", "badger", "basin", "bay",
"beacon", "beam", "bell", "birch", "blade", "bloom", "bluff", "bolt", "bower", "breeze",
"bridge", "brook", "bunting", "cabin", "cairn", "canyon", "cape", "cedar", "chasm", "cliff",
"cloud", "clover", "coast", "cobble", "colt", "comet", "condor", "coral", "cove", "crane",
"crater", "creek", "crest", "curlew", "cypress", "dale", "dawn", "delta", "den", "dove",
"drake", "drift", "drum", "dune", "dusk", "eagle", "echo", "egret", "elk", "elm", "ember",
"falcon", "fawn", "fern", "ferry", "field", "finch", "fjord", "flame", "flint", "flower",
"forge", "fossil", "fox", "frost", "gale", "garnet", "gate", "gazelle", "geyser", "glade",
"glen", "gorge", "granite", "grove", "gull", "harbor", "hare", "haven", "hawk", "hazel",
"heath", "hedge", "heron", "hill", "hollow", "horizon", "ibis", "inlet", "isle", "ivy",
"jackal", "jasper", "juniper", "kestrel", "kinglet", "knoll", "lagoon", "lake", "lantern",
"larch", "lark", "laurel", "lava", "leaf", "ledge", "lily", "linden", "lodge", "loft", "lotus",
"lynx", "mantle", "maple", "marble", "marsh", "marten", "meadow", "merlin", "mesa", "mill",
"mint", "moon", "moose", "moss", "newt", "north", "nutmeg", "oak", "oasis", "obsidian",
"orbit", "orchid", "oriole", "osprey", "otter", "owl", "palm", "panther", "pass", "path",
"peak", "pebble", "pelican", "peony", "perch", "pier", "pine", "plover", "plume", "pond",
"poppy", "prairie", "prism", "puma", "quail", "quarry", "quartz", "rain", "rampart", "range",
"raven", "ravine", "reed", "reef", "ridge", "river", "robin", "rowan", "sage", "salmon",
"sequoia", "shore", "shrike", "sigma", "sky", "slate", "slope", "snow", "spark", "sparrow",
"spider", "spruce", "stag", "star", "stone", "stork", "storm", "stream", "summit", "swift",
"sycamore", "tern", "terrace", "thistle", "thorn", "thrush", "tide", "timber", "torch",
"tower", "trail", "trout", "tulip", "tundra", "vale", "valley", "veranda", "viper", "vista",
"vole", "walrus", "warbler", "willow", "wolf", "wren", "yew", "zenith",
];
/// Generates a worktree name in `"adjective-noun"` format (e.g. `"swift-falcon"`).
///
/// Tries up to 10 random combinations, skipping any name that already appears
/// in `existing_names`. Returns `None` if no unused name is found.
pub fn generate_worktree_name(existing_names: &[&str], rng: &mut impl Rng) -> Option<String> {
let existing: HashSet<&str> = existing_names.iter().copied().collect();
for _ in 0..10 {
let adjective = ADJECTIVES[rng.random_range(0..ADJECTIVES.len())];
let noun = NOUNS[rng.random_range(0..NOUNS.len())];
let name = format!("{adjective}-{noun}");
if !existing.contains(name.as_str()) {
return Some(name);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use rand::rngs::StdRng;
#[gpui::test(iterations = 10)]
fn test_generate_worktree_name_format(mut rng: StdRng) {
let name = generate_worktree_name(&[], &mut rng).unwrap();
let (adjective, noun) = name.split_once('-').expect("name should contain a hyphen");
assert!(
ADJECTIVES.contains(&adjective),
"{adjective:?} is not in ADJECTIVES"
);
assert!(NOUNS.contains(&noun), "{noun:?} is not in NOUNS");
}
#[gpui::test(iterations = 100)]
fn test_generate_worktree_name_avoids_existing(mut rng: StdRng) {
let existing = &["swift-falcon", "calm-river", "bold-cedar"];
let name = generate_worktree_name(existing, &mut rng).unwrap();
for &branch in existing {
assert_ne!(
name, branch,
"generated name should not match an existing branch"
);
}
}
#[gpui::test]
fn test_generate_worktree_name_returns_none_when_stuck(mut rng: StdRng) {
let all_names: Vec<String> = ADJECTIVES
.iter()
.flat_map(|adj| NOUNS.iter().map(move |noun| format!("{adj}-{noun}")))
.collect();
let refs: Vec<&str> = all_names.iter().map(|s| s.as_str()).collect();
let result = generate_worktree_name(&refs, &mut rng);
assert!(result.is_none());
}
#[test]
fn test_adjectives_are_valid() {
let mut seen = HashSet::default();
for &word in ADJECTIVES {
assert!(seen.insert(word), "duplicate entry in ADJECTIVES: {word:?}");
}
for window in ADJECTIVES.windows(2) {
assert!(
window[0] < window[1],
"ADJECTIVES is not sorted: {0:?} should come before {1:?}",
window[0],
window[1],
);
}
for &word in ADJECTIVES {
assert!(
!word.contains('-'),
"ADJECTIVES entry contains a hyphen: {word:?}"
);
assert!(
word.chars().all(|c| c.is_lowercase()),
"ADJECTIVES entry is not all lowercase: {word:?}"
);
}
}
#[test]
fn test_nouns_are_valid() {
let mut seen = HashSet::default();
for &word in NOUNS {
assert!(seen.insert(word), "duplicate entry in NOUNS: {word:?}");
}
for window in NOUNS.windows(2) {
assert!(
window[0] < window[1],
"NOUNS is not sorted: {0:?} should come before {1:?}",
window[0],
window[1],
);
}
for &word in NOUNS {
assert!(
!word.contains('-'),
"NOUNS entry contains a hyphen: {word:?}"
);
assert!(
word.chars().all(|c| c.is_lowercase()),
"NOUNS entry is not all lowercase: {word:?}"
);
}
}
}