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 <conrad.irwin@gmail.com>
This commit is contained in:
co-authored by
Conrad Irwin
parent
edb1ea2433
commit
ea4419076e
@@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<usize>, 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
|
||||
|
||||
@@ -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<usize>, 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
|
||||
|
||||
@@ -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<usize>, 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)
|
||||
|
||||
@@ -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<usize>, 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Workspace>| {
|
||||
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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<usize>,
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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<usize>, 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
|
||||
|
||||
@@ -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<usize>, 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
|
||||
|
||||
@@ -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<PreviewEvent> for MarkdownPreviewView {}
|
||||
impl Item for MarkdownPreviewView {
|
||||
type Event = PreviewEvent;
|
||||
|
||||
fn tab_content(
|
||||
&self,
|
||||
_detail: Option<usize>,
|
||||
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
|
||||
|
||||
@@ -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>) {
|
||||
self.open_internal(true, false, cx);
|
||||
}
|
||||
|
||||
fn open_permanent(&mut self, _: &OpenPermanent, cx: &mut ViewContext<Self>) {
|
||||
self.open_internal(false, true, cx);
|
||||
}
|
||||
|
||||
fn open_internal(
|
||||
&mut self,
|
||||
allow_preview: bool,
|
||||
focus_opened_item: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
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<Self>,
|
||||
) {
|
||||
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))
|
||||
|
||||
@@ -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<usize>, selected: bool, cx: &WindowContext<'_>) -> AnyElement {
|
||||
fn tab_content(&self, params: TabContentParams, cx: &WindowContext<'_>) -> AnyElement {
|
||||
let last_query: Option<SharedString> = 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
|
||||
|
||||
@@ -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<dyn ItemHandle>,
|
||||
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
|
||||
|
||||
@@ -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<usize>,
|
||||
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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<usize>, 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
|
||||
|
||||
@@ -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<ClosePosition>,
|
||||
}
|
||||
|
||||
#[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<bool>,
|
||||
/// Whether to open a preview editor when opening a file using the file finder.
|
||||
///
|
||||
/// Default: false
|
||||
enable_preview_from_file_finder: Option<bool>,
|
||||
}
|
||||
|
||||
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<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
|
||||
pub enum ItemEvent {
|
||||
CloseItem,
|
||||
@@ -95,14 +124,16 @@ pub struct BreadcrumbText {
|
||||
pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct TabContentParams {
|
||||
pub detail: Option<usize>,
|
||||
pub selected: bool,
|
||||
pub preview: bool,
|
||||
}
|
||||
|
||||
pub trait Item: FocusableView + EventEmitter<Self::Event> {
|
||||
type Event;
|
||||
fn tab_content(
|
||||
&self,
|
||||
_detail: Option<usize>,
|
||||
_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<SharedString>;
|
||||
fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString>;
|
||||
fn tab_content(&self, detail: Option<usize>, 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<usize>, cx: &WindowContext) -> AnyElement;
|
||||
fn dragged_tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement;
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
|
||||
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<T: Item> ItemHandle for View<T> {
|
||||
self.read(cx).tab_description(detail, cx)
|
||||
}
|
||||
|
||||
fn tab_content(&self, detail: Option<usize>, 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<usize>, 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<ProjectPath> {
|
||||
@@ -532,6 +569,7 @@ impl<T: Item> ItemHandle for View<T> {
|
||||
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<T: FollowableItem> WeakFollowableItemHandle for WeakView<T> {
|
||||
|
||||
#[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<usize>,
|
||||
_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()
|
||||
}
|
||||
|
||||
|
||||
+156
-14
@@ -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<EntityId>,
|
||||
last_focus_handle_by_item: HashMap<EntityId, WeakFocusHandle>,
|
||||
nav_history: NavHistory,
|
||||
toolbar: View<Toolbar>,
|
||||
@@ -207,6 +212,7 @@ pub struct Pane {
|
||||
pub struct ItemNavHistory {
|
||||
history: NavHistory,
|
||||
item: Arc<dyn WeakItemHandle>,
|
||||
is_preview: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -242,6 +248,7 @@ pub struct NavigationEntry {
|
||||
pub item: Arc<dyn WeakItemHandle>,
|
||||
pub data: Option<Box<dyn Any + Send>>,
|
||||
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>) {
|
||||
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<EntityId> {
|
||||
self.preview_item_id
|
||||
}
|
||||
|
||||
fn preview_item_idx(&self) -> Option<usize> {
|
||||
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<EntityId>, 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<ProjectEntryId>,
|
||||
focus_item: bool,
|
||||
allow_preview: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
|
||||
) -> Box<dyn ItemHandle> {
|
||||
@@ -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>,
|
||||
) {
|
||||
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 is_preview = self
|
||||
.preview_item_id
|
||||
.map(|id| id == item.item_id())
|
||||
.unwrap_or(false);
|
||||
|
||||
let label = item.tab_content(Some(detail), is_active, cx);
|
||||
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<D: 'static + Send + Any>(&mut self, data: Option<D>, 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<NavigationEntry> {
|
||||
@@ -2020,6 +2150,7 @@ impl NavHistory {
|
||||
&mut self,
|
||||
data: Option<D>,
|
||||
item: Arc<dyn WeakItemHandle>,
|
||||
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<dyn Any + Send>),
|
||||
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<dyn Any + Send>),
|
||||
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<dyn Any + Send>),
|
||||
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<dyn Any + Send>),
|
||||
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<Self>) -> 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)
|
||||
|
||||
@@ -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<Vec<SerializedItem>> {
|
||||
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,
|
||||
)),
|
||||
|
||||
@@ -246,6 +246,7 @@ impl SerializedPane {
|
||||
) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
|
||||
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<str>,
|
||||
pub item_id: ItemId,
|
||||
pub active: bool,
|
||||
pub preview: bool,
|
||||
}
|
||||
|
||||
impl SerializedItem {
|
||||
pub fn new(kind: impl AsRef<str>, item_id: ItemId, active: bool) -> Self {
|
||||
pub fn new(kind: impl AsRef<str>, 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<i32> {
|
||||
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::<str>::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,
|
||||
))
|
||||
|
||||
@@ -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<usize>,
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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<WeakView<Pane>>,
|
||||
focus_item: bool,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
|
||||
self.open_path_preview(path, pane, focus_item, false, cx)
|
||||
}
|
||||
|
||||
pub fn open_path_preview(
|
||||
&mut self,
|
||||
path: impl Into<ProjectPath>,
|
||||
pane: Option<WeakView<Pane>>,
|
||||
focus_item: bool,
|
||||
allow_preview: bool,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<Box<dyn ItemHandle>, 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<ProjectPath>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
|
||||
self.split_path_preview(path, false, cx)
|
||||
}
|
||||
|
||||
pub fn split_path_preview(
|
||||
&mut self,
|
||||
path: impl Into<ProjectPath>,
|
||||
allow_preview: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<Box<dyn ItemHandle>, 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::<Vec<_>>(),
|
||||
|
||||
Reference in New Issue
Block a user