Skip to content

Commit 0cabafb

Browse files
committed
project_panel: Add granular auto-open file settings (zed-industries#40234)
- Based on zed-industries#40234, and improvement of zed-industries#40331 Release Notes: - Added granular settings to control when files auto-open in the project panel (project_panel.auto_open.on_create, on_paste, on_drop) --- - Add nested �uto_open settings structure with three boolean options: - on_create: Auto-open newly created files (default: true) - on_paste: Auto-open pasted/duplicated files (default: false - fixes binary file errors) - on_drop: Auto-open externally dropped files (default: true) - Implement settings checks in paste, new file, and drop operations - Add Settings UI controls for all three auto-open options - Add test coverage with helper function for settings configuration - Add user documentation with examples and use cases
1 parent 4dd463f commit 0cabafb

File tree

8 files changed

+425
-35
lines changed

8 files changed

+425
-35
lines changed

assets/settings/default.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -725,8 +725,15 @@
725725
"hide_root": false,
726726
// Whether to hide the hidden entries in the project panel.
727727
"hide_hidden": false,
728-
// Whether to automatically open files when pasting them in the project panel.
729-
"open_file_on_paste": true
728+
// Settings for automatically opening files in various operations.
729+
"auto_open": {
730+
// Automatically open newly created files in the editor
731+
"on_create": true,
732+
// Automatically open files after pasting or duplicating them
733+
"on_paste": false,
734+
// Automatically open files dropped from external sources
735+
"on_drop": true
736+
}
730737
},
731738
"outline_panel": {
732739
// Whether to show the outline panel button in the status bar

crates/project_panel/src/project_panel.rs

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,7 +1647,10 @@ impl ProjectPanel {
16471647
}
16481648
project_panel.update_visible_entries(None, false, false, window, cx);
16491649
if is_new_entry && !is_dir {
1650-
project_panel.open_entry(new_entry.id, true, false, cx);
1650+
let settings = ProjectPanelSettings::get_global(cx);
1651+
if settings.auto_open.should_open_on_create() {
1652+
project_panel.open_entry(new_entry.id, true, false, cx);
1653+
}
16511654
}
16521655
cx.notify();
16531656
})?;
@@ -2698,16 +2701,16 @@ impl ProjectPanel {
26982701
});
26992702

27002703
if item_count == 1 {
2701-
// open entry if not dir, setting is enabled, and only focus if rename is not pending
2702-
if !entry.is_dir()
2703-
&& ProjectPanelSettings::get_global(cx).open_file_on_paste
2704-
{
2705-
project_panel.open_entry(
2706-
entry.id,
2707-
disambiguation_range.is_none(),
2708-
false,
2709-
cx,
2710-
);
2704+
if !entry.is_dir() {
2705+
let settings = ProjectPanelSettings::get_global(cx);
2706+
if settings.auto_open.should_open_on_paste() {
2707+
project_panel.open_entry(
2708+
entry.id,
2709+
disambiguation_range.is_none(),
2710+
false,
2711+
cx,
2712+
);
2713+
}
27112714
}
27122715

27132716
// if only one entry was pasted and it was disambiguated, open the rename editor
@@ -3582,11 +3585,13 @@ impl ProjectPanel {
35823585

35833586
let opened_entries = task.await.with_context(|| "failed to copy external paths")?;
35843587
this.update(cx, |this, cx| {
3585-
if open_file_after_drop && !opened_entries.is_empty() {
3588+
if open_file_after_drop && !opened_entries.is_empty() {
3589+
let settings = ProjectPanelSettings::get_global(cx);
3590+
if settings.auto_open.should_open_on_drop() {
35863591
this.open_entry(opened_entries[0], true, false, cx);
35873592
}
3588-
})
3589-
}
3593+
}
3594+
})
35903595
.log_err().await
35913596
})
35923597
.detach();

