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:
Bennet Bo Fenner 2024-04-11 23:09:12 +02:00 committed by GitHub
parent edb1ea2433
commit ea4419076e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 783 additions and 152 deletions

View file

@ -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()
}

View file

@ -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)

View file

@ -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,
)),

View file

@ -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,
))

View file

@ -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()
}

View file

@ -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<_>>(),