Add more preview tab settings and fix janky behavior (#43921)

Andrew Farkas , Smit Barmase , and Cole Miller created

Closes #41495

Known issues:
- File path links always open as non-preview tabs. Fixing this is not
technically too difficult but requires more invasive changes and so
should be done in a future PR.

Release Notes:

- Fixed strange behavior when reopening closed preview tabs
- Overhauled preview tabs settings:
- Added setting `preview_tabs.enable_preview_from_project_panel`
(default `true`)
- Kept setting `preview_tabs.enable_preview_from_file_finder` (default
`false`)
- Added setting `preview_tabs.enable_preview_from_multibuffer` (default
`true`)
- Added setting
`preview_tabs.enable_preview_multibuffer_from_code_navigation` (default
`false`)
- Added setting `preview_tabs.enable_preview_file_from_code_navigation`
(default `true`)
- Renamed setting `preview_tabs.enable_preview_from_code_navigation` to
`preview_tabs.enable_keep_preview_on_code_navigation` (default `false`)

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
Co-authored-by: Cole Miller <cole@zed.dev>

Change summary

assets/settings/default.json                            |  15 +
crates/editor/src/editor.rs                             |  87 ++++---
crates/migrator/src/migrations.rs                       |   6 
crates/migrator/src/migrations/m_2025_12_01/settings.rs |  55 +++++
crates/migrator/src/migrator.rs                         | 119 ++++++++--
crates/project_panel/src/project_panel.rs               |   5 
crates/project_symbols/src/project_symbols.rs           |   5 
crates/settings/src/settings_content/workspace.rs       |  21 +
crates/settings/src/vscode_import.rs                    |   6 
crates/settings_ui/src/page_data.rs                     | 102 ++++++++
crates/workspace/src/item.rs                            |  20 +
crates/workspace/src/pane.rs                            |  89 ++++---
crates/workspace/src/workspace.rs                       |  31 ++
docs/src/configuring-zed.md                             |  52 ++++
14 files changed, 478 insertions(+), 135 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -1100,13 +1100,22 @@
   "preview_tabs": {
     // Whether preview tabs should be enabled.
     // Preview tabs allow you to open files in preview mode, where they close automatically
-    // when you switch to another file unless you explicitly pin them.
+    // when you open another preview tab.
     // This is useful for quickly viewing files without cluttering your workspace.
     "enabled": true,
+    // Whether to open tabs in preview mode when opened from the project panel with a single click.
+    "enable_preview_from_project_panel": true,
     // Whether to open tabs in preview mode when selected from the file finder.
     "enable_preview_from_file_finder": false,
-    // Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.
-    "enable_preview_from_code_navigation": false
+    // Whether to open tabs in preview mode when opened from a multibuffer.
+    "enable_preview_from_multibuffer": true,
+    // Whether to open tabs in preview mode when code navigation is used to open a multibuffer.
+    "enable_preview_multibuffer_from_code_navigation": false,
+    // Whether to open tabs in preview mode when code navigation is used to open a single file.
+    "enable_preview_file_from_code_navigation": true,
+    // Whether to keep tabs in preview mode when code navigation is used to navigate away from them.
+    // If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one.
+    "enable_keep_preview_on_code_navigation": false
   },
   // Settings related to the file finder.
   "file_finder": {

crates/editor/src/editor.rs 🔗

@@ -17012,7 +17012,9 @@ impl Editor {
             })
             .collect();
 
-        let workspace = self.workspace();
+        let Some(workspace) = self.workspace() else {
+            return Task::ready(Ok(Navigated::No));
+        };
 
         cx.spawn_in(window, async move |editor, cx| {
             let locations: Vec<Location> = future::join_all(definitions)
@@ -17038,10 +17040,6 @@ impl Editor {
             }
 
             if num_locations > 1 {
-                let Some(workspace) = workspace else {
-                    return Ok(Navigated::No);
-                };
-
                 let tab_kind = match kind {
                     Some(GotoDefinitionKind::Implementation) => "Implementations",
                     Some(GotoDefinitionKind::Symbol) | None => "Definitions",
@@ -17073,11 +17071,14 @@ impl Editor {
 
                 let opened = workspace
                     .update_in(cx, |workspace, window, cx| {
+                        let allow_preview = PreviewTabsSettings::get_global(cx)
+                            .enable_preview_multibuffer_from_code_navigation;
                         Self::open_locations_in_multibuffer(
                             workspace,
                             locations,
                             title,
                             split,
+                            allow_preview,
                             MultibufferSelectionMode::First,
                             window,
                             cx,
@@ -17094,10 +17095,9 @@ impl Editor {
                         Ok(Navigated::Yes)
                     }
                     Some(Either::Right(path)) => {
-                        let Some(workspace) = workspace else {
-                            return Ok(Navigated::No);
-                        };
-
+                        // TODO(andrew): respect preview tab settings
+                        //               `enable_keep_preview_on_code_navigation` and
+                        //               `enable_preview_file_from_code_navigation`
                         workspace
                             .update_in(cx, |workspace, window, cx| {
                                 workspace.open_resolved_path(path, window, cx)
@@ -17108,10 +17108,6 @@ impl Editor {
                     None => Ok(Navigated::No),
                 }
             } else {
-                let Some(workspace) = workspace else {
-                    return Ok(Navigated::No);
-                };
-
                 let (target_buffer, target_ranges) = locations.into_iter().next().unwrap();
                 let target_range = target_ranges.first().unwrap().clone();
 
@@ -17135,11 +17131,19 @@ impl Editor {
                                         workspace.active_pane().clone()
                                     };
 
+                                    let preview_tabs_settings = PreviewTabsSettings::get_global(cx);
+                                    let keep_old_preview = preview_tabs_settings
+                                        .enable_keep_preview_on_code_navigation;
+                                    let allow_new_preview = preview_tabs_settings
+                                        .enable_preview_file_from_code_navigation;
+
                                     workspace.open_project_item(
                                         pane,
                                         target_buffer.clone(),
                                         true,
                                         true,
+                                        keep_old_preview,
+                                        allow_new_preview,
                                         window,
                                         cx,
                                     )
@@ -17416,11 +17420,14 @@ impl Editor {
                 } else {
                     format!("References to {target}")
                 };
+                let allow_preview = PreviewTabsSettings::get_global(cx)
+                    .enable_preview_multibuffer_from_code_navigation;
                 Self::open_locations_in_multibuffer(
                     workspace,
                     locations,
                     title,
                     false,
+                    allow_preview,
                     MultibufferSelectionMode::First,
                     window,
                     cx,
@@ -17436,6 +17443,7 @@ impl Editor {
         locations: std::collections::HashMap<Entity<Buffer>, Vec<Range<Point>>>,
         title: String,
         split: bool,
+        allow_preview: bool,
         multibuffer_selection_mode: MultibufferSelectionMode,
         window: &mut Window,
         cx: &mut Context<Workspace>,
@@ -17483,6 +17491,7 @@ impl Editor {
                         .is_some_and(|it| *it == key)
                 })
         });
+        let was_existing = existing.is_some();
         let editor = existing.unwrap_or_else(|| {
             cx.new(|cx| {
                 let mut editor = Editor::for_multibuffer(
@@ -17523,29 +17532,23 @@ impl Editor {
         });
 
         let item = Box::new(editor);
-        let item_id = item.item_id();
-
-        if split {
-            let pane = workspace.adjacent_pane(window, cx);
-            workspace.add_item(pane, item, None, true, true, window, cx);
-        } else if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
-            let (preview_item_id, preview_item_idx) =
-                workspace.active_pane().read_with(cx, |pane, _| {
-                    (pane.preview_item_id(), pane.preview_item_idx())
-                });
 
-            workspace.add_item_to_active_pane(item, preview_item_idx, true, window, cx);
+        let pane = if split {
+            workspace.adjacent_pane(window, cx)
+        } else {
+            workspace.active_pane().clone()
+        };
+        let activate_pane = split;
 
-            if let Some(preview_item_id) = preview_item_id {
-                workspace.active_pane().update(cx, |pane, cx| {
-                    pane.remove_item(preview_item_id, false, false, window, cx);
-                });
+        let mut destination_index = None;
+        pane.update(cx, |pane, cx| {
+            if allow_preview && !was_existing {
+                destination_index = pane.replace_preview_item_id(item.item_id(), window, cx);
             }
-        } else {
-            workspace.add_item_to_active_pane(item, None, true, window, cx);
-        }
-        workspace.active_pane().update(cx, |pane, cx| {
-            pane.set_preview_item_id(Some(item_id), cx);
+            if was_existing && !allow_preview {
+                pane.unpreview_item_if_preview(item.item_id());
+            }
+            pane.add_item(item, activate_pane, true, destination_index, window, cx);
         });
     }
 
@@ -20783,6 +20786,7 @@ impl Editor {
                     locations,
                     format!("Selections for '{title}'"),
                     false,
+                    false,
                     MultibufferSelectionMode::All,
                     window,
                     cx,
@@ -22002,29 +22006,40 @@ impl Editor {
                             // Handle file-less buffers separately: those are not really the project items, so won't have a project path or entity id,
                             // so `workspace.open_project_item` will never find them, always opening a new editor.
                             // Instead, we try to activate the existing editor in the pane first.
-                            let (editor, pane_item_index) =
+                            let (editor, pane_item_index, pane_item_id) =
                                 pane.read(cx).items().enumerate().find_map(|(i, item)| {
                                     let editor = item.downcast::<Editor>()?;
                                     let singleton_buffer =
                                         editor.read(cx).buffer().read(cx).as_singleton()?;
                                     if singleton_buffer == buffer {
-                                        Some((editor, i))
+                                        Some((editor, i, item.item_id()))
                                     } else {
                                         None
                                     }
                                 })?;
                             pane.update(cx, |pane, cx| {
-                                pane.activate_item(pane_item_index, true, true, window, cx)
+                                pane.activate_item(pane_item_index, true, true, window, cx);
+                                if !PreviewTabsSettings::get_global(cx)
+                                    .enable_preview_from_multibuffer
+                                {
+                                    pane.unpreview_item_if_preview(pane_item_id);
+                                }
                             });
                             Some(editor)
                         })
                         .flatten()
                         .unwrap_or_else(|| {
+                            let keep_old_preview = PreviewTabsSettings::get_global(cx)
+                                .enable_keep_preview_on_code_navigation;
+                            let allow_new_preview =
+                                PreviewTabsSettings::get_global(cx).enable_preview_from_multibuffer;
                             workspace.open_project_item::<Self>(
                                 pane.clone(),
                                 buffer,
                                 true,
                                 true,
+                                keep_old_preview,
+                                allow_new_preview,
                                 window,
                                 cx,
                             )

crates/migrator/src/migrations.rs 🔗

@@ -153,3 +153,9 @@ pub(crate) mod m_2025_11_25 {
 
     pub(crate) use settings::remove_context_server_source;
 }
+
+pub(crate) mod m_2025_12_01 {
+    mod settings;
+
+    pub(crate) use settings::SETTINGS_PATTERNS;
+}

crates/migrator/src/migrations/m_2025_12_01/settings.rs 🔗

@@ -0,0 +1,55 @@
+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_enable_preview_from_code_navigation_setting,
+)];
+
+fn rename_enable_preview_from_code_navigation_setting(
+    contents: &str,
+    mat: &QueryMatch,
+    query: &Query,
+) -> Option<(Range<usize>, String)> {
+    if !is_enable_preview_from_code_navigation(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,
+        "enable_keep_preview_on_code_navigation".to_string(),
+    ))
+}
+
+fn is_enable_preview_from_code_navigation(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("preview_tabs") {
+        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("enable_preview_from_code_navigation")
+}

crates/migrator/src/migrator.rs 🔗

@@ -219,6 +219,10 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
             migrations::m_2025_11_12::SETTINGS_PATTERNS,
             &SETTINGS_QUERY_2025_11_12,
         ),
+        MigrationType::TreeSitter(
+            migrations::m_2025_12_01::SETTINGS_PATTERNS,
+            &SETTINGS_QUERY_2025_12_01,
+        ),
         MigrationType::TreeSitter(
             migrations::m_2025_11_20::SETTINGS_PATTERNS,
             &SETTINGS_QUERY_2025_11_20,
@@ -346,6 +350,10 @@ define_query!(
     SETTINGS_QUERY_2025_11_12,
     migrations::m_2025_11_12::SETTINGS_PATTERNS
 );
+define_query!(
+    SETTINGS_QUERY_2025_12_01,
+    migrations::m_2025_12_01::SETTINGS_PATTERNS
+);
 define_query!(
     SETTINGS_QUERY_2025_11_20,
     migrations::m_2025_11_20::SETTINGS_PATTERNS
@@ -2262,6 +2270,54 @@ mod tests {
         );
     }
 
+    #[test]
+    fn test_remove_context_server_source() {
+        assert_migrate_settings(
+            &r#"
+            {
+                "context_servers": {
+                    "extension_server": {
+                        "source": "extension",
+                        "settings": {
+                            "foo": "bar"
+                        }
+                    },
+                    "custom_server": {
+                        "source": "custom",
+                        "command": "foo",
+                        "args": ["bar"],
+                        "env": {
+                            "FOO": "BAR"
+                        }
+                    },
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "context_servers": {
+                        "extension_server": {
+                            "settings": {
+                                "foo": "bar"
+                            }
+                        },
+                        "custom_server": {
+                            "command": "foo",
+                            "args": ["bar"],
+                            "env": {
+                                "FOO": "BAR"
+                            }
+                        },
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+    }
+
     #[test]
     fn test_project_panel_open_file_on_paste_migration() {
         assert_migrate_settings(
@@ -2308,25 +2364,14 @@ mod tests {
     }
 
     #[test]
-    fn test_remove_context_server_source() {
+    fn test_enable_preview_from_code_navigation_migration() {
         assert_migrate_settings(
             &r#"
             {
-                "context_servers": {
-                    "extension_server": {
-                        "source": "extension",
-                        "settings": {
-                            "foo": "bar"
-                        }
-                    },
-                    "custom_server": {
-                        "source": "custom",
-                        "command": "foo",
-                        "args": ["bar"],
-                        "env": {
-                            "FOO": "BAR"
-                        }
-                    },
+                "other_setting_1": 1,
+                "preview_tabs": {
+                    "other_setting_2": 2,
+                    "enable_preview_from_code_navigation": false
                 }
             }
             "#
@@ -2334,19 +2379,35 @@ mod tests {
             Some(
                 &r#"
                 {
-                    "context_servers": {
-                        "extension_server": {
-                            "settings": {
-                                "foo": "bar"
-                            }
-                        },
-                        "custom_server": {
-                            "command": "foo",
-                            "args": ["bar"],
-                            "env": {
-                                "FOO": "BAR"
-                            }
-                        },
+                    "other_setting_1": 1,
+                    "preview_tabs": {
+                        "other_setting_2": 2,
+                        "enable_keep_preview_on_code_navigation": false
+                    }
+                }
+                "#
+                .unindent(),
+            ),
+        );
+
+        assert_migrate_settings(
+            &r#"
+            {
+                "other_setting_1": 1,
+                "preview_tabs": {
+                    "other_setting_2": 2,
+                    "enable_preview_from_code_navigation": true
+                }
+            }
+            "#
+            .unindent(),
+            Some(
+                &r#"
+                {
+                    "other_setting_1": 1,
+                    "preview_tabs": {
+                        "other_setting_2": 2,
+                        "enable_keep_preview_on_code_navigation": true
                     }
                 }
                 "#

crates/project_panel/src/project_panel.rs 🔗

@@ -1529,7 +1529,8 @@ impl ProjectPanel {
     }
 
     fn open(&mut self, _: &Open, window: &mut Window, cx: &mut Context<Self>) {
-        let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
+        let preview_tabs_enabled =
+            PreviewTabsSettings::get_global(cx).enable_preview_from_project_panel;
         self.open_internal(true, !preview_tabs_enabled, None, window, cx);
     }
 
@@ -4819,7 +4820,7 @@ impl ProjectPanel {
                             project_panel.toggle_expanded(entry_id, window, cx);
                         }
                     } else {
-                        let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
+                        let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enable_preview_from_project_panel;
                         let click_count = event.click_count();
                         let focus_opened_item = click_count > 1;
                         let allow_preview = preview_tabs_enabled && click_count == 1;

crates/project_symbols/src/project_symbols.rs 🔗

@@ -133,8 +133,9 @@ impl PickerDelegate for ProjectSymbolsDelegate {
                         workspace.active_pane().clone()
                     };
 
-                    let editor =
-                        workspace.open_project_item::<Editor>(pane, buffer, true, true, window, cx);
+                    let editor = workspace.open_project_item::<Editor>(
+                        pane, buffer, true, true, true, true, window, cx,
+                    );
 
                     editor.update(cx, |editor, cx| {
                         editor.change_selections(

crates/settings/src/settings_content/workspace.rs 🔗

@@ -152,14 +152,31 @@ pub struct PreviewTabsSettingsContent {
     ///
     /// Default: true
     pub enabled: Option<bool>,
+    /// Whether to open tabs in preview mode when opened from the project panel with a single click.
+    ///
+    /// Default: true
+    pub enable_preview_from_project_panel: Option<bool>,
     /// Whether to open tabs in preview mode when selected from the file finder.
     ///
     /// Default: false
     pub enable_preview_from_file_finder: Option<bool>,
-    /// Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.
+    /// Whether to open tabs in preview mode when opened from a multibuffer.
+    ///
+    /// Default: true
+    pub enable_preview_from_multibuffer: Option<bool>,
+    /// Whether to open tabs in preview mode when code navigation is used to open a multibuffer.
+    ///
+    /// Default: false
+    pub enable_preview_multibuffer_from_code_navigation: Option<bool>,
+    /// Whether to open tabs in preview mode when code navigation is used to open a single file.
+    ///
+    /// Default: true
+    pub enable_preview_file_from_code_navigation: Option<bool>,
+    /// Whether to keep tabs in preview mode when code navigation is used to navigate away from them.
+    /// If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one.
     ///
     /// Default: false
-    pub enable_preview_from_code_navigation: Option<bool>,
+    pub enable_keep_preview_on_code_navigation: Option<bool>,
 }
 
 #[derive(

crates/settings/src/vscode_import.rs 🔗

@@ -619,9 +619,13 @@ impl VsCodeSettings {
     fn preview_tabs_settings_content(&self) -> Option<PreviewTabsSettingsContent> {
         skip_default(PreviewTabsSettingsContent {
             enabled: self.read_bool("workbench.editor.enablePreview"),
+            enable_preview_from_project_panel: None,
             enable_preview_from_file_finder: self
                 .read_bool("workbench.editor.enablePreviewFromQuickOpen"),
-            enable_preview_from_code_navigation: self
+            enable_preview_from_multibuffer: None,
+            enable_preview_multibuffer_from_code_navigation: None,
+            enable_preview_file_from_code_navigation: None,
+            enable_keep_preview_on_code_navigation: self
                 .read_bool("workbench.editor.enablePreviewFromCodeNavigation"),
         })
     }

crates/settings_ui/src/page_data.rs 🔗

@@ -3145,7 +3145,7 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                 SettingsPageItem::SectionHeader("Preview Tabs"),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Preview Tabs Enabled",
-                    description: "Show opened editors as Preview tabs.",
+                    description: "Show opened editors as preview tabs.",
                     field: Box::new(SettingField {
                         json_path: Some("preview_tabs.enabled"),
                         pick: |settings_content| {
@@ -3161,9 +3161,31 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     metadata: None,
                     files: USER,
                 }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Enable Preview From Project Panel",
+                    description: "Whether to open tabs in preview mode when opened from the project panel with a single click.",
+                    field: Box::new(SettingField {
+                        json_path: Some("preview_tabs.enable_preview_from_project_panel"),
+                        pick: |settings_content| {
+                            settings_content
+                                .preview_tabs
+                                .as_ref()?
+                                .enable_preview_from_project_panel
+                                .as_ref()
+                        },
+                        write: |settings_content, value| {
+                            settings_content
+                                .preview_tabs
+                                .get_or_insert_default()
+                                .enable_preview_from_project_panel = value;
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
                 SettingsPageItem::SettingItem(SettingItem {
                     title: "Enable Preview From File Finder",
-                    description: "Whether to open tabs in Preview mode when selected from the file finder.",
+                    description: "Whether to open tabs in preview mode when selected from the file finder.",
                     field: Box::new(SettingField {
                         json_path: Some("preview_tabs.enable_preview_from_file_finder"),
                         pick: |settings_content| {
@@ -3184,22 +3206,88 @@ pub(crate) fn settings_data(cx: &App) -> Vec<SettingsPage> {
                     files: USER,
                 }),
                 SettingsPageItem::SettingItem(SettingItem {
-                    title: "Enable Preview From Code Navigation",
-                    description: "Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.",
+                    title: "Enable Preview From Multibuffer",
+                    description: "Whether to open tabs in preview mode when opened from a multibuffer.",
+                    field: Box::new(SettingField {
+                        json_path: Some("preview_tabs.enable_preview_from_multibuffer"),
+                        pick: |settings_content| {
+                            settings_content
+                                .preview_tabs
+                                .as_ref()?
+                                .enable_preview_from_multibuffer
+                                .as_ref()
+                        },
+                        write: |settings_content, value| {
+                            settings_content
+                                .preview_tabs
+                                .get_or_insert_default()
+                                .enable_preview_from_multibuffer = value;
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Enable Preview Multibuffer From Code Navigation",
+                    description: "Whether to open tabs in preview mode when code navigation is used to open a multibuffer.",
+                    field: Box::new(SettingField {
+                        json_path: Some("preview_tabs.enable_preview_multibuffer_from_code_navigation"),
+                        pick: |settings_content| {
+                            settings_content
+                                .preview_tabs
+                                .as_ref()?
+                                .enable_preview_multibuffer_from_code_navigation
+                                .as_ref()
+                        },
+                        write: |settings_content, value| {
+                            settings_content
+                                .preview_tabs
+                                .get_or_insert_default()
+                                .enable_preview_multibuffer_from_code_navigation = value;
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Enable Preview File From Code Navigation",
+                    description: "Whether to open tabs in preview mode when code navigation is used to open a single file.",
+                    field: Box::new(SettingField {
+                        json_path: Some("preview_tabs.enable_preview_file_from_code_navigation"),
+                        pick: |settings_content| {
+                            settings_content
+                                .preview_tabs
+                                .as_ref()?
+                                .enable_preview_file_from_code_navigation
+                                .as_ref()
+                        },
+                        write: |settings_content, value| {
+                            settings_content
+                                .preview_tabs
+                                .get_or_insert_default()
+                                .enable_preview_file_from_code_navigation = value;
+                        },
+                    }),
+                    metadata: None,
+                    files: USER,
+                }),
+                SettingsPageItem::SettingItem(SettingItem {
+                    title: "Enable Keep Preview On Code Navigation",
+                    description: "Whether to keep tabs in preview mode when code navigation is used to navigate away from them. If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one.",
                     field: Box::new(SettingField {
-                        json_path: Some("preview_tabs.enable_preview_from_code_navigation"),
+                        json_path: Some("preview_tabs.enable_keep_preview_on_code_navigation"),
                         pick: |settings_content| {
                             settings_content
                                 .preview_tabs
                                 .as_ref()?
-                                .enable_preview_from_code_navigation
+                                .enable_keep_preview_on_code_navigation
                                 .as_ref()
                         },
                         write: |settings_content, value| {
                             settings_content
                                 .preview_tabs
                                 .get_or_insert_default()
-                                .enable_preview_from_code_navigation = value;
+                                .enable_keep_preview_on_code_navigation = value;
                         },
                     }),
                     metadata: None,

crates/workspace/src/item.rs 🔗

@@ -64,8 +64,12 @@ pub struct ItemSettings {
 #[derive(RegisterSetting)]
 pub struct PreviewTabsSettings {
     pub enabled: bool,
+    pub enable_preview_from_project_panel: bool,
     pub enable_preview_from_file_finder: bool,
-    pub enable_preview_from_code_navigation: bool,
+    pub enable_preview_from_multibuffer: bool,
+    pub enable_preview_multibuffer_from_code_navigation: bool,
+    pub enable_preview_file_from_code_navigation: bool,
+    pub enable_keep_preview_on_code_navigation: bool,
 }
 
 impl Settings for ItemSettings {
@@ -87,9 +91,19 @@ impl Settings for PreviewTabsSettings {
         let preview_tabs = content.preview_tabs.as_ref().unwrap();
         Self {
             enabled: preview_tabs.enabled.unwrap(),
+            enable_preview_from_project_panel: preview_tabs
+                .enable_preview_from_project_panel
+                .unwrap(),
             enable_preview_from_file_finder: preview_tabs.enable_preview_from_file_finder.unwrap(),
-            enable_preview_from_code_navigation: preview_tabs
-                .enable_preview_from_code_navigation
+            enable_preview_from_multibuffer: preview_tabs.enable_preview_from_multibuffer.unwrap(),
+            enable_preview_multibuffer_from_code_navigation: preview_tabs
+                .enable_preview_multibuffer_from_code_navigation
+                .unwrap(),
+            enable_preview_file_from_code_navigation: preview_tabs
+                .enable_preview_file_from_code_navigation
+                .unwrap(),
+            enable_keep_preview_on_code_navigation: preview_tabs
+                .enable_keep_preview_on_code_navigation
                 .unwrap(),
         }
     }

crates/workspace/src/pane.rs 🔗

@@ -873,10 +873,35 @@ impl Pane {
         self.preview_item_id == Some(item_id)
     }
 
+    /// Promotes the item with the given ID to not be a preview item.
+    /// This does nothing if it wasn't already a preview item.
+    pub fn unpreview_item_if_preview(&mut self, item_id: EntityId) {
+        if self.is_active_preview_item(item_id) {
+            self.preview_item_id = None;
+        }
+    }
+
+    /// Marks the item with the given ID as the preview item.
+    /// This will be ignored if the global setting `preview_tabs` is disabled.
+    ///
+    /// The old preview item (if there was one) is closed and its index is returned.
+    pub fn replace_preview_item_id(
+        &mut self,
+        item_id: EntityId,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Option<usize> {
+        let idx = self.close_current_preview_item(window, cx);
+        self.set_preview_item_id(Some(item_id), cx);
+        idx
+    }
+
     /// Marks the item with the given ID as the preview item.
     /// This will be ignored if the global setting `preview_tabs` is disabled.
-    pub fn set_preview_item_id(&mut self, item_id: Option<EntityId>, cx: &App) {
-        if PreviewTabsSettings::get_global(cx).enabled {
+    ///
+    /// This is a low-level method. Prefer `unpreview_item_if_preview()` or `set_new_preview_item()`.
+    pub(crate) fn set_preview_item_id(&mut self, item_id: Option<EntityId>, cx: &App) {
+        if item_id.is_none() || PreviewTabsSettings::get_global(cx).enabled {
             self.preview_item_id = item_id;
         }
     }
@@ -895,7 +920,7 @@ impl Pane {
             && preview_item.item_id() == item_id
             && !preview_item.preserve_preview(cx)
         {
-            self.set_preview_item_id(None, cx);
+            self.unpreview_item_if_preview(item_id);
         }
     }
 
@@ -936,14 +961,8 @@ impl Pane {
 
         let set_up_existing_item =
             |index: usize, pane: &mut Self, window: &mut Window, cx: &mut Context<Self>| {
-                // If the item is already open, and the item is a preview item
-                // and we are not allowing items to open as preview, mark the item as persistent.
-                if let Some(preview_item_id) = pane.preview_item_id
-                    && let Some(tab) = pane.items.get(index)
-                    && tab.item_id() == preview_item_id
-                    && !allow_preview
-                {
-                    pane.set_preview_item_id(None, cx);
+                if !allow_preview && let Some(item) = pane.items.get(index) {
+                    pane.unpreview_item_if_preview(item.item_id());
                 }
                 if activate {
                     pane.activate_item(index, focus_item, focus_item, window, cx);
@@ -955,7 +974,7 @@ impl Pane {
                                window: &mut Window,
                                cx: &mut Context<Self>| {
             if allow_preview {
-                pane.set_preview_item_id(Some(new_item.item_id()), cx);
+                pane.replace_preview_item_id(new_item.item_id(), window, cx);
             }
 
             if let Some(text) = new_item.telemetry_event_text(cx) {
@@ -1036,6 +1055,7 @@ impl Pane {
     ) -> Option<usize> {
         let item_idx = self.preview_item_idx()?;
         let id = self.preview_item_id()?;
+        self.set_preview_item_id(None, cx);
 
         let prev_active_item_index = self.active_item_index;
         self.remove_item(id, false, false, window, cx);
@@ -1981,9 +2001,7 @@ impl Pane {
         item.on_removed(cx);
         self.nav_history.set_mode(mode);
 
-        if self.is_active_preview_item(item.item_id()) {
-            self.set_preview_item_id(None, cx);
-        }
+        self.unpreview_item_if_preview(item.item_id());
 
         if let Some(path) = item.project_path(cx) {
             let abs_path = self
@@ -2194,9 +2212,7 @@ impl Pane {
 
             if can_save {
                 pane.update_in(cx, |pane, window, cx| {
-                    if pane.is_active_preview_item(item.item_id()) {
-                        pane.set_preview_item_id(None, cx);
-                    }
+                    pane.unpreview_item_if_preview(item.item_id());
                     item.save(
                         SaveOptions {
                             format: should_format,
@@ -2450,8 +2466,8 @@ impl Pane {
             let id = self.item_for_index(ix)?.item_id();
             let should_activate = ix == self.active_item_index;
 
-            if matches!(operation, PinOperation::Pin) && self.is_active_preview_item(id) {
-                self.set_preview_item_id(None, cx);
+            if matches!(operation, PinOperation::Pin) {
+                self.unpreview_item_if_preview(id);
             }
 
             match operation {
@@ -2624,12 +2640,9 @@ impl Pane {
             )
             .on_mouse_down(
                 MouseButton::Left,
-                cx.listener(move |pane, event: &MouseDownEvent, _, cx| {
-                    if let Some(id) = pane.preview_item_id
-                        && id == item_id
-                        && event.click_count > 1
-                    {
-                        pane.set_preview_item_id(None, cx);
+                cx.listener(move |pane, event: &MouseDownEvent, _, _| {
+                    if event.click_count > 1 {
+                        pane.unpreview_item_if_preview(item_id);
                     }
                 }),
             )
@@ -3272,11 +3285,7 @@ impl Pane {
         let mut to_pane = cx.entity();
         let split_direction = self.drag_split_direction;
         let item_id = dragged_tab.item.item_id();
-        if let Some(preview_item_id) = self.preview_item_id
-            && item_id == preview_item_id
-        {
-            self.set_preview_item_id(None, cx);
-        }
+        self.unpreview_item_if_preview(item_id);
 
         let is_clone = cfg!(target_os = "macos") && window.modifiers().alt
             || cfg!(not(target_os = "macos")) && window.modifiers().control;
@@ -3788,15 +3797,17 @@ impl Render for Pane {
             .on_action(cx.listener(Self::toggle_pin_tab))
             .on_action(cx.listener(Self::unpin_all_tabs))
             .when(PreviewTabsSettings::get_global(cx).enabled, |this| {
-                this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| {
-                    if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
-                        if pane.is_active_preview_item(active_item_id) {
-                            pane.set_preview_item_id(None, cx);
-                        } else {
-                            pane.set_preview_item_id(Some(active_item_id), cx);
+                this.on_action(
+                    cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, window, cx| {
+                        if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) {
+                            if pane.is_active_preview_item(active_item_id) {
+                                pane.unpreview_item_if_preview(active_item_id);
+                            } else {
+                                pane.replace_preview_item_id(active_item_id, window, cx);
+                            }
                         }
-                    }
-                }))
+                    }),
+                )
             })
             .on_action(
                 cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| {

crates/workspace/src/workspace.rs 🔗

@@ -3636,14 +3636,33 @@ impl Workspace {
         project_item: Entity<T::Item>,
         activate_pane: bool,
         focus_item: bool,
+        keep_old_preview: bool,
+        allow_new_preview: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Entity<T>
     where
         T: ProjectItem,
     {
+        let old_item_id = pane.read(cx).active_item().map(|item| item.item_id());
+
         if let Some(item) = self.find_project_item(&pane, &project_item, cx) {
+            if !keep_old_preview
+                && let Some(old_id) = old_item_id
+                && old_id != item.item_id()
+            {
+                // switching to a different item, so unpreview old active item
+                pane.update(cx, |pane, _| {
+                    pane.unpreview_item_if_preview(old_id);
+                });
+            }
+
             self.activate_item(&item, activate_pane, focus_item, window, cx);
+            if !allow_new_preview {
+                pane.update(cx, |pane, _| {
+                    pane.unpreview_item_if_preview(item.item_id());
+                });
+            }
             return item;
         }
 
@@ -3652,16 +3671,14 @@ impl Workspace {
                 T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx)
             })
         });
-        let item_id = item.item_id();
         let mut destination_index = None;
         pane.update(cx, |pane, cx| {
-            if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation
-                && let Some(preview_item_id) = pane.preview_item_id()
-                && preview_item_id != item_id
-            {
-                destination_index = pane.close_current_preview_item(window, cx);
+            if !keep_old_preview && let Some(old_id) = old_item_id {
+                pane.unpreview_item_if_preview(old_id);
+            }
+            if allow_new_preview {
+                destination_index = pane.replace_preview_item_id(item.item_id(), window, cx);
             }
-            pane.set_preview_item_id(Some(item.item_id()), cx)
         });
 
         self.add_item(

docs/src/configuring-zed.md 🔗

@@ -2861,11 +2861,25 @@ Configuration object for defining settings profiles. Example:
 ```json [settings]
 "preview_tabs": {
   "enabled": true,
+  "enable_preview_from_project_panel": true,
   "enable_preview_from_file_finder": false,
-  "enable_preview_from_code_navigation": false,
+  "enable_preview_from_multibuffer": true,
+  "enable_preview_multibuffer_from_code_navigation": false,
+  "enable_preview_file_from_code_navigation": true,
+  "enable_keep_preview_on_code_navigation": false,
 }
 ```
 
+### Enable preview from project panel
+
+- Description: Determines whether to open files in preview mode when opened from the project panel with a single click.
+- Setting: `enable_preview_from_project_panel`
+- Default: `true`
+
+**Options**
+
+`boolean` values
+
 ### Enable preview from file finder
 
 - Description: Determines whether to open files in preview mode when selected from the file finder.
@@ -2876,10 +2890,40 @@ Configuration object for defining settings profiles. Example:
 
 `boolean` values
 
-### Enable preview from code navigation
+### Enable preview from multibuffer
+
+- Description: Determines whether to open files in preview mode when opened from a multibuffer.
+- Setting: `enable_preview_from_multibuffer`
+- Default: `true`
+
+**Options**
+
+`boolean` values
+
+### Enable preview multibuffer from code navigation
+
+- Description: Determines whether to open tabs in preview mode when code navigation is used to open a multibuffer.
+- Setting: `enable_preview_multibuffer_from_code_navigation`
+- Default: `false`
+
+**Options**
+
+`boolean` values
+
+### Enable preview file from code navigation
+
+- Description: Determines whether to open tabs in preview mode when code navigation is used to open a single file.
+- Setting: `enable_preview_file_from_code_navigation`
+- Default: `true`
+
+**Options**
+
+`boolean` values
+
+### Enable keep preview on code navigation
 
-- Description: Determines whether a preview tab gets replaced when code navigation is used to navigate away from the tab.
-- Setting: `enable_preview_from_code_navigation`
+- Description: Determines whether to keep tabs in preview mode when code navigation is used to navigate away from them. If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one.
+- Setting: `enable_keep_preview_on_code_navigation`
 - Default: `false`
 
 **Options**