crates/project_panel/src/project_panel_settings.rs

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub struct ProjectPanelSettings {
2929
pub hide_root: bool,
3030
pub hide_hidden: bool,
3131
pub drag_and_drop: bool,
32-
pub open_file_on_paste: bool,
32+
pub auto_open: AutoOpenSettings,
3333
}
3434

3535
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -45,6 +45,30 @@ pub struct ScrollbarSettings {
4545
pub show: Option<ShowScrollbar>,
4646
}
4747

48+
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
49+
pub struct AutoOpenSettings {
50+
pub on_create: bool,
51+
pub on_paste: bool,
52+
pub on_drop: bool,
53+
}
54+
55+
impl AutoOpenSettings {
56+
#[inline]
57+
pub fn should_open_on_create(self) -> bool {
58+
self.on_create
59+
}
60+
61+
#[inline]
62+
pub fn should_open_on_paste(self) -> bool {
63+
self.on_paste
64+
}
65+
66+
#[inline]
67+
pub fn should_open_on_drop(self) -> bool {
68+
self.on_drop
69+
}
70+
}
71+
4872
impl ScrollbarVisibility for ProjectPanelSettings {
4973
fn visibility(&self, cx: &ui::App) -> ShowScrollbar {
5074
self.scrollbar
@@ -80,7 +104,52 @@ impl Settings for ProjectPanelSettings {
80104
hide_root: project_panel.hide_root.unwrap(),
81105
hide_hidden: project_panel.hide_hidden.unwrap(),
82106
drag_and_drop: project_panel.drag_and_drop.unwrap(),
83-
open_file_on_paste: project_panel.open_file_on_paste.unwrap(),
107+
auto_open: {
108+
let auto_open = project_panel.auto_open.unwrap();
109+
AutoOpenSettings {
110+
on_create: auto_open.on_create.unwrap(),
111+
on_paste: auto_open.on_paste.unwrap(),
112+
on_drop: auto_open.on_drop.unwrap(),
113+
}
114+
},
115+
}
116+
}
117+
118+
fn import_from_vscode(
119+
vscode: &settings::VsCodeSettings,
120+
current: &mut settings::SettingsContent,
121+
) {
122+
if let Some(hide_gitignore) = vscode.read_bool("explorer.excludeGitIgnore") {
123+
current.project_panel.get_or_insert_default().hide_gitignore = Some(hide_gitignore);
124+
}
125+
if let Some(auto_reveal) = vscode.read_bool("explorer.autoReveal") {
126+
current
127+
.project_panel
128+
.get_or_insert_default()
129+
.auto_reveal_entries = Some(auto_reveal);
130+
}
131+
if let Some(compact_folders) = vscode.read_bool("explorer.compactFolders") {
132+
current.project_panel.get_or_insert_default().auto_fold_dirs = Some(compact_folders);
133+
}
134+
135+
if Some(false) == vscode.read_bool("git.decorations.enabled") {
136+
current.project_panel.get_or_insert_default().git_status = Some(false);
137+
}
138+
if Some(false) == vscode.read_bool("problems.decorations.enabled") {
139+
current
140+
.project_panel
141+
.get_or_insert_default()
142+
.show_diagnostics = Some(ShowDiagnostics::Off);
143+
}
144+
if let (Some(false), Some(false)) = (
145+
vscode.read_bool("explorer.decorations.badges"),
146+
vscode.read_bool("explorer.decorations.colors"),
147+
) {
148+
current.project_panel.get_or_insert_default().git_status = Some(false);
149+
current
150+
.project_panel
151+
.get_or_insert_default()
152+
.show_diagnostics = Some(ShowDiagnostics::Off);
84153
}
85154
}
86155
}

crates/project_panel/src/project_panel_tests.rs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,7 @@ async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) {
11611161
#[gpui::test]
11621162
async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
11631163
init_test(cx);
1164+
set_auto_open_settings(cx, true, true, true);
11641165

11651166
let fs = FakeFs::new(cx.executor());
11661167
fs.insert_tree(
@@ -1257,6 +1258,140 @@ async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
12571258
});
12581259
}
12591260

1261+
#[gpui::test]
1262+
async fn test_paste_with_auto_open_disabled(cx: &mut gpui::TestAppContext) {
1263+
init_test(cx);
1264+
set_auto_open_settings(cx, true, false, true);
1265+
1266+
let fs = FakeFs::new(cx.executor());
1267+
fs.insert_tree(
1268+
"/root1",
1269+
json!({
1270+
"test.txt": "content"
1271+
}),
1272+
)
1273+
.await;
1274+
1275+
let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
1276+
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1277+
let cx = &mut VisualTestContext::from_window(*workspace, cx);
1278+
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1279+
cx.run_until_parked();
1280+
1281+
panel.update_in(cx, |panel, window, cx| {
1282+
panel.select_next(&Default::default(), window, cx);
1283+
});
1284+
1285+
panel.update_in(cx, |panel, window, cx| {
1286+
panel.copy(&Default::default(), window, cx);
1287+
panel.paste(&Default::default(), window, cx);
1288+
});
1289+
cx.executor().run_until_parked();
1290+
1291+
// File should be pasted but not opened (no EDITOR marker)
1292+
assert_eq!(
1293+
visible_entries_as_strings(&panel, 0..50, cx),
1294+
&[
1295+
"v root1",
1296+
" test.txt",
1297+
" [EDITOR: 'test copy.txt'] <== selected <== marked",
1298+
]
1299+
);
1300+
1301+
// Rename editor appears for disambiguation, confirm it
1302+
panel.update_in(cx, |panel, window, cx| {
1303+
assert!(panel.confirm_edit(window, cx).is_none())
1304+
});
1305+
cx.executor().run_until_parked();
1306+
1307+
// After confirming rename, file should exist but still not be opened as a separate editor
1308+
let entries = visible_entries_as_strings(&panel, 0..50, cx);
1309+
assert!(entries.contains(&" test copy.txt".to_string()));
1310+
assert!(entries.contains(&" test.txt".to_string()));
1311+
}
1312+
1313+
#[gpui::test]
1314+
async fn test_paste_with_auto_open_enabled(cx: &mut gpui::TestAppContext) {
1315+
init_test(cx);
1316+
set_auto_open_settings(cx, true, true, true);
1317+
1318+
let fs = FakeFs::new(cx.executor());
1319+
fs.insert_tree(
1320+
"/root1",
1321+
json!({
1322+
"test.txt": "content"
1323+
}),
1324+
)
1325+
.await;
1326+
1327+
let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
1328+
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1329+
let cx = &mut VisualTestContext::from_window(*workspace, cx);
1330+
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1331+
cx.run_until_parked();
1332+
1333+
panel.update_in(cx, |panel, window, cx| {
1334+
panel.select_next(&Default::default(), window, cx);
1335+
});
1336+
1337+
panel.update_in(cx, |panel, window, cx| {
1338+
panel.copy(&Default::default(), window, cx);
1339+
panel.paste(&Default::default(), window, cx);
1340+
});
1341+
cx.executor().run_until_parked();
1342+
1343+
// File should be pasted AND opened (EDITOR marker present)
1344+
assert_eq!(
1345+
visible_entries_as_strings(&panel, 0..50, cx),
1346+
&[
1347+
"v root1",
1348+
" test.txt",
1349+
" [EDITOR: 'test copy.txt'] <== selected <== marked",
1350+
]
1351+
);
1352+
}
1353+
1354+
#[gpui::test]
1355+
async fn test_duplicate_respects_auto_open_setting(cx: &mut gpui::TestAppContext) {
1356+
init_test(cx);
1357+
set_auto_open_settings(cx, true, false, true);
1358+
1359+
let fs = FakeFs::new(cx.executor());
1360+
fs.insert_tree(
1361+
"/root1",
1362+
json!({
1363+
"test.txt": "content"
1364+
}),
1365+
)
1366+
.await;
1367+
1368+
let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
1369+
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1370+
let cx = &mut VisualTestContext::from_window(*workspace, cx);
1371+
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
1372+
cx.run_until_parked();
1373+
1374+
panel.update_in(cx, |panel, window, cx| {
1375+
panel.select_next(&Default::default(), window, cx);
1376+
});
1377+
1378+
// Duplicate uses paste internally, so it should respect on_paste setting
1379+
panel.update_in(cx, |panel, window, cx| {
1380+
panel.duplicate(&Duplicate, window, cx);
1381+
});
1382+
cx.executor().run_until_parked();
1383+
1384+
// File should be duplicated but not opened (no additional EDITOR marker beyond rename)
1385+
assert_eq!(
1386+
visible_entries_as_strings(&panel, 0..50, cx),
1387+
&[
1388+
"v root1",
1389+
" test.txt",
1390+
" [EDITOR: 'test copy.txt'] <== selected <== marked",
1391+
]
1392+
);
1393+
}
1394+
12601395
#[gpui::test]
12611396
async fn test_cut_paste(cx: &mut gpui::TestAppContext) {
12621397
init_test(cx);
@@ -6987,6 +7122,21 @@ fn init_test_with_editor(cx: &mut TestAppContext) {
69877122
});
69887123
}
69897124

7125+
fn set_auto_open_settings(cx: &mut TestAppContext, on_create: bool, on_paste: bool, on_drop: bool) {
7126+
cx.update(|cx| {
7127+
cx.update_global::<SettingsStore, _>(|store, cx| {
7128+
store.update_user_settings(cx, |settings| {
7129+
settings.project_panel.get_or_insert_default().auto_open =
7130+
Some(settings::ProjectPanelAutoOpenSettings {
7131+
on_create: Some(on_create),
7132+
on_paste: Some(on_paste),
7133+
on_drop: Some(on_drop),
7134+
});
7135+
});
7136+
})
7137+
});
7138+
}
7139+
69907140
fn ensure_single_file_is_opened(
69917141
window: &WindowHandle<Workspace>,
69927142
expected_path: &str,

crates/settings/src/settings_content/workspace.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,23 @@ impl OnLastWindowClosed {
488488
}
489489
}
490490

491+
#[skip_serializing_none]
492+
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
493+
pub struct ProjectPanelAutoOpenSettings {
494+
/// Whether to automatically open newly created files in the editor.
495+
///
496+
/// Default: true
497+
pub on_create: Option<bool>,
498+
/// Whether to automatically open files after pasting or duplicating them.
499+
///
500+
/// Default: false
501+
pub on_paste: Option<bool>,
502+
/// Whether to automatically open files dropped from external sources.
503+
///
504+
/// Default: true
505+
pub on_drop: Option<bool>,
506+
}
507+
491508
#[skip_serializing_none]
492509
#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
493510
pub struct ProjectPanelSettingsContent {
@@ -566,10 +583,8 @@ pub struct ProjectPanelSettingsContent {
566583
///
567584
/// Default: true
568585
pub drag_and_drop: Option<bool>,
569-
/// Whether to automatically open files when pasting them in the project panel.
570-
///
571-
/// Default: true
572-
pub open_file_on_paste: Option<bool>,
586+
/// Settings for automatically opening files in various operations.
587+
pub auto_open: Option<ProjectPanelAutoOpenSettings>,
573588
}
574589

575590
#[derive(

crates/settings/src/vscode_import.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,6 @@ impl VsCodeSettings {
655655
hide_root: None,
656656
indent_guides: None,
657657
indent_size: None,
658-
open_file_on_paste: None,
659658
scrollbar: None,
660659
show_diagnostics: self
661660
.read_bool("problems.decorations.enabled")

0 commit comments

Comments
 (0)