From c0f405980655a020fcbb29f9b2b89efee7b0bc69 Mon Sep 17 00:00:00 2001 From: Ibrahim Khan Date: Tue, 16 Jun 2026 02:48:24 -0700 Subject: [PATCH] project_panel: Select the whole folder name when renaming (#59390) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective - Renaming a folder whose name contains a dot pre-selects only the text before the last dot, treating the suffix as if it were a file extension. - Fixes #59294 ## Solution - `ProjectPanel::rename_impl` computed the rename editor's initial selection with `Path::file_stem().len()` for every entry. For a directory such as `my.folder`, `file_stem()` returns `my`, so only `my` was selected. - Directories have no extension, so select the whole name for them. Files keep the existing behavior (the last extension is left unselected for quick renames). ## Testing - Added `test_rename_folder_with_dot_selects_whole_name` in `project_panel_tests.rs`. It fails before the change (folder selection ends at offset 2 of `my.folder`) and passes after (offset 9, the whole name); it also asserts a file still leaves the last extension unselected. - `cargo test -p project_panel` — all 100 tests pass. - `cargo clippy -p project_panel --all-targets --all-features -- --deny warnings` and `cargo fmt -p project_panel -- --check` are clean. ## 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 and icon guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable --- Release Notes: - Fixed renaming a folder with a dot in its name selecting only the part before the dot. --------- Co-authored-by: Finn Evers --- crates/project_panel/src/project_panel.rs | 11 +++- .../project_panel/src/project_panel_tests.rs | 65 +++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 473d590356d..9c23e7ff8cf 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -2184,9 +2184,14 @@ impl ProjectPanel { }); let file_name = entry.path.file_name().unwrap_or_default().to_string(); let selection = selection.unwrap_or_else(|| { - let file_stem = entry.path.file_stem().map(|s| s.to_string()); - let selection_end = - file_stem.map_or(file_name.len(), |file_stem| file_stem.len()); + // Folders have no extension, so select the whole name. Only + // files keep their extension unselected for quick renames. + let selection_end = if entry.is_dir() { + file_name.len() + } else { + let file_stem = entry.path.file_stem(); + file_stem.map_or(file_name.len(), |file_stem| file_stem.len()) + }; 0..selection_end }); self.filename_editor.update(cx, |editor, cx| { diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index 8a745af7857..a228749dc16 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -1165,6 +1165,71 @@ async fn test_editing_files(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_rename_folder_with_dot_selects_whole_name(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/root1", + json!({ + "my.folder": {}, + "archive.tar.gz": "", + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; + let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = window + .read_with(cx, |mw, _| mw.workspace().clone()) + .unwrap(); + let cx = &mut VisualTestContext::from_window(window.into(), cx); + let panel = workspace.update_in(cx, |workspace, window, cx| { + let panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(panel.clone(), window, cx); + panel + }); + cx.run_until_parked(); + + // Renaming a folder whose name contains a dot should pre-select the whole + // name. The dot belongs to the directory name; it is not a file extension. + select_path(&panel, "root1/my.folder", cx); + panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); + panel.update_in(cx, |panel, window, cx| { + panel.filename_editor.update(cx, |editor, cx| { + let selections = editor + .selections + .all::(&editor.display_snapshot(cx)); + assert_eq!(selections.len(), 1); + assert_eq!(selections[0].start, MultiBufferOffset(0)); + assert_eq!( + selections[0].end, + MultiBufferOffset("my.folder".len()), + "Renaming a folder should select the whole name, including dots" + ); + }); + panel.cancel(&Cancel, window, cx); + }); + cx.run_until_parked(); + + // Files keep the existing behavior: the last extension stays unselected. + select_path(&panel, "root1/archive.tar.gz", cx); + panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); + panel.update_in(cx, |panel, _, cx| { + panel.filename_editor.update(cx, |editor, cx| { + let selections = editor + .selections + .all::(&editor.display_snapshot(cx)); + assert_eq!( + selections[0].end, + MultiBufferOffset("archive.tar".len()), + "Renaming a file should keep the last extension unselected" + ); + }); + }); +} + #[gpui::test(iterations = 10)] async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { init_test(cx);