From ea4419076ece52500c131617613ca2eeaee56532 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> Date: Thu, 11 Apr 2024 23:09:12 +0200 Subject: [PATCH] Add preview tabs (#9125) This PR implements the preview tabs feature from VSCode. More details and thanks for the head start of the implementation here #6782. Here is what I have observed from using the vscode implementation ([x] -> already implemented): - [x] Single click on project file opens tab as preview - [x] Double click on item in project panel opens tab as permanent - [x] Double click on the tab makes it permanent - [x] Navigating away from the tab makes the tab permanent and the new tab is shown as preview (e.g. GoToReference) - [x] Existing preview tab is reused when opening a new tab - [x] Dragging tab to the same/another panel makes the tab permanent - [x] Opening a tab from the file finder makes the tab permanent - [x] Editing a preview tab will make the tab permanent - [x] Using the space key in the project panel opens the tab as preview - [x] Handle navigation history correctly (restore a preview tab as preview as well) - [x] Restore preview tabs after restarting - [x] Support opening files from file finder in preview mode (vscode: "Enable Preview From Quick Open") I need to do some more testing of the vscode implementation, there might be other behaviors/workflows which im not aware of that open an item as preview/make them permanent. Showcase: https://github.com/zed-industries/zed/assets/53836821/9be16515-c740-4905-bea1-88871112ef86 TODOs - [x] Provide `enable_preview_tabs` setting - [x] Write some tests - [x] How should we handle this in collaboration mode (have not tested the behavior so far) - [x] Keyboard driven usage (probably need workspace commands) - [x] Register `TogglePreviewTab` only when setting enabled? - [x] Render preview tabs in tab switcher as italic - [x] Render preview tabs in image viewer as italic - [x] Should this be enabled by default (it is the default behavior in VSCode)? - [x] Docs Future improvements (out of scope for now): - Support preview mode for find all references and possibly other multibuffers (VSCode: "Enable Preview From Code Navigation") Release Notes: - Added preview tabs ([#4922](https://github.com/zed-industries/zed/issues/4922)). --------- Co-authored-by: Conrad Irwin --- Cargo.lock | 1 + assets/settings/default.json | 10 + crates/collab/src/tests/integration_tests.rs | 267 ++++++++++++++++++ crates/collab_ui/src/channel_view.rs | 6 +- crates/diagnostics/src/diagnostics.rs | 10 +- crates/editor/src/items.rs | 18 +- crates/extensions_ui/src/extensions_ui.rs | 5 +- crates/file_finder/Cargo.toml | 1 + crates/file_finder/src/file_finder.rs | 28 +- crates/gpui/src/styled.rs | 22 +- crates/image_viewer/src/image_viewer.rs | 12 +- crates/language_tools/src/lsp_log.rs | 6 +- crates/language_tools/src/syntax_tree_view.rs | 6 +- .../src/markdown_preview_view.rs | 13 +- crates/project_panel/src/project_panel.rs | 34 ++- crates/search/src/project_search.rs | 23 +- crates/tab_switcher/src/tab_switcher.rs | 11 +- crates/terminal_view/src/terminal_view.rs | 11 +- .../src/components/label/highlighted_label.rs | 5 + crates/ui/src/components/label/label.rs | 14 + crates/ui/src/components/label/label_like.rs | 11 + crates/welcome/src/welcome.rs | 6 +- crates/workspace/src/item.rs | 69 +++-- crates/workspace/src/pane.rs | 172 ++++++++++- crates/workspace/src/persistence.rs | 53 ++-- crates/workspace/src/persistence/model.rs | 24 +- crates/workspace/src/shared_screen.rs | 21 +- crates/workspace/src/workspace.rs | 44 ++- docs/src/configuring_zed.md | 34 +++ 29 files changed, 784 insertions(+), 153 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f0c154378a1e27e86cf77db17ac5e5877dd056c..da08ad636377c30a79f689fbfa1e7717737d977f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3772,6 +3772,7 @@ dependencies = [ "picker", "project", "serde_json", + "settings", "text", "theme", "ui", diff --git a/assets/settings/default.json b/assets/settings/default.json index 53948023ed788bd9dcb1fca331cccb313e14b134..6bf04d322927c86aad4b4c0c2494aaa900f7a278 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -298,6 +298,16 @@ // Position of the close button on the editor tabs. "close_position": "right" }, + // Settings related to preview tabs. + "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. + // This is useful for quickly viewing files without cluttering your workspace. + "enabled": true, + // Whether to open files in preview mode when selected from the file finder. + "enable_preview_from_file_finder": false + }, // Whether or not to remove any trailing whitespace from lines of a buffer // before saving it. "remove_trailing_whitespace_on_save": true, diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 81cde74cbbd3ca1f7eb388a0b675f2d0b3bde393..87eb5d51ba3795aecb5830671b3a2aea0f85a30e 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -42,6 +42,7 @@ use std::{ time::Duration, }; use unindent::Unindent as _; +use workspace::Pane; #[ctor::ctor] fn init_logger() { @@ -6127,3 +6128,269 @@ async fn test_join_after_restart(cx1: &mut TestAppContext, cx2: &mut TestAppCont let client2 = server.create_client(cx2, "user_a").await; join_channel(channel2, &client2, cx2).await.unwrap(); } + +#[gpui::test] +async fn test_preview_tabs(cx: &mut TestAppContext) { + let (_server, client) = TestServer::start1(cx).await; + let (workspace, cx) = client.build_test_workspace(cx).await; + let project = workspace.update(cx, |workspace, _| workspace.project().clone()); + + let worktree_id = project.update(cx, |project, cx| { + project.worktrees().next().unwrap().read(cx).id() + }); + + let path_1 = ProjectPath { + worktree_id, + path: Path::new("1.txt").into(), + }; + let path_2 = ProjectPath { + worktree_id, + path: Path::new("2.js").into(), + }; + let path_3 = ProjectPath { + worktree_id, + path: Path::new("3.rs").into(), + }; + + let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone()); + + let get_path = |pane: &Pane, idx: usize, cx: &AppContext| { + pane.item_for_index(idx).unwrap().project_path(cx).unwrap() + }; + + // Opening item 3 as a "permanent" tab + workspace + .update(cx, |workspace, cx| { + workspace.open_path(path_3.clone(), None, false, cx) + }) + .await + .unwrap(); + + pane.update(cx, |pane, cx| { + assert_eq!(pane.items_len(), 1); + assert_eq!(get_path(pane, 0, cx), path_3.clone()); + assert_eq!(pane.preview_item_id(), None); + + assert!(!pane.can_navigate_backward()); + assert!(!pane.can_navigate_forward()); + }); + + // Open item 1 as preview + workspace + .update(cx, |workspace, cx| { + workspace.open_path_preview(path_1.clone(), None, true, true, cx) + }) + .await + .unwrap(); + + pane.update(cx, |pane, cx| { + assert_eq!(pane.items_len(), 2); + assert_eq!(get_path(pane, 0, cx), path_3.clone()); + assert_eq!(get_path(pane, 1, cx), path_1.clone()); + assert_eq!( + pane.preview_item_id(), + Some(pane.items().nth(1).unwrap().item_id()) + ); + + assert!(pane.can_navigate_backward()); + assert!(!pane.can_navigate_forward()); + }); + + // Open item 2 as preview + workspace + .update(cx, |workspace, cx| { + workspace.open_path_preview(path_2.clone(), None, true, true, cx) + }) + .await + .unwrap(); + + pane.update(cx, |pane, cx| { + assert_eq!(pane.items_len(), 2); + assert_eq!(get_path(pane, 0, cx), path_3.clone()); + assert_eq!(get_path(pane, 1, cx), path_2.clone()); + assert_eq!( + pane.preview_item_id(), + Some(pane.items().nth(1).unwrap().item_id()) + ); + + assert!(pane.can_navigate_backward()); + assert!(!pane.can_navigate_forward()); + }); + + // Going back should show item 1 as preview + workspace + .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx)) + .await + .unwrap(); + + pane.update(cx, |pane, cx| { + assert_eq!(pane.items_len(), 2); + assert_eq!(get_path(pane, 0, cx), path_3.clone()); + assert_eq!(get_path(pane, 1, cx), path_1.clone()); + assert_eq!( + pane.preview_item_id(), + Some(pane.items().nth(1).unwrap().item_id()) + ); + + assert!(pane.can_navigate_backward()); + assert!(pane.can_navigate_forward()); + }); + + // Closing item 1 + pane.update(cx, |pane, cx| { + pane.close_item_by_id( + pane.active_item().unwrap().item_id(), + workspace::SaveIntent::Skip, + cx, + ) + }) + .await + .unwrap(); + + pane.update(cx, |pane, cx| { + assert_eq!(pane.items_len(), 1); + assert_eq!(get_path(pane, 0, cx), path_3.clone()); + assert_eq!(pane.preview_item_id(), None); + + assert!(pane.can_navigate_backward()); + assert!(!pane.can_navigate_forward()); + }); + + // Going back should show item 1 as preview + workspace + .update(cx, |workspace, cx| workspace.go_back(pane.downgrade(), cx)) + .await + .unwrap(); + + pane.update(cx, |pane, cx| { + assert_eq!(pane.items_len(), 2); + assert_eq!(get_path(pane, 0, cx), path_3.clone()); + assert_eq!(get_path(pane, 1, cx), path_1.clone()); + assert_eq!( + pane.preview_item_id(), + Some(pane.items().nth(1).unwrap().item_id()) + ); + + assert!(pane.can_navigate_backward()); + assert!(pane.can_navigate_forward()); + }); + + // Close permanent tab + pane.update(cx, |pane, cx| { + let id = pane.items().nth(0).unwrap().item_id(); + pane.close_item_by_id(id, workspace::SaveIntent::Skip, cx) + }) + .await + .unwrap(); + + pane.update(cx, |pane, cx| { + assert_eq!(pane.items_len(), 1); + assert_eq!(get_path(pane, 0, cx), path_1.clone()); + assert_eq!( + pane.preview_item_id(), + Some(pane.items().nth(0).unwrap().item_id()) + ); + + assert!(pane.can_navigate_backward()); + assert!(pane.can_navigate_forward()); + }); + + // Split pane to the right + pane.update(cx, |pane, cx| { + pane.split(workspace::SplitDirection::Right, cx); + }); + + let right_pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone()); + + pane.update(cx, |pane, cx| { + assert_eq!(pane.items_len(), 1); + assert_eq!(get_path(pane, 0, cx), path_1.clone()); + assert_eq!( + pane.preview_item_id(), + Some(pane.items().nth(0).unwrap().item_id()) + ); + + assert!(pane.can_navigate_backward()); + assert!(pane.can_navigate_forward()); + }); + + right_pane.update(cx, |pane, cx| { + assert_eq!(pane.items_len(), 1); + assert_eq!(get_path(pane, 0, cx), path_1.clone()); + assert_eq!(pane.preview_item_id(), None); + + assert!(!pane.can_navigate_backward()); + assert!(!pane.can_navigate_forward()); + }); + + // Open item 2 as preview in right pane + workspace + .update(cx, |workspace, cx| { + workspace.open_path_preview(path_2.clone(), None, true, true, cx) + }) + .await + .unwrap(); + + pane.update(cx, |pane, cx| { + assert_eq!(pane.items_len(), 1); + assert_eq!(get_path(pane, 0, cx), path_1.clone()); + assert_eq!( + pane.preview_item_id(), + Some(pane.items().nth(0).unwrap().item_id()) + ); + + assert!(pane.can_navigate_backward()); + assert!(pane.can_navigate_forward()); + }); + + right_pane.update(cx, |pane, cx| { + assert_eq!(pane.items_len(), 2); + assert_eq!(get_path(pane, 0, cx), path_1.clone()); + assert_eq!(get_path(pane, 1, cx), path_2.clone()); + assert_eq!( + pane.preview_item_id(), + Some(pane.items().nth(1).unwrap().item_id()) + ); + + assert!(pane.can_navigate_backward()); + assert!(!pane.can_navigate_forward()); + }); + + // Focus left pane + workspace.update(cx, |workspace, cx| { + workspace.activate_pane_in_direction(workspace::SplitDirection::Left, cx) + }); + + // Open item 2 as preview in left pane + workspace + .update(cx, |workspace, cx| { + workspace.open_path_preview(path_2.clone(), None, true, true, cx) + }) + .await + .unwrap(); + + pane.update(cx, |pane, cx| { + assert_eq!(pane.items_len(), 1); + assert_eq!(get_path(pane, 0, cx), path_2.clone()); + assert_eq!( + pane.preview_item_id(), + Some(pane.items().nth(0).unwrap().item_id()) + ); + + assert!(pane.can_navigate_backward()); + assert!(!pane.can_navigate_forward()); + }); + + right_pane.update(cx, |pane, cx| { + assert_eq!(pane.items_len(), 2); + assert_eq!(get_path(pane, 0, cx), path_1.clone()); + assert_eq!(get_path(pane, 1, cx), path_2.clone()); + assert_eq!( + pane.preview_item_id(), + Some(pane.items().nth(1).unwrap().item_id()) + ); + + assert!(pane.can_navigate_backward()); + assert!(!pane.can_navigate_forward()); + }); +} diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index bae647b82668422506f2e09ac240210eda206859..59099dd486ea334e673f2567555a0378ffc8804d 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -24,7 +24,7 @@ use ui::{prelude::*, Label}; use util::ResultExt; use workspace::notifications::NotificationId; use workspace::{ - item::{FollowableItem, Item, ItemEvent, ItemHandle}, + item::{FollowableItem, Item, ItemEvent, ItemHandle, TabContentParams}, register_followable_item, searchable::SearchableItemHandle, ItemNavHistory, Pane, SaveIntent, Toast, ViewId, Workspace, WorkspaceId, @@ -374,7 +374,7 @@ impl Item for ChannelView { } } - fn tab_content(&self, _: Option, selected: bool, cx: &WindowContext) -> AnyElement { + fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement { let label = if let Some(channel) = self.channel(cx) { match ( self.channel_buffer.read(cx).buffer().read(cx).read_only(), @@ -388,7 +388,7 @@ impl Item for ChannelView { "channel notes (disconnected)".to_string() }; Label::new(label) - .color(if selected { + .color(if params.selected { Color::Default } else { Color::Muted diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 7dec4be5cd9a7a77d257f16f9dfe6bf6c16b9105..e764a9e8ef0de77bee5f041761d387f91d7006c2 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -38,7 +38,7 @@ pub use toolbar_controls::ToolbarControls; use ui::{h_flex, prelude::*, Icon, IconName, Label}; use util::TryFutureExt; use workspace::{ - item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, + item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, ItemNavHistory, Pane, ToolbarItemLocation, Workspace, }; @@ -645,10 +645,10 @@ impl Item for ProjectDiagnosticsEditor { Some("Project Diagnostics".into()) } - fn tab_content(&self, _detail: Option, selected: bool, _: &WindowContext) -> AnyElement { + fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement { if self.summary.error_count == 0 && self.summary.warning_count == 0 { Label::new("No problems") - .color(if selected { + .color(if params.selected { Color::Default } else { Color::Muted @@ -663,7 +663,7 @@ impl Item for ProjectDiagnosticsEditor { .gap_1() .child(Icon::new(IconName::XCircle).color(Color::Error)) .child(Label::new(self.summary.error_count.to_string()).color( - if selected { + if params.selected { Color::Default } else { Color::Muted @@ -677,7 +677,7 @@ impl Item for ProjectDiagnosticsEditor { .gap_1() .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning)) .child(Label::new(self.summary.warning_count.to_string()).color( - if selected { + if params.selected { Color::Default } else { Color::Muted diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index e3e596a90af986e49c030d140016cc2cfad165be..042661dff4720fa994f02ed84b7b82cd099e921a 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -19,7 +19,7 @@ use project::repository::GitFileStatus; use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath}; use rpc::proto::{self, update_view, PeerId}; use settings::Settings; -use workspace::item::ItemSettings; +use workspace::item::{ItemSettings, TabContentParams}; use std::{ borrow::Cow, @@ -594,7 +594,7 @@ impl Item for Editor { Some(path.to_string_lossy().to_string().into()) } - fn tab_content(&self, detail: Option, selected: bool, cx: &WindowContext) -> AnyElement { + fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement { let label_color = if ItemSettings::get_global(cx).git_status { self.buffer() .read(cx) @@ -602,14 +602,14 @@ impl Item for Editor { .and_then(|buffer| buffer.read(cx).project_path(cx)) .and_then(|path| self.project.as_ref()?.read(cx).entry_for_path(&path, cx)) .map(|entry| { - entry_git_aware_label_color(entry.git_status, entry.is_ignored, selected) + entry_git_aware_label_color(entry.git_status, entry.is_ignored, params.selected) }) - .unwrap_or_else(|| entry_label_color(selected)) + .unwrap_or_else(|| entry_label_color(params.selected)) } else { - entry_label_color(selected) + entry_label_color(params.selected) }; - let description = detail.and_then(|detail| { + let description = params.detail.and_then(|detail| { let path = path_for_buffer(&self.buffer, detail, false, cx)?; let description = path.to_string_lossy(); let description = description.trim(); @@ -623,7 +623,11 @@ impl Item for Editor { h_flex() .gap_2() - .child(Label::new(self.title(cx).to_string()).color(label_color)) + .child( + Label::new(self.title(cx).to_string()) + .color(label_color) + .italic(params.preview), + ) .when_some(description, |this, description| { this.child( Label::new(description) diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 13209272c02997a661f5861b8c7c72e966375f64..b9b7817309191e9f19d98dd0de5fb55e7a775dc2 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -23,6 +23,7 @@ use std::{ops::Range, sync::Arc}; use theme::ThemeSettings; use ui::{popover_menu, prelude::*, ContextMenu, ToggleButton, Tooltip}; use util::ResultExt as _; +use workspace::item::TabContentParams; use workspace::{ item::{Item, ItemEvent}, Workspace, WorkspaceId, @@ -925,9 +926,9 @@ impl FocusableView for ExtensionsPage { impl Item for ExtensionsPage { type Event = ItemEvent; - fn tab_content(&self, _: Option, selected: bool, _: &WindowContext) -> AnyElement { + fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement { Label::new("Extensions") - .color(if selected { + .color(if params.selected { Color::Default } else { Color::Muted diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 283b7c54950f09cec81536022787f10981b7d699..03411c130a9977e070f8e21da5355f4e7ad9c31d 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -22,6 +22,7 @@ itertools = "0.11" menu.workspace = true picker.workspace = true project.workspace = true +settings.workspace = true text.workspace = true theme.workspace = true ui.workspace = true diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index d7c7dfd4b3d779f4ba83efb291876e08beec7ebf..8447dc8a5539b64a46d912ad652aa4a0bfc4b6b1 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -12,6 +12,7 @@ use gpui::{ use itertools::Itertools; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; +use settings::Settings; use std::{ cmp, path::{Path, PathBuf}, @@ -23,7 +24,7 @@ use std::{ use text::Point; use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing}; use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; -use workspace::{ModalView, Workspace}; +use workspace::{item::PreviewTabsSettings, ModalView, Workspace}; actions!(file_finder, [Toggle, SelectPrev]); @@ -782,13 +783,24 @@ impl PickerDelegate for FileFinderDelegate { if let Some(m) = self.matches.get(self.selected_index()) { if let Some(workspace) = self.workspace.upgrade() { let open_task = workspace.update(cx, move |workspace, cx| { - let split_or_open = |workspace: &mut Workspace, project_path, cx| { - if secondary { - workspace.split_path(project_path, cx) - } else { - workspace.open_path(project_path, None, true, cx) - } - }; + let split_or_open = + |workspace: &mut Workspace, + project_path, + cx: &mut ViewContext| { + let allow_preview = + PreviewTabsSettings::get_global(cx).enable_preview_from_file_finder; + if secondary { + workspace.split_path_preview(project_path, allow_preview, cx) + } else { + workspace.open_path_preview( + project_path, + None, + true, + allow_preview, + cx, + ) + } + }; match m { Match::History(history_match, _) => { let worktree_id = history_match.project.worktree_id; diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index 854559c1024b83d271180c5d408557abe99bfd1f..54adbb38915985e4f8a8459e2f13659164b41d23 100644 --- a/crates/gpui/src/styled.rs +++ b/crates/gpui/src/styled.rs @@ -1,7 +1,7 @@ use crate::{ self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, - DefiniteLength, Fill, FlexDirection, FlexWrap, FontWeight, Hsla, JustifyContent, Length, - Position, SharedString, StyleRefinement, Visibility, WhiteSpace, + DefiniteLength, Fill, FlexDirection, FlexWrap, FontStyle, FontWeight, Hsla, JustifyContent, + Length, Position, SharedString, StyleRefinement, Visibility, WhiteSpace, }; use crate::{BoxShadow, TextStyleRefinement}; use smallvec::{smallvec, SmallVec}; @@ -681,6 +681,24 @@ pub trait Styled: Sized { self } + /// Set the font style to 'non-italic', + /// see the [Tailwind Docs](https://tailwindcss.com/docs/font-style#italicizing-text) + fn non_italic(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_style = Some(FontStyle::Normal); + self + } + + /// Set the font style to 'italic', + /// see the [Tailwind Docs](https://tailwindcss.com/docs/font-style#italicizing-text) + fn italic(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .font_style = Some(FontStyle::Italic); + self + } + /// Remove the text decoration on this element, this value cascades to its child elements. fn text_decoration_none(mut self) -> Self { self.text_style() diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 22dc3fb5d6a6423bdaabf599acf5687890b2f0eb..62ed376463bd27b3f6e14e37389c936b846d49e0 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -11,7 +11,7 @@ use project::{Project, ProjectEntryId, ProjectPath}; use std::{ffi::OsStr, path::PathBuf}; use util::ResultExt; use workspace::{ - item::{Item, ProjectItem}, + item::{Item, ProjectItem, TabContentParams}, ItemId, Pane, Workspace, WorkspaceId, }; @@ -72,12 +72,7 @@ pub struct ImageView { impl Item for ImageView { type Event = (); - fn tab_content( - &self, - _detail: Option, - selected: bool, - _cx: &WindowContext, - ) -> AnyElement { + fn tab_content(&self, params: TabContentParams, _cx: &WindowContext) -> AnyElement { let title = self .path .file_name() @@ -86,11 +81,12 @@ impl Item for ImageView { .to_string(); Label::new(title) .single_line() - .color(if selected { + .color(if params.selected { Color::Default } else { Color::Muted }) + .italic(params.preview) .into_any_element() } diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 0a106a3f80e5919353821c67a5d86dbf9e7b7444..429e0c278c6ac1c3f678a9346ca9e9a4501350fc 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -13,7 +13,7 @@ use std::{borrow::Cow, sync::Arc}; use ui::{popover_menu, prelude::*, Button, Checkbox, ContextMenu, Label, Selection}; use util::maybe; use workspace::{ - item::{Item, ItemHandle}, + item::{Item, ItemHandle, TabContentParams}, searchable::{SearchEvent, SearchableItem, SearchableItemHandle}, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; @@ -628,9 +628,9 @@ impl Item for LspLogView { Editor::to_item_events(event, f) } - fn tab_content(&self, _: Option, selected: bool, _: &WindowContext<'_>) -> AnyElement { + fn tab_content(&self, params: TabContentParams, _: &WindowContext<'_>) -> AnyElement { Label::new("LSP Logs") - .color(if selected { + .color(if params.selected { Color::Default } else { Color::Muted diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 1dd647fc82d63494f6afdc9617ecf88541c830d1..a8a3906b5331df6077cffa78431c2521058afe16 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -11,7 +11,7 @@ use theme::ActiveTheme; use tree_sitter::{Node, TreeCursor}; use ui::{h_flex, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu}; use workspace::{ - item::{Item, ItemHandle}, + item::{Item, ItemHandle, TabContentParams}, SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; @@ -391,9 +391,9 @@ impl Item for SyntaxTreeView { fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {} - fn tab_content(&self, _: Option, selected: bool, _: &WindowContext<'_>) -> AnyElement { + fn tab_content(&self, params: TabContentParams, _: &WindowContext<'_>) -> AnyElement { Label::new("Syntax Tree") - .color(if selected { + .color(if params.selected { Color::Default } else { Color::Muted diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index 7c9ecbfd260d061d2854502ddd0d4a3678deb54e..4f8bb784752c41d3f224505869a4ea393db37435 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -12,7 +12,7 @@ use gpui::{ }; use language::LanguageRegistry; use ui::prelude::*; -use workspace::item::{Item, ItemHandle}; +use workspace::item::{Item, ItemHandle, TabContentParams}; use workspace::{Pane, Workspace}; use crate::OpenPreviewToTheSide; @@ -439,15 +439,10 @@ impl EventEmitter for MarkdownPreviewView {} impl Item for MarkdownPreviewView { type Event = PreviewEvent; - fn tab_content( - &self, - _detail: Option, - selected: bool, - _cx: &WindowContext, - ) -> AnyElement { + fn tab_content(&self, params: TabContentParams, _cx: &WindowContext) -> AnyElement { h_flex() .gap_2() - .child(Icon::new(IconName::FileDoc).color(if selected { + .child(Icon::new(IconName::FileDoc).color(if params.selected { Color::Default } else { Color::Muted @@ -458,7 +453,7 @@ impl Item for MarkdownPreviewView { } else { self.fallback_tab_description.clone() }) - .color(if selected { + .color(if params.selected { Color::Default } else { Color::Muted diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 4e8df2d93e293fe63977b5ac007f80df202c3900..f939111e5d97b1c0037569ad1dbe3c5909556113 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -130,6 +130,7 @@ actions!( Paste, Rename, Open, + OpenPermanent, ToggleFocus, NewSearchInDirectory, ] @@ -156,6 +157,7 @@ pub enum Event { OpenedEntry { entry_id: ProjectEntryId, focus_opened_item: bool, + allow_preview: bool, }, SplitEntry { entry_id: ProjectEntryId, @@ -262,6 +264,7 @@ impl ProjectPanel { &Event::OpenedEntry { entry_id, focus_opened_item, + allow_preview, } => { if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { @@ -270,13 +273,14 @@ impl ProjectPanel { let entry_id = entry.id; workspace - .open_path( + .open_path_preview( ProjectPath { worktree_id, path: file_path.clone(), }, None, focus_opened_item, + allow_preview, cx, ) .detach_and_prompt_err("Failed to open file", cx, move |e, _| { @@ -592,9 +596,22 @@ impl ProjectPanel { } fn open(&mut self, _: &Open, cx: &mut ViewContext) { + self.open_internal(true, false, cx); + } + + fn open_permanent(&mut self, _: &OpenPermanent, cx: &mut ViewContext) { + self.open_internal(false, true, cx); + } + + fn open_internal( + &mut self, + allow_preview: bool, + focus_opened_item: bool, + cx: &mut ViewContext, + ) { if let Some((_, entry)) = self.selected_entry(cx) { if entry.is_file() { - self.open_entry(entry.id, true, cx); + self.open_entry(entry.id, focus_opened_item, allow_preview, cx); } else { self.toggle_expanded(entry.id, cx); } @@ -666,7 +683,7 @@ impl ProjectPanel { } this.update_visible_entries(None, cx); if is_new_entry && !is_dir { - this.open_entry(new_entry.id, true, cx); + this.open_entry(new_entry.id, true, false, cx); } cx.notify(); })?; @@ -686,11 +703,13 @@ impl ProjectPanel { &mut self, entry_id: ProjectEntryId, focus_opened_item: bool, + allow_preview: bool, cx: &mut ViewContext, ) { cx.emit(Event::OpenedEntry { entry_id, focus_opened_item, + allow_preview, }); } @@ -1461,7 +1480,13 @@ impl ProjectPanel { if event.down.modifiers.secondary() { this.split_entry(entry_id, cx); } else { - this.open_entry(entry_id, event.up.click_count > 1, cx); + let click_count = event.up.click_count; + this.open_entry( + entry_id, + click_count > 1, + click_count == 1, + cx, + ); } } } @@ -1535,6 +1560,7 @@ impl Render for ProjectPanel { .on_action(cx.listener(Self::collapse_selected_entry)) .on_action(cx.listener(Self::collapse_all_entries)) .on_action(cx.listener(Self::open)) + .on_action(cx.listener(Self::open_permanent)) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::copy_path)) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 4b8ad21daad67907f1b4d8eb81c5c70d89129a8a..ebbef2fddaf69b5d45eeacf5c45bc02e33d84eea 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -36,12 +36,11 @@ use ui::{ }; use util::paths::PathMatcher; use workspace::{ - item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, + item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, searchable::{Direction, SearchableItem, SearchableItemHandle}, - ItemNavHistory, Pane, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, - WorkspaceId, + DeploySearch, ItemNavHistory, NewSearch, Pane, ToolbarItemEvent, ToolbarItemLocation, + ToolbarItemView, Workspace, WorkspaceId, }; -use workspace::{DeploySearch, NewSearch}; const MIN_INPUT_WIDTH_REMS: f32 = 15.; const MAX_INPUT_WIDTH_REMS: f32 = 30.; @@ -379,7 +378,7 @@ impl Item for ProjectSearchView { .update(cx, |editor, cx| editor.deactivated(cx)); } - fn tab_content(&self, _: Option, selected: bool, cx: &WindowContext<'_>) -> AnyElement { + fn tab_content(&self, params: TabContentParams, cx: &WindowContext<'_>) -> AnyElement { let last_query: Option = self .model .read(cx) @@ -395,12 +394,14 @@ impl Item for ProjectSearchView { .unwrap_or_else(|| "Project Search".into()); h_flex() .gap_2() - .child(Icon::new(IconName::MagnifyingGlass).color(if selected { - Color::Default - } else { - Color::Muted - })) - .child(Label::new(tab_name).color(if selected { + .child( + Icon::new(IconName::MagnifyingGlass).color(if params.selected { + Color::Default + } else { + Color::Muted + }), + ) + .child(Label::new(tab_name).color(if params.selected { Color::Default } else { Color::Muted diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index 913877af1051adb58586aa0bbbe8ce91acba04ae..651d2ba284318274ea5fc683e1114ad3d0c40a2e 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -13,7 +13,7 @@ use std::sync::Arc; use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip}; use util::ResultExt; use workspace::{ - item::ItemHandle, + item::{ItemHandle, TabContentParams}, pane::{render_item_indicator, tab_details, Event as PaneEvent}, ModalView, Pane, SaveIntent, Workspace, }; @@ -130,6 +130,7 @@ struct TabMatch { item_index: usize, item: Box, detail: usize, + preview: bool, } pub struct TabSwitcherDelegate { @@ -202,6 +203,7 @@ impl TabSwitcherDelegate { item_index, item: item.boxed_clone(), detail, + preview: pane.is_active_preview_item(item.item_id()), }) .for_each(|tab_match| self.matches.push(tab_match)); @@ -324,7 +326,12 @@ impl PickerDelegate for TabSwitcherDelegate { .get(ix) .expect("Invalid matches state: no element for index {ix}"); - let label = tab_match.item.tab_content(Some(tab_match.detail), true, cx); + let params = TabContentParams { + detail: Some(tab_match.detail), + selected: true, + preview: tab_match.preview, + }; + let label = tab_match.item.tab_content(params, cx); let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx); let indicator_color = if let Some(ref indicator) = indicator { indicator.color diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 2c62e40a3c69e492af2c8427fbad08903bf832b1..12a67ab537ff1534085e564157693a2dd84c8b6e 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -26,7 +26,7 @@ use terminal_element::TerminalElement; use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label}; use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ - item::{BreadcrumbText, Item, ItemEvent}, + item::{BreadcrumbText, Item, ItemEvent, TabContentParams}, notifications::NotifyResultExt, register_deserializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, @@ -783,12 +783,7 @@ impl Item for TerminalView { Some(self.terminal().read(cx).title(false).into()) } - fn tab_content( - &self, - _detail: Option, - selected: bool, - cx: &WindowContext, - ) -> AnyElement { + fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement { let terminal = self.terminal().read(cx); let title = terminal.title(true); let icon = match terminal.task() { @@ -808,7 +803,7 @@ impl Item for TerminalView { h_flex() .gap_2() .child(Icon::new(icon)) - .child(Label::new(title).color(if selected { + .child(Label::new(title).color(if params.selected { Color::Default } else { Color::Muted diff --git a/crates/ui/src/components/label/highlighted_label.rs b/crates/ui/src/components/label/highlighted_label.rs index 88174f489599c1cc77868a9f29921c3664af6a4f..876f5846723533d7263e7f1a9db62865677f9c4a 100644 --- a/crates/ui/src/components/label/highlighted_label.rs +++ b/crates/ui/src/components/label/highlighted_label.rs @@ -43,6 +43,11 @@ impl LabelCommon for HighlightedLabel { self.base = self.base.strikethrough(strikethrough); self } + + fn italic(mut self, italic: bool) -> Self { + self.base = self.base.italic(italic); + self + } } impl RenderOnce for HighlightedLabel { diff --git a/crates/ui/src/components/label/label.rs b/crates/ui/src/components/label/label.rs index 09ded5db63742f12a2b5a2c2159e7cf7c2283924..5560e9fea8eb593eab029917751b8afacbfe1aee 100644 --- a/crates/ui/src/components/label/label.rs +++ b/crates/ui/src/components/label/label.rs @@ -126,6 +126,20 @@ impl LabelCommon for Label { self.base = self.base.strikethrough(strikethrough); self } + + /// Sets the italic property of the label. + /// + /// # Examples + /// + /// ``` + /// use ui::prelude::*; + /// + /// let my_label = Label::new("Hello, World!").italic(true); + /// ``` + fn italic(mut self, italic: bool) -> Self { + self.base = self.base.italic(italic); + self + } } impl RenderOnce for Label { diff --git a/crates/ui/src/components/label/label_like.rs b/crates/ui/src/components/label/label_like.rs index 04fb1936603268ef86621c38df6fb51c8fcd4ac8..2d4577f05ceff1052978c03a6dbb16f44b77165b 100644 --- a/crates/ui/src/components/label/label_like.rs +++ b/crates/ui/src/components/label/label_like.rs @@ -33,6 +33,9 @@ pub trait LabelCommon { /// Sets the strikethrough property of the label. fn strikethrough(self, strikethrough: bool) -> Self; + + /// Sets the italic property of the label. + fn italic(self, italic: bool) -> Self; } #[derive(IntoElement)] @@ -41,6 +44,7 @@ pub struct LabelLike { line_height_style: LineHeightStyle, pub(crate) color: Color, strikethrough: bool, + italic: bool, children: SmallVec<[AnyElement; 2]>, } @@ -51,6 +55,7 @@ impl LabelLike { line_height_style: LineHeightStyle::default(), color: Color::Default, strikethrough: false, + italic: false, children: SmallVec::new(), } } @@ -76,6 +81,11 @@ impl LabelCommon for LabelLike { self.strikethrough = strikethrough; self } + + fn italic(mut self, italic: bool) -> Self { + self.italic = italic; + self + } } impl ParentElement for LabelLike { @@ -106,6 +116,7 @@ impl RenderOnce for LabelLike { .when(self.line_height_style == LineHeightStyle::UiLabel, |this| { this.line_height(relative(1.)) }) + .when(self.italic, |this| this.italic()) .text_color(self.color.color(cx)) .children(self.children) } diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index d36fcf53a8b58259f520425084156692d9807e64..62a82b2b54849ae26dc6b9da4468ea4860ab5c25 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -15,7 +15,7 @@ use ui::{prelude::*, CheckboxWithLabel}; use vim::VimModeSetting; use workspace::{ dock::DockPosition, - item::{Item, ItemEvent}, + item::{Item, ItemEvent, TabContentParams}, open_new, AppState, Welcome, Workspace, WorkspaceId, }; @@ -284,9 +284,9 @@ impl FocusableView for WelcomePage { impl Item for WelcomePage { type Event = ItemEvent; - fn tab_content(&self, _: Option, selected: bool, _: &WindowContext) -> AnyElement { + fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement { Label::new("Welcome to Zed!") - .color(if selected { + .color(if params.selected { Color::Default } else { Color::Muted diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 26b46ea3d32ac393648096d314514be3acf45f0f..8ee69ced3f589997480093b953a2454fe4ea7608 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -42,6 +42,12 @@ pub struct ItemSettings { pub close_position: ClosePosition, } +#[derive(Deserialize)] +pub struct PreviewTabsSettings { + pub enabled: bool, + pub enable_preview_from_file_finder: bool, +} + #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "lowercase")] pub enum ClosePosition { @@ -71,6 +77,19 @@ pub struct ItemSettingsContent { close_position: Option, } +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct PreviewTabsSettingsContent { + /// Whether to show opened editors as preview editors. + /// Preview editors do not stay open, are reused until explicitly set to be kept open opened (via double-click or editing) and show file names in italic. + /// + /// Default: true + enabled: Option, + /// Whether to open a preview editor when opening a file using the file finder. + /// + /// Default: false + enable_preview_from_file_finder: Option, +} + impl Settings for ItemSettings { const KEY: Option<&'static str> = Some("tabs"); @@ -81,6 +100,16 @@ impl Settings for ItemSettings { } } +impl Settings for PreviewTabsSettings { + const KEY: Option<&'static str> = Some("preview_tabs"); + + type FileContent = PreviewTabsSettingsContent; + + fn load(sources: SettingsSources, _: &mut AppContext) -> Result { + sources.json_merge() + } +} + #[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] pub enum ItemEvent { CloseItem, @@ -95,14 +124,16 @@ pub struct BreadcrumbText { pub highlights: Option, HighlightStyle)>>, } +#[derive(Debug, Clone, Copy)] +pub struct TabContentParams { + pub detail: Option, + pub selected: bool, + pub preview: bool, +} + pub trait Item: FocusableView + EventEmitter { type Event; - fn tab_content( - &self, - _detail: Option, - _selected: bool, - _cx: &WindowContext, - ) -> AnyElement { + fn tab_content(&self, _params: TabContentParams, _cx: &WindowContext) -> AnyElement { gpui::Empty.into_any() } fn to_item_events(_event: &Self::Event, _f: impl FnMut(ItemEvent)) {} @@ -236,9 +267,9 @@ pub trait ItemHandle: 'static + Send { fn focus_handle(&self, cx: &WindowContext) -> FocusHandle; fn tab_tooltip_text(&self, cx: &AppContext) -> Option; fn tab_description(&self, detail: usize, cx: &AppContext) -> Option; - fn tab_content(&self, detail: Option, selected: bool, cx: &WindowContext) -> AnyElement; + fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement; fn telemetry_event_text(&self, cx: &WindowContext) -> Option<&'static str>; - fn dragged_tab_content(&self, detail: Option, cx: &WindowContext) -> AnyElement; + fn dragged_tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement; fn project_path(&self, cx: &AppContext) -> Option; fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>; fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[EntityId; 3]>; @@ -339,12 +370,18 @@ impl ItemHandle for View { self.read(cx).tab_description(detail, cx) } - fn tab_content(&self, detail: Option, selected: bool, cx: &WindowContext) -> AnyElement { - self.read(cx).tab_content(detail, selected, cx) + fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement { + self.read(cx).tab_content(params, cx) } - fn dragged_tab_content(&self, detail: Option, cx: &WindowContext) -> AnyElement { - self.read(cx).tab_content(detail, true, cx) + fn dragged_tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement { + self.read(cx).tab_content( + TabContentParams { + selected: true, + ..params + }, + cx, + ) } fn project_path(&self, cx: &AppContext) -> Option { @@ -532,6 +569,7 @@ impl ItemHandle for View { Pane::autosave_item(&item, workspace.project().clone(), cx) }); } + pane.update(cx, |pane, cx| pane.handle_item_edit(item.item_id(), cx)); } _ => {} @@ -817,7 +855,7 @@ impl WeakFollowableItemHandle for WeakView { #[cfg(any(test, feature = "test-support"))] pub mod test { - use super::{Item, ItemEvent}; + use super::{Item, ItemEvent, TabContentParams}; use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId}; use gpui::{ AnyElement, AppContext, Context as _, EntityId, EventEmitter, FocusableView, @@ -990,11 +1028,10 @@ pub mod test { fn tab_content( &self, - detail: Option, - _selected: bool, + params: TabContentParams, _cx: &ui::prelude::WindowContext, ) -> AnyElement { - self.tab_detail.set(detail); + self.tab_detail.set(params.detail); gpui::div().into_any_element() } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 57320256e6fb4f0cea917a60a0db2d7488baca78..449dae95d79435e4f70083881986ee66f9e2e89c 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1,5 +1,8 @@ use crate::{ - item::{ClosePosition, Item, ItemHandle, ItemSettings, WeakItemHandle}, + item::{ + ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, TabContentParams, + WeakItemHandle, + }, toolbar::Toolbar, workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings}, NewCenterTerminal, NewFile, NewSearch, OpenVisible, SplitDirection, ToggleZoom, Workspace, @@ -11,8 +14,8 @@ use gpui::{ actions, anchored, deferred, impl_actions, prelude::*, Action, AnchorCorner, AnyElement, AppContext, AsyncWindowContext, ClickEvent, DismissEvent, Div, DragMoveEvent, EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusableView, KeyContext, Model, MouseButton, - NavigationDirection, Pixels, Point, PromptLevel, Render, ScrollHandle, Subscription, Task, - View, ViewContext, VisualContext, WeakFocusHandle, WeakView, WindowContext, + MouseDownEvent, NavigationDirection, Pixels, Point, PromptLevel, Render, ScrollHandle, + Subscription, Task, View, ViewContext, VisualContext, WeakFocusHandle, WeakView, WindowContext, }; use parking_lot::Mutex; use project::{Project, ProjectEntryId, ProjectPath}; @@ -120,6 +123,7 @@ actions!( SplitUp, SplitRight, SplitDown, + TogglePreviewTab, ] ); @@ -184,6 +188,7 @@ pub struct Pane { zoomed: bool, was_focused: bool, active_item_index: usize, + preview_item_id: Option, last_focus_handle_by_item: HashMap, nav_history: NavHistory, toolbar: View, @@ -207,6 +212,7 @@ pub struct Pane { pub struct ItemNavHistory { history: NavHistory, item: Arc, + is_preview: bool, } #[derive(Clone)] @@ -242,6 +248,7 @@ pub struct NavigationEntry { pub item: Arc, pub data: Option>, pub timestamp: usize, + pub is_preview: bool, } #[derive(Clone)] @@ -281,6 +288,7 @@ impl Pane { was_focused: false, zoomed: false, active_item_index: 0, + preview_item_id: None, last_focus_handle_by_item: Default::default(), nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState { mode: NavigationMode::Normal, @@ -435,6 +443,10 @@ impl Pane { fn settings_changed(&mut self, cx: &mut ViewContext) { self.display_nav_history_buttons = TabBarSettings::get_global(cx).show_nav_history_buttons; + + if !PreviewTabsSettings::get_global(cx).enabled { + self.preview_item_id = None; + } cx.notify(); } @@ -478,6 +490,7 @@ impl Pane { ItemNavHistory { history: self.nav_history.clone(), item: Arc::new(item.downgrade()), + is_preview: self.preview_item_id == Some(item.item_id()), } } @@ -531,10 +544,45 @@ impl Pane { self.toolbar.update(cx, |_, cx| cx.notify()); } + pub fn preview_item_id(&self) -> Option { + self.preview_item_id + } + + fn preview_item_idx(&self) -> Option { + if let Some(preview_item_id) = self.preview_item_id { + self.items + .iter() + .position(|item| item.item_id() == preview_item_id) + } else { + None + } + } + + pub fn is_active_preview_item(&self, item_id: EntityId) -> bool { + self.preview_item_id == Some(item_id) + } + + /// 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, cx: &AppContext) { + if PreviewTabsSettings::get_global(cx).enabled { + self.preview_item_id = item_id; + } + } + + pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &AppContext) { + if let Some(preview_item_id) = self.preview_item_id { + if preview_item_id == item_id { + self.set_preview_item_id(None, cx) + } + } + } + pub(crate) fn open_item( &mut self, project_entry_id: Option, focus_item: bool, + allow_preview: bool, cx: &mut ViewContext, build_item: impl FnOnce(&mut ViewContext) -> Box, ) -> Box { @@ -552,11 +600,43 @@ impl Pane { } if let Some((index, existing_item)) = existing_item { + // 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) = self.preview_item_id { + if let Some(tab) = self.items.get(index) { + if tab.item_id() == preview_item_id && !allow_preview { + self.set_preview_item_id(None, cx); + } + } + } + self.activate_item(index, focus_item, focus_item, cx); existing_item } else { + let mut destination_index = None; + if allow_preview { + // If we are opening a new item as preview and we have an existing preview tab, remove it. + if let Some(item_idx) = self.preview_item_idx() { + let prev_active_item_index = self.active_item_index; + self.remove_item(item_idx, false, false, cx); + self.active_item_index = prev_active_item_index; + + // If the item is being opened as preview and we have an existing preview tab, + // open the new item in the position of the existing preview tab. + if item_idx < self.items.len() { + destination_index = Some(item_idx); + } + } + } + let new_item = build_item(cx); - self.add_item(new_item.clone(), true, focus_item, None, cx); + + if allow_preview { + self.set_preview_item_id(Some(new_item.item_id()), cx); + } + + self.add_item(new_item.clone(), true, focus_item, destination_index, cx); + new_item } } @@ -648,7 +728,10 @@ impl Pane { self.activate_item(insertion_index, activate_pane, focus_item, cx); } else { self.items.insert(insertion_index, item.clone()); - if insertion_index <= self.active_item_index { + + if insertion_index <= self.active_item_index + && self.preview_item_idx() != Some(self.active_item_index) + { self.active_item_index += 1; } @@ -1043,7 +1126,7 @@ impl Pane { .iter() .position(|i| i.item_id() == item.item_id()) { - pane.remove_item(item_ix, false, cx); + pane.remove_item(item_ix, false, true, cx); } }) .ok(); @@ -1058,6 +1141,7 @@ impl Pane { &mut self, item_index: usize, activate_pane: bool, + close_pane_if_empty: bool, cx: &mut ViewContext, ) { self.activation_history @@ -1091,17 +1175,24 @@ impl Pane { }); if self.items.is_empty() { item.deactivated(cx); - self.update_toolbar(cx); - cx.emit(Event::Remove); + if close_pane_if_empty { + self.update_toolbar(cx); + cx.emit(Event::Remove); + } } if item_index < self.active_item_index { self.active_item_index -= 1; } + let mode = self.nav_history.mode(); self.nav_history.set_mode(NavigationMode::ClosingItem); item.deactivated(cx); - self.nav_history.set_mode(NavigationMode::Normal); + self.nav_history.set_mode(mode); + + if self.is_active_preview_item(item.item_id()) { + self.set_preview_item_id(None, cx); + } if let Some(path) = item.project_path(cx) { let abs_path = self @@ -1125,7 +1216,7 @@ impl Pane { .remove(&item.item_id()); } - if self.items.is_empty() && self.zoomed { + if self.items.is_empty() && close_pane_if_empty && self.zoomed { cx.emit(Event::ZoomOut); } @@ -1290,7 +1381,7 @@ impl Pane { } })?; - self.remove_item(item_index_to_delete, false, cx); + self.remove_item(item_index_to_delete, false, true, cx); self.nav_history.remove_item(item_id); Some(()) @@ -1330,8 +1421,19 @@ impl Pane { cx: &mut ViewContext<'_, Pane>, ) -> impl IntoElement { let is_active = ix == self.active_item_index; - - let label = item.tab_content(Some(detail), is_active, cx); + let is_preview = self + .preview_item_id + .map(|id| id == item.item_id()) + .unwrap_or(false); + + let label = item.tab_content( + TabContentParams { + detail: Some(detail), + selected: is_active, + preview: is_preview, + }, + cx, + ); let close_side = &ItemSettings::get_global(cx).close_position; let indicator = render_item_indicator(item.boxed_clone(), cx); let item_id = item.item_id(); @@ -1363,6 +1465,16 @@ impl Pane { .detach_and_log_err(cx); }), ) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |pane, event: &MouseDownEvent, cx| { + if let Some(id) = pane.preview_item_id { + if id == item_id && event.click_count > 1 { + pane.set_preview_item_id(None, cx); + } + } + }), + ) .on_drag( DraggedTab { item: item.boxed_clone(), @@ -1639,6 +1751,12 @@ impl Pane { let mut to_pane = cx.view().clone(); 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 { + if item_id == preview_item_id { + self.set_preview_item_id(None, cx); + } + } + let from_pane = dragged_tab.pane.clone(); self.workspace .update(cx, |_, cx| { @@ -1786,6 +1904,17 @@ impl Render for Pane { .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| { pane.activate_next_item(true, cx); })) + .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); + } + } + })) + }) .on_action( cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| { if let Some(task) = pane.close_active_item(action, cx) { @@ -1946,7 +2075,8 @@ impl Render for Pane { impl ItemNavHistory { pub fn push(&mut self, data: Option, cx: &mut WindowContext) { - self.history.push(data, self.item.clone(), cx); + self.history + .push(data, self.item.clone(), self.is_preview, cx); } pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option { @@ -2020,6 +2150,7 @@ impl NavHistory { &mut self, data: Option, item: Arc, + is_preview: bool, cx: &mut WindowContext, ) { let state = &mut *self.0.lock(); @@ -2033,6 +2164,7 @@ impl NavHistory { item, data: data.map(|data| Box::new(data) as Box), timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), + is_preview, }); state.forward_stack.clear(); } @@ -2044,6 +2176,7 @@ impl NavHistory { item, data: data.map(|data| Box::new(data) as Box), timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), + is_preview, }); } NavigationMode::GoingForward => { @@ -2054,6 +2187,7 @@ impl NavHistory { item, data: data.map(|data| Box::new(data) as Box), timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), + is_preview, }); } NavigationMode::ClosingItem => { @@ -2064,6 +2198,7 @@ impl NavHistory { item, data: data.map(|data| Box::new(data) as Box), timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), + is_preview, }); } } @@ -2706,7 +2841,14 @@ mod tests { impl Render for DraggedTab { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); - let label = self.item.tab_content(Some(self.detail), false, cx); + let label = self.item.tab_content( + TabContentParams { + detail: Some(self.detail), + selected: false, + preview: false, + }, + cx, + ); Tab::new("") .selected(self.is_active) .child(label) diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index b719b1f70357f62322f67ca115039fa7f755e0f6..77cadc25c5e6451f06bc9748885dbeeec6cf4130 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -168,6 +168,7 @@ define_connection! { // kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global // position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column // active: bool, // Indicates if this item is the active one in the pane + // preview: bool // Indicates if this item is a preview item // ) pub static ref DB: WorkspaceDb<()> = &[sql!( @@ -279,6 +280,10 @@ define_connection! { sql!( ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool ), + // Add preview field to items + sql!( + ALTER TABLE items ADD COLUMN preview INTEGER; //bool + ), ]; } @@ -623,7 +628,7 @@ impl WorkspaceDb { fn get_items(&self, pane_id: PaneId) -> Result> { self.select_bound(sql!( - SELECT kind, item_id, active FROM items + SELECT kind, item_id, active, preview FROM items WHERE pane_id = ? ORDER BY position ))?(pane_id) @@ -636,7 +641,7 @@ impl WorkspaceDb { items: &[SerializedItem], ) -> Result<()> { let mut insert = conn.exec_bound(sql!( - INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active) VALUES (?, ?, ?, ?, ?, ?) + INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?) )).context("Preparing insertion")?; for (position, item) in items.iter().enumerate() { insert((workspace_id, pane_id, position, item))?; @@ -836,15 +841,15 @@ mod tests { vec![ SerializedPaneGroup::Pane(SerializedPane::new( vec![ - SerializedItem::new("Terminal", 5, false), - SerializedItem::new("Terminal", 6, true), + SerializedItem::new("Terminal", 5, false, false), + SerializedItem::new("Terminal", 6, true, false), ], false, )), SerializedPaneGroup::Pane(SerializedPane::new( vec![ - SerializedItem::new("Terminal", 7, true), - SerializedItem::new("Terminal", 8, false), + SerializedItem::new("Terminal", 7, true, false), + SerializedItem::new("Terminal", 8, false, false), ], false, )), @@ -852,8 +857,8 @@ mod tests { ), SerializedPaneGroup::Pane(SerializedPane::new( vec![ - SerializedItem::new("Terminal", 9, false), - SerializedItem::new("Terminal", 10, true), + SerializedItem::new("Terminal", 9, false, false), + SerializedItem::new("Terminal", 10, true, false), ], false, )), @@ -1000,15 +1005,15 @@ mod tests { vec![ SerializedPaneGroup::Pane(SerializedPane::new( vec![ - SerializedItem::new("Terminal", 1, false), - SerializedItem::new("Terminal", 2, true), + SerializedItem::new("Terminal", 1, false, false), + SerializedItem::new("Terminal", 2, true, false), ], false, )), SerializedPaneGroup::Pane(SerializedPane::new( vec![ - SerializedItem::new("Terminal", 4, false), - SerializedItem::new("Terminal", 3, true), + SerializedItem::new("Terminal", 4, false, false), + SerializedItem::new("Terminal", 3, true, false), ], true, )), @@ -1016,8 +1021,8 @@ mod tests { ), SerializedPaneGroup::Pane(SerializedPane::new( vec![ - SerializedItem::new("Terminal", 5, true), - SerializedItem::new("Terminal", 6, false), + SerializedItem::new("Terminal", 5, true, false), + SerializedItem::new("Terminal", 6, false, false), ], false, )), @@ -1047,15 +1052,15 @@ mod tests { vec![ SerializedPaneGroup::Pane(SerializedPane::new( vec![ - SerializedItem::new("Terminal", 1, false), - SerializedItem::new("Terminal", 2, true), + SerializedItem::new("Terminal", 1, false, false), + SerializedItem::new("Terminal", 2, true, false), ], false, )), SerializedPaneGroup::Pane(SerializedPane::new( vec![ - SerializedItem::new("Terminal", 4, false), - SerializedItem::new("Terminal", 3, true), + SerializedItem::new("Terminal", 4, false, false), + SerializedItem::new("Terminal", 3, true, false), ], true, )), @@ -1063,8 +1068,8 @@ mod tests { ), SerializedPaneGroup::Pane(SerializedPane::new( vec![ - SerializedItem::new("Terminal", 5, false), - SerializedItem::new("Terminal", 6, true), + SerializedItem::new("Terminal", 5, false, false), + SerializedItem::new("Terminal", 6, true, false), ], false, )), @@ -1082,15 +1087,15 @@ mod tests { vec![ SerializedPaneGroup::Pane(SerializedPane::new( vec![ - SerializedItem::new("Terminal", 1, false), - SerializedItem::new("Terminal", 2, true), + SerializedItem::new("Terminal", 1, false, false), + SerializedItem::new("Terminal", 2, true, false), ], false, )), SerializedPaneGroup::Pane(SerializedPane::new( vec![ - SerializedItem::new("Terminal", 4, true), - SerializedItem::new("Terminal", 3, false), + SerializedItem::new("Terminal", 4, true, false), + SerializedItem::new("Terminal", 3, false, false), ], true, )), diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index ac8fa763776988d9e250c1b724fcc8338067819c..9dffa3c27e5ae71019e9d79ccd223c44fad90337 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -246,6 +246,7 @@ impl SerializedPane { ) -> Result>>> { let mut item_tasks = Vec::new(); let mut active_item_index = None; + let mut preview_item_index = None; for (index, item) in self.children.iter().enumerate() { let project = project.clone(); item_tasks.push(pane.update(cx, |_, cx| { @@ -261,6 +262,9 @@ impl SerializedPane { if item.active { active_item_index = Some(index); } + if item.preview { + preview_item_index = Some(index); + } } let mut items = Vec::new(); @@ -281,6 +285,14 @@ impl SerializedPane { })?; } + if let Some(preview_item_index) = preview_item_index { + pane.update(cx, |pane, cx| { + if let Some(item) = pane.item_for_index(preview_item_index) { + pane.set_preview_item_id(Some(item.item_id()), cx); + } + })?; + } + anyhow::Ok(items) } } @@ -294,14 +306,16 @@ pub struct SerializedItem { pub kind: Arc, pub item_id: ItemId, pub active: bool, + pub preview: bool, } impl SerializedItem { - pub fn new(kind: impl AsRef, item_id: ItemId, active: bool) -> Self { + pub fn new(kind: impl AsRef, item_id: ItemId, active: bool, preview: bool) -> Self { Self { kind: Arc::from(kind.as_ref()), item_id, active, + preview, } } } @@ -313,20 +327,22 @@ impl Default for SerializedItem { kind: Arc::from("Terminal"), item_id: 100000, active: false, + preview: false, } } } impl StaticColumnCount for SerializedItem { fn column_count() -> usize { - 3 + 4 } } impl Bind for &SerializedItem { fn bind(&self, statement: &Statement, start_index: i32) -> Result { let next_index = statement.bind(&self.kind, start_index)?; let next_index = statement.bind(&self.item_id, next_index)?; - statement.bind(&self.active, next_index) + let next_index = statement.bind(&self.active, next_index)?; + statement.bind(&self.preview, next_index) } } @@ -335,11 +351,13 @@ impl Column for SerializedItem { let (kind, next_index) = Arc::::column(statement, start_index)?; let (item_id, next_index) = ItemId::column(statement, next_index)?; let (active, next_index) = bool::column(statement, next_index)?; + let (preview, next_index) = bool::column(statement, next_index)?; Ok(( SerializedItem { kind, item_id, active, + preview, }, next_index, )) diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index b49da4febed90b1f25609d3a638aa7cf1a679725..a1b8c2eee3149e374d3b61bcffc3afc5abb3b4f2 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -1,5 +1,5 @@ use crate::{ - item::{Item, ItemEvent}, + item::{Item, ItemEvent, TabContentParams}, ItemNavHistory, WorkspaceId, }; use anyhow::Result; @@ -93,21 +93,18 @@ impl Item for SharedScreen { } } - fn tab_content( - &self, - _: Option, - selected: bool, - _: &WindowContext<'_>, - ) -> gpui::AnyElement { + fn tab_content(&self, params: TabContentParams, _: &WindowContext<'_>) -> gpui::AnyElement { h_flex() .gap_1() .child(Icon::new(IconName::Screen)) .child( - Label::new(format!("{}'s screen", self.user.github_login)).color(if selected { - Color::Default - } else { - Color::Muted - }), + Label::new(format!("{}'s screen", self.user.github_login)).color( + if params.selected { + Color::Default + } else { + Color::Muted + }, + ), ) .into_any() } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b72ca6af18ccef0a1d526cdfd74e7acad793b5fa..d38f252a68e57013417b9d9cd3f8f4d435dcb429 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -32,7 +32,10 @@ use gpui::{ LayoutId, ManagedView, Model, ModelContext, PathPromptOptions, Point, PromptLevel, Render, Size, Subscription, Task, View, WeakView, WindowHandle, WindowOptions, }; -use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; +use item::{ + FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings, + ProjectItem, +}; use itertools::Itertools; use language::{LanguageRegistry, Rope}; use lazy_static::lazy_static; @@ -261,6 +264,7 @@ impl Column for WorkspaceId { pub fn init_settings(cx: &mut AppContext) { WorkspaceSettings::register(cx); ItemSettings::register(cx); + PreviewTabsSettings::register(cx); TabBarSettings::register(cx); } @@ -1142,7 +1146,13 @@ impl Workspace { })?; pane.update(&mut cx, |pane, cx| { - let item = pane.open_item(project_entry_id, true, cx, build_item); + let item = pane.open_item( + project_entry_id, + true, + entry.is_preview, + cx, + build_item, + ); navigated |= Some(item.item_id()) != prev_active_item_id; pane.nav_history_mut().set_mode(NavigationMode::Normal); if let Some(data) = entry.data { @@ -2066,6 +2076,17 @@ impl Workspace { pane: Option>, focus_item: bool, cx: &mut WindowContext, + ) -> Task, anyhow::Error>> { + self.open_path_preview(path, pane, focus_item, false, cx) + } + + pub fn open_path_preview( + &mut self, + path: impl Into, + pane: Option>, + focus_item: bool, + allow_preview: bool, + cx: &mut WindowContext, ) -> Task, anyhow::Error>> { let pane = pane.unwrap_or_else(|| { self.last_active_center_pane.clone().unwrap_or_else(|| { @@ -2080,7 +2101,7 @@ impl Workspace { cx.spawn(move |mut cx| async move { let (project_entry_id, build_item) = task.await?; pane.update(&mut cx, |pane, cx| { - pane.open_item(project_entry_id, focus_item, cx, build_item) + pane.open_item(project_entry_id, focus_item, allow_preview, cx, build_item) }) }) } @@ -2089,6 +2110,15 @@ impl Workspace { &mut self, path: impl Into, cx: &mut ViewContext, + ) -> Task, anyhow::Error>> { + self.split_path_preview(path, false, cx) + } + + pub fn split_path_preview( + &mut self, + path: impl Into, + allow_preview: bool, + cx: &mut ViewContext, ) -> Task, anyhow::Error>> { let pane = self.last_active_center_pane.clone().unwrap_or_else(|| { self.panes @@ -2110,7 +2140,7 @@ impl Workspace { let pane = pane.upgrade()?; let new_pane = this.split_pane(pane, SplitDirection::Right, cx); new_pane.update(cx, |new_pane, cx| { - Some(new_pane.open_item(project_entry_id, true, cx, build_item)) + Some(new_pane.open_item(project_entry_id, true, allow_preview, cx, build_item)) }) }) .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))? @@ -2155,6 +2185,9 @@ impl Workspace { } let item = cx.new_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); + pane.update(cx, |pane, cx| { + pane.set_preview_item_id(Some(item.item_id()), cx) + }); self.add_item(pane, Box::new(item.clone()), cx); item } @@ -2536,7 +2569,7 @@ impl Workspace { if source != destination { // Close item from previous pane source.update(cx, |source, cx| { - source.remove_item(item_ix, false, cx); + source.remove_item(item_ix, false, true, cx); }); } @@ -3408,6 +3441,7 @@ impl Workspace { kind: Arc::from(item_handle.serialized_item_kind()?), item_id: item_handle.item_id().as_u64(), active: Some(item_handle.item_id()) == active_item_id, + preview: pane.is_active_preview_item(item_handle.item_id()), }) }) .collect::>(), diff --git a/docs/src/configuring_zed.md b/docs/src/configuring_zed.md index e62ac95eec7bc3903a0c909849f184cdf5cc28e4..bbaca79953c2749cc4671828270d8ad9e26ad55a 100644 --- a/docs/src/configuring_zed.md +++ b/docs/src/configuring_zed.md @@ -613,6 +613,40 @@ The following settings can be overridden for each specific language: These values take in the same options as the root-level settings with the same name. +## Preview tabs + +- Description: + 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. This is useful for quickly viewing files without cluttering your workspace. Preview tabs display their file names in italics. \ + There are several ways to convert a preview tab into a regular tab: + + - Double-clicking on the file + - Double-clicking on the tab header + - Using the 'project_panel::OpenPermanent' action + - Editing the file + - Dragging the file to a different pane + +- Setting: `preview_tabs` +- Default: + +```json +"preview_tabs": { + "enabled": true, + "enable_preview_from_file_finder": false +} +``` + +**Options** + +### Enable preview from file finder + +- Description: Determines whether to open files in preview mode when selected from the file finder. +- Setting: `enable_preview_from_file_finder` +- Default: `false` + +**Options** + +`boolean` values + ## Preferred Line Length - Description: The column at which to soft-wrap lines, for buffers where soft-wrap is enabled.