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
ce6a5f34d5
22 changed files with 661 additions and 123 deletions
10
Cargo.lock
generated
10
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,17 @@ dependencies = [
|
|||
name = "svg_preview"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"editor",
|
||||
"file_icons",
|
||||
"gpui",
|
||||
"language",
|
||||
"multi_buffer",
|
||||
"project",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1320,6 +1320,7 @@
|
|||
{
|
||||
"context": "MarkdownPreview",
|
||||
"bindings": {
|
||||
"ctrl-shift-v": "preview::OpenSource",
|
||||
"pageup": "markdown::ScrollPageUp",
|
||||
"pagedown": "markdown::ScrollPageDown",
|
||||
"up": "markdown::ScrollUp",
|
||||
|
|
@ -1332,6 +1333,12 @@
|
|||
"ctrl-f": "buffer_search::Deploy",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "SvgPreview",
|
||||
"bindings": {
|
||||
"ctrl-shift-v": "preview::OpenSource",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
|
|
|
|||
|
|
@ -1413,6 +1413,7 @@
|
|||
{
|
||||
"context": "MarkdownPreview",
|
||||
"bindings": {
|
||||
"cmd-shift-v": "preview::OpenSource",
|
||||
"pageup": "markdown::ScrollPageUp",
|
||||
"pagedown": "markdown::ScrollPageDown",
|
||||
"up": "markdown::ScrollUp",
|
||||
|
|
@ -1424,6 +1425,12 @@
|
|||
"cmd-f": "buffer_search::Deploy",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "SvgPreview",
|
||||
"bindings": {
|
||||
"cmd-shift-v": "preview::OpenSource",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
|
|
|
|||
|
|
@ -1344,6 +1344,7 @@
|
|||
"context": "MarkdownPreview",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-v": "preview::OpenSource",
|
||||
"pageup": "markdown::ScrollPageUp",
|
||||
"pagedown": "markdown::ScrollPageDown",
|
||||
"up": "markdown::ScrollUp",
|
||||
|
|
@ -1356,6 +1357,13 @@
|
|||
"ctrl-f": "buffer_search::Deploy",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "SvgPreview",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-v": "preview::OpenSource",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
|
|
|
|||
|
|
@ -182,6 +182,10 @@
|
|||
//
|
||||
// Default: true
|
||||
"restore_on_file_reopen": true,
|
||||
// Whether to open files in their preview instead of a text editor, when a
|
||||
// preview is available for the file type (e.g. Markdown or SVG files).
|
||||
// The text editor can be opened from the preview with the `preview::OpenSource` action.
|
||||
"auto_preview": false,
|
||||
// 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::{Item, Pane, SplitDirection, Workspace, WorkspaceSettings};
|
||||
use zed_actions::preview::OpenSource;
|
||||
|
||||
use crate::{parser::EditorState, settings::CsvPreviewSettings, types::TableLikeContent};
|
||||
|
||||
|
|
@ -54,6 +59,7 @@ pub struct CsvPreviewView {
|
|||
}
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
workspace::register_project_item::<CsvPreviewView>(cx);
|
||||
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
||||
CsvPreviewView::register(workspace);
|
||||
})
|
||||
|
|
@ -108,8 +114,8 @@ impl CsvPreviewView {
|
|||
cx.notify();
|
||||
}
|
||||
}))
|
||||
.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,6 +152,30 @@ impl CsvPreviewView {
|
|||
});
|
||||
cx.notify();
|
||||
}
|
||||
}),
|
||||
)
|
||||
.on_action(cx.listener(
|
||||
|workspace, _: &OpenSource, window, cx| {
|
||||
let Some(preview) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<CsvPreviewView>())
|
||||
else {
|
||||
cx.propagate();
|
||||
return;
|
||||
};
|
||||
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,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
))
|
||||
})
|
||||
|
|
@ -153,6 +183,10 @@ impl CsvPreviewView {
|
|||
}
|
||||
|
||||
fn new(editor: &Entity<Editor>, cx: &mut Context<Workspace>) -> Entity<Self> {
|
||||
cx.new(|cx| Self::build(editor.clone(), cx))
|
||||
}
|
||||
|
||||
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,9 +194,8 @@ impl CsvPreviewView {
|
|||
>())
|
||||
});
|
||||
|
||||
cx.new(|cx| {
|
||||
let subscription = cx.subscribe(
|
||||
editor,
|
||||
&editor,
|
||||
|this: &mut CsvPreviewView, _editor, event: &EditorEvent, cx| {
|
||||
match event {
|
||||
EditorEvent::Edited { .. } | EditorEvent::DirtyChanged => {
|
||||
|
|
@ -176,7 +209,7 @@ impl CsvPreviewView {
|
|||
let mut view = CsvPreviewView {
|
||||
focus_handle: cx.focus_handle(),
|
||||
active_editor_state: EditorState {
|
||||
editor: editor.clone(),
|
||||
editor,
|
||||
_subscription: subscription,
|
||||
},
|
||||
table_interaction_state,
|
||||
|
|
@ -194,7 +227,6 @@ impl CsvPreviewView {
|
|||
|
||||
view.parse_csv_from_active_editor(false, cx);
|
||||
view
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn editor_state(&self) -> &EditorState {
|
||||
|
|
@ -260,6 +292,21 @@ impl CsvPreviewView {
|
|||
Self::is_csv_file(&editor, cx).then_some(editor)
|
||||
}
|
||||
|
||||
fn is_csv_path(path: &ProjectPath) -> bool {
|
||||
path.path
|
||||
.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 +356,81 @@ 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 enabled, 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
|
||||
|| !CsvPreviewView::is_csv_path(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)]
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ actions!(
|
|||
|
||||
pub fn init(cx: &mut App) {
|
||||
workspace::register_serializable_item::<MarkdownPreviewView>(cx);
|
||||
workspace::register_project_item::<MarkdownPreviewView>(cx);
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, window, cx| {
|
||||
let Some(window) = window else {
|
||||
|
|
|
|||
|
|
@ -14,25 +14,30 @@ use gpui::{
|
|||
InteractiveElement, IntoElement, IsZero, Pixels, Render, Resource, RetainAllImageCache,
|
||||
ScrollHandle, SharedString, SharedUri, Subscription, Task, WeakEntity, Window, point, px,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use markdown::{
|
||||
CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownFont,
|
||||
MarkdownOptions, MarkdownStyle,
|
||||
};
|
||||
use project::search::SearchQuery;
|
||||
use project::{Project, ProjectPath};
|
||||
use project::{Project, ProjectEntryId, ProjectPath};
|
||||
use settings::{SeedQuerySetting, Settings, update_settings_file};
|
||||
use theme::{SystemAppearance, Theme, ThemeRegistry};
|
||||
use theme_settings::ThemeSettings;
|
||||
use ui::utils::WithRemSize;
|
||||
use ui::{ContextMenu, WithScrollbar, prelude::*, right_click_menu};
|
||||
use util::markdown::split_local_url_fragment;
|
||||
use workspace::item::{Item, ItemBufferKind, ItemHandle, SaveOptions, SerializableItem};
|
||||
use workspace::item::{
|
||||
Item, ItemBufferKind, ItemHandle, ProjectItem, SaveOptions, SerializableItem,
|
||||
};
|
||||
use workspace::notifications::NotifyResultExt;
|
||||
use workspace::searchable::{
|
||||
Direction, SearchEvent, SearchOptions, SearchToken, SearchableItem, SearchableItemHandle,
|
||||
};
|
||||
use workspace::{ItemId, Pane, Workspace, WorkspaceId, delete_unloaded_items};
|
||||
use workspace::{
|
||||
ItemId, MultiWorkspace, Pane, Workspace, WorkspaceId, WorkspaceSettings, delete_unloaded_items,
|
||||
};
|
||||
use zed_actions::preview::OpenSource;
|
||||
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
|
||||
|
||||
use crate::markdown_preview_settings::MarkdownPreviewSettings;
|
||||
|
|
@ -137,6 +142,29 @@ impl MarkdownPreviewView {
|
|||
}
|
||||
});
|
||||
|
||||
workspace.register_action(move |workspace, _: &OpenSource, window, cx| {
|
||||
let Some(preview) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<MarkdownPreviewView>())
|
||||
else {
|
||||
cx.propagate();
|
||||
return;
|
||||
};
|
||||
let Some(editor) = preview
|
||||
.read(cx)
|
||||
.active_editor
|
||||
.as_ref()
|
||||
.map(|state| state.editor.clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
workspace.register_action(move |workspace, _: &OpenFollowingPreview, window, cx| {
|
||||
if let Some(editor) = Self::resolve_active_item_as_markdown_editor(workspace, cx) {
|
||||
// Check if there's already a following preview
|
||||
|
|
@ -254,6 +282,25 @@ impl MarkdownPreviewView {
|
|||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
Self::build(
|
||||
mode,
|
||||
active_editor,
|
||||
workspace,
|
||||
language_registry,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn build(
|
||||
mode: MarkdownPreviewMode,
|
||||
active_editor: Entity<Editor>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let markdown = cx.new(|cx| {
|
||||
Markdown::new_with_options(
|
||||
SharedString::default(),
|
||||
|
|
@ -320,7 +367,6 @@ impl MarkdownPreviewView {
|
|||
}
|
||||
|
||||
this
|
||||
})
|
||||
}
|
||||
|
||||
fn workspace_updated(
|
||||
|
|
@ -375,6 +421,16 @@ impl MarkdownPreviewView {
|
|||
.detach();
|
||||
}
|
||||
|
||||
fn source_buffer(&self, cx: &App) -> Option<Entity<Buffer>> {
|
||||
self.active_editor
|
||||
.as_ref()?
|
||||
.editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
}
|
||||
|
||||
pub fn is_markdown_file<V>(editor: &Entity<Editor>, cx: &mut Context<V>) -> bool {
|
||||
let buffer = editor.read(cx).buffer().read(cx);
|
||||
if let Some(buffer) = buffer.as_singleton()
|
||||
|
|
@ -1125,6 +1181,7 @@ impl Item for MarkdownPreviewView {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.workspace = workspace.weak_handle();
|
||||
if self.mode != MarkdownPreviewMode::Default {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1208,6 +1265,24 @@ impl Item for MarkdownPreviewView {
|
|||
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 self.mode != MarkdownPreviewMode::Default {
|
||||
return;
|
||||
}
|
||||
if let Some(buffer) = self.source_buffer(cx) {
|
||||
f(buffer.entity_id(), buffer.read(cx))
|
||||
}
|
||||
}
|
||||
|
||||
fn as_searchable(
|
||||
&self,
|
||||
handle: &Entity<Self>,
|
||||
|
|
@ -1217,6 +1292,82 @@ impl Item for MarkdownPreviewView {
|
|||
}
|
||||
}
|
||||
|
||||
/// A [`project::ProjectItem`] that claims markdown files when the `auto_preview` setting
|
||||
/// is enabled, so that opening such files shows their rendered preview instead of an editor.
|
||||
pub struct MarkdownPreviewItem {
|
||||
buffer: Entity<Buffer>,
|
||||
}
|
||||
|
||||
impl project::ProjectItem for MarkdownPreviewItem {
|
||||
fn try_open(
|
||||
project: &Entity<Project>,
|
||||
path: &ProjectPath,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Result<Entity<Self>>>> {
|
||||
if !WorkspaceSettings::get_global(cx).auto_preview
|
||||
|| !MarkdownPreviewView::is_markdown_path(path.path.as_std_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(|_| MarkdownPreviewItem { 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 MarkdownPreviewView {
|
||||
type Item = MarkdownPreviewItem;
|
||||
|
||||
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 language_registry = project.read(cx).languages().clone();
|
||||
let editor = cx.new(|cx| Editor::for_buffer(buffer, Some(project), window, cx));
|
||||
let workspace = pane
|
||||
.map(|pane| pane.workspace().clone())
|
||||
.or_else(|| {
|
||||
Some(
|
||||
window
|
||||
.root::<MultiWorkspace>()
|
||||
.flatten()?
|
||||
.read(cx)
|
||||
.workspace()
|
||||
.downgrade(),
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(WeakEntity::new_invalid);
|
||||
Self::build(
|
||||
MarkdownPreviewMode::Default,
|
||||
editor,
|
||||
workspace,
|
||||
language_registry,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for MarkdownPreviewView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let preview_theme = self.resolve_preview_theme(cx);
|
||||
|
|
|
|||
|
|
@ -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(true)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -134,6 +134,12 @@ pub struct WorkspaceSettingsContent {
|
|||
/// Whether the focused panel follows the mouse location
|
||||
/// Default: false
|
||||
pub focus_follows_mouse: Option<FocusFollowsMouse>,
|
||||
/// Whether to open files in their preview instead of a text editor, when a
|
||||
/// preview is available for the file type (e.g. Markdown or SVG files).
|
||||
/// The text editor can be opened from the preview with the `preview::OpenSource` action.
|
||||
///
|
||||
/// Default: false
|
||||
pub auto_preview: Option<bool>,
|
||||
}
|
||||
|
||||
#[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: "Open files in their preview instead of a text editor, when a preview is available for the 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,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,10 +12,22 @@ 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"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
serde_json.workspace = true
|
||||
util.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ actions!(
|
|||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
workspace::register_project_item::<svg_preview_view::SvgPreviewView>(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::{Pane, Workspace, WorkspaceSettings};
|
||||
use zed_actions::preview::OpenSource;
|
||||
|
||||
use crate::{OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide};
|
||||
|
||||
|
|
@ -202,6 +207,12 @@ impl SvgPreviewView {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn is_svg_path(path: &ProjectPath) -> bool {
|
||||
path.path
|
||||
.extension()
|
||||
.is_some_and(|extension| extension.eq_ignore_ascii_case("svg"))
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
@ -274,6 +285,93 @@ impl SvgPreviewView {
|
|||
cx.notify();
|
||||
}
|
||||
});
|
||||
|
||||
workspace.register_action(move |workspace, _: &OpenSource, window, cx| {
|
||||
let Some(preview) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<SvgPreviewView>())
|
||||
else {
|
||||
cx.propagate();
|
||||
return;
|
||||
};
|
||||
let Some(buffer) = preview.read(cx).buffer.clone() else {
|
||||
return;
|
||||
};
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`project::ProjectItem`] that claims SVG files when the `auto_preview` setting
|
||||
/// is enabled, 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 || !SvgPreviewView::is_svg_path(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 +435,29 @@ 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._workspace_subscription.is_some() {
|
||||
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)) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ pub struct WorkspaceSettings {
|
|||
pub zoomed_padding: bool,
|
||||
pub window_decorations: settings::WindowDecorations,
|
||||
pub focus_follows_mouse: FocusFollowsMouse,
|
||||
pub auto_preview: bool,
|
||||
}
|
||||
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use csv_preview::{
|
|||
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,
|
||||
markdown_preview_view::MarkdownPreviewView,
|
||||
|
|
@ -25,6 +25,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::OpenSource, cx)
|
||||
})
|
||||
.on_click(move |_, window, cx| {
|
||||
window.dispatch_action(zed_actions::preview::OpenSource.boxed_clone(), cx);
|
||||
});
|
||||
|
||||
Some(button.into_any_element())
|
||||
}
|
||||
|
||||
pub fn render_preview_button(
|
||||
&self,
|
||||
workspace_handle: WeakEntity<Workspace>,
|
||||
|
|
|
|||
|
|
@ -856,6 +856,16 @@ 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,
|
||||
]
|
||||
);
|
||||
|
||||
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,8 +174,9 @@ 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 |
|
||||
|
|
|
|||
|
|
@ -214,6 +214,16 @@ 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: Whether to open files in their preview instead of a text editor, when a preview is available for the file type (e.g. Markdown or SVG files). The text editor can be opened from the preview with the {#action preview::OpenSource} action.
|
||||
- Setting: `auto_preview`
|
||||
- Default: `false`
|
||||
|
||||
**Options**
|
||||
|
||||
`boolean` values
|
||||
|
||||
## Autosave
|
||||
|
||||
- Description: When to automatically save edited buffers.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue