project_panel: Add `auto_open` settings (#40435)

Miguel CΓ‘rdenas and Smit Barmase created

- Based on #40234, and improvement of #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)

<img width="662" height="367" alt="Screenshot_2025-10-16_17-28-31"
src="https://github.com/user-attachments/assets/930a0a50-fc89-4c5d-8d05-b1fa2279de8b"
/>

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>

Change summary

Cargo.lock                                              |   1 
assets/settings/default.json                            |  11 
crates/migrator/src/migrations.rs                       |   6 
crates/migrator/src/migrations/m_2025_11_12/settings.rs |  84 +++
crates/migrator/src/migrator.rs                         |  53 ++
crates/project_panel/Cargo.toml                         |   1 
crates/project_panel/src/project_panel.rs               |  29 
crates/project_panel/src/project_panel_settings.rs      |  35 +
crates/project_panel/src/project_panel_tests.rs         | 257 ++++++++++
crates/settings/src/settings_content/workspace.rs       |  23 
crates/settings/src/vscode_import.rs                    |   2 
crates/settings_ui/src/page_data.rs                     |  48 +
docs/src/configuring-zed.md                             |  26 +
13 files changed, 542 insertions(+), 34 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -13078,6 +13078,7 @@ dependencies = [
  "settings",
  "smallvec",
  "telemetry",
+ "tempfile",
  "theme",
  "ui",
  "util",

assets/settings/default.json πŸ”—

@@ -748,8 +748,15 @@
     "hide_root": false,
     // Whether to hide the hidden entries in the project panel.
     "hide_hidden": false,
-    // Whether to automatically open files when pasting them in the project panel.
-    "open_file_on_paste": true
+    // Settings for automatically opening files.
+    "auto_open": {
+      // Whether to automatically open newly created files in the editor.
+      "on_create": true,
+      // Whether to automatically open files after pasting or duplicating them.
+      "on_paste": true,
+      // Whether to automatically open files dropped from external sources.
+      "on_drop": true
+    }
   },
   "outline_panel": {
     // Whether to show the outline panel button in the status bar

crates/migrator/src/migrations.rs πŸ”—

@@ -135,3 +135,9 @@ pub(crate) mod m_2025_10_21 {
 
     pub(crate) use settings::make_relative_line_numbers_an_enum;
 }
+
+pub(crate) mod m_2025_11_12 {
+    mod settings;
+
+    pub(crate) use settings::SETTINGS_PATTERNS;
+}

crates/migrator/src/migrations/m_2025_11_12/settings.rs πŸ”—

@@ -0,0 +1,84 @@
+use std::ops::Range;
+use tree_sitter::{Query, QueryMatch};
+
+use crate::MigrationPatterns;
+use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
+
+pub const SETTINGS_PATTERNS: MigrationPatterns = &[
+    (
+        SETTINGS_NESTED_KEY_VALUE_PATTERN,
+        rename_open_file_on_paste_setting,
+    ),
+    (
+        SETTINGS_NESTED_KEY_VALUE_PATTERN,
+        replace_open_file_on_paste_setting_value,
+    ),
+];
+
+fn rename_open_file_on_paste_setting(
+    contents: &str,
+    mat: &QueryMatch,
+    query: &Query,
+) -> Option<(Range<usize>, String)> {
+    if !is_project_panel_open_file_on_paste(contents, mat, query) {
+        return None;
+    }
+
+    let setting_name_ix = query.capture_index_for_name("setting_name")?;
+    let setting_name_range = mat
+        .nodes_for_capture_index(setting_name_ix)
+        .next()?
+        .byte_range();
+
+    Some((setting_name_range, "auto_open".to_string()))
+}
+
+fn replace_open_file_on_paste_setting_value(
+    contents: &str,
+    mat: &QueryMatch,
+    query: &Query,
+) -> Option<(Range<usize>, String)> {
+    if !is_project_panel_open_file_on_paste(contents, mat, query) {
+        return None;
+    }
+
+    let value_ix = query.capture_index_for_name("setting_value")?;
+    let value_node = mat.nodes_for_capture_index(value_ix).next()?;
+    let value_range = value_node.byte_range();
+    let value_text = contents.get(value_range.clone())?.trim();
+
+    let normalized_value = match value_text {
+        "true" => "true",
+        "false" => "false",
+        _ => return None,
+    };
+
+    Some((
+        value_range,
+        format!("{{ \"on_paste\": {normalized_value} }}"),
+    ))
+}
+
+fn is_project_panel_open_file_on_paste(contents: &str, mat: &QueryMatch, query: &Query) -> bool {
+    let parent_key_ix = match query.capture_index_for_name("parent_key") {
+        Some(ix) => ix,
+        None => return false,
+    };
+    let parent_range = match mat.nodes_for_capture_index(parent_key_ix).next() {
+        Some(node) => node.byte_range(),
+        None => return false,
+    };
+    if contents.get(parent_range) != Some("project_panel") {
+        return false;
+    }
+
+    let setting_name_ix = match query.capture_index_for_name("setting_name") {
+        Some(ix) => ix,
+        None => return false,
+    };
+    let setting_name_range = match mat.nodes_for_capture_index(setting_name_ix).next() {
+        Some(node) => node.byte_range(),
+        None => return false,
+    };
+    contents.get(setting_name_range) == Some("open_file_on_paste")
+}

crates/migrator/src/migrator.rs πŸ”—

@@ -215,6 +215,10 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
         MigrationType::Json(migrations::m_2025_10_16::restore_code_actions_on_format),
         MigrationType::Json(migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum),
         MigrationType::Json(migrations::m_2025_10_21::make_relative_line_numbers_an_enum),
+        MigrationType::TreeSitter(
+            migrations::m_2025_11_12::SETTINGS_PATTERNS,
+            &SETTINGS_QUERY_2025_11_12,
+        ),
     ];
     run_migrations(text, migrations)
 }
@@ -333,6 +337,10 @@ define_query!(
     SETTINGS_QUERY_2025_10_03,
     migrations::m_2025_10_03::SETTINGS_PATTERNS
 );
+define_query!(
+    SETTINGS_QUERY_2025_11_12,
+    migrations::m_2025_11_12::SETTINGS_PATTERNS
+);
 
 // custom query
 static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
@@ -2193,4 +2201,49 @@ mod tests {
             ),
         );
     }
+
+    #[test]
+    fn test_project_panel_open_file_on_paste_migration() {
+        assert_migrate_settings(
+            &r#"
+            {
+                "project_panel": {
+                    "open_file_on_paste": true
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "project_panel": {
+                        "auto_open": { "on_paste": true }
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+
+        assert_migrate_settings(
+            &r#"
+            {
+                "project_panel": {
+                    "open_file_on_paste": false
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "project_panel": {
+                        "auto_open": { "on_paste": false }
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+    }
 }

crates/project_panel/Cargo.toml πŸ”—

@@ -53,4 +53,5 @@ editor = { workspace = true, features = ["test-support"] }
 gpui = { workspace = true, features = ["test-support"] }
 language = { workspace = true, features = ["test-support"] }
 serde_json.workspace = true
+tempfile.workspace = true
 workspace = { workspace = true, features = ["test-support"] }

crates/project_panel/src/project_panel.rs πŸ”—

@@ -1655,7 +1655,10 @@ impl ProjectPanel {
                             }
                         project_panel.update_visible_entries(None, false, false, window, cx);
                         if is_new_entry && !is_dir {
-                            project_panel.open_entry(new_entry.id, true, false, cx);
+                            let settings = ProjectPanelSettings::get_global(cx);
+                            if settings.auto_open.should_open_on_create() {
+                                project_panel.open_entry(new_entry.id, true, false, cx);
+                            }
                         }
                         cx.notify();
                     })?;
@@ -2709,15 +2712,16 @@ impl ProjectPanel {
 
                             if item_count == 1 {
                                 // open entry if not dir, setting is enabled, and only focus if rename is not pending
-                                if !entry.is_dir()
-                                    && ProjectPanelSettings::get_global(cx).open_file_on_paste
-                                {
-                                    project_panel.open_entry(
-                                        entry.id,
-                                        disambiguation_range.is_none(),
-                                        false,
-                                        cx,
-                                    );
+                                if !entry.is_dir() {
+                                    let settings = ProjectPanelSettings::get_global(cx);
+                                    if settings.auto_open.should_open_on_paste() {
+                                        project_panel.open_entry(
+                                            entry.id,
+                                            disambiguation_range.is_none(),
+                                            false,
+                                            cx,
+                                        );
+                                    }
                                 }
 
                                 // if only one entry was pasted and it was disambiguated, open the rename editor
@@ -3593,7 +3597,10 @@ impl ProjectPanel {
                 let opened_entries = task.await.with_context(|| "failed to copy external paths")?;
                 this.update(cx, |this, cx| {
                     if open_file_after_drop && !opened_entries.is_empty() {
-                        this.open_entry(opened_entries[0], true, false, cx);
+                        let settings = ProjectPanelSettings::get_global(cx);
+                        if settings.auto_open.should_open_on_drop() {
+                            this.open_entry(opened_entries[0], true, false, cx);
+                        }
                     }
                 })
             }

crates/project_panel/src/project_panel_settings.rs πŸ”—

@@ -32,7 +32,7 @@ pub struct ProjectPanelSettings {
     pub hide_root: bool,
     pub hide_hidden: bool,
     pub drag_and_drop: bool,
-    pub open_file_on_paste: bool,
+    pub auto_open: AutoOpenSettings,
 }
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -48,6 +48,30 @@ pub struct ScrollbarSettings {
     pub show: Option<ShowScrollbar>,
 }
 
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct AutoOpenSettings {
+    pub on_create: bool,
+    pub on_paste: bool,
+    pub on_drop: bool,
+}
+
+impl AutoOpenSettings {
+    #[inline]
+    pub fn should_open_on_create(self) -> bool {
+        self.on_create
+    }
+
+    #[inline]
+    pub fn should_open_on_paste(self) -> bool {
+        self.on_paste
+    }
+
+    #[inline]
+    pub fn should_open_on_drop(self) -> bool {
+        self.on_drop
+    }
+}
+
 impl ScrollbarVisibility for ProjectPanelSettings {
     fn visibility(&self, cx: &ui::App) -> ShowScrollbar {
         self.scrollbar
@@ -83,7 +107,14 @@ impl Settings for ProjectPanelSettings {
             hide_root: project_panel.hide_root.unwrap(),
             hide_hidden: project_panel.hide_hidden.unwrap(),
             drag_and_drop: project_panel.drag_and_drop.unwrap(),
-            open_file_on_paste: project_panel.open_file_on_paste.unwrap(),
+            auto_open: {
+                let auto_open = project_panel.auto_open.unwrap();
+                AutoOpenSettings {
+                    on_create: auto_open.on_create.unwrap(),
+                    on_paste: auto_open.on_paste.unwrap(),
+                    on_drop: auto_open.on_drop.unwrap(),
+                }
+            },
         }
     }
 }

crates/project_panel/src/project_panel_tests.rs πŸ”—

@@ -4,7 +4,7 @@ use gpui::{Empty, Entity, TestAppContext, VisualTestContext, WindowHandle};
 use pretty_assertions::assert_eq;
 use project::FakeFs;
 use serde_json::json;
-use settings::SettingsStore;
+use settings::{ProjectPanelAutoOpenSettings, SettingsStore};
 use std::path::{Path, PathBuf};
 use util::{path, paths::PathStyle, rel_path::rel_path};
 use workspace::{
@@ -1998,6 +1998,248 @@ async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
     ensure_no_open_items_and_panes(&workspace, cx);
 }
 
+#[gpui::test]
+async fn test_auto_open_new_file_when_enabled(cx: &mut gpui::TestAppContext) {
+    init_test_with_editor(cx);
+    set_auto_open_settings(
+        cx,
+        ProjectPanelAutoOpenSettings {
+            on_create: Some(true),
+            ..Default::default()
+        },
+    );
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(path!("/root"), json!({})).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, ProjectPanel::new).unwrap();
+    cx.run_until_parked();
+
+    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
+    cx.run_until_parked();
+    panel
+        .update_in(cx, |panel, window, cx| {
+            panel.filename_editor.update(cx, |editor, cx| {
+                editor.set_text("auto-open.rs", window, cx);
+            });
+            panel.confirm_edit(true, window, cx).unwrap()
+        })
+        .await
+        .unwrap();
+    cx.run_until_parked();
+
+    ensure_single_file_is_opened(&workspace, "auto-open.rs", cx);
+}
+
+#[gpui::test]
+async fn test_auto_open_new_file_when_disabled(cx: &mut gpui::TestAppContext) {
+    init_test_with_editor(cx);
+    set_auto_open_settings(
+        cx,
+        ProjectPanelAutoOpenSettings {
+            on_create: Some(false),
+            ..Default::default()
+        },
+    );
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(path!("/root"), json!({})).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, ProjectPanel::new).unwrap();
+    cx.run_until_parked();
+
+    panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx));
+    cx.run_until_parked();
+    panel
+        .update_in(cx, |panel, window, cx| {
+            panel.filename_editor.update(cx, |editor, cx| {
+                editor.set_text("manual-open.rs", window, cx);
+            });
+            panel.confirm_edit(true, window, cx).unwrap()
+        })
+        .await
+        .unwrap();
+    cx.run_until_parked();
+
+    ensure_no_open_items_and_panes(&workspace, cx);
+}
+
+#[gpui::test]
+async fn test_auto_open_on_paste_when_enabled(cx: &mut gpui::TestAppContext) {
+    init_test_with_editor(cx);
+    set_auto_open_settings(
+        cx,
+        ProjectPanelAutoOpenSettings {
+            on_paste: Some(true),
+            ..Default::default()
+        },
+    );
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        path!("/root"),
+        json!({
+            "src": {
+                "original.rs": ""
+            },
+            "target": {}
+        }),
+    )
+    .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, ProjectPanel::new).unwrap();
+    cx.run_until_parked();
+
+    toggle_expand_dir(&panel, "root/src", cx);
+    toggle_expand_dir(&panel, "root/target", cx);
+
+    select_path(&panel, "root/src/original.rs", cx);
+    panel.update_in(cx, |panel, window, cx| {
+        panel.copy(&Default::default(), window, cx);
+    });
+
+    select_path(&panel, "root/target", cx);
+    panel.update_in(cx, |panel, window, cx| {
+        panel.paste(&Default::default(), window, cx);
+    });
+    cx.executor().run_until_parked();
+
+    ensure_single_file_is_opened(&workspace, "target/original.rs", cx);
+}
+
+#[gpui::test]
+async fn test_auto_open_on_paste_when_disabled(cx: &mut gpui::TestAppContext) {
+    init_test_with_editor(cx);
+    set_auto_open_settings(
+        cx,
+        ProjectPanelAutoOpenSettings {
+            on_paste: Some(false),
+            ..Default::default()
+        },
+    );
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(
+        path!("/root"),
+        json!({
+            "src": {
+                "original.rs": ""
+            },
+            "target": {}
+        }),
+    )
+    .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, ProjectPanel::new).unwrap();
+    cx.run_until_parked();
+
+    toggle_expand_dir(&panel, "root/src", cx);
+    toggle_expand_dir(&panel, "root/target", cx);
+
+    select_path(&panel, "root/src/original.rs", cx);
+    panel.update_in(cx, |panel, window, cx| {
+        panel.copy(&Default::default(), window, cx);
+    });
+
+    select_path(&panel, "root/target", cx);
+    panel.update_in(cx, |panel, window, cx| {
+        panel.paste(&Default::default(), window, cx);
+    });
+    cx.executor().run_until_parked();
+
+    ensure_no_open_items_and_panes(&workspace, cx);
+    assert!(
+        find_project_entry(&panel, "root/target/original.rs", cx).is_some(),
+        "Pasted entry should exist even when auto-open is disabled"
+    );
+}
+
+#[gpui::test]
+async fn test_auto_open_on_drop_when_enabled(cx: &mut gpui::TestAppContext) {
+    init_test_with_editor(cx);
+    set_auto_open_settings(
+        cx,
+        ProjectPanelAutoOpenSettings {
+            on_drop: Some(true),
+            ..Default::default()
+        },
+    );
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(path!("/root"), json!({})).await;
+
+    let temp_dir = tempfile::tempdir().unwrap();
+    let external_path = temp_dir.path().join("dropped.rs");
+    std::fs::write(&external_path, "// dropped").unwrap();
+    fs.insert_tree_from_real_fs(temp_dir.path(), temp_dir.path())
+        .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, ProjectPanel::new).unwrap();
+    cx.run_until_parked();
+
+    let root_entry = find_project_entry(&panel, "root", cx).unwrap();
+    panel.update_in(cx, |panel, window, cx| {
+        panel.drop_external_files(std::slice::from_ref(&external_path), root_entry, window, cx);
+    });
+    cx.executor().run_until_parked();
+
+    ensure_single_file_is_opened(&workspace, "dropped.rs", cx);
+}
+
+#[gpui::test]
+async fn test_auto_open_on_drop_when_disabled(cx: &mut gpui::TestAppContext) {
+    init_test_with_editor(cx);
+    set_auto_open_settings(
+        cx,
+        ProjectPanelAutoOpenSettings {
+            on_drop: Some(false),
+            ..Default::default()
+        },
+    );
+
+    let fs = FakeFs::new(cx.executor());
+    fs.insert_tree(path!("/root"), json!({})).await;
+
+    let temp_dir = tempfile::tempdir().unwrap();
+    let external_path = temp_dir.path().join("manual.rs");
+    std::fs::write(&external_path, "// dropped").unwrap();
+    fs.insert_tree_from_real_fs(temp_dir.path(), temp_dir.path())
+        .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, ProjectPanel::new).unwrap();
+    cx.run_until_parked();
+
+    let root_entry = find_project_entry(&panel, "root", cx).unwrap();
+    panel.update_in(cx, |panel, window, cx| {
+        panel.drop_external_files(std::slice::from_ref(&external_path), root_entry, window, cx);
+    });
+    cx.executor().run_until_parked();
+
+    ensure_no_open_items_and_panes(&workspace, cx);
+    assert!(
+        find_project_entry(&panel, "root/manual.rs", cx).is_some(),
+        "Dropped entry should exist even when auto-open is disabled"
+    );
+}
+
 #[gpui::test]
 async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
     init_test_with_editor(cx);
@@ -7368,6 +7610,19 @@ fn init_test_with_editor(cx: &mut TestAppContext) {
     });
 }
 
+fn set_auto_open_settings(
+    cx: &mut TestAppContext,
+    auto_open_settings: ProjectPanelAutoOpenSettings,
+) {
+    cx.update(|cx| {
+        cx.update_global::<SettingsStore, _>(|store, cx| {
+            store.update_user_settings(cx, |settings| {
+                settings.project_panel.get_or_insert_default().auto_open = Some(auto_open_settings);
+            });
+        })
+    });
+}
+
 fn ensure_single_file_is_opened(
     window: &WindowHandle<Workspace>,
     expected_path: &str,

crates/settings/src/settings_content/workspace.rs πŸ”—

@@ -510,6 +510,23 @@ impl OnLastWindowClosed {
     }
 }
 
+#[skip_serializing_none]
+#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
+pub struct ProjectPanelAutoOpenSettings {
+    /// Whether to automatically open newly created files in the editor.
+    ///
+    /// Default: true
+    pub on_create: Option<bool>,
+    /// Whether to automatically open files after pasting or duplicating them.
+    ///
+    /// Default: true
+    pub on_paste: Option<bool>,
+    /// Whether to automatically open files dropped from external sources.
+    ///
+    /// Default: true
+    pub on_drop: Option<bool>,
+}
+
 #[skip_serializing_none]
 #[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)]
 pub struct ProjectPanelSettingsContent {
@@ -590,10 +607,8 @@ pub struct ProjectPanelSettingsContent {
     ///
     /// Default: true
     pub drag_and_drop: Option<bool>,
-    /// Whether to automatically open files when pasting them in the project panel.
-    ///
-    /// Default: true
-    pub open_file_on_paste: Option<bool>,
+    /// Settings for automatically opening files.
+    pub auto_open: Option<ProjectPanelAutoOpenSettings>,
 }
 
 #[derive(

crates/settings/src/vscode_import.rs πŸ”—

@@ -664,13 +664,13 @@ impl VsCodeSettings {
             hide_root: None,
             indent_guides: None,
             indent_size: None,
-            open_file_on_paste: None,
             scrollbar: None,
             show_diagnostics: self
                 .read_bool("problems.decorations.enabled")
                 .and_then(|b| if b { Some(ShowDiagnostics::Off) } else { None }),
             starts_open: None,
             sticky_scroll: None,
+            auto_open: None,
         };
 
         if let (Some(false), Some(false)) = (

crates/settings_ui/src/page_data.rs πŸ”—

@@ -3776,23 +3776,47 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     metadata: None,
                     files: USER,
                 }),
+                SettingsPageItem::SectionHeader("Auto Open Files"),
                 SettingsPageItem::SettingItem(SettingItem {
-                    title: "Open File on Paste",
-                    description: "Whether to automatically open files when pasting them in the project panel.",
+                    title: "On Create",
+                    description: "Whether to automatically open newly created files in the editor.",
                     field: Box::new(SettingField {
-                        json_path: Some("project_panel.open_file_on_paste"),
+                        json_path: Some("project_panel.auto_open.on_create"),
                         pick: |settings_content| {
-                            settings_content
-                                .project_panel
-                                .as_ref()?
-                                .open_file_on_paste
-                                .as_ref()
+                            settings_content.project_panel.as_ref()?.auto_open.as_ref()?.on_create.as_ref()
                         },
                         write: |settings_content, value| {
-                            settings_content
-                                .project_panel
-                                .get_or_insert_default()
-                                .open_file_on_paste = value;
+                            settings_content.project_panel.get_or_insert_default().auto_open.get_or_insert_default().on_create = value;
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "On Paste",
+                    description: "Whether to automatically open files after pasting or duplicating them.",
+                    field: Box::new(SettingField {
+                        json_path: Some("project_panel.auto_open.on_paste"),
+                        pick: |settings_content| {
+                            settings_content.project_panel.as_ref()?.auto_open.as_ref()?.on_paste.as_ref()
+                        },
+                        write: |settings_content, value| {
+                            settings_content.project_panel.get_or_insert_default().auto_open.get_or_insert_default().on_paste = value;
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "On Drop",
+                    description: "Whether to automatically open files dropped from external sources.",
+                    field: Box::new(SettingField {
+                        json_path: Some("project_panel.auto_open.on_drop"),
+                        pick: |settings_content| {
+                            settings_content.project_panel.as_ref()?.auto_open.as_ref()?.on_drop.as_ref()
+                        },
+                        write: |settings_content, value| {
+                            settings_content.project_panel.get_or_insert_default().auto_open.get_or_insert_default().on_drop = value;
                         },
                     }),
                     metadata: None,

docs/src/configuring-zed.md πŸ”—

@@ -4280,7 +4280,11 @@ Run the {#action theme_selector::Toggle} action in the command palette to see a
     "hide_root": false,
     "hide_hidden": false,
     "starts_open": true,
-    "open_file_on_paste": true
+    "auto_open": {
+      "on_create": true,
+      "on_paste": true,
+      "on_drop": true
+    }
   }
 }
 ```
@@ -4489,6 +4493,26 @@ Run the {#action theme_selector::Toggle} action in the command palette to see a
 }
 ```
 
+### Auto Open
+
+- Description: Control whether files are opened automatically after different creation flows in the project panel.
+- Setting: `auto_open`
+- Default:
+
+```json [settings]
+"auto_open": {
+  "on_create": true,
+  "on_paste": true,
+  "on_drop": true
+}
+```
+
+**Options**
+
+- `on_create`: Whether to automatically open newly created files in the editor.
+- `on_paste`: Whether to automatically open files after pasting or duplicating them.
+- `on_drop`: Whether to automatically open files dropped from external sources.
+
 ## Agent
 
 Visit [the Configuration page](./ai/configuration.md) under the AI section to learn more about all the agent-related settings.