mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-09 16:00:35 +00:00
Add auto_preview setting to open previewable files in their preview
This commit is contained in:
parent
6b733d1058
commit
525c36c9f6
26 changed files with 2748 additions and 225 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -4575,10 +4575,14 @@ dependencies = [
|
|||
"editor",
|
||||
"feature_flags",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"project",
|
||||
"settings",
|
||||
"text",
|
||||
"ui",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -17806,11 +17810,18 @@ dependencies = [
|
|||
name = "svg_preview"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"editor",
|
||||
"file_icons",
|
||||
"gpui",
|
||||
"language",
|
||||
"markdown_preview",
|
||||
"multi_buffer",
|
||||
"project",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -614,7 +614,7 @@
|
|||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-k v": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-shift-v": "preview::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -622,7 +622,15 @@
|
|||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-k v": "svg::OpenPreviewToTheSide",
|
||||
"ctrl-shift-v": "svg::OpenPreview",
|
||||
"ctrl-shift-v": "preview::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && extension == csv",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-k v": "csv::OpenPreviewToTheSide",
|
||||
"ctrl-shift-v": "preview::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -1320,6 +1328,7 @@
|
|||
{
|
||||
"context": "MarkdownPreview",
|
||||
"bindings": {
|
||||
"ctrl-shift-v": "preview::Toggle",
|
||||
"pageup": "markdown::ScrollPageUp",
|
||||
"pagedown": "markdown::ScrollPageDown",
|
||||
"up": "markdown::ScrollUp",
|
||||
|
|
@ -1332,6 +1341,18 @@
|
|||
"ctrl-f": "buffer_search::Deploy",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "SvgPreview",
|
||||
"bindings": {
|
||||
"ctrl-shift-v": "preview::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "CsvPreview",
|
||||
"bindings": {
|
||||
"ctrl-shift-v": "preview::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
|
|
|
|||
|
|
@ -656,7 +656,7 @@
|
|||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-k v": "markdown::OpenPreviewToTheSide",
|
||||
"cmd-shift-v": "markdown::OpenPreview",
|
||||
"cmd-shift-v": "preview::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -664,7 +664,15 @@
|
|||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-k v": "svg::OpenPreviewToTheSide",
|
||||
"cmd-shift-v": "svg::OpenPreview",
|
||||
"cmd-shift-v": "preview::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && extension == csv",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-k v": "csv::OpenPreviewToTheSide",
|
||||
"cmd-shift-v": "preview::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -1413,6 +1421,7 @@
|
|||
{
|
||||
"context": "MarkdownPreview",
|
||||
"bindings": {
|
||||
"cmd-shift-v": "preview::Toggle",
|
||||
"pageup": "markdown::ScrollPageUp",
|
||||
"pagedown": "markdown::ScrollPageDown",
|
||||
"up": "markdown::ScrollUp",
|
||||
|
|
@ -1424,6 +1433,18 @@
|
|||
"cmd-f": "buffer_search::Deploy",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "SvgPreview",
|
||||
"bindings": {
|
||||
"cmd-shift-v": "preview::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "CsvPreview",
|
||||
"bindings": {
|
||||
"cmd-shift-v": "preview::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
|
|
|
|||
|
|
@ -610,7 +610,7 @@
|
|||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-k v": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-shift-v": "preview::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -618,7 +618,15 @@
|
|||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-k v": "svg::OpenPreviewToTheSide",
|
||||
"ctrl-shift-v": "svg::OpenPreview",
|
||||
"ctrl-shift-v": "preview::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "Editor && extension == csv",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-k v": "csv::OpenPreviewToTheSide",
|
||||
"ctrl-shift-v": "preview::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -1344,6 +1352,7 @@
|
|||
"context": "MarkdownPreview",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-v": "preview::Toggle",
|
||||
"pageup": "markdown::ScrollPageUp",
|
||||
"pagedown": "markdown::ScrollPageDown",
|
||||
"up": "markdown::ScrollUp",
|
||||
|
|
@ -1356,6 +1365,20 @@
|
|||
"ctrl-f": "buffer_search::Deploy",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "SvgPreview",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-v": "preview::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "CsvPreview",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-v": "preview::Toggle",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
|
|
|
|||
|
|
@ -182,6 +182,18 @@
|
|||
//
|
||||
// Default: true
|
||||
"restore_on_file_reopen": true,
|
||||
// How to open files that have a preview available for their file type
|
||||
// (e.g. Markdown or SVG files).
|
||||
// May take 3 values:
|
||||
// 1. Open previewable files in a text editor, previews have to be opened manually.
|
||||
// "auto_preview": "off"
|
||||
// 2. Open previewable files in their preview instead of a text editor.
|
||||
// The text editor can be opened from the preview with the `preview::OpenSource` action.
|
||||
// "auto_preview": "in_place"
|
||||
// 3. Open previewable files in a text editor and keep a preview following the
|
||||
// active editor in a pane to the side.
|
||||
// "auto_preview": "to_the_side"
|
||||
"auto_preview": "off",
|
||||
// Whether to automatically close files that have been deleted on disk.
|
||||
"close_on_file_delete": false,
|
||||
// Whether toggling a panel (e.g. with its keyboard shortcut) also closes
|
||||
|
|
|
|||
|
|
@ -9,13 +9,17 @@ path = "src/csv_preview.rs"
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
gpui.workspace = true
|
||||
editor.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
project.workspace = true
|
||||
settings.workspace = true
|
||||
text.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
log.workspace = true
|
||||
text.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[features]
|
||||
dev-tools = []
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
use ::settings::Settings as _;
|
||||
use editor::{Editor, EditorEvent};
|
||||
use feature_flags::{FeatureFlag, FeatureFlagAppExt as _, PresenceFlag, register_feature_flag};
|
||||
use gpui::{
|
||||
AppContext, Entity, EventEmitter, FocusHandle, Focusable, ListAlignment, Task, actions,
|
||||
};
|
||||
use language::Buffer;
|
||||
use project::{Project, ProjectPath};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
time::{Duration, Instant},
|
||||
|
|
@ -13,7 +16,9 @@ use ui::{
|
|||
AbsoluteLength, ResizableColumnsState, SharedString, TableInteractionState,
|
||||
TableResizeBehavior, prelude::*,
|
||||
};
|
||||
use workspace::{Item, SplitDirection, Workspace};
|
||||
use workspace::item::{ItemBufferKind, ProjectItem};
|
||||
use workspace::{AutoPreview, Item, Pane, SplitDirection, Workspace, WorkspaceSettings};
|
||||
use zed_actions::preview::{OpenSource, Toggle};
|
||||
|
||||
use crate::{parser::EditorState, settings::CsvPreviewSettings, types::TableLikeContent};
|
||||
|
||||
|
|
@ -34,6 +39,7 @@ impl FeatureFlag for TabularDataPreviewFeatureFlag {
|
|||
register_feature_flag!(TabularDataPreviewFeatureFlag);
|
||||
|
||||
pub struct CsvPreviewView {
|
||||
_workspace_subscription: Option<gpui::Subscription>,
|
||||
pub(crate) engine: TableDataEngine,
|
||||
|
||||
pub(crate) focus_handle: FocusHandle,
|
||||
|
|
@ -54,6 +60,8 @@ pub struct CsvPreviewView {
|
|||
}
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
workspace::register_project_item::<CsvPreviewView>(cx);
|
||||
workspace::register_auto_preview_provider(CsvPreviewView::auto_preview_provider(), cx);
|
||||
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
||||
CsvPreviewView::register(workspace);
|
||||
})
|
||||
|
|
@ -85,31 +93,65 @@ impl CsvPreviewView {
|
|||
});
|
||||
}
|
||||
|
||||
/// Opens (or reveals) a preview for the active CSV editor.
|
||||
/// Returns false when the active item is not a CSV editor.
|
||||
fn open_preview_for_active_editor(
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> bool {
|
||||
let Some(editor) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))
|
||||
.filter(|editor| Self::is_csv_file(editor, cx))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let csv_preview = Self::new(&editor, cx);
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
let existing = pane
|
||||
.items_of_type::<CsvPreviewView>()
|
||||
.find(|view| view.read(cx).active_editor_state.editor == editor);
|
||||
if let Some(idx) = existing.and_then(|e| pane.index_for_item(&e)) {
|
||||
pane.activate_item(idx, true, true, window, cx);
|
||||
} else {
|
||||
pane.add_item(Box::new(csv_preview), true, true, None, window, cx);
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
true
|
||||
}
|
||||
|
||||
/// Activates (or opens) a text editor for the active CSV preview.
|
||||
/// Returns false when the active item is not a CSV preview.
|
||||
fn open_source_for_active_preview(
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> bool {
|
||||
let Some(preview) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<CsvPreviewView>())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let editor = preview.read(cx).active_editor_state.editor.clone();
|
||||
if !workspace.activate_item(&editor, true, true, window, cx) {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
|
||||
});
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn register(workspace: &mut Workspace) {
|
||||
workspace.register_action_renderer(|div, _, _, cx| {
|
||||
div.when(cx.has_flag::<TabularDataPreviewFeatureFlag>(), |div| {
|
||||
div.on_action(cx.listener(|workspace, _: &OpenPreview, window, cx| {
|
||||
if let Some(editor) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))
|
||||
.filter(|editor| Self::is_csv_file(editor, cx))
|
||||
{
|
||||
let csv_preview = Self::new(&editor, cx);
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
let existing = pane
|
||||
.items_of_type::<CsvPreviewView>()
|
||||
.find(|view| view.read(cx).active_editor_state.editor == editor);
|
||||
if let Some(idx) = existing.and_then(|e| pane.index_for_item(&e)) {
|
||||
pane.activate_item(idx, true, true, window, cx);
|
||||
} else {
|
||||
pane.add_item(Box::new(csv_preview), true, true, None, window, cx);
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
Self::open_preview_for_active_editor(workspace, window, cx);
|
||||
}))
|
||||
.on_action(cx.listener(
|
||||
|workspace, _: &OpenPreviewToTheSide, window, cx| {
|
||||
.on_action(
|
||||
cx.listener(|workspace, _: &OpenPreviewToTheSide, window, cx| {
|
||||
if let Some(editor) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.act_as::<Editor>(cx))
|
||||
|
|
@ -146,13 +188,126 @@ impl CsvPreviewView {
|
|||
});
|
||||
cx.notify();
|
||||
}
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
.on_action(cx.listener(|workspace, _: &OpenSource, window, cx| {
|
||||
if !Self::open_source_for_active_preview(workspace, window, cx) {
|
||||
cx.propagate();
|
||||
}
|
||||
}))
|
||||
.on_action(cx.listener(|workspace, _: &Toggle, window, cx| {
|
||||
if !Self::open_source_for_active_preview(workspace, window, cx)
|
||||
&& !Self::open_preview_for_active_editor(workspace, window, cx)
|
||||
{
|
||||
cx.propagate();
|
||||
}
|
||||
}))
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn new(editor: &Entity<Editor>, cx: &mut Context<Workspace>) -> Entity<Self> {
|
||||
cx.new(|cx| Self::build(editor.clone(), cx))
|
||||
}
|
||||
|
||||
fn new_following(
|
||||
editor: &Entity<Editor>,
|
||||
window: &Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Entity<Self> {
|
||||
let workspace = cx.entity();
|
||||
cx.new(|cx| {
|
||||
let mut this = Self::build(editor.clone(), cx);
|
||||
this._workspace_subscription = Some(cx.subscribe_in(
|
||||
&workspace,
|
||||
window,
|
||||
|this: &mut Self, workspace, event: &workspace::Event, _window, cx| {
|
||||
if let workspace::Event::ActiveItemChanged = event
|
||||
&& let Some(editor) = workspace
|
||||
.read(cx)
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
&& Self::is_csv_file(&editor, cx)
|
||||
&& this.active_editor_state.editor != editor
|
||||
{
|
||||
this.set_editor(editor, cx);
|
||||
}
|
||||
},
|
||||
));
|
||||
this
|
||||
})
|
||||
}
|
||||
|
||||
fn is_following(&self) -> bool {
|
||||
self._workspace_subscription.is_some()
|
||||
}
|
||||
|
||||
fn set_editor(&mut self, editor: Entity<Editor>, cx: &mut Context<Self>) {
|
||||
let subscription = Self::subscribe_to_editor(&editor, cx);
|
||||
self.active_editor_state = EditorState {
|
||||
editor,
|
||||
_subscription: subscription,
|
||||
};
|
||||
self.parse_csv_from_active_editor(false, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn subscribe_to_editor(editor: &Entity<Editor>, cx: &mut Context<Self>) -> gpui::Subscription {
|
||||
cx.subscribe(
|
||||
editor,
|
||||
|this: &mut CsvPreviewView, _editor, event: &EditorEvent, cx| {
|
||||
match event {
|
||||
EditorEvent::Edited { .. } | EditorEvent::DirtyChanged => {
|
||||
this.parse_csv_from_active_editor(true, cx);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn auto_preview_provider() -> workspace::AutoPreviewProvider {
|
||||
workspace::AutoPreviewProvider {
|
||||
applies_to: |item, cx| {
|
||||
cx.has_flag::<TabularDataPreviewFeatureFlag>()
|
||||
&& item
|
||||
.downcast::<Editor>()
|
||||
.is_some_and(|editor| Self::is_csv_file(&editor, cx))
|
||||
},
|
||||
has_open_sources: |workspace, cx| {
|
||||
workspace
|
||||
.items_of_type::<Editor>(cx)
|
||||
.any(|editor| Self::is_csv_file(&editor, cx))
|
||||
},
|
||||
is_follow_view: |item, cx| {
|
||||
item.downcast::<CsvPreviewView>()
|
||||
.is_some_and(|view| view.read(cx).is_following())
|
||||
},
|
||||
is_preview_view: |item, cx| {
|
||||
item.downcast::<CsvPreviewView>()
|
||||
.is_some_and(|view| !view.read(cx).is_following())
|
||||
},
|
||||
build_follow_view: |workspace, window, cx| {
|
||||
let editor = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
.filter(|editor| Self::is_csv_file(editor, cx))?;
|
||||
Some(Box::new(Self::new_following(&editor, window, cx)))
|
||||
},
|
||||
build_preview_view: |_, item, _, cx| {
|
||||
let editor = item.downcast::<Editor>()?;
|
||||
Some(Box::new(Self::new(&editor, cx)))
|
||||
},
|
||||
source_view: |_, item, _, cx| {
|
||||
let preview = item.downcast::<CsvPreviewView>()?;
|
||||
Some(Box::new(
|
||||
preview.read(cx).active_editor_state.editor.clone(),
|
||||
))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn build(editor: Entity<Editor>, cx: &mut Context<Self>) -> Self {
|
||||
let contents = TableLikeContent::default();
|
||||
let table_interaction_state = cx.new(|cx| {
|
||||
TableInteractionState::new(cx).with_custom_scrollbar(ui::Scrollbars::for_settings::<
|
||||
|
|
@ -160,41 +315,30 @@ impl CsvPreviewView {
|
|||
>())
|
||||
});
|
||||
|
||||
cx.new(|cx| {
|
||||
let subscription = cx.subscribe(
|
||||
let subscription = Self::subscribe_to_editor(&editor, cx);
|
||||
|
||||
let mut view = CsvPreviewView {
|
||||
_workspace_subscription: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
active_editor_state: EditorState {
|
||||
editor,
|
||||
|this: &mut CsvPreviewView, _editor, event: &EditorEvent, cx| {
|
||||
match event {
|
||||
EditorEvent::Edited { .. } | EditorEvent::DirtyChanged => {
|
||||
this.parse_csv_from_active_editor(true, cx);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
},
|
||||
);
|
||||
_subscription: subscription,
|
||||
},
|
||||
table_interaction_state,
|
||||
column_widths: ColumnWidths::new(cx, 1),
|
||||
parsing_task: None,
|
||||
is_parsing: false,
|
||||
filter_sort_task: None,
|
||||
performance_metrics: PerformanceMetrics::default(),
|
||||
list_state: gpui::ListState::new(contents.rows.len(), ListAlignment::Top, px(1.))
|
||||
.with_uniform_item_height(px(24.)),
|
||||
settings: CsvPreviewSettings::default(),
|
||||
last_parse_end_time: None,
|
||||
engine: TableDataEngine::default(),
|
||||
};
|
||||
|
||||
let mut view = CsvPreviewView {
|
||||
focus_handle: cx.focus_handle(),
|
||||
active_editor_state: EditorState {
|
||||
editor: editor.clone(),
|
||||
_subscription: subscription,
|
||||
},
|
||||
table_interaction_state,
|
||||
column_widths: ColumnWidths::new(cx, 1),
|
||||
parsing_task: None,
|
||||
is_parsing: false,
|
||||
filter_sort_task: None,
|
||||
performance_metrics: PerformanceMetrics::default(),
|
||||
list_state: gpui::ListState::new(contents.rows.len(), ListAlignment::Top, px(1.))
|
||||
.with_uniform_item_height(px(24.)),
|
||||
settings: CsvPreviewSettings::default(),
|
||||
last_parse_end_time: None,
|
||||
engine: TableDataEngine::default(),
|
||||
};
|
||||
|
||||
view.parse_csv_from_active_editor(false, cx);
|
||||
view
|
||||
})
|
||||
view.parse_csv_from_active_editor(false, cx);
|
||||
view
|
||||
}
|
||||
|
||||
pub(crate) fn editor_state(&self) -> &EditorState {
|
||||
|
|
@ -260,6 +404,21 @@ impl CsvPreviewView {
|
|||
Self::is_csv_file(&editor, cx).then_some(editor)
|
||||
}
|
||||
|
||||
fn is_csv_path(path: impl AsRef<std::path::Path>) -> bool {
|
||||
path.as_ref()
|
||||
.extension()
|
||||
.is_some_and(|extension| extension.eq_ignore_ascii_case("csv"))
|
||||
}
|
||||
|
||||
fn source_buffer(&self, cx: &App) -> Option<Entity<Buffer>> {
|
||||
self.active_editor_state
|
||||
.editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
}
|
||||
|
||||
fn is_csv_file(editor: &Entity<Editor>, cx: &App) -> bool {
|
||||
editor
|
||||
.read(cx)
|
||||
|
|
@ -309,6 +468,84 @@ impl Item for CsvPreviewView {
|
|||
})
|
||||
.unwrap_or_else(|| SharedString::from("CSV Preview"))
|
||||
}
|
||||
|
||||
fn buffer_kind(&self, _cx: &App) -> ItemBufferKind {
|
||||
ItemBufferKind::Singleton
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &App) -> bool {
|
||||
self.source_buffer(cx)
|
||||
.is_some_and(|buffer| buffer.read(cx).is_dirty())
|
||||
}
|
||||
|
||||
fn for_each_project_item(
|
||||
&self,
|
||||
cx: &App,
|
||||
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
|
||||
) {
|
||||
if let Some(buffer) = self.source_buffer(cx) {
|
||||
f(buffer.entity_id(), buffer.read(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`project::ProjectItem`] that claims CSV files when the `auto_preview` setting
|
||||
/// is set to `in_place`, so that opening such files shows their rendered preview instead of an editor.
|
||||
pub struct CsvPreviewItem {
|
||||
buffer: Entity<Buffer>,
|
||||
}
|
||||
|
||||
impl project::ProjectItem for CsvPreviewItem {
|
||||
fn try_open(
|
||||
project: &Entity<Project>,
|
||||
path: &ProjectPath,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<anyhow::Result<Entity<Self>>>> {
|
||||
if !cx.has_flag::<TabularDataPreviewFeatureFlag>()
|
||||
|| WorkspaceSettings::get_global(cx).auto_preview != AutoPreview::InPlace
|
||||
|| !project
|
||||
.read(cx)
|
||||
.absolute_path(path, cx)
|
||||
.is_some_and(CsvPreviewView::is_csv_path)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let buffer = project.update(cx, |project, cx| project.open_buffer(path.clone(), cx));
|
||||
Some(cx.spawn(async move |cx| {
|
||||
let buffer = buffer.await?;
|
||||
Ok(cx.new(|_| CsvPreviewItem { buffer }))
|
||||
}))
|
||||
}
|
||||
|
||||
fn entry_id(&self, cx: &App) -> Option<project::ProjectEntryId> {
|
||||
project::ProjectItem::entry_id(self.buffer.read(cx), cx)
|
||||
}
|
||||
|
||||
fn project_path(&self, cx: &App) -> Option<ProjectPath> {
|
||||
project::ProjectItem::project_path(self.buffer.read(cx), cx)
|
||||
}
|
||||
|
||||
fn is_dirty(&self) -> bool {
|
||||
// This item is only a carrier between `try_open` and `for_project_item`: the
|
||||
// preview reports its dirty state through the buffer it renders.
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl ProjectItem for CsvPreviewView {
|
||||
type Item = CsvPreviewItem;
|
||||
|
||||
fn for_project_item(
|
||||
project: Entity<Project>,
|
||||
_pane: Option<&Pane>,
|
||||
item: Entity<Self::Item>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let buffer = item.read(cx).buffer.clone();
|
||||
let editor = cx.new(|cx| Editor::for_buffer(buffer, Some(project), window, cx));
|
||||
Self::build(editor, cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ impl Render for CsvPreviewView {
|
|||
|
||||
let render_prep_start = Instant::now();
|
||||
let table_with_settings = v_flex()
|
||||
.key_context("CsvPreview")
|
||||
.size_full()
|
||||
.bg(theme.colors().editor_background)
|
||||
.track_focus(&self.focus_handle)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ actions!(
|
|||
|
||||
pub fn init(cx: &mut App) {
|
||||
workspace::register_serializable_item::<MarkdownPreviewView>(cx);
|
||||
workspace::register_project_item::<MarkdownPreviewView>(cx);
|
||||
workspace::register_auto_preview_provider(MarkdownPreviewView::auto_preview_provider(), cx);
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, window, cx| {
|
||||
let Some(window) = window else {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1058,6 +1058,15 @@ impl VsCodeSettings {
|
|||
}),
|
||||
zoomed_padding: None,
|
||||
focus_follows_mouse: None,
|
||||
auto_preview: self
|
||||
.read_value("workbench.editorAssociations")
|
||||
.and_then(|value| {
|
||||
let associations = value.as_object()?;
|
||||
associations
|
||||
.values()
|
||||
.any(|editor| editor.as_str() == Some("vscode.markdown.preview.editor"))
|
||||
.then_some(AutoPreview::InPlace)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -134,6 +134,38 @@ pub struct WorkspaceSettingsContent {
|
|||
/// Whether the focused panel follows the mouse location
|
||||
/// Default: false
|
||||
pub focus_follows_mouse: Option<FocusFollowsMouse>,
|
||||
/// How to open files that have a preview available for their file type
|
||||
/// (e.g. Markdown or SVG files).
|
||||
///
|
||||
/// Default: off
|
||||
pub auto_preview: Option<AutoPreview>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy,
|
||||
Clone,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
MergeFrom,
|
||||
Debug,
|
||||
strum::VariantArray,
|
||||
strum::VariantNames,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AutoPreview {
|
||||
/// Open previewable files in a text editor, previews have to be opened manually.
|
||||
#[default]
|
||||
Off,
|
||||
/// Open previewable files in their preview instead of a text editor.
|
||||
/// The text editor can be opened from the preview with the `preview::OpenSource` action.
|
||||
InPlace,
|
||||
/// Open previewable files in a text editor and keep a preview following the
|
||||
/// active editor in a pane to the side.
|
||||
ToTheSide,
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
|
|
|
|||
|
|
@ -3652,7 +3652,7 @@ fn search_and_files_page() -> SettingsPage {
|
|||
]
|
||||
}
|
||||
|
||||
fn file_scan_section() -> [SettingsPageItem; 6] {
|
||||
fn file_scan_section() -> [SettingsPageItem; 7] {
|
||||
[
|
||||
SettingsPageItem::SectionHeader("File Scan"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
|
|
@ -3749,6 +3749,20 @@ fn search_and_files_page() -> SettingsPage {
|
|||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Auto Preview",
|
||||
description: "How to open files that have a preview available for their file type (e.g. Markdown or SVG files).",
|
||||
field: Box::new(SettingField {
|
||||
organization_override: None,
|
||||
json_path: Some("auto_preview"),
|
||||
pick: |settings_content| settings_content.workspace.auto_preview.as_ref(),
|
||||
write: |settings_content, value, _| {
|
||||
settings_content.workspace.auto_preview = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -530,6 +530,7 @@ fn init_renderers(cx: &mut App) {
|
|||
.add_basic_renderer::<settings::SaturatingBool>(render_toggle_button)
|
||||
.add_basic_renderer::<settings::CursorShape>(render_dropdown)
|
||||
.add_basic_renderer::<settings::RestoreOnStartupBehavior>(render_dropdown)
|
||||
.add_basic_renderer::<settings::AutoPreview>(render_dropdown)
|
||||
.add_basic_renderer::<settings::BottomDockLayout>(render_dropdown)
|
||||
.add_basic_renderer::<settings::OnLastWindowClosed>(render_dropdown)
|
||||
.add_basic_renderer::<settings::CliDefaultOpenBehavior>(render_dropdown)
|
||||
|
|
|
|||
|
|
@ -12,10 +12,23 @@ workspace = true
|
|||
path = "src/svg_preview.rs"
|
||||
|
||||
[dependencies]
|
||||
multi_buffer.workspace = true
|
||||
anyhow.workspace = true
|
||||
editor.workspace = true
|
||||
file_icons.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
project.workspace = true
|
||||
settings.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
markdown_preview.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
serde_json.workspace = true
|
||||
util.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ actions!(
|
|||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
workspace::register_project_item::<svg_preview_view::SvgPreviewView>(cx);
|
||||
workspace::register_auto_preview_provider(
|
||||
svg_preview_view::SvgPreviewView::auto_preview_provider(),
|
||||
cx,
|
||||
);
|
||||
cx.observe_new(|workspace: &mut Workspace, window, cx| {
|
||||
let Some(window) = window else {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use std::mem;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use editor::Editor;
|
||||
use file_icons::FileIcons;
|
||||
use gpui::{
|
||||
App, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render,
|
||||
|
|
@ -8,9 +10,12 @@ use gpui::{
|
|||
};
|
||||
use language::{Buffer, BufferEvent};
|
||||
use multi_buffer::MultiBuffer;
|
||||
use project::{Project, ProjectEntryId, ProjectPath};
|
||||
use settings::Settings as _;
|
||||
use ui::prelude::*;
|
||||
use workspace::item::Item;
|
||||
use workspace::{Pane, Workspace};
|
||||
use workspace::item::{Item, ItemBufferKind, ProjectItem};
|
||||
use workspace::{AutoPreview, Pane, Workspace, WorkspaceSettings};
|
||||
use zed_actions::preview::{OpenSource, Toggle};
|
||||
|
||||
use crate::{OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide};
|
||||
|
||||
|
|
@ -79,6 +84,8 @@ impl SvgPreviewView {
|
|||
move |this: &mut SvgPreviewView, workspace, event: &workspace::Event, window, cx| {
|
||||
if let workspace::Event::ActiveItemChanged = event {
|
||||
let workspace = workspace.read(cx);
|
||||
// When the active item is not an SVG buffer, keep showing the last
|
||||
// previewed file instead of blanking the view.
|
||||
if let Some(active_item) = workspace.active_item(cx)
|
||||
&& let Some(buffer) = active_item.downcast::<MultiBuffer>()
|
||||
&& Self::is_svg_file(&buffer, cx)
|
||||
|
|
@ -93,8 +100,6 @@ impl SvgPreviewView {
|
|||
this.render_image(window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
} else {
|
||||
this.set_current(None, window, cx);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -202,30 +207,135 @@ impl SvgPreviewView {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn register(workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context<Workspace>) {
|
||||
workspace.register_action(move |workspace, _: &OpenPreview, window, cx| {
|
||||
if let Some(buffer) = Self::resolve_active_item_as_svg_buffer(workspace, cx)
|
||||
&& Self::is_svg_file(&buffer, cx)
|
||||
{
|
||||
let view = Self::create_svg_view(
|
||||
SvgPreviewMode::Default,
|
||||
pub fn is_svg_path(path: impl AsRef<std::path::Path>) -> bool {
|
||||
path.as_ref()
|
||||
.extension()
|
||||
.is_some_and(|extension| extension.eq_ignore_ascii_case("svg"))
|
||||
}
|
||||
|
||||
fn is_following(&self) -> bool {
|
||||
self._workspace_subscription.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn auto_preview_provider() -> workspace::AutoPreviewProvider {
|
||||
workspace::AutoPreviewProvider {
|
||||
applies_to: |item, cx| {
|
||||
item.downcast::<Editor>()
|
||||
.is_some_and(|editor| Self::is_svg_file(editor.read(cx).buffer(), cx))
|
||||
},
|
||||
has_open_sources: |workspace, cx| {
|
||||
workspace
|
||||
.items_of_type::<Editor>(cx)
|
||||
.any(|editor| Self::is_svg_file(editor.read(cx).buffer(), cx))
|
||||
},
|
||||
is_follow_view: |item, cx| {
|
||||
item.downcast::<SvgPreviewView>()
|
||||
.is_some_and(|view| view.read(cx).is_following())
|
||||
},
|
||||
is_preview_view: |item, cx| {
|
||||
item.downcast::<SvgPreviewView>()
|
||||
.is_some_and(|view| !view.read(cx).is_following())
|
||||
},
|
||||
build_follow_view: |workspace, window, cx| {
|
||||
let buffer = Self::resolve_active_item_as_svg_buffer(workspace, cx)?;
|
||||
Some(Box::new(Self::create_svg_view(
|
||||
SvgPreviewMode::Follow,
|
||||
workspace,
|
||||
buffer.clone(),
|
||||
buffer,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
if let Some(existing_view_idx) =
|
||||
Self::find_existing_preview_item_idx(pane, &buffer, cx)
|
||||
{
|
||||
pane.activate_item(existing_view_idx, true, true, window, cx);
|
||||
} else {
|
||||
pane.add_item(Box::new(view), true, true, None, window, cx)
|
||||
}
|
||||
)))
|
||||
},
|
||||
build_preview_view: |workspace, item, window, cx| {
|
||||
let editor = item.downcast::<Editor>()?;
|
||||
let buffer = editor.read(cx).buffer().clone();
|
||||
Some(Box::new(Self::create_svg_view(
|
||||
SvgPreviewMode::Default,
|
||||
workspace,
|
||||
buffer,
|
||||
window,
|
||||
cx,
|
||||
)))
|
||||
},
|
||||
source_view: |workspace, item, window, cx| {
|
||||
let preview = item.downcast::<SvgPreviewView>()?;
|
||||
let buffer = preview.read(cx).buffer.clone()?;
|
||||
let existing_editor = workspace.items_of_type::<Editor>(cx).find(|editor| {
|
||||
editor.read(cx).buffer().read(cx).as_singleton().as_ref() == Some(&buffer)
|
||||
});
|
||||
cx.notify();
|
||||
let editor = existing_editor.unwrap_or_else(|| {
|
||||
let project = workspace.project().clone();
|
||||
cx.new(|cx| Editor::for_buffer(buffer, Some(project), window, cx))
|
||||
});
|
||||
Some(Box::new(editor))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens (or reveals) a preview for the active SVG editor.
|
||||
/// Returns false when the active item is not an SVG editor.
|
||||
fn open_preview_for_active_editor(
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> bool {
|
||||
let Some(buffer) = Self::resolve_active_item_as_svg_buffer(workspace, cx) else {
|
||||
return false;
|
||||
};
|
||||
let view = Self::create_svg_view(
|
||||
SvgPreviewMode::Default,
|
||||
workspace,
|
||||
buffer.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
if let Some(existing_view_idx) = Self::find_existing_preview_item_idx(pane, &buffer, cx)
|
||||
{
|
||||
pane.activate_item(existing_view_idx, true, true, window, cx);
|
||||
} else {
|
||||
pane.add_item(Box::new(view), true, true, None, window, cx)
|
||||
}
|
||||
});
|
||||
cx.notify();
|
||||
true
|
||||
}
|
||||
|
||||
/// Activates (or opens) a text editor for the active SVG preview.
|
||||
/// Returns false when the active item is not an SVG preview.
|
||||
fn open_source_for_active_preview(
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> bool {
|
||||
let Some(preview) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<SvgPreviewView>())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let Some(buffer) = preview.read(cx).buffer.clone() else {
|
||||
return true;
|
||||
};
|
||||
let existing_editor = workspace.items_of_type::<Editor>(cx).find(|editor| {
|
||||
editor.read(cx).buffer().read(cx).as_singleton().as_ref() == Some(&buffer)
|
||||
});
|
||||
if let Some(editor) = existing_editor {
|
||||
workspace.activate_item(&editor, true, true, window, cx);
|
||||
} else {
|
||||
let project = workspace.project().clone();
|
||||
let editor = cx.new(|cx| Editor::for_buffer(buffer, Some(project), window, cx));
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.add_item(Box::new(editor), true, true, None, window, cx);
|
||||
});
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn register(workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context<Workspace>) {
|
||||
workspace.register_action(move |workspace, _: &OpenPreview, window, cx| {
|
||||
Self::open_preview_for_active_editor(workspace, window, cx);
|
||||
});
|
||||
|
||||
workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, window, cx| {
|
||||
if let Some(editor) = Self::resolve_active_item_as_svg_buffer(workspace, cx)
|
||||
|
|
@ -274,6 +384,87 @@ impl SvgPreviewView {
|
|||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
workspace.register_action(move |workspace, _: &OpenSource, window, cx| {
|
||||
if !Self::open_source_for_active_preview(workspace, window, cx) {
|
||||
cx.propagate();
|
||||
}
|
||||
});
|
||||
|
||||
workspace.register_action(move |workspace, _: &Toggle, window, cx| {
|
||||
if !Self::open_source_for_active_preview(workspace, window, cx)
|
||||
&& !Self::open_preview_for_active_editor(workspace, window, cx)
|
||||
{
|
||||
cx.propagate();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`project::ProjectItem`] that claims SVG files when the `auto_preview` setting
|
||||
/// is set to `in_place`, so that opening such files shows their rendered preview instead of an editor.
|
||||
pub struct SvgPreviewItem {
|
||||
buffer: Entity<Buffer>,
|
||||
}
|
||||
|
||||
impl project::ProjectItem for SvgPreviewItem {
|
||||
fn try_open(
|
||||
project: &Entity<Project>,
|
||||
path: &ProjectPath,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Result<Entity<Self>>>> {
|
||||
if WorkspaceSettings::get_global(cx).auto_preview != AutoPreview::InPlace
|
||||
|| !project
|
||||
.read(cx)
|
||||
.absolute_path(path, cx)
|
||||
.is_some_and(SvgPreviewView::is_svg_path)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let buffer = project.update(cx, |project, cx| project.open_buffer(path.clone(), cx));
|
||||
Some(cx.spawn(async move |cx| {
|
||||
let buffer = buffer.await?;
|
||||
Ok(cx.new(|_| SvgPreviewItem { buffer }))
|
||||
}))
|
||||
}
|
||||
|
||||
fn entry_id(&self, cx: &App) -> Option<ProjectEntryId> {
|
||||
project::ProjectItem::entry_id(self.buffer.read(cx), cx)
|
||||
}
|
||||
|
||||
fn project_path(&self, cx: &App) -> Option<ProjectPath> {
|
||||
project::ProjectItem::project_path(self.buffer.read(cx), cx)
|
||||
}
|
||||
|
||||
fn is_dirty(&self) -> bool {
|
||||
// This item is only a carrier between `try_open` and `for_project_item`: the
|
||||
// preview reports its dirty state through the buffer it renders.
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl ProjectItem for SvgPreviewView {
|
||||
type Item = SvgPreviewItem;
|
||||
|
||||
fn for_project_item(
|
||||
_project: Entity<Project>,
|
||||
_pane: Option<&Pane>,
|
||||
item: Entity<Self::Item>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let buffer = item.read(cx).buffer.clone();
|
||||
let subscription = Self::create_buffer_subscription(&buffer, window, cx);
|
||||
let mut this = Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
buffer: Some(buffer),
|
||||
current_svg: None,
|
||||
_buffer_subscription: Some(subscription),
|
||||
_workspace_subscription: None,
|
||||
_refresh: Task::ready(()),
|
||||
};
|
||||
this.render_image(window, cx);
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -337,5 +528,483 @@ impl Item for SvgPreviewView {
|
|||
Some("svg preview: open")
|
||||
}
|
||||
|
||||
fn buffer_kind(&self, _cx: &App) -> ItemBufferKind {
|
||||
ItemBufferKind::Singleton
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &App) -> bool {
|
||||
self.buffer
|
||||
.as_ref()
|
||||
.is_some_and(|buffer| buffer.read(cx).is_dirty())
|
||||
}
|
||||
|
||||
fn for_each_project_item(
|
||||
&self,
|
||||
cx: &App,
|
||||
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
|
||||
) {
|
||||
// Previews that follow the active editor are not bound to a single file.
|
||||
if self.is_following() {
|
||||
return;
|
||||
}
|
||||
if let Some(buffer) = &self.buffer {
|
||||
f(buffer.entity_id(), buffer.read(cx))
|
||||
}
|
||||
}
|
||||
|
||||
fn to_item_events(_event: &Self::Event, _f: &mut dyn FnMut(workspace::item::ItemEvent)) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::Editor;
|
||||
use gpui::{BorrowAppContext as _, Focusable as _, TestAppContext, WindowHandle};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
use util::rel_path::rel_path;
|
||||
use workspace::{AppState, AutoPreview, MultiWorkspace, open_paths};
|
||||
use zed_actions::preview::OpenSource;
|
||||
|
||||
use super::SvgPreviewView;
|
||||
|
||||
const SVG_CONTENTS: &str = r#"<svg xmlns="http://www.w3.org/2000/svg"></svg>"#;
|
||||
|
||||
#[gpui::test]
|
||||
async fn auto_preview_opens_svg_files_as_preview(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
set_auto_preview(cx, AutoPreview::InPlace);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
path!("/dir"),
|
||||
json!({
|
||||
"a.svg": SVG_CONTENTS,
|
||||
"b.txt": "plain text",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.update(|cx| {
|
||||
open_paths(
|
||||
&[PathBuf::from(path!("/dir"))],
|
||||
app_state.clone(),
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
|
||||
open_workspace_path(&multi_workspace, "a.svg", cx).await;
|
||||
let preview = multi_workspace
|
||||
.update(cx, |multi_workspace, window, cx| {
|
||||
let workspace = multi_workspace.workspace().clone();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
assert_eq!(workspace.active_pane().read(cx).items_len(), 1);
|
||||
let preview = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<SvgPreviewView>())
|
||||
.expect("SVG file should have been opened as a preview");
|
||||
assert!(
|
||||
preview.read(cx).focus_handle.contains_focused(window, cx),
|
||||
"the opened preview should be focused"
|
||||
);
|
||||
preview
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
preview
|
||||
.read_with(cx, |preview, cx| preview
|
||||
.buffer
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.file()
|
||||
.unwrap()
|
||||
.path()
|
||||
.clone())
|
||||
.as_ref(),
|
||||
rel_path("a.svg")
|
||||
);
|
||||
|
||||
// Reopening the file should reuse the existing preview.
|
||||
open_workspace_path(&multi_workspace, "a.svg", cx).await;
|
||||
multi_workspace
|
||||
.update(cx, |multi_workspace, _, cx| {
|
||||
let workspace = multi_workspace.workspace().clone();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
assert_eq!(workspace.active_pane().read(cx).items_len(), 1);
|
||||
assert_eq!(
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<SvgPreviewView>()),
|
||||
Some(preview.clone())
|
||||
);
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
open_workspace_path(&multi_workspace, "b.txt", cx).await;
|
||||
multi_workspace
|
||||
.update(cx, |multi_workspace, _, cx| {
|
||||
let workspace = multi_workspace.workspace().clone();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
assert_eq!(workspace.active_pane().read(cx).items_len(), 2);
|
||||
assert!(
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
.is_some(),
|
||||
"non-previewable files should still open in an editor"
|
||||
);
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn open_source_opens_an_editor_for_the_previewed_file(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
set_auto_preview(cx, AutoPreview::InPlace);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(path!("/dir"), json!({ "a.svg": SVG_CONTENTS }))
|
||||
.await;
|
||||
|
||||
cx.update(|cx| {
|
||||
open_paths(
|
||||
&[PathBuf::from(path!("/dir"))],
|
||||
app_state.clone(),
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
|
||||
open_workspace_path(&multi_workspace, "a.svg", cx).await;
|
||||
multi_workspace
|
||||
.update(cx, |_, window, cx| {
|
||||
window.dispatch_action(Box::new(OpenSource), cx);
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let editor = multi_workspace
|
||||
.update(cx, |multi_workspace, _, cx| {
|
||||
let workspace = multi_workspace.workspace().clone();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.active_pane().read(cx).items_len(),
|
||||
2,
|
||||
"an editor should have been added next to the preview"
|
||||
);
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
.expect("the editor for the previewed file should be active")
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
let editor_path = editor.read_with(cx, |editor, cx| {
|
||||
editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.file()
|
||||
.unwrap()
|
||||
.path()
|
||||
.clone()
|
||||
});
|
||||
assert_eq!(editor_path.as_ref(), rel_path("a.svg"));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn svg_files_open_in_an_editor_by_default(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(path!("/dir"), json!({ "a.svg": SVG_CONTENTS }))
|
||||
.await;
|
||||
|
||||
cx.update(|cx| {
|
||||
open_paths(
|
||||
&[PathBuf::from(path!("/dir"))],
|
||||
app_state.clone(),
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
|
||||
open_workspace_path(&multi_workspace, "a.svg", cx).await;
|
||||
multi_workspace
|
||||
.update(cx, |multi_workspace, _, cx| {
|
||||
let workspace = multi_workspace.workspace().clone();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
assert!(
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
.is_some(),
|
||||
"with auto_preview off, SVG files should open in an editor"
|
||||
);
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn to_the_side_auto_preview_follows_editors_and_closes_with_them(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let app_state = init_test(cx);
|
||||
set_auto_preview(cx, AutoPreview::ToTheSide);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(path!("/dir"), json!({ "a.svg": SVG_CONTENTS }))
|
||||
.await;
|
||||
|
||||
cx.update(|cx| {
|
||||
open_paths(
|
||||
&[PathBuf::from(path!("/dir"))],
|
||||
app_state.clone(),
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
|
||||
open_workspace_path(&multi_workspace, "a.svg", cx).await;
|
||||
multi_workspace
|
||||
.update(cx, |multi_workspace, window, cx| {
|
||||
let workspace = multi_workspace.workspace().clone();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let editor = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
.expect("SVG files should still open in an editor");
|
||||
assert!(
|
||||
editor
|
||||
.read(cx)
|
||||
.focus_handle(cx)
|
||||
.contains_focused(window, cx),
|
||||
"the editor should keep the focus"
|
||||
);
|
||||
assert_eq!(
|
||||
workspace.panes().len(),
|
||||
2,
|
||||
"a pane should have been split for the preview"
|
||||
);
|
||||
let preview = workspace
|
||||
.items_of_type::<SvgPreviewView>(cx)
|
||||
.next()
|
||||
.expect("a preview should have been opened to the side");
|
||||
assert!(preview.read(cx).is_following());
|
||||
assert_ne!(
|
||||
workspace.pane_for(&preview),
|
||||
Some(workspace.active_pane().clone()),
|
||||
"the preview should live in the other pane"
|
||||
);
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
multi_workspace
|
||||
.update(cx, |multi_workspace, window, cx| {
|
||||
let workspace = multi_workspace.workspace().clone();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
|
||||
for editor in editors {
|
||||
let pane = workspace.pane_for(&editor).unwrap();
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.remove_item(editor.entity_id(), false, true, window, cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
multi_workspace
|
||||
.update(cx, |multi_workspace, _, cx| {
|
||||
let workspace = multi_workspace.workspace().clone();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.items_of_type::<SvgPreviewView>(cx).count(),
|
||||
0,
|
||||
"the preview should close when no SVG editors remain"
|
||||
);
|
||||
assert_eq!(
|
||||
workspace.panes().len(),
|
||||
1,
|
||||
"the preview pane should be removed with the preview"
|
||||
);
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn single_side_preview_is_shared_between_preview_kinds(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
cx.update(markdown_preview::init);
|
||||
set_auto_preview(cx, AutoPreview::ToTheSide);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
path!("/dir"),
|
||||
json!({
|
||||
"a.md": "# a",
|
||||
"b.svg": SVG_CONTENTS,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.update(|cx| {
|
||||
open_paths(
|
||||
&[PathBuf::from(path!("/dir"))],
|
||||
app_state.clone(),
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
|
||||
open_workspace_path(&multi_workspace, "a.md", cx).await;
|
||||
let markdown_preview_pane = multi_workspace
|
||||
.update(cx, |multi_workspace, _, cx| {
|
||||
let workspace = multi_workspace.workspace().clone();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
assert_eq!(workspace.panes().len(), 2);
|
||||
let preview = workspace
|
||||
.items_of_type::<markdown_preview::markdown_preview_view::MarkdownPreviewView>(cx)
|
||||
.next()
|
||||
.expect("a markdown preview should have been opened to the side");
|
||||
let pane = workspace.pane_for(&preview).unwrap();
|
||||
assert_eq!(pane.read(cx).items_len(), 1);
|
||||
pane
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
open_workspace_path(&multi_workspace, "b.svg", cx).await;
|
||||
multi_workspace
|
||||
.update(cx, |multi_workspace, _, cx| {
|
||||
let workspace = multi_workspace.workspace().clone();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.panes().len(),
|
||||
2,
|
||||
"the preview pane should be reused for the other preview kind"
|
||||
);
|
||||
assert_eq!(
|
||||
workspace
|
||||
.items_of_type::<markdown_preview::markdown_preview_view::MarkdownPreviewView>(cx)
|
||||
.count(),
|
||||
0,
|
||||
"the markdown preview should have been replaced"
|
||||
);
|
||||
let preview = workspace
|
||||
.items_of_type::<SvgPreviewView>(cx)
|
||||
.next()
|
||||
.expect("an SVG preview should have taken the preview tab slot");
|
||||
assert!(preview.read(cx).is_following());
|
||||
let pane = workspace.pane_for(&preview).unwrap();
|
||||
assert_eq!(pane, markdown_preview_pane);
|
||||
assert_eq!(
|
||||
pane.read(cx).items_len(),
|
||||
1,
|
||||
"a single dynamic preview tab should be kept to the side"
|
||||
);
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
open_workspace_path(&multi_workspace, "a.md", cx).await;
|
||||
multi_workspace
|
||||
.update(cx, |multi_workspace, _, cx| {
|
||||
let workspace = multi_workspace.workspace().clone();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
assert_eq!(workspace.panes().len(), 2);
|
||||
assert_eq!(workspace.items_of_type::<SvgPreviewView>(cx).count(), 0);
|
||||
let preview = workspace
|
||||
.items_of_type::<markdown_preview::markdown_preview_view::MarkdownPreviewView>(cx)
|
||||
.next()
|
||||
.expect("the markdown preview should be back in the preview tab slot");
|
||||
let pane = workspace.pane_for(&preview).unwrap();
|
||||
assert_eq!(pane.read(cx).items_len(), 1);
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn set_auto_preview(cx: &mut TestAppContext, auto_preview: AutoPreview) {
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings(cx, |settings| {
|
||||
settings.workspace.auto_preview = Some(auto_preview);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async fn open_workspace_path(
|
||||
multi_workspace: &WindowHandle<MultiWorkspace>,
|
||||
file: &str,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let open_task = multi_workspace
|
||||
.update(cx, |multi_workspace, window, cx| {
|
||||
let workspace = multi_workspace.workspace().clone();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let worktree_id = workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.next()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.id();
|
||||
workspace.open_path((worktree_id, rel_path(file)), None, true, window, cx)
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
open_task.await.unwrap();
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||
cx.update(|cx| {
|
||||
let state = AppState::test(cx);
|
||||
editor::init(cx);
|
||||
crate::init(cx);
|
||||
state
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
356
crates/workspace/src/auto_preview.rs
Normal file
356
crates/workspace/src/auto_preview.rs
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
use gpui::{App, Context, EntityId, Focusable as _, Global, Window};
|
||||
use settings::Settings as _;
|
||||
|
||||
use crate::{AutoPreview, ItemHandle, SplitDirection, Workspace, WorkspaceSettings};
|
||||
|
||||
/// Integration point for preview views (e.g. Markdown or SVG previews) that can
|
||||
/// automatically accompany or replace text editors, driven by the `auto_preview` setting.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct AutoPreviewProvider {
|
||||
/// Whether this provider can preview the file shown by the given item.
|
||||
pub applies_to: fn(&dyn ItemHandle, &App) -> bool,
|
||||
/// Whether any items this provider applies to are open in the workspace.
|
||||
pub has_open_sources: fn(&Workspace, &App) -> bool,
|
||||
/// Whether the given item is this provider's preview that follows the active editor.
|
||||
pub is_follow_view: fn(&dyn ItemHandle, &App) -> bool,
|
||||
/// Whether the given item is this provider's preview of a single file.
|
||||
pub is_preview_view: fn(&dyn ItemHandle, &App) -> bool,
|
||||
/// Builds a preview that follows the workspace's active editor.
|
||||
pub build_follow_view:
|
||||
fn(&mut Workspace, &mut Window, &mut Context<Workspace>) -> Option<Box<dyn ItemHandle>>,
|
||||
/// Builds a preview of the file shown by the given item.
|
||||
pub build_preview_view: fn(
|
||||
&mut Workspace,
|
||||
&dyn ItemHandle,
|
||||
&mut Window,
|
||||
&mut Context<Workspace>,
|
||||
) -> Option<Box<dyn ItemHandle>>,
|
||||
/// Returns an editor for the file shown by the given preview item.
|
||||
pub source_view: fn(
|
||||
&mut Workspace,
|
||||
&dyn ItemHandle,
|
||||
&mut Window,
|
||||
&mut Context<Workspace>,
|
||||
) -> Option<Box<dyn ItemHandle>>,
|
||||
}
|
||||
|
||||
/// Per-workspace state of the side preview managed by [`sync_side_preview`].
|
||||
#[derive(Default)]
|
||||
pub(crate) struct AutoPreviewState {
|
||||
/// The follow preview currently managed in this workspace.
|
||||
follow_view_id: Option<EntityId>,
|
||||
/// The item that was active when the user closed the side preview: the preview is
|
||||
/// not reopened until another item gets activated.
|
||||
suppressed_for_item: Option<EntityId>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct GlobalAutoPreviewProviders(Vec<AutoPreviewProvider>);
|
||||
|
||||
impl Global for GlobalAutoPreviewProviders {}
|
||||
|
||||
pub fn register_auto_preview_provider(provider: AutoPreviewProvider, cx: &mut App) {
|
||||
cx.default_global::<GlobalAutoPreviewProviders>()
|
||||
.0
|
||||
.push(provider);
|
||||
}
|
||||
|
||||
fn providers(cx: &App) -> Vec<AutoPreviewProvider> {
|
||||
cx.try_global::<GlobalAutoPreviewProviders>()
|
||||
.map(|providers| providers.0.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Keeps a single follow-mode preview in a pane to the side of previewable editors
|
||||
/// when the `auto_preview` setting is set to `to_the_side`: the preview is created when
|
||||
/// a previewable editor becomes active, switches its kind together with the active
|
||||
/// editor's file type, and is removed when no previewable editors remain open.
|
||||
pub(crate) fn sync_side_preview(
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
if WorkspaceSettings::get_global(cx).auto_preview != AutoPreview::ToTheSide {
|
||||
return;
|
||||
}
|
||||
let providers = providers(cx);
|
||||
if providers.is_empty() {
|
||||
return;
|
||||
}
|
||||
let follow_views = collect_follow_views(workspace, &providers, cx);
|
||||
let active_item = workspace.active_item(cx);
|
||||
let active_item_id = active_item.as_ref().map(|item| item.item_id());
|
||||
|
||||
// The managed preview disappeared without the sync removing it: the user has
|
||||
// closed its tab. Pause reopening until another item gets activated.
|
||||
if let Some(managed_id) = workspace.auto_preview_state.follow_view_id
|
||||
&& !follow_views
|
||||
.iter()
|
||||
.any(|(_, view)| view.item_id() == managed_id)
|
||||
{
|
||||
workspace.auto_preview_state.follow_view_id = None;
|
||||
workspace.auto_preview_state.suppressed_for_item = active_item_id;
|
||||
}
|
||||
if workspace.auto_preview_state.suppressed_for_item.is_some()
|
||||
&& workspace.auto_preview_state.suppressed_for_item != active_item_id
|
||||
{
|
||||
workspace.auto_preview_state.suppressed_for_item = None;
|
||||
}
|
||||
|
||||
let active_provider = active_item.as_ref().and_then(|item| {
|
||||
providers
|
||||
.iter()
|
||||
.position(|provider| (provider.applies_to)(item.as_ref(), cx))
|
||||
});
|
||||
let Some(provider_index) = active_provider else {
|
||||
for (index, view) in follow_views {
|
||||
if !(providers[index].has_open_sources)(workspace, cx)
|
||||
&& let Some(pane) = workspace.pane_for(view.as_ref())
|
||||
{
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.remove_item(view.item_id(), false, true, window, cx);
|
||||
});
|
||||
if workspace.auto_preview_state.follow_view_id == Some(view.item_id()) {
|
||||
workspace.auto_preview_state.follow_view_id = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
if workspace.auto_preview_state.suppressed_for_item.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let existing = follow_views
|
||||
.iter()
|
||||
.find(|(index, _)| *index == provider_index)
|
||||
.map(|(_, view)| view.boxed_clone());
|
||||
|
||||
// Remove follow previews of other types: a single, dynamic preview is kept to the
|
||||
// side, and the new preview takes the tab slot vacated by the previous one.
|
||||
let mut vacated_slot = None;
|
||||
for (index, view) in &follow_views {
|
||||
if *index == provider_index {
|
||||
continue;
|
||||
}
|
||||
let Some(pane) = workspace.pane_for(view.as_ref()) else {
|
||||
continue;
|
||||
};
|
||||
let reuse_slot = existing.is_none() && vacated_slot.is_none();
|
||||
if reuse_slot && let Some(item_index) = pane.read(cx).index_for_item(view.as_ref()) {
|
||||
vacated_slot = Some((pane.clone(), item_index));
|
||||
}
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.remove_item(view.item_id(), false, !reuse_slot, window, cx);
|
||||
});
|
||||
if workspace.auto_preview_state.follow_view_id == Some(view.item_id()) {
|
||||
workspace.auto_preview_state.follow_view_id = None;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(view) = existing {
|
||||
workspace.auto_preview_state.follow_view_id = Some(view.item_id());
|
||||
if let Some(pane) = workspace.pane_for(view.as_ref()) {
|
||||
pane.update(cx, |pane, cx| {
|
||||
if let Some(index) = pane.index_for_item(view.as_ref())
|
||||
&& pane
|
||||
.active_item()
|
||||
.is_none_or(|item| item.item_id() != view.item_id())
|
||||
{
|
||||
pane.activate_item(index, false, false, window, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
let Some(view) = (providers[provider_index].build_follow_view)(workspace, window, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
workspace.auto_preview_state.follow_view_id = Some(view.item_id());
|
||||
let (pane, destination_index) = match vacated_slot {
|
||||
Some((pane, index)) => (pane, Some(index)),
|
||||
None => {
|
||||
let pane = workspace
|
||||
.find_pane_in_direction(SplitDirection::Right, cx)
|
||||
.unwrap_or_else(|| {
|
||||
workspace.split_pane(
|
||||
workspace.active_pane().clone(),
|
||||
SplitDirection::Right,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
(pane, None)
|
||||
}
|
||||
};
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.add_item(view, false, false, destination_index, window, cx);
|
||||
});
|
||||
// Splitting a pane moves the focus into it: return the focus to the source item.
|
||||
if let Some(item) = active_item {
|
||||
item.item_focus_handle(cx).focus(window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a change of the `auto_preview` setting to the already open items:
|
||||
/// previewable editors and their previews are converted into each other in place,
|
||||
/// and the side preview is created or removed.
|
||||
pub(crate) fn auto_preview_setting_changed(
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let providers = providers(cx);
|
||||
if providers.is_empty() {
|
||||
return;
|
||||
}
|
||||
workspace.auto_preview_state = AutoPreviewState::default();
|
||||
match WorkspaceSettings::get_global(cx).auto_preview {
|
||||
AutoPreview::Off => {
|
||||
remove_follow_views(workspace, &providers, window, cx);
|
||||
convert_previews_to_sources(workspace, &providers, window, cx);
|
||||
}
|
||||
AutoPreview::InPlace => {
|
||||
remove_follow_views(workspace, &providers, window, cx);
|
||||
convert_items(workspace, window, cx, |workspace, item, window, cx| {
|
||||
let provider = providers
|
||||
.iter()
|
||||
.find(|provider| (provider.applies_to)(item, cx))?;
|
||||
(provider.build_preview_view)(workspace, item, window, cx)
|
||||
});
|
||||
}
|
||||
AutoPreview::ToTheSide => {
|
||||
convert_previews_to_sources(workspace, &providers, window, cx);
|
||||
sync_side_preview(workspace, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_follow_views(
|
||||
workspace: &Workspace,
|
||||
providers: &[AutoPreviewProvider],
|
||||
cx: &App,
|
||||
) -> Vec<(usize, Box<dyn ItemHandle>)> {
|
||||
let mut views = Vec::new();
|
||||
for pane in workspace.panes() {
|
||||
for item in pane.read(cx).items() {
|
||||
if let Some(index) = providers
|
||||
.iter()
|
||||
.position(|provider| (provider.is_follow_view)(item.as_ref(), cx))
|
||||
{
|
||||
views.push((index, item.boxed_clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
views
|
||||
}
|
||||
|
||||
fn remove_follow_views(
|
||||
workspace: &mut Workspace,
|
||||
providers: &[AutoPreviewProvider],
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
for (_, view) in collect_follow_views(workspace, providers, cx) {
|
||||
if let Some(pane) = workspace.pane_for(view.as_ref()) {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.remove_item(view.item_id(), false, true, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_previews_to_sources(
|
||||
workspace: &mut Workspace,
|
||||
providers: &[AutoPreviewProvider],
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
convert_items(workspace, window, cx, |workspace, item, window, cx| {
|
||||
let provider = providers
|
||||
.iter()
|
||||
.find(|provider| (provider.is_preview_view)(item, cx))?;
|
||||
(provider.source_view)(workspace, item, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
/// Replaces items in their tab slots with the converted counterparts, keeping the
|
||||
/// panes' active tabs, ephemeral (preview tab) statuses and the focus in place.
|
||||
fn convert_items(
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
mut convert: impl FnMut(
|
||||
&mut Workspace,
|
||||
&dyn ItemHandle,
|
||||
&mut Window,
|
||||
&mut Context<Workspace>,
|
||||
) -> Option<Box<dyn ItemHandle>>,
|
||||
) {
|
||||
let panes = workspace.panes().to_vec();
|
||||
for pane in panes {
|
||||
let pane_had_focus = pane.read(cx).focus_handle(cx).contains_focused(window, cx);
|
||||
let original_active_id = pane.read(cx).active_item().map(|item| item.item_id());
|
||||
let items = pane
|
||||
.read(cx)
|
||||
.items()
|
||||
.map(|item| item.boxed_clone())
|
||||
.collect::<Vec<_>>();
|
||||
let mut converted = false;
|
||||
let mut active_replacement = None;
|
||||
for item in items {
|
||||
let Some(new_item) = convert(workspace, item.as_ref(), window, cx) else {
|
||||
continue;
|
||||
};
|
||||
// The replacement may already be open elsewhere (e.g. an editor opened via
|
||||
// `preview::OpenSource`): drop the converted item and let that tab stand.
|
||||
let already_open = workspace.pane_for(new_item.as_ref()).is_some();
|
||||
let was_active = original_active_id == Some(item.item_id());
|
||||
pane.update(cx, |pane, cx| {
|
||||
let Some(index) = pane.index_for_item(item.as_ref()) else {
|
||||
return;
|
||||
};
|
||||
let was_ephemeral = pane.is_active_preview_item(item.item_id());
|
||||
if !already_open {
|
||||
pane.add_item(
|
||||
new_item.boxed_clone(),
|
||||
false,
|
||||
false,
|
||||
Some(index),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
pane.remove_item(item.item_id(), false, already_open, window, cx);
|
||||
if was_ephemeral && !already_open {
|
||||
pane.set_preview_item_id(Some(new_item.item_id()), cx);
|
||||
}
|
||||
converted = true;
|
||||
});
|
||||
if was_active && !already_open {
|
||||
active_replacement = Some(new_item);
|
||||
}
|
||||
}
|
||||
if !converted || !workspace.panes().contains(&pane) {
|
||||
continue;
|
||||
}
|
||||
// Adding items disturbs the pane's active tab: restore it (or its replacement).
|
||||
let target = active_replacement.or_else(|| {
|
||||
let pane = pane.read(cx);
|
||||
original_active_id.and_then(|id| {
|
||||
pane.items()
|
||||
.find(|item| item.item_id() == id)
|
||||
.map(|item| item.boxed_clone())
|
||||
})
|
||||
});
|
||||
if let Some(target) = target {
|
||||
pane.update(cx, |pane, cx| {
|
||||
if let Some(index) = pane.index_for_item(target.as_ref()) {
|
||||
pane.activate_item(index, pane_had_focus, pane_had_focus, window, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -885,6 +885,10 @@ impl Pane {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn workspace(&self) -> &WeakEntity<Workspace> {
|
||||
&self.workspace
|
||||
}
|
||||
|
||||
pub fn nav_history_for_item<T: Item>(&self, item: &Entity<T>) -> ItemNavHistory {
|
||||
ItemNavHistory {
|
||||
history: self.nav_history.clone(),
|
||||
|
|
@ -1279,10 +1283,15 @@ impl Pane {
|
|||
None
|
||||
};
|
||||
|
||||
let item_type = item.to_any_view().entity_type();
|
||||
let existing_item_index = self.items.iter().position(|existing_item| {
|
||||
if existing_item.item_id() == item.item_id() {
|
||||
true
|
||||
} else if existing_item.buffer_kind(cx) == ItemBufferKind::Singleton {
|
||||
} else if existing_item.buffer_kind(cx) == ItemBufferKind::Singleton
|
||||
// Different view types may represent the same project entry (e.g. an editor
|
||||
// and a preview of the same file) and should get separate tabs.
|
||||
&& existing_item.to_any_view().entity_type() == item_type
|
||||
{
|
||||
existing_item
|
||||
.project_entry_ids(cx)
|
||||
.first()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
pub mod active_file_name;
|
||||
mod auto_preview;
|
||||
pub mod dock;
|
||||
pub mod history_manager;
|
||||
pub mod invalid_item_view;
|
||||
|
|
@ -115,6 +116,7 @@ use settings::{
|
|||
update_settings_file,
|
||||
};
|
||||
|
||||
pub use auto_preview::{AutoPreviewProvider, register_auto_preview_provider};
|
||||
use sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
statement::Statement,
|
||||
|
|
@ -155,7 +157,7 @@ use util::{
|
|||
};
|
||||
use uuid::Uuid;
|
||||
pub use workspace_settings::{
|
||||
AutosaveSetting, BottomDockLayout, EncodingDisplayOptions, FocusFollowsMouse,
|
||||
AutoPreview, AutosaveSetting, BottomDockLayout, EncodingDisplayOptions, FocusFollowsMouse,
|
||||
RestoreOnStartupBehavior, StatusBarSettings, TabBarSettings, WorkspaceSettings,
|
||||
};
|
||||
use zed_actions::{Spawn, feedback::FileBugReport, theme::ToggleMode};
|
||||
|
|
@ -1423,6 +1425,7 @@ pub struct Workspace {
|
|||
active_workspace_id: Option<Rc<Cell<EntityId>>>,
|
||||
active_worktree_creation: ActiveWorktreeCreation,
|
||||
deferred_save_items: Vec<Box<dyn WeakItemHandle>>,
|
||||
auto_preview_state: auto_preview::AutoPreviewState,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for Workspace {}
|
||||
|
|
@ -1762,6 +1765,22 @@ impl Workspace {
|
|||
Self::serialize_items(&this, serializable_items_rx, cx).await
|
||||
});
|
||||
|
||||
cx.subscribe_in(&cx.entity(), window, |this, _, event, window, cx| {
|
||||
if matches!(event, Event::ActiveItemChanged | Event::ItemRemoved { .. }) {
|
||||
auto_preview::sync_side_preview(this, window, cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut auto_preview_setting = WorkspaceSettings::get_global(cx).auto_preview;
|
||||
cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
|
||||
let new_setting = WorkspaceSettings::get_global(cx).auto_preview;
|
||||
if std::mem::replace(&mut auto_preview_setting, new_setting) != new_setting {
|
||||
auto_preview::auto_preview_setting_changed(this, window, cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let subscriptions = vec![
|
||||
cx.observe_window_activation(window, Self::on_window_activation_changed),
|
||||
cx.observe_window_bounds(window, move |this, window, cx| {
|
||||
|
|
@ -1870,6 +1889,7 @@ impl Workspace {
|
|||
open_in_dev_container: false,
|
||||
_dev_container_task: None,
|
||||
deferred_save_items: Vec::new(),
|
||||
auto_preview_state: auto_preview::AutoPreviewState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use collections::HashMap;
|
|||
use serde::Deserialize;
|
||||
use settings::CommandAliasTarget;
|
||||
pub use settings::{
|
||||
AutosaveSetting, BottomDockLayout, EncodingDisplayOptions, InactiveOpacity,
|
||||
AutoPreview, AutosaveSetting, BottomDockLayout, EncodingDisplayOptions, InactiveOpacity,
|
||||
PaneSplitDirectionHorizontal, PaneSplitDirectionVertical, RegisterSetting,
|
||||
RestoreOnStartupBehavior, Settings,
|
||||
};
|
||||
|
|
@ -39,6 +39,7 @@ pub struct WorkspaceSettings {
|
|||
pub zoomed_padding: bool,
|
||||
pub window_decorations: settings::WindowDecorations,
|
||||
pub focus_follows_mouse: FocusFollowsMouse,
|
||||
pub auto_preview: AutoPreview,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Deserialize)]
|
||||
|
|
@ -139,6 +140,7 @@ impl Settings for WorkspaceSettings {
|
|||
.unwrap_or(250),
|
||||
),
|
||||
},
|
||||
auto_preview: workspace.auto_preview.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,14 +88,15 @@ impl QuickActionBar {
|
|||
let new_show = EditorSettings::get_global(cx).toolbar.quick_actions;
|
||||
if new_show != self.show {
|
||||
self.show = new_show;
|
||||
cx.emit(ToolbarItemEvent::ChangeLocation(
|
||||
self.get_toolbar_item_location(),
|
||||
));
|
||||
let new_location = self.get_toolbar_item_location(cx);
|
||||
cx.emit(ToolbarItemEvent::ChangeLocation(new_location));
|
||||
}
|
||||
}
|
||||
|
||||
fn get_toolbar_item_location(&self) -> ToolbarItemLocation {
|
||||
if self.show && self.active_editor().is_some() {
|
||||
fn get_toolbar_item_location(&self, cx: &mut Context<Self>) -> ToolbarItemLocation {
|
||||
if self.show
|
||||
&& (self.active_editor().is_some() || self.render_open_source_button(cx).is_some())
|
||||
{
|
||||
ToolbarItemLocation::PrimaryRight
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
|
|
@ -106,6 +107,12 @@ impl QuickActionBar {
|
|||
impl Render for QuickActionBar {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let Some(editor) = self.active_editor() else {
|
||||
if let Some(open_source_button) = self.render_open_source_button(cx) {
|
||||
return h_flex()
|
||||
.id("quick action bar")
|
||||
.gap(DynamicSpacing::Base01.rems(cx))
|
||||
.child(open_source_button);
|
||||
}
|
||||
return div().id("empty quick action bar");
|
||||
};
|
||||
|
||||
|
|
@ -788,6 +795,6 @@ impl ToolbarItemView for QuickActionBar {
|
|||
}));
|
||||
}
|
||||
}
|
||||
self.get_toolbar_item_location()
|
||||
self.get_toolbar_item_location(cx)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
use csv_preview::{
|
||||
CsvPreviewView, OpenPreview as CsvOpenPreview, OpenPreviewToTheSide as CsvOpenPreviewToTheSide,
|
||||
TabularDataPreviewFeatureFlag,
|
||||
CsvPreviewView, OpenPreviewToTheSide as CsvOpenPreviewToTheSide, TabularDataPreviewFeatureFlag,
|
||||
};
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use gpui::{AnyElement, Modifiers, WeakEntity};
|
||||
use gpui::{Action as _, AnyElement, Modifiers, WeakEntity};
|
||||
use markdown_preview::{
|
||||
OpenPreview as MarkdownOpenPreview, OpenPreviewToTheSide as MarkdownOpenPreviewToTheSide,
|
||||
OpenPreviewToTheSide as MarkdownOpenPreviewToTheSide,
|
||||
markdown_preview_view::MarkdownPreviewView,
|
||||
};
|
||||
use svg_preview::{
|
||||
OpenPreview as SvgOpenPreview, OpenPreviewToTheSide as SvgOpenPreviewToTheSide,
|
||||
svg_preview_view::SvgPreviewView,
|
||||
OpenPreviewToTheSide as SvgOpenPreviewToTheSide, svg_preview_view::SvgPreviewView,
|
||||
};
|
||||
use ui::{Tooltip, prelude::*, text_for_keystroke};
|
||||
use workspace::Workspace;
|
||||
|
|
@ -25,6 +23,31 @@ enum PreviewType {
|
|||
}
|
||||
|
||||
impl QuickActionBar {
|
||||
pub fn render_open_source_button(&self, _cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let item = self.active_item.as_ref()?;
|
||||
let (button_id, tooltip_text) = if item.downcast::<MarkdownPreviewView>().is_some() {
|
||||
("edit-markdown-source", "Edit Markdown")
|
||||
} else if item.downcast::<SvgPreviewView>().is_some() {
|
||||
("edit-svg-source", "Edit SVG")
|
||||
} else if item.downcast::<CsvPreviewView>().is_some() {
|
||||
("edit-csv-source", "Edit CSV")
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let button = IconButton::new(button_id, IconName::Pencil)
|
||||
.icon_size(IconSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |_window, cx| {
|
||||
Tooltip::for_action(tooltip_text, &zed_actions::preview::Toggle, cx)
|
||||
})
|
||||
.on_click(move |_, window, cx| {
|
||||
window.dispatch_action(zed_actions::preview::Toggle.boxed_clone(), cx);
|
||||
});
|
||||
|
||||
Some(button.into_any_element())
|
||||
}
|
||||
|
||||
pub fn render_preview_button(
|
||||
&self,
|
||||
workspace_handle: WeakEntity<Workspace>,
|
||||
|
|
@ -51,30 +74,23 @@ impl QuickActionBar {
|
|||
|
||||
let preview_type = preview_type?;
|
||||
|
||||
let (button_id, tooltip_text, open_action, open_to_side_action, open_action_for_tooltip) =
|
||||
match preview_type {
|
||||
PreviewType::Markdown => (
|
||||
"toggle-markdown-preview",
|
||||
"Preview Markdown",
|
||||
Box::new(MarkdownOpenPreview) as Box<dyn gpui::Action>,
|
||||
Box::new(MarkdownOpenPreviewToTheSide) as Box<dyn gpui::Action>,
|
||||
&markdown_preview::OpenPreview as &dyn gpui::Action,
|
||||
),
|
||||
PreviewType::Svg => (
|
||||
"toggle-svg-preview",
|
||||
"Preview SVG",
|
||||
Box::new(SvgOpenPreview) as Box<dyn gpui::Action>,
|
||||
Box::new(SvgOpenPreviewToTheSide) as Box<dyn gpui::Action>,
|
||||
&svg_preview::OpenPreview as &dyn gpui::Action,
|
||||
),
|
||||
PreviewType::Csv => (
|
||||
"toggle-csv-preview",
|
||||
"Preview CSV",
|
||||
Box::new(CsvOpenPreview) as Box<dyn gpui::Action>,
|
||||
Box::new(CsvOpenPreviewToTheSide) as Box<dyn gpui::Action>,
|
||||
&csv_preview::OpenPreview as &dyn gpui::Action,
|
||||
),
|
||||
};
|
||||
let (button_id, tooltip_text, open_to_side_action) = match preview_type {
|
||||
PreviewType::Markdown => (
|
||||
"toggle-markdown-preview",
|
||||
"Preview Markdown",
|
||||
Box::new(MarkdownOpenPreviewToTheSide) as Box<dyn gpui::Action>,
|
||||
),
|
||||
PreviewType::Svg => (
|
||||
"toggle-svg-preview",
|
||||
"Preview SVG",
|
||||
Box::new(SvgOpenPreviewToTheSide) as Box<dyn gpui::Action>,
|
||||
),
|
||||
PreviewType::Csv => (
|
||||
"toggle-csv-preview",
|
||||
"Preview CSV",
|
||||
Box::new(CsvOpenPreviewToTheSide) as Box<dyn gpui::Action>,
|
||||
),
|
||||
};
|
||||
|
||||
let alt_click = gpui::Keystroke {
|
||||
key: "click".into(),
|
||||
|
|
@ -88,7 +104,7 @@ impl QuickActionBar {
|
|||
.tooltip(move |_window, cx| {
|
||||
Tooltip::with_meta(
|
||||
tooltip_text,
|
||||
Some(open_action_for_tooltip),
|
||||
Some(&zed_actions::preview::Toggle),
|
||||
format!(
|
||||
"{} to open in a split",
|
||||
text_for_keystroke(&alt_click.modifiers, &alt_click.key, cx)
|
||||
|
|
@ -102,7 +118,7 @@ impl QuickActionBar {
|
|||
if window.modifiers().alt {
|
||||
window.dispatch_action(open_to_side_action.boxed_clone(), cx);
|
||||
} else {
|
||||
window.dispatch_action(open_action.boxed_clone(), cx);
|
||||
window.dispatch_action(zed_actions::preview::Toggle.boxed_clone(), cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -856,6 +856,18 @@ pub mod wsl_actions {
|
|||
}
|
||||
|
||||
pub mod preview {
|
||||
use gpui::actions;
|
||||
|
||||
actions!(
|
||||
preview,
|
||||
[
|
||||
/// Opens a text editor for the file shown in the current preview.
|
||||
OpenSource,
|
||||
/// Toggles between a previewable file's text editor and its preview.
|
||||
Toggle,
|
||||
]
|
||||
);
|
||||
|
||||
pub mod markdown {
|
||||
use gpui::actions;
|
||||
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ The following VS Code settings are automatically imported when you use **Import
|
|||
| `workbench.editor.editorActionsLocation` | `tab_bar.show_tab_bar_buttons` |
|
||||
| `workbench.editor.limit.enabled` / `value` | `max_tabs` |
|
||||
| `workbench.editor.restoreViewState` | `restore_on_file_reopen` |
|
||||
| `workbench.editorAssociations` | `auto_preview` |
|
||||
| `workbench.statusBar.visible` | `status_bar.show` |
|
||||
|
||||
**Project Panel (File Explorer)**
|
||||
|
|
@ -173,14 +174,15 @@ Zed doesn’t import extensions or keybindings, but this import gets core editor
|
|||
You can configure most settings in the Settings Editor ({#kb zed::OpenSettings}). For advanced settings, run {#action zed::OpenSettingsFile} from the Command Palette to edit your settings file directly.
|
||||
|
||||
Here’s how common VS Code settings translate:
|
||||
| VS Code | Zed | Notes |
|
||||
| --- | --- | --- |
|
||||
| editor.fontFamily | buffer_font_family | Zed uses Zed Mono by default |
|
||||
| editor.fontSize | buffer_font_size | Set in pixels |
|
||||
| editor.tabSize | tab_size | Can override per language |
|
||||
| editor.insertSpaces | insert_spaces | Boolean |
|
||||
| editor.formatOnSave | format_on_save | Works with formatter enabled |
|
||||
| editor.wordWrap | soft_wrap | Supports optional wrap column |
|
||||
|
||||
| VS Code | Zed | Notes |
|
||||
| ------------------- | ------------------ | ----------------------------- |
|
||||
| editor.fontFamily | buffer_font_family | Zed uses Zed Mono by default |
|
||||
| editor.fontSize | buffer_font_size | Set in pixels |
|
||||
| editor.tabSize | tab_size | Can override per language |
|
||||
| editor.insertSpaces | insert_spaces | Boolean |
|
||||
| editor.formatOnSave | format_on_save | Works with formatter enabled |
|
||||
| editor.wordWrap | soft_wrap | Supports optional wrap column |
|
||||
|
||||
Zed also supports per-project settings. You can find these in the Settings Editor as well.
|
||||
|
||||
|
|
|
|||
|
|
@ -214,6 +214,40 @@ Add an extension here with `false` to pin it to its currently installed version.
|
|||
Selecting **Install Another Version…** from an extension's `⋯` menu on the Extensions
|
||||
page ({#action zed::Extensions}) does this automatically.
|
||||
|
||||
## Auto Preview
|
||||
|
||||
- Description: How to open files that have a preview available for their file type (e.g. Markdown or SVG files).
|
||||
- Setting: `auto_preview`
|
||||
- Default: `off`
|
||||
|
||||
**Options**
|
||||
|
||||
1. Open previewable files in a text editor, previews have to be opened manually:
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
"auto_preview": "off"
|
||||
}
|
||||
```
|
||||
|
||||
2. Open previewable files in their preview instead of a text editor. The text editor can be opened from the preview with the {#action preview::OpenSource} action:
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
"auto_preview": "in_place"
|
||||
}
|
||||
```
|
||||
|
||||
3. Open previewable files in a text editor and keep a preview following the active editor in a pane to the side:
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
"auto_preview": "to_the_side"
|
||||
}
|
||||
```
|
||||
|
||||
The {#action preview::Toggle} action switches between a previewable file's text editor and its preview.
|
||||
|
||||
## Autosave
|
||||
|
||||
- Description: When to automatically save edited buffers.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue