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:
parent
edb1ea2433
commit
ea4419076e
29 changed files with 783 additions and 152 deletions
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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<_>>(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue