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
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3772,6 +3772,7 @@ dependencies = [
|
||||||
"picker",
|
"picker",
|
||||||
"project",
|
"project",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"settings",
|
||||||
"text",
|
"text",
|
||||||
"theme",
|
"theme",
|
||||||
"ui",
|
"ui",
|
||||||
|
|
|
@ -298,6 +298,16 @@
|
||||||
// Position of the close button on the editor tabs.
|
// Position of the close button on the editor tabs.
|
||||||
"close_position": "right"
|
"close_position": "right"
|
||||||
},
|
},
|
||||||
|
// Settings related to preview tabs.
|
||||||
|
"preview_tabs": {
|
||||||
|
// Whether preview tabs should be enabled.
|
||||||
|
// Preview tabs allow you to open files in preview mode, where they close automatically
|
||||||
|
// when you switch to another file unless you explicitly pin them.
|
||||||
|
// This is useful for quickly viewing files without cluttering your workspace.
|
||||||
|
"enabled": true,
|
||||||
|
// Whether to open files in preview mode when selected from the file finder.
|
||||||
|
"enable_preview_from_file_finder": false
|
||||||
|
},
|
||||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||||
// before saving it.
|
// before saving it.
|
||||||
"remove_trailing_whitespace_on_save": true,
|
"remove_trailing_whitespace_on_save": true,
|
||||||
|
|
|
@ -42,6 +42,7 @@ use std::{
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use unindent::Unindent as _;
|
use unindent::Unindent as _;
|
||||||
|
use workspace::Pane;
|
||||||
|
|
||||||
#[ctor::ctor]
|
#[ctor::ctor]
|
||||||
fn init_logger() {
|
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;
|
let client2 = server.create_client(cx2, "user_a").await;
|
||||||
join_channel(channel2, &client2, cx2).await.unwrap();
|
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 util::ResultExt;
|
||||||
use workspace::notifications::NotificationId;
|
use workspace::notifications::NotificationId;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
item::{FollowableItem, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||||
register_followable_item,
|
register_followable_item,
|
||||||
searchable::SearchableItemHandle,
|
searchable::SearchableItemHandle,
|
||||||
ItemNavHistory, Pane, SaveIntent, Toast, ViewId, Workspace, WorkspaceId,
|
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) {
|
let label = if let Some(channel) = self.channel(cx) {
|
||||||
match (
|
match (
|
||||||
self.channel_buffer.read(cx).buffer().read(cx).read_only(),
|
self.channel_buffer.read(cx).buffer().read(cx).read_only(),
|
||||||
|
@ -388,7 +388,7 @@ impl Item for ChannelView {
|
||||||
"channel notes (disconnected)".to_string()
|
"channel notes (disconnected)".to_string()
|
||||||
};
|
};
|
||||||
Label::new(label)
|
Label::new(label)
|
||||||
.color(if selected {
|
.color(if params.selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
|
|
@ -38,7 +38,7 @@ pub use toolbar_controls::ToolbarControls;
|
||||||
use ui::{h_flex, prelude::*, Icon, IconName, Label};
|
use ui::{h_flex, prelude::*, Icon, IconName, Label};
|
||||||
use util::TryFutureExt;
|
use util::TryFutureExt;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
|
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||||
ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
|
ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -645,10 +645,10 @@ impl Item for ProjectDiagnosticsEditor {
|
||||||
Some("Project Diagnostics".into())
|
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 {
|
if self.summary.error_count == 0 && self.summary.warning_count == 0 {
|
||||||
Label::new("No problems")
|
Label::new("No problems")
|
||||||
.color(if selected {
|
.color(if params.selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
@ -663,7 +663,7 @@ impl Item for ProjectDiagnosticsEditor {
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||||
.child(Label::new(self.summary.error_count.to_string()).color(
|
.child(Label::new(self.summary.error_count.to_string()).color(
|
||||||
if selected {
|
if params.selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
@ -677,7 +677,7 @@ impl Item for ProjectDiagnosticsEditor {
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
|
.child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
|
||||||
.child(Label::new(self.summary.warning_count.to_string()).color(
|
.child(Label::new(self.summary.warning_count.to_string()).color(
|
||||||
if selected {
|
if params.selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
|
|
@ -19,7 +19,7 @@ use project::repository::GitFileStatus;
|
||||||
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
|
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
|
||||||
use rpc::proto::{self, update_view, PeerId};
|
use rpc::proto::{self, update_view, PeerId};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use workspace::item::ItemSettings;
|
use workspace::item::{ItemSettings, TabContentParams};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
@ -594,7 +594,7 @@ impl Item for Editor {
|
||||||
Some(path.to_string_lossy().to_string().into())
|
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 {
|
let label_color = if ItemSettings::get_global(cx).git_status {
|
||||||
self.buffer()
|
self.buffer()
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
@ -602,14 +602,14 @@ impl Item for Editor {
|
||||||
.and_then(|buffer| buffer.read(cx).project_path(cx))
|
.and_then(|buffer| buffer.read(cx).project_path(cx))
|
||||||
.and_then(|path| self.project.as_ref()?.read(cx).entry_for_path(&path, cx))
|
.and_then(|path| self.project.as_ref()?.read(cx).entry_for_path(&path, cx))
|
||||||
.map(|entry| {
|
.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 {
|
} 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 path = path_for_buffer(&self.buffer, detail, false, cx)?;
|
||||||
let description = path.to_string_lossy();
|
let description = path.to_string_lossy();
|
||||||
let description = description.trim();
|
let description = description.trim();
|
||||||
|
@ -623,7 +623,11 @@ impl Item for Editor {
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.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| {
|
.when_some(description, |this, description| {
|
||||||
this.child(
|
this.child(
|
||||||
Label::new(description)
|
Label::new(description)
|
||||||
|
|
|
@ -23,6 +23,7 @@ use std::{ops::Range, sync::Arc};
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{popover_menu, prelude::*, ContextMenu, ToggleButton, Tooltip};
|
use ui::{popover_menu, prelude::*, ContextMenu, ToggleButton, Tooltip};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
|
use workspace::item::TabContentParams;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{Item, ItemEvent},
|
item::{Item, ItemEvent},
|
||||||
Workspace, WorkspaceId,
|
Workspace, WorkspaceId,
|
||||||
|
@ -925,9 +926,9 @@ impl FocusableView for ExtensionsPage {
|
||||||
impl Item for ExtensionsPage {
|
impl Item for ExtensionsPage {
|
||||||
type Event = ItemEvent;
|
type Event = ItemEvent;
|
||||||
|
|
||||||
fn tab_content(&self, _: Option<usize>, selected: bool, _: &WindowContext) -> AnyElement {
|
fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement {
|
||||||
Label::new("Extensions")
|
Label::new("Extensions")
|
||||||
.color(if selected {
|
.color(if params.selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
|
|
@ -22,6 +22,7 @@ itertools = "0.11"
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
picker.workspace = true
|
picker.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
|
settings.workspace = true
|
||||||
text.workspace = true
|
text.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
|
|
|
@ -12,6 +12,7 @@ use gpui::{
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
||||||
|
use settings::Settings;
|
||||||
use std::{
|
use std::{
|
||||||
cmp,
|
cmp,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
@ -23,7 +24,7 @@ use std::{
|
||||||
use text::Point;
|
use text::Point;
|
||||||
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
|
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
|
||||||
use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
|
use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
|
||||||
use workspace::{ModalView, Workspace};
|
use workspace::{item::PreviewTabsSettings, ModalView, Workspace};
|
||||||
|
|
||||||
actions!(file_finder, [Toggle, SelectPrev]);
|
actions!(file_finder, [Toggle, SelectPrev]);
|
||||||
|
|
||||||
|
@ -782,11 +783,22 @@ impl PickerDelegate for FileFinderDelegate {
|
||||||
if let Some(m) = self.matches.get(self.selected_index()) {
|
if let Some(m) = self.matches.get(self.selected_index()) {
|
||||||
if let Some(workspace) = self.workspace.upgrade() {
|
if let Some(workspace) = self.workspace.upgrade() {
|
||||||
let open_task = workspace.update(cx, move |workspace, cx| {
|
let open_task = workspace.update(cx, move |workspace, cx| {
|
||||||
let split_or_open = |workspace: &mut Workspace, project_path, 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 {
|
if secondary {
|
||||||
workspace.split_path(project_path, cx)
|
workspace.split_path_preview(project_path, allow_preview, cx)
|
||||||
} else {
|
} else {
|
||||||
workspace.open_path(project_path, None, true, cx)
|
workspace.open_path_preview(
|
||||||
|
project_path,
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
allow_preview,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match m {
|
match m {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
|
self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
|
||||||
DefiniteLength, Fill, FlexDirection, FlexWrap, FontWeight, Hsla, JustifyContent, Length,
|
DefiniteLength, Fill, FlexDirection, FlexWrap, FontStyle, FontWeight, Hsla, JustifyContent,
|
||||||
Position, SharedString, StyleRefinement, Visibility, WhiteSpace,
|
Length, Position, SharedString, StyleRefinement, Visibility, WhiteSpace,
|
||||||
};
|
};
|
||||||
use crate::{BoxShadow, TextStyleRefinement};
|
use crate::{BoxShadow, TextStyleRefinement};
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
@ -681,6 +681,24 @@ pub trait Styled: Sized {
|
||||||
self
|
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.
|
/// Remove the text decoration on this element, this value cascades to its child elements.
|
||||||
fn text_decoration_none(mut self) -> Self {
|
fn text_decoration_none(mut self) -> Self {
|
||||||
self.text_style()
|
self.text_style()
|
||||||
|
|
|
@ -11,7 +11,7 @@ use project::{Project, ProjectEntryId, ProjectPath};
|
||||||
use std::{ffi::OsStr, path::PathBuf};
|
use std::{ffi::OsStr, path::PathBuf};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{Item, ProjectItem},
|
item::{Item, ProjectItem, TabContentParams},
|
||||||
ItemId, Pane, Workspace, WorkspaceId,
|
ItemId, Pane, Workspace, WorkspaceId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -72,12 +72,7 @@ pub struct ImageView {
|
||||||
impl Item for ImageView {
|
impl Item for ImageView {
|
||||||
type Event = ();
|
type Event = ();
|
||||||
|
|
||||||
fn tab_content(
|
fn tab_content(&self, params: TabContentParams, _cx: &WindowContext) -> AnyElement {
|
||||||
&self,
|
|
||||||
_detail: Option<usize>,
|
|
||||||
selected: bool,
|
|
||||||
_cx: &WindowContext,
|
|
||||||
) -> AnyElement {
|
|
||||||
let title = self
|
let title = self
|
||||||
.path
|
.path
|
||||||
.file_name()
|
.file_name()
|
||||||
|
@ -86,11 +81,12 @@ impl Item for ImageView {
|
||||||
.to_string();
|
.to_string();
|
||||||
Label::new(title)
|
Label::new(title)
|
||||||
.single_line()
|
.single_line()
|
||||||
.color(if selected {
|
.color(if params.selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
})
|
})
|
||||||
|
.italic(params.preview)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ use std::{borrow::Cow, sync::Arc};
|
||||||
use ui::{popover_menu, prelude::*, Button, Checkbox, ContextMenu, Label, Selection};
|
use ui::{popover_menu, prelude::*, Button, Checkbox, ContextMenu, Label, Selection};
|
||||||
use util::maybe;
|
use util::maybe;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{Item, ItemHandle},
|
item::{Item, ItemHandle, TabContentParams},
|
||||||
searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
|
searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
|
||||||
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||||
};
|
};
|
||||||
|
@ -628,9 +628,9 @@ impl Item for LspLogView {
|
||||||
Editor::to_item_events(event, f)
|
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")
|
Label::new("LSP Logs")
|
||||||
.color(if selected {
|
.color(if params.selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
|
|
@ -11,7 +11,7 @@ use theme::ActiveTheme;
|
||||||
use tree_sitter::{Node, TreeCursor};
|
use tree_sitter::{Node, TreeCursor};
|
||||||
use ui::{h_flex, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu};
|
use ui::{h_flex, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{Item, ItemHandle},
|
item::{Item, ItemHandle, TabContentParams},
|
||||||
SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
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 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")
|
Label::new("Syntax Tree")
|
||||||
.color(if selected {
|
.color(if params.selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
|
|
@ -12,7 +12,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use workspace::item::{Item, ItemHandle};
|
use workspace::item::{Item, ItemHandle, TabContentParams};
|
||||||
use workspace::{Pane, Workspace};
|
use workspace::{Pane, Workspace};
|
||||||
|
|
||||||
use crate::OpenPreviewToTheSide;
|
use crate::OpenPreviewToTheSide;
|
||||||
|
@ -439,15 +439,10 @@ impl EventEmitter<PreviewEvent> for MarkdownPreviewView {}
|
||||||
impl Item for MarkdownPreviewView {
|
impl Item for MarkdownPreviewView {
|
||||||
type Event = PreviewEvent;
|
type Event = PreviewEvent;
|
||||||
|
|
||||||
fn tab_content(
|
fn tab_content(&self, params: TabContentParams, _cx: &WindowContext) -> AnyElement {
|
||||||
&self,
|
|
||||||
_detail: Option<usize>,
|
|
||||||
selected: bool,
|
|
||||||
_cx: &WindowContext,
|
|
||||||
) -> AnyElement {
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(Icon::new(IconName::FileDoc).color(if selected {
|
.child(Icon::new(IconName::FileDoc).color(if params.selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
@ -458,7 +453,7 @@ impl Item for MarkdownPreviewView {
|
||||||
} else {
|
} else {
|
||||||
self.fallback_tab_description.clone()
|
self.fallback_tab_description.clone()
|
||||||
})
|
})
|
||||||
.color(if selected {
|
.color(if params.selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
|
|
@ -130,6 +130,7 @@ actions!(
|
||||||
Paste,
|
Paste,
|
||||||
Rename,
|
Rename,
|
||||||
Open,
|
Open,
|
||||||
|
OpenPermanent,
|
||||||
ToggleFocus,
|
ToggleFocus,
|
||||||
NewSearchInDirectory,
|
NewSearchInDirectory,
|
||||||
]
|
]
|
||||||
|
@ -156,6 +157,7 @@ pub enum Event {
|
||||||
OpenedEntry {
|
OpenedEntry {
|
||||||
entry_id: ProjectEntryId,
|
entry_id: ProjectEntryId,
|
||||||
focus_opened_item: bool,
|
focus_opened_item: bool,
|
||||||
|
allow_preview: bool,
|
||||||
},
|
},
|
||||||
SplitEntry {
|
SplitEntry {
|
||||||
entry_id: ProjectEntryId,
|
entry_id: ProjectEntryId,
|
||||||
|
@ -262,6 +264,7 @@ impl ProjectPanel {
|
||||||
&Event::OpenedEntry {
|
&Event::OpenedEntry {
|
||||||
entry_id,
|
entry_id,
|
||||||
focus_opened_item,
|
focus_opened_item,
|
||||||
|
allow_preview,
|
||||||
} => {
|
} => {
|
||||||
if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
|
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) {
|
if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
|
||||||
|
@ -270,13 +273,14 @@ impl ProjectPanel {
|
||||||
let entry_id = entry.id;
|
let entry_id = entry.id;
|
||||||
|
|
||||||
workspace
|
workspace
|
||||||
.open_path(
|
.open_path_preview(
|
||||||
ProjectPath {
|
ProjectPath {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
path: file_path.clone(),
|
path: file_path.clone(),
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
focus_opened_item,
|
focus_opened_item,
|
||||||
|
allow_preview,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.detach_and_prompt_err("Failed to open file", cx, move |e, _| {
|
.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>) {
|
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 let Some((_, entry)) = self.selected_entry(cx) {
|
||||||
if entry.is_file() {
|
if entry.is_file() {
|
||||||
self.open_entry(entry.id, true, cx);
|
self.open_entry(entry.id, focus_opened_item, allow_preview, cx);
|
||||||
} else {
|
} else {
|
||||||
self.toggle_expanded(entry.id, cx);
|
self.toggle_expanded(entry.id, cx);
|
||||||
}
|
}
|
||||||
|
@ -666,7 +683,7 @@ impl ProjectPanel {
|
||||||
}
|
}
|
||||||
this.update_visible_entries(None, cx);
|
this.update_visible_entries(None, cx);
|
||||||
if is_new_entry && !is_dir {
|
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();
|
cx.notify();
|
||||||
})?;
|
})?;
|
||||||
|
@ -686,11 +703,13 @@ impl ProjectPanel {
|
||||||
&mut self,
|
&mut self,
|
||||||
entry_id: ProjectEntryId,
|
entry_id: ProjectEntryId,
|
||||||
focus_opened_item: bool,
|
focus_opened_item: bool,
|
||||||
|
allow_preview: bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
cx.emit(Event::OpenedEntry {
|
cx.emit(Event::OpenedEntry {
|
||||||
entry_id,
|
entry_id,
|
||||||
focus_opened_item,
|
focus_opened_item,
|
||||||
|
allow_preview,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1461,7 +1480,13 @@ impl ProjectPanel {
|
||||||
if event.down.modifiers.secondary() {
|
if event.down.modifiers.secondary() {
|
||||||
this.split_entry(entry_id, cx);
|
this.split_entry(entry_id, cx);
|
||||||
} else {
|
} 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_selected_entry))
|
||||||
.on_action(cx.listener(Self::collapse_all_entries))
|
.on_action(cx.listener(Self::collapse_all_entries))
|
||||||
.on_action(cx.listener(Self::open))
|
.on_action(cx.listener(Self::open))
|
||||||
|
.on_action(cx.listener(Self::open_permanent))
|
||||||
.on_action(cx.listener(Self::confirm))
|
.on_action(cx.listener(Self::confirm))
|
||||||
.on_action(cx.listener(Self::cancel))
|
.on_action(cx.listener(Self::cancel))
|
||||||
.on_action(cx.listener(Self::copy_path))
|
.on_action(cx.listener(Self::copy_path))
|
||||||
|
|
|
@ -36,12 +36,11 @@ use ui::{
|
||||||
};
|
};
|
||||||
use util::paths::PathMatcher;
|
use util::paths::PathMatcher;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
|
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||||
searchable::{Direction, SearchableItem, SearchableItemHandle},
|
searchable::{Direction, SearchableItem, SearchableItemHandle},
|
||||||
ItemNavHistory, Pane, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
DeploySearch, ItemNavHistory, NewSearch, Pane, ToolbarItemEvent, ToolbarItemLocation,
|
||||||
WorkspaceId,
|
ToolbarItemView, Workspace, WorkspaceId,
|
||||||
};
|
};
|
||||||
use workspace::{DeploySearch, NewSearch};
|
|
||||||
|
|
||||||
const MIN_INPUT_WIDTH_REMS: f32 = 15.;
|
const MIN_INPUT_WIDTH_REMS: f32 = 15.;
|
||||||
const MAX_INPUT_WIDTH_REMS: f32 = 30.;
|
const MAX_INPUT_WIDTH_REMS: f32 = 30.;
|
||||||
|
@ -379,7 +378,7 @@ impl Item for ProjectSearchView {
|
||||||
.update(cx, |editor, cx| editor.deactivated(cx));
|
.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
|
let last_query: Option<SharedString> = self
|
||||||
.model
|
.model
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
@ -395,12 +394,14 @@ impl Item for ProjectSearchView {
|
||||||
.unwrap_or_else(|| "Project Search".into());
|
.unwrap_or_else(|| "Project Search".into());
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(Icon::new(IconName::MagnifyingGlass).color(if selected {
|
.child(
|
||||||
|
Icon::new(IconName::MagnifyingGlass).color(if params.selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
}))
|
}),
|
||||||
.child(Label::new(tab_name).color(if selected {
|
)
|
||||||
|
.child(Label::new(tab_name).color(if params.selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
|
|
@ -13,7 +13,7 @@ use std::sync::Arc;
|
||||||
use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip};
|
use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::ItemHandle,
|
item::{ItemHandle, TabContentParams},
|
||||||
pane::{render_item_indicator, tab_details, Event as PaneEvent},
|
pane::{render_item_indicator, tab_details, Event as PaneEvent},
|
||||||
ModalView, Pane, SaveIntent, Workspace,
|
ModalView, Pane, SaveIntent, Workspace,
|
||||||
};
|
};
|
||||||
|
@ -130,6 +130,7 @@ struct TabMatch {
|
||||||
item_index: usize,
|
item_index: usize,
|
||||||
item: Box<dyn ItemHandle>,
|
item: Box<dyn ItemHandle>,
|
||||||
detail: usize,
|
detail: usize,
|
||||||
|
preview: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TabSwitcherDelegate {
|
pub struct TabSwitcherDelegate {
|
||||||
|
@ -202,6 +203,7 @@ impl TabSwitcherDelegate {
|
||||||
item_index,
|
item_index,
|
||||||
item: item.boxed_clone(),
|
item: item.boxed_clone(),
|
||||||
detail,
|
detail,
|
||||||
|
preview: pane.is_active_preview_item(item.item_id()),
|
||||||
})
|
})
|
||||||
.for_each(|tab_match| self.matches.push(tab_match));
|
.for_each(|tab_match| self.matches.push(tab_match));
|
||||||
|
|
||||||
|
@ -324,7 +326,12 @@ impl PickerDelegate for TabSwitcherDelegate {
|
||||||
.get(ix)
|
.get(ix)
|
||||||
.expect("Invalid matches state: no element for index {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 = render_item_indicator(tab_match.item.boxed_clone(), cx);
|
||||||
let indicator_color = if let Some(ref indicator) = indicator {
|
let indicator_color = if let Some(ref indicator) = indicator {
|
||||||
indicator.color
|
indicator.color
|
||||||
|
|
|
@ -26,7 +26,7 @@ use terminal_element::TerminalElement;
|
||||||
use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label};
|
use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label};
|
||||||
use util::{paths::PathLikeWithPosition, ResultExt};
|
use util::{paths::PathLikeWithPosition, ResultExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{BreadcrumbText, Item, ItemEvent},
|
item::{BreadcrumbText, Item, ItemEvent, TabContentParams},
|
||||||
notifications::NotifyResultExt,
|
notifications::NotifyResultExt,
|
||||||
register_deserializable_item,
|
register_deserializable_item,
|
||||||
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
|
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
|
||||||
|
@ -783,12 +783,7 @@ impl Item for TerminalView {
|
||||||
Some(self.terminal().read(cx).title(false).into())
|
Some(self.terminal().read(cx).title(false).into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tab_content(
|
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
|
||||||
&self,
|
|
||||||
_detail: Option<usize>,
|
|
||||||
selected: bool,
|
|
||||||
cx: &WindowContext,
|
|
||||||
) -> AnyElement {
|
|
||||||
let terminal = self.terminal().read(cx);
|
let terminal = self.terminal().read(cx);
|
||||||
let title = terminal.title(true);
|
let title = terminal.title(true);
|
||||||
let icon = match terminal.task() {
|
let icon = match terminal.task() {
|
||||||
|
@ -808,7 +803,7 @@ impl Item for TerminalView {
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(Icon::new(icon))
|
.child(Icon::new(icon))
|
||||||
.child(Label::new(title).color(if selected {
|
.child(Label::new(title).color(if params.selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
|
|
@ -43,6 +43,11 @@ impl LabelCommon for HighlightedLabel {
|
||||||
self.base = self.base.strikethrough(strikethrough);
|
self.base = self.base.strikethrough(strikethrough);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn italic(mut self, italic: bool) -> Self {
|
||||||
|
self.base = self.base.italic(italic);
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for HighlightedLabel {
|
impl RenderOnce for HighlightedLabel {
|
||||||
|
|
|
@ -126,6 +126,20 @@ impl LabelCommon for Label {
|
||||||
self.base = self.base.strikethrough(strikethrough);
|
self.base = self.base.strikethrough(strikethrough);
|
||||||
self
|
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 {
|
impl RenderOnce for Label {
|
||||||
|
|
|
@ -33,6 +33,9 @@ pub trait LabelCommon {
|
||||||
|
|
||||||
/// Sets the strikethrough property of the label.
|
/// Sets the strikethrough property of the label.
|
||||||
fn strikethrough(self, strikethrough: bool) -> Self;
|
fn strikethrough(self, strikethrough: bool) -> Self;
|
||||||
|
|
||||||
|
/// Sets the italic property of the label.
|
||||||
|
fn italic(self, italic: bool) -> Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
|
@ -41,6 +44,7 @@ pub struct LabelLike {
|
||||||
line_height_style: LineHeightStyle,
|
line_height_style: LineHeightStyle,
|
||||||
pub(crate) color: Color,
|
pub(crate) color: Color,
|
||||||
strikethrough: bool,
|
strikethrough: bool,
|
||||||
|
italic: bool,
|
||||||
children: SmallVec<[AnyElement; 2]>,
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +55,7 @@ impl LabelLike {
|
||||||
line_height_style: LineHeightStyle::default(),
|
line_height_style: LineHeightStyle::default(),
|
||||||
color: Color::Default,
|
color: Color::Default,
|
||||||
strikethrough: false,
|
strikethrough: false,
|
||||||
|
italic: false,
|
||||||
children: SmallVec::new(),
|
children: SmallVec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,6 +81,11 @@ impl LabelCommon for LabelLike {
|
||||||
self.strikethrough = strikethrough;
|
self.strikethrough = strikethrough;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn italic(mut self, italic: bool) -> Self {
|
||||||
|
self.italic = italic;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParentElement for LabelLike {
|
impl ParentElement for LabelLike {
|
||||||
|
@ -106,6 +116,7 @@ impl RenderOnce for LabelLike {
|
||||||
.when(self.line_height_style == LineHeightStyle::UiLabel, |this| {
|
.when(self.line_height_style == LineHeightStyle::UiLabel, |this| {
|
||||||
this.line_height(relative(1.))
|
this.line_height(relative(1.))
|
||||||
})
|
})
|
||||||
|
.when(self.italic, |this| this.italic())
|
||||||
.text_color(self.color.color(cx))
|
.text_color(self.color.color(cx))
|
||||||
.children(self.children)
|
.children(self.children)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ use ui::{prelude::*, CheckboxWithLabel};
|
||||||
use vim::VimModeSetting;
|
use vim::VimModeSetting;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::DockPosition,
|
dock::DockPosition,
|
||||||
item::{Item, ItemEvent},
|
item::{Item, ItemEvent, TabContentParams},
|
||||||
open_new, AppState, Welcome, Workspace, WorkspaceId,
|
open_new, AppState, Welcome, Workspace, WorkspaceId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -284,9 +284,9 @@ impl FocusableView for WelcomePage {
|
||||||
impl Item for WelcomePage {
|
impl Item for WelcomePage {
|
||||||
type Event = ItemEvent;
|
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!")
|
Label::new("Welcome to Zed!")
|
||||||
.color(if selected {
|
.color(if params.selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
|
|
@ -42,6 +42,12 @@ pub struct ItemSettings {
|
||||||
pub close_position: ClosePosition,
|
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)]
|
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ClosePosition {
|
pub enum ClosePosition {
|
||||||
|
@ -71,6 +77,19 @@ pub struct ItemSettingsContent {
|
||||||
close_position: Option<ClosePosition>,
|
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 {
|
impl Settings for ItemSettings {
|
||||||
const KEY: Option<&'static str> = Some("tabs");
|
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)]
|
#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
|
||||||
pub enum ItemEvent {
|
pub enum ItemEvent {
|
||||||
CloseItem,
|
CloseItem,
|
||||||
|
@ -95,14 +124,16 @@ pub struct BreadcrumbText {
|
||||||
pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
|
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> {
|
pub trait Item: FocusableView + EventEmitter<Self::Event> {
|
||||||
type Event;
|
type Event;
|
||||||
fn tab_content(
|
fn tab_content(&self, _params: TabContentParams, _cx: &WindowContext) -> AnyElement {
|
||||||
&self,
|
|
||||||
_detail: Option<usize>,
|
|
||||||
_selected: bool,
|
|
||||||
_cx: &WindowContext,
|
|
||||||
) -> AnyElement {
|
|
||||||
gpui::Empty.into_any()
|
gpui::Empty.into_any()
|
||||||
}
|
}
|
||||||
fn to_item_events(_event: &Self::Event, _f: impl FnMut(ItemEvent)) {}
|
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 focus_handle(&self, cx: &WindowContext) -> FocusHandle;
|
||||||
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
|
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
|
||||||
fn tab_description(&self, detail: usize, 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 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_path(&self, cx: &AppContext) -> Option<ProjectPath>;
|
||||||
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
|
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
|
||||||
fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[EntityId; 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)
|
self.read(cx).tab_description(detail, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tab_content(&self, detail: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
|
fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
|
||||||
self.read(cx).tab_content(detail, selected, cx)
|
self.read(cx).tab_content(params, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dragged_tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
|
fn dragged_tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
|
||||||
self.read(cx).tab_content(detail, true, cx)
|
self.read(cx).tab_content(
|
||||||
|
TabContentParams {
|
||||||
|
selected: true,
|
||||||
|
..params
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
|
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::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"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub mod test {
|
pub mod test {
|
||||||
use super::{Item, ItemEvent};
|
use super::{Item, ItemEvent, TabContentParams};
|
||||||
use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
|
use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, AppContext, Context as _, EntityId, EventEmitter, FocusableView,
|
AnyElement, AppContext, Context as _, EntityId, EventEmitter, FocusableView,
|
||||||
|
@ -990,11 +1028,10 @@ pub mod test {
|
||||||
|
|
||||||
fn tab_content(
|
fn tab_content(
|
||||||
&self,
|
&self,
|
||||||
detail: Option<usize>,
|
params: TabContentParams,
|
||||||
_selected: bool,
|
|
||||||
_cx: &ui::prelude::WindowContext,
|
_cx: &ui::prelude::WindowContext,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
self.tab_detail.set(detail);
|
self.tab_detail.set(params.detail);
|
||||||
gpui::div().into_any_element()
|
gpui::div().into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
item::{ClosePosition, Item, ItemHandle, ItemSettings, WeakItemHandle},
|
item::{
|
||||||
|
ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, TabContentParams,
|
||||||
|
WeakItemHandle,
|
||||||
|
},
|
||||||
toolbar::Toolbar,
|
toolbar::Toolbar,
|
||||||
workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
|
workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
|
||||||
NewCenterTerminal, NewFile, NewSearch, OpenVisible, SplitDirection, ToggleZoom, Workspace,
|
NewCenterTerminal, NewFile, NewSearch, OpenVisible, SplitDirection, ToggleZoom, Workspace,
|
||||||
|
@ -11,8 +14,8 @@ use gpui::{
|
||||||
actions, anchored, deferred, impl_actions, prelude::*, Action, AnchorCorner, AnyElement,
|
actions, anchored, deferred, impl_actions, prelude::*, Action, AnchorCorner, AnyElement,
|
||||||
AppContext, AsyncWindowContext, ClickEvent, DismissEvent, Div, DragMoveEvent, EntityId,
|
AppContext, AsyncWindowContext, ClickEvent, DismissEvent, Div, DragMoveEvent, EntityId,
|
||||||
EventEmitter, ExternalPaths, FocusHandle, FocusableView, KeyContext, Model, MouseButton,
|
EventEmitter, ExternalPaths, FocusHandle, FocusableView, KeyContext, Model, MouseButton,
|
||||||
NavigationDirection, Pixels, Point, PromptLevel, Render, ScrollHandle, Subscription, Task,
|
MouseDownEvent, NavigationDirection, Pixels, Point, PromptLevel, Render, ScrollHandle,
|
||||||
View, ViewContext, VisualContext, WeakFocusHandle, WeakView, WindowContext,
|
Subscription, Task, View, ViewContext, VisualContext, WeakFocusHandle, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::{Project, ProjectEntryId, ProjectPath};
|
use project::{Project, ProjectEntryId, ProjectPath};
|
||||||
|
@ -120,6 +123,7 @@ actions!(
|
||||||
SplitUp,
|
SplitUp,
|
||||||
SplitRight,
|
SplitRight,
|
||||||
SplitDown,
|
SplitDown,
|
||||||
|
TogglePreviewTab,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -184,6 +188,7 @@ pub struct Pane {
|
||||||
zoomed: bool,
|
zoomed: bool,
|
||||||
was_focused: bool,
|
was_focused: bool,
|
||||||
active_item_index: usize,
|
active_item_index: usize,
|
||||||
|
preview_item_id: Option<EntityId>,
|
||||||
last_focus_handle_by_item: HashMap<EntityId, WeakFocusHandle>,
|
last_focus_handle_by_item: HashMap<EntityId, WeakFocusHandle>,
|
||||||
nav_history: NavHistory,
|
nav_history: NavHistory,
|
||||||
toolbar: View<Toolbar>,
|
toolbar: View<Toolbar>,
|
||||||
|
@ -207,6 +212,7 @@ pub struct Pane {
|
||||||
pub struct ItemNavHistory {
|
pub struct ItemNavHistory {
|
||||||
history: NavHistory,
|
history: NavHistory,
|
||||||
item: Arc<dyn WeakItemHandle>,
|
item: Arc<dyn WeakItemHandle>,
|
||||||
|
is_preview: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -242,6 +248,7 @@ pub struct NavigationEntry {
|
||||||
pub item: Arc<dyn WeakItemHandle>,
|
pub item: Arc<dyn WeakItemHandle>,
|
||||||
pub data: Option<Box<dyn Any + Send>>,
|
pub data: Option<Box<dyn Any + Send>>,
|
||||||
pub timestamp: usize,
|
pub timestamp: usize,
|
||||||
|
pub is_preview: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -281,6 +288,7 @@ impl Pane {
|
||||||
was_focused: false,
|
was_focused: false,
|
||||||
zoomed: false,
|
zoomed: false,
|
||||||
active_item_index: 0,
|
active_item_index: 0,
|
||||||
|
preview_item_id: None,
|
||||||
last_focus_handle_by_item: Default::default(),
|
last_focus_handle_by_item: Default::default(),
|
||||||
nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
|
nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
|
||||||
mode: NavigationMode::Normal,
|
mode: NavigationMode::Normal,
|
||||||
|
@ -435,6 +443,10 @@ impl Pane {
|
||||||
|
|
||||||
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
|
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
self.display_nav_history_buttons = TabBarSettings::get_global(cx).show_nav_history_buttons;
|
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();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -478,6 +490,7 @@ impl Pane {
|
||||||
ItemNavHistory {
|
ItemNavHistory {
|
||||||
history: self.nav_history.clone(),
|
history: self.nav_history.clone(),
|
||||||
item: Arc::new(item.downgrade()),
|
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());
|
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(
|
pub(crate) fn open_item(
|
||||||
&mut self,
|
&mut self,
|
||||||
project_entry_id: Option<ProjectEntryId>,
|
project_entry_id: Option<ProjectEntryId>,
|
||||||
focus_item: bool,
|
focus_item: bool,
|
||||||
|
allow_preview: bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
|
build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
|
||||||
) -> Box<dyn ItemHandle> {
|
) -> Box<dyn ItemHandle> {
|
||||||
|
@ -552,11 +600,43 @@ impl Pane {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((index, existing_item)) = existing_item {
|
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);
|
self.activate_item(index, focus_item, focus_item, cx);
|
||||||
existing_item
|
existing_item
|
||||||
} else {
|
} 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);
|
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
|
new_item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -648,7 +728,10 @@ impl Pane {
|
||||||
self.activate_item(insertion_index, activate_pane, focus_item, cx);
|
self.activate_item(insertion_index, activate_pane, focus_item, cx);
|
||||||
} else {
|
} else {
|
||||||
self.items.insert(insertion_index, item.clone());
|
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;
|
self.active_item_index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1043,7 +1126,7 @@ impl Pane {
|
||||||
.iter()
|
.iter()
|
||||||
.position(|i| i.item_id() == item.item_id())
|
.position(|i| i.item_id() == item.item_id())
|
||||||
{
|
{
|
||||||
pane.remove_item(item_ix, false, cx);
|
pane.remove_item(item_ix, false, true, cx);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
@ -1058,6 +1141,7 @@ impl Pane {
|
||||||
&mut self,
|
&mut self,
|
||||||
item_index: usize,
|
item_index: usize,
|
||||||
activate_pane: bool,
|
activate_pane: bool,
|
||||||
|
close_pane_if_empty: bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
self.activation_history
|
self.activation_history
|
||||||
|
@ -1091,17 +1175,24 @@ impl Pane {
|
||||||
});
|
});
|
||||||
if self.items.is_empty() {
|
if self.items.is_empty() {
|
||||||
item.deactivated(cx);
|
item.deactivated(cx);
|
||||||
|
if close_pane_if_empty {
|
||||||
self.update_toolbar(cx);
|
self.update_toolbar(cx);
|
||||||
cx.emit(Event::Remove);
|
cx.emit(Event::Remove);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if item_index < self.active_item_index {
|
if item_index < self.active_item_index {
|
||||||
self.active_item_index -= 1;
|
self.active_item_index -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mode = self.nav_history.mode();
|
||||||
self.nav_history.set_mode(NavigationMode::ClosingItem);
|
self.nav_history.set_mode(NavigationMode::ClosingItem);
|
||||||
item.deactivated(cx);
|
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) {
|
if let Some(path) = item.project_path(cx) {
|
||||||
let abs_path = self
|
let abs_path = self
|
||||||
|
@ -1125,7 +1216,7 @@ impl Pane {
|
||||||
.remove(&item.item_id());
|
.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);
|
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);
|
self.nav_history.remove_item(item_id);
|
||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
|
@ -1330,8 +1421,19 @@ impl Pane {
|
||||||
cx: &mut ViewContext<'_, Pane>,
|
cx: &mut ViewContext<'_, Pane>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let is_active = ix == self.active_item_index;
|
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 close_side = &ItemSettings::get_global(cx).close_position;
|
||||||
let indicator = render_item_indicator(item.boxed_clone(), cx);
|
let indicator = render_item_indicator(item.boxed_clone(), cx);
|
||||||
let item_id = item.item_id();
|
let item_id = item.item_id();
|
||||||
|
@ -1363,6 +1465,16 @@ impl Pane {
|
||||||
.detach_and_log_err(cx);
|
.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(
|
.on_drag(
|
||||||
DraggedTab {
|
DraggedTab {
|
||||||
item: item.boxed_clone(),
|
item: item.boxed_clone(),
|
||||||
|
@ -1639,6 +1751,12 @@ impl Pane {
|
||||||
let mut to_pane = cx.view().clone();
|
let mut to_pane = cx.view().clone();
|
||||||
let split_direction = self.drag_split_direction;
|
let split_direction = self.drag_split_direction;
|
||||||
let item_id = dragged_tab.item.item_id();
|
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();
|
let from_pane = dragged_tab.pane.clone();
|
||||||
self.workspace
|
self.workspace
|
||||||
.update(cx, |_, cx| {
|
.update(cx, |_, cx| {
|
||||||
|
@ -1786,6 +1904,17 @@ impl Render for Pane {
|
||||||
.on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
|
.on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
|
||||||
pane.activate_next_item(true, 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(
|
.on_action(
|
||||||
cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
|
cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
|
||||||
if let Some(task) = pane.close_active_item(action, cx) {
|
if let Some(task) = pane.close_active_item(action, cx) {
|
||||||
|
@ -1946,7 +2075,8 @@ impl Render for Pane {
|
||||||
|
|
||||||
impl ItemNavHistory {
|
impl ItemNavHistory {
|
||||||
pub fn push<D: 'static + Send + Any>(&mut self, data: Option<D>, cx: &mut WindowContext) {
|
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> {
|
pub fn pop_backward(&mut self, cx: &mut WindowContext) -> Option<NavigationEntry> {
|
||||||
|
@ -2020,6 +2150,7 @@ impl NavHistory {
|
||||||
&mut self,
|
&mut self,
|
||||||
data: Option<D>,
|
data: Option<D>,
|
||||||
item: Arc<dyn WeakItemHandle>,
|
item: Arc<dyn WeakItemHandle>,
|
||||||
|
is_preview: bool,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) {
|
) {
|
||||||
let state = &mut *self.0.lock();
|
let state = &mut *self.0.lock();
|
||||||
|
@ -2033,6 +2164,7 @@ impl NavHistory {
|
||||||
item,
|
item,
|
||||||
data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
|
data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
|
||||||
timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
|
timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
|
||||||
|
is_preview,
|
||||||
});
|
});
|
||||||
state.forward_stack.clear();
|
state.forward_stack.clear();
|
||||||
}
|
}
|
||||||
|
@ -2044,6 +2176,7 @@ impl NavHistory {
|
||||||
item,
|
item,
|
||||||
data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
|
data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
|
||||||
timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
|
timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
|
||||||
|
is_preview,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
NavigationMode::GoingForward => {
|
NavigationMode::GoingForward => {
|
||||||
|
@ -2054,6 +2187,7 @@ impl NavHistory {
|
||||||
item,
|
item,
|
||||||
data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
|
data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
|
||||||
timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
|
timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
|
||||||
|
is_preview,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
NavigationMode::ClosingItem => {
|
NavigationMode::ClosingItem => {
|
||||||
|
@ -2064,6 +2198,7 @@ impl NavHistory {
|
||||||
item,
|
item,
|
||||||
data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
|
data: data.map(|data| Box::new(data) as Box<dyn Any + Send>),
|
||||||
timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
|
timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst),
|
||||||
|
is_preview,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2706,7 +2841,14 @@ mod tests {
|
||||||
impl Render for DraggedTab {
|
impl Render for DraggedTab {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
|
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("")
|
Tab::new("")
|
||||||
.selected(self.is_active)
|
.selected(self.is_active)
|
||||||
.child(label)
|
.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
|
// 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
|
// 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
|
// 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<()> =
|
pub static ref DB: WorkspaceDb<()> =
|
||||||
&[sql!(
|
&[sql!(
|
||||||
|
@ -279,6 +280,10 @@ define_connection! {
|
||||||
sql!(
|
sql!(
|
||||||
ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
|
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>> {
|
fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
|
||||||
self.select_bound(sql!(
|
self.select_bound(sql!(
|
||||||
SELECT kind, item_id, active FROM items
|
SELECT kind, item_id, active, preview FROM items
|
||||||
WHERE pane_id = ?
|
WHERE pane_id = ?
|
||||||
ORDER BY position
|
ORDER BY position
|
||||||
))?(pane_id)
|
))?(pane_id)
|
||||||
|
@ -636,7 +641,7 @@ impl WorkspaceDb {
|
||||||
items: &[SerializedItem],
|
items: &[SerializedItem],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut insert = conn.exec_bound(sql!(
|
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")?;
|
)).context("Preparing insertion")?;
|
||||||
for (position, item) in items.iter().enumerate() {
|
for (position, item) in items.iter().enumerate() {
|
||||||
insert((workspace_id, pane_id, position, item))?;
|
insert((workspace_id, pane_id, position, item))?;
|
||||||
|
@ -836,15 +841,15 @@ mod tests {
|
||||||
vec![
|
vec![
|
||||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||||
vec![
|
vec![
|
||||||
SerializedItem::new("Terminal", 5, false),
|
SerializedItem::new("Terminal", 5, false, false),
|
||||||
SerializedItem::new("Terminal", 6, true),
|
SerializedItem::new("Terminal", 6, true, false),
|
||||||
],
|
],
|
||||||
false,
|
false,
|
||||||
)),
|
)),
|
||||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||||
vec![
|
vec![
|
||||||
SerializedItem::new("Terminal", 7, true),
|
SerializedItem::new("Terminal", 7, true, false),
|
||||||
SerializedItem::new("Terminal", 8, false),
|
SerializedItem::new("Terminal", 8, false, false),
|
||||||
],
|
],
|
||||||
false,
|
false,
|
||||||
)),
|
)),
|
||||||
|
@ -852,8 +857,8 @@ mod tests {
|
||||||
),
|
),
|
||||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||||
vec![
|
vec![
|
||||||
SerializedItem::new("Terminal", 9, false),
|
SerializedItem::new("Terminal", 9, false, false),
|
||||||
SerializedItem::new("Terminal", 10, true),
|
SerializedItem::new("Terminal", 10, true, false),
|
||||||
],
|
],
|
||||||
false,
|
false,
|
||||||
)),
|
)),
|
||||||
|
@ -1000,15 +1005,15 @@ mod tests {
|
||||||
vec![
|
vec![
|
||||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||||
vec![
|
vec![
|
||||||
SerializedItem::new("Terminal", 1, false),
|
SerializedItem::new("Terminal", 1, false, false),
|
||||||
SerializedItem::new("Terminal", 2, true),
|
SerializedItem::new("Terminal", 2, true, false),
|
||||||
],
|
],
|
||||||
false,
|
false,
|
||||||
)),
|
)),
|
||||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||||
vec![
|
vec![
|
||||||
SerializedItem::new("Terminal", 4, false),
|
SerializedItem::new("Terminal", 4, false, false),
|
||||||
SerializedItem::new("Terminal", 3, true),
|
SerializedItem::new("Terminal", 3, true, false),
|
||||||
],
|
],
|
||||||
true,
|
true,
|
||||||
)),
|
)),
|
||||||
|
@ -1016,8 +1021,8 @@ mod tests {
|
||||||
),
|
),
|
||||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||||
vec![
|
vec![
|
||||||
SerializedItem::new("Terminal", 5, true),
|
SerializedItem::new("Terminal", 5, true, false),
|
||||||
SerializedItem::new("Terminal", 6, false),
|
SerializedItem::new("Terminal", 6, false, false),
|
||||||
],
|
],
|
||||||
false,
|
false,
|
||||||
)),
|
)),
|
||||||
|
@ -1047,15 +1052,15 @@ mod tests {
|
||||||
vec![
|
vec![
|
||||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||||
vec![
|
vec![
|
||||||
SerializedItem::new("Terminal", 1, false),
|
SerializedItem::new("Terminal", 1, false, false),
|
||||||
SerializedItem::new("Terminal", 2, true),
|
SerializedItem::new("Terminal", 2, true, false),
|
||||||
],
|
],
|
||||||
false,
|
false,
|
||||||
)),
|
)),
|
||||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||||
vec![
|
vec![
|
||||||
SerializedItem::new("Terminal", 4, false),
|
SerializedItem::new("Terminal", 4, false, false),
|
||||||
SerializedItem::new("Terminal", 3, true),
|
SerializedItem::new("Terminal", 3, true, false),
|
||||||
],
|
],
|
||||||
true,
|
true,
|
||||||
)),
|
)),
|
||||||
|
@ -1063,8 +1068,8 @@ mod tests {
|
||||||
),
|
),
|
||||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||||
vec![
|
vec![
|
||||||
SerializedItem::new("Terminal", 5, false),
|
SerializedItem::new("Terminal", 5, false, false),
|
||||||
SerializedItem::new("Terminal", 6, true),
|
SerializedItem::new("Terminal", 6, true, false),
|
||||||
],
|
],
|
||||||
false,
|
false,
|
||||||
)),
|
)),
|
||||||
|
@ -1082,15 +1087,15 @@ mod tests {
|
||||||
vec![
|
vec![
|
||||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||||
vec![
|
vec![
|
||||||
SerializedItem::new("Terminal", 1, false),
|
SerializedItem::new("Terminal", 1, false, false),
|
||||||
SerializedItem::new("Terminal", 2, true),
|
SerializedItem::new("Terminal", 2, true, false),
|
||||||
],
|
],
|
||||||
false,
|
false,
|
||||||
)),
|
)),
|
||||||
SerializedPaneGroup::Pane(SerializedPane::new(
|
SerializedPaneGroup::Pane(SerializedPane::new(
|
||||||
vec![
|
vec![
|
||||||
SerializedItem::new("Terminal", 4, true),
|
SerializedItem::new("Terminal", 4, true, false),
|
||||||
SerializedItem::new("Terminal", 3, false),
|
SerializedItem::new("Terminal", 3, false, false),
|
||||||
],
|
],
|
||||||
true,
|
true,
|
||||||
)),
|
)),
|
||||||
|
|
|
@ -246,6 +246,7 @@ impl SerializedPane {
|
||||||
) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
|
) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
|
||||||
let mut item_tasks = Vec::new();
|
let mut item_tasks = Vec::new();
|
||||||
let mut active_item_index = None;
|
let mut active_item_index = None;
|
||||||
|
let mut preview_item_index = None;
|
||||||
for (index, item) in self.children.iter().enumerate() {
|
for (index, item) in self.children.iter().enumerate() {
|
||||||
let project = project.clone();
|
let project = project.clone();
|
||||||
item_tasks.push(pane.update(cx, |_, cx| {
|
item_tasks.push(pane.update(cx, |_, cx| {
|
||||||
|
@ -261,6 +262,9 @@ impl SerializedPane {
|
||||||
if item.active {
|
if item.active {
|
||||||
active_item_index = Some(index);
|
active_item_index = Some(index);
|
||||||
}
|
}
|
||||||
|
if item.preview {
|
||||||
|
preview_item_index = Some(index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut items = Vec::new();
|
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)
|
anyhow::Ok(items)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -294,14 +306,16 @@ pub struct SerializedItem {
|
||||||
pub kind: Arc<str>,
|
pub kind: Arc<str>,
|
||||||
pub item_id: ItemId,
|
pub item_id: ItemId,
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
|
pub preview: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SerializedItem {
|
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 {
|
Self {
|
||||||
kind: Arc::from(kind.as_ref()),
|
kind: Arc::from(kind.as_ref()),
|
||||||
item_id,
|
item_id,
|
||||||
active,
|
active,
|
||||||
|
preview,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -313,20 +327,22 @@ impl Default for SerializedItem {
|
||||||
kind: Arc::from("Terminal"),
|
kind: Arc::from("Terminal"),
|
||||||
item_id: 100000,
|
item_id: 100000,
|
||||||
active: false,
|
active: false,
|
||||||
|
preview: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StaticColumnCount for SerializedItem {
|
impl StaticColumnCount for SerializedItem {
|
||||||
fn column_count() -> usize {
|
fn column_count() -> usize {
|
||||||
3
|
4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl Bind for &SerializedItem {
|
impl Bind for &SerializedItem {
|
||||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
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.kind, start_index)?;
|
||||||
let next_index = statement.bind(&self.item_id, next_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 (kind, next_index) = Arc::<str>::column(statement, start_index)?;
|
||||||
let (item_id, next_index) = ItemId::column(statement, next_index)?;
|
let (item_id, next_index) = ItemId::column(statement, next_index)?;
|
||||||
let (active, next_index) = bool::column(statement, next_index)?;
|
let (active, next_index) = bool::column(statement, next_index)?;
|
||||||
|
let (preview, next_index) = bool::column(statement, next_index)?;
|
||||||
Ok((
|
Ok((
|
||||||
SerializedItem {
|
SerializedItem {
|
||||||
kind,
|
kind,
|
||||||
item_id,
|
item_id,
|
||||||
active,
|
active,
|
||||||
|
preview,
|
||||||
},
|
},
|
||||||
next_index,
|
next_index,
|
||||||
))
|
))
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
item::{Item, ItemEvent},
|
item::{Item, ItemEvent, TabContentParams},
|
||||||
ItemNavHistory, WorkspaceId,
|
ItemNavHistory, WorkspaceId,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
@ -93,21 +93,18 @@ impl Item for SharedScreen {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tab_content(
|
fn tab_content(&self, params: TabContentParams, _: &WindowContext<'_>) -> gpui::AnyElement {
|
||||||
&self,
|
|
||||||
_: Option<usize>,
|
|
||||||
selected: bool,
|
|
||||||
_: &WindowContext<'_>,
|
|
||||||
) -> gpui::AnyElement {
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(Icon::new(IconName::Screen))
|
.child(Icon::new(IconName::Screen))
|
||||||
.child(
|
.child(
|
||||||
Label::new(format!("{}'s screen", self.user.github_login)).color(if selected {
|
Label::new(format!("{}'s screen", self.user.github_login)).color(
|
||||||
|
if params.selected {
|
||||||
Color::Default
|
Color::Default
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,10 @@ use gpui::{
|
||||||
LayoutId, ManagedView, Model, ModelContext, PathPromptOptions, Point, PromptLevel, Render,
|
LayoutId, ManagedView, Model, ModelContext, PathPromptOptions, Point, PromptLevel, Render,
|
||||||
Size, Subscription, Task, View, WeakView, WindowHandle, WindowOptions,
|
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 itertools::Itertools;
|
||||||
use language::{LanguageRegistry, Rope};
|
use language::{LanguageRegistry, Rope};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
@ -261,6 +264,7 @@ impl Column for WorkspaceId {
|
||||||
pub fn init_settings(cx: &mut AppContext) {
|
pub fn init_settings(cx: &mut AppContext) {
|
||||||
WorkspaceSettings::register(cx);
|
WorkspaceSettings::register(cx);
|
||||||
ItemSettings::register(cx);
|
ItemSettings::register(cx);
|
||||||
|
PreviewTabsSettings::register(cx);
|
||||||
TabBarSettings::register(cx);
|
TabBarSettings::register(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1142,7 +1146,13 @@ impl Workspace {
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
pane.update(&mut cx, |pane, cx| {
|
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;
|
navigated |= Some(item.item_id()) != prev_active_item_id;
|
||||||
pane.nav_history_mut().set_mode(NavigationMode::Normal);
|
pane.nav_history_mut().set_mode(NavigationMode::Normal);
|
||||||
if let Some(data) = entry.data {
|
if let Some(data) = entry.data {
|
||||||
|
@ -2066,6 +2076,17 @@ impl Workspace {
|
||||||
pane: Option<WeakView<Pane>>,
|
pane: Option<WeakView<Pane>>,
|
||||||
focus_item: bool,
|
focus_item: bool,
|
||||||
cx: &mut WindowContext,
|
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>> {
|
) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
|
||||||
let pane = pane.unwrap_or_else(|| {
|
let pane = pane.unwrap_or_else(|| {
|
||||||
self.last_active_center_pane.clone().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 {
|
cx.spawn(move |mut cx| async move {
|
||||||
let (project_entry_id, build_item) = task.await?;
|
let (project_entry_id, build_item) = task.await?;
|
||||||
pane.update(&mut cx, |pane, cx| {
|
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,
|
&mut self,
|
||||||
path: impl Into<ProjectPath>,
|
path: impl Into<ProjectPath>,
|
||||||
cx: &mut ViewContext<Self>,
|
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>> {
|
) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
|
||||||
let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
|
let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
|
||||||
self.panes
|
self.panes
|
||||||
|
@ -2110,7 +2140,7 @@ impl Workspace {
|
||||||
let pane = pane.upgrade()?;
|
let pane = pane.upgrade()?;
|
||||||
let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
|
let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
|
||||||
new_pane.update(cx, |new_pane, 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")))?
|
.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));
|
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);
|
self.add_item(pane, Box::new(item.clone()), cx);
|
||||||
item
|
item
|
||||||
}
|
}
|
||||||
|
@ -2536,7 +2569,7 @@ impl Workspace {
|
||||||
if source != destination {
|
if source != destination {
|
||||||
// Close item from previous pane
|
// Close item from previous pane
|
||||||
source.update(cx, |source, cx| {
|
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()?),
|
kind: Arc::from(item_handle.serialized_item_kind()?),
|
||||||
item_id: item_handle.item_id().as_u64(),
|
item_id: item_handle.item_id().as_u64(),
|
||||||
active: Some(item_handle.item_id()) == active_item_id,
|
active: Some(item_handle.item_id()) == active_item_id,
|
||||||
|
preview: pane.is_active_preview_item(item_handle.item_id()),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
|
|
|
@ -613,6 +613,40 @@ The following settings can be overridden for each specific language:
|
||||||
|
|
||||||
These values take in the same options as the root-level settings with the same name.
|
These values take in the same options as the root-level settings with the same name.
|
||||||
|
|
||||||
|
## Preview tabs
|
||||||
|
|
||||||
|
- Description:
|
||||||
|
Preview tabs allow you to open files in preview mode, where they close automatically when you switch to another file unless you explicitly pin them. This is useful for quickly viewing files without cluttering your workspace. Preview tabs display their file names in italics. \
|
||||||
|
There are several ways to convert a preview tab into a regular tab:
|
||||||
|
|
||||||
|
- Double-clicking on the file
|
||||||
|
- Double-clicking on the tab header
|
||||||
|
- Using the 'project_panel::OpenPermanent' action
|
||||||
|
- Editing the file
|
||||||
|
- Dragging the file to a different pane
|
||||||
|
|
||||||
|
- Setting: `preview_tabs`
|
||||||
|
- Default:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"preview_tabs": {
|
||||||
|
"enabled": true,
|
||||||
|
"enable_preview_from_file_finder": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options**
|
||||||
|
|
||||||
|
### Enable preview from file finder
|
||||||
|
|
||||||
|
- Description: Determines whether to open files in preview mode when selected from the file finder.
|
||||||
|
- Setting: `enable_preview_from_file_finder`
|
||||||
|
- Default: `false`
|
||||||
|
|
||||||
|
**Options**
|
||||||
|
|
||||||
|
`boolean` values
|
||||||
|
|
||||||
## Preferred Line Length
|
## Preferred Line Length
|
||||||
|
|
||||||
- Description: The column at which to soft-wrap lines, for buffers where soft-wrap is enabled.
|
- Description: The column at which to soft-wrap lines, for buffers where soft-wrap is enabled.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue