From 30597a0cbafdf44255e7a252f1b1b3e2e12f0668 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 10 Dec 2025 11:33:49 +0100 Subject: [PATCH] project_panel: Fix create entry with trailing dot duplicating on windows (#44524) Release Notes: - Fixed an issue where creating a file through the project panel with a trailing dot in its name would duplicate the entries with and without the dot Co-authored by: Smit Barmase --- crates/languages/src/go.rs | 4 +- crates/project_panel/src/project_panel.rs | 12 +++- .../project_panel/src/project_panel_tests.rs | 68 +++++++++++++++++++ 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index a8699fe9c2dc8cf99ca46a16fe75b1de6eea7ffa..130e142076b8c6ec0393e4f0d617c3a522b2ef22 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -73,7 +73,9 @@ impl LspInstaller for GoLspAdapter { delegate.show_notification(NOTIFICATION_MESSAGE, cx); })? } - anyhow::bail!("cannot install gopls"); + anyhow::bail!( + "Could not install the Go language server `gopls`, because `go` was not found." + ); } let release = diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index e53be8cd33fa265dfadb201b2bcd613c54ffb9dd..0c633bcfcda2415bae84201f7e67a91fbd5a866d 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1663,12 +1663,20 @@ impl ProjectPanel { let edit_state = self.state.edit_state.as_mut()?; let worktree_id = edit_state.worktree_id; let is_new_entry = edit_state.is_new_entry(); - let filename = self.filename_editor.read(cx).text(cx); + let mut filename = self.filename_editor.read(cx).text(cx); + let path_style = self.project.read(cx).path_style(cx); + if path_style.is_windows() { + // on windows, trailing dots are ignored in paths + // this can cause project panel to create a new entry with a trailing dot + // while the actual one without the dot gets populated by the file watcher + while let Some(trimmed) = filename.strip_suffix('.') { + filename = trimmed.to_string(); + } + } if filename.trim().is_empty() { return None; } - let path_style = self.project.read(cx).path_style(cx); let filename_indicates_dir = if path_style.is_windows() { filename.ends_with('/') || filename.ends_with('\\') } else { diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs index 6cf487bf9849a9252abc21504171b8c6bdf7e298..3f54e01927d67541fb3b17e88facadd1e6776bb6 100644 --- a/crates/project_panel/src/project_panel_tests.rs +++ b/crates/project_panel/src/project_panel_tests.rs @@ -6612,6 +6612,74 @@ async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppC ); } +#[cfg(windows)] +#[gpui::test] +async fn test_create_entry_with_trailing_dot_windows(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/root"), + json!({ + "dir1": { + "file1.txt": "", + }, + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; + let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let panel = workspace + .update(cx, |workspace, window, cx| { + let panel = ProjectPanel::new(workspace, window, cx); + workspace.add_panel(panel.clone(), window, cx); + panel + }) + .unwrap(); + cx.run_until_parked(); + + #[rustfmt::skip] + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root", + " > dir1", + ], + "Initial state with nothing selected" + ); + + panel.update_in(cx, |panel, window, cx| { + panel.new_file(&NewFile, window, cx); + }); + cx.run_until_parked(); + panel.update_in(cx, |panel, window, cx| { + assert!(panel.filename_editor.read(cx).is_focused(window)); + }); + panel + .update_in(cx, |panel, window, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("foo.", window, cx)); + panel.confirm_edit(true, window, cx).unwrap() + }) + .await + .unwrap(); + cx.run_until_parked(); + #[rustfmt::skip] + assert_eq!( + visible_entries_as_strings(&panel, 0..20, cx), + &[ + "v root", + " > dir1", + " foo <== selected <== marked", + ], + "A new file is created under the root directory without the trailing dot" + ); +} + #[gpui::test] async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) { init_test(cx);