project_panel: Allow renaming a root to match a child directory (#64268)

# Objective

- Allow renaming a project root folder to the same name as one of its
child folders.
- Fixes #64255

## Solution

- Skip child-entry collision checks when renaming a project root.
- Keep existing collision checks for other entries.

## Testing

- Updated and ran `test_rename_root_of_worktree`.
- Manually tested on macOS.

## Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and
[icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md)
guidelines)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

---

Release Notes:

- Fixed an issue where a project root folder could not be renamed to
match a child folder.

---------

Co-authored-by: dino <dinojoaocosta@gmail.com>
This commit is contained in:
D4r3N 2026-09-22 10:19:27 +00:00 • committed by GitHub
parent 05c2c9aba0
commit 02238cd4b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 34 additions and 28 deletions

View file

@ -2041,10 +2041,8 @@ impl ProjectPanel {
return;
};
if let Some(worktree) = self
.project
.read(cx)
.worktree_for_id(edit_state.worktree_id, cx)
let project = self.project.read(cx);
if let Some(worktree) = project.worktree_for_id(edit_state.worktree_id, cx)
&& let Some(entry) = worktree.read(cx).entry_for_id(edit_state.entry_id)
{
let mut already_exists = false;
@ -2054,12 +2052,18 @@ impl ProjectPanel {
already_exists = true;
}
} else {
let new_path = if let Some(parent) = entry.path.clone().parent() {
parent.join(&filename)
} else {
filename.to_owned()
let new_path = match entry.path.clone().parent() {
Some(parent) => parent.join(&filename),
None => filename.to_owned(),
};
if let Some(existing) = worktree.read(cx).entry_for_path(&new_path)
// We skip the collision check for worktree roots as the
// lookup resolves paths relative to the root itself,
// whereas renaming the root should resolve it against its
// parent directory. Otherwise, renaming `bar/` to `foo/`
// would report a false collision with `bar/foo/`.
if !project.entry_is_worktree_root(entry.id, cx)
&& let Some(existing) = worktree.read(cx).entry_for_path(&new_path)
&& existing.id != entry.id
{
already_exists = true;
@ -2142,12 +2146,19 @@ impl ProjectPanel {
});
changes = vec![Change::Created(new_project_path)];
} else {
let new_path = if let Some(parent) = entry.path.parent() {
parent.join(&filename).into()
} else {
filename.clone()
let new_path = match entry.path.parent() {
Some(parent) => parent.join(&filename).into(),
None => filename.clone(),
};
if let Some(existing) = worktree.read(cx).entry_for_path(&new_path) {
// We skip the collision check for worktree roots as the lookup
// resolves paths relative to the root itself, whereas renaming the
// root should resolve it against its parent directory. Otherwise,
// renaming `bar/` to `foo/` would report a false collision with
// `bar/foo/`.
if !self.project.read(cx).entry_is_worktree_root(entry.id, cx)
&& let Some(existing) = worktree.read(cx).entry_for_path(&new_path)
{
if existing.id == entry.id && refocus {
window.focus(&self.focus_handle, cx);
}

View file

@ -4620,7 +4620,7 @@ async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
&["v root1 <== selected", " v dir1", " file1.txt",],
);
// Rename root1 to new_root1
// Rename root1 to match the name of its child directory.
panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx));
assert_eq!(
@ -4635,30 +4635,25 @@ async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) {
let confirm = panel.update_in(cx, |panel, window, cx| {
panel
.filename_editor
.update(cx, |editor, cx| editor.set_text("new_root1", window, cx));
panel.confirm_edit(true, window, cx).unwrap()
.update(cx, |editor, cx| editor.set_text("dir1", window, cx));
panel
.confirm_edit(true, window, cx)
.expect("should be able to rename `root1` to `dir1`")
});
confirm.await.unwrap();
cx.run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..20, cx),
&[
"v new_root1 <== selected",
" v dir1",
" file1.txt",
],
&["v dir1 <== selected", " v dir1", " file1.txt",],
"Should update worktree name"
);
// Ensure internal paths have been updated
select_path(&panel, "new_root1/dir1/file1.txt", cx);
select_path(&panel, "dir1/dir1/file1.txt", cx);
assert_eq!(
visible_entries_as_strings(&panel, 0..20, cx),
&[
"v new_root1",
" v dir1",
" file1.txt <== selected",
],
&["v dir1", " v dir1", " file1.txt <== selected",],
"Files in renamed worktree are selectable"
);
}