Add SVG preview (#32694)
Closes #10454 Implements SVG file preview capability similar to the existing markdown preview. - Adds `svg_preview` crate with preview view and live reloading upon file save. - Integrates SVG preview button in quick action bar. - File preview shortcuts (`ctrl/cmd+k v` and `ctrl/cmd+shift+v`) are extension-aware. Release Notes: - Added SVG file preview, accessible via the quick action bar button or keyboard shortcuts (`ctrl/cmd+k v` and `ctrl/cmd+shift+v`) when editing SVG files.
This commit is contained in:
parent
6c46e1129d
commit
e6bc1308af
16 changed files with 528 additions and 93 deletions
13
Cargo.lock
generated
13
Cargo.lock
generated
|
@ -15526,6 +15526,18 @@ version = "0.4.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb"
|
checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "svg_preview"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"editor",
|
||||||
|
"file_icons",
|
||||||
|
"gpui",
|
||||||
|
"ui",
|
||||||
|
"workspace",
|
||||||
|
"workspace-hack",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "svgtypes"
|
name = "svgtypes"
|
||||||
version = "0.15.3"
|
version = "0.15.3"
|
||||||
|
@ -20021,6 +20033,7 @@ dependencies = [
|
||||||
"snippet_provider",
|
"snippet_provider",
|
||||||
"snippets_ui",
|
"snippets_ui",
|
||||||
"supermaven",
|
"supermaven",
|
||||||
|
"svg_preview",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tab_switcher",
|
"tab_switcher",
|
||||||
"task",
|
"task",
|
||||||
|
|
|
@ -95,6 +95,7 @@ members = [
|
||||||
"crates/markdown_preview",
|
"crates/markdown_preview",
|
||||||
"crates/media",
|
"crates/media",
|
||||||
"crates/menu",
|
"crates/menu",
|
||||||
|
"crates/svg_preview",
|
||||||
"crates/migrator",
|
"crates/migrator",
|
||||||
"crates/mistral",
|
"crates/mistral",
|
||||||
"crates/multi_buffer",
|
"crates/multi_buffer",
|
||||||
|
@ -304,6 +305,7 @@ lmstudio = { path = "crates/lmstudio" }
|
||||||
lsp = { path = "crates/lsp" }
|
lsp = { path = "crates/lsp" }
|
||||||
markdown = { path = "crates/markdown" }
|
markdown = { path = "crates/markdown" }
|
||||||
markdown_preview = { path = "crates/markdown_preview" }
|
markdown_preview = { path = "crates/markdown_preview" }
|
||||||
|
svg_preview = { path = "crates/svg_preview" }
|
||||||
media = { path = "crates/media" }
|
media = { path = "crates/media" }
|
||||||
menu = { path = "crates/menu" }
|
menu = { path = "crates/menu" }
|
||||||
migrator = { path = "crates/migrator" }
|
migrator = { path = "crates/migrator" }
|
||||||
|
|
|
@ -491,13 +491,27 @@
|
||||||
"ctrl-k r": "editor::RevealInFileManager",
|
"ctrl-k r": "editor::RevealInFileManager",
|
||||||
"ctrl-k p": "editor::CopyPath",
|
"ctrl-k p": "editor::CopyPath",
|
||||||
"ctrl-\\": "pane::SplitRight",
|
"ctrl-\\": "pane::SplitRight",
|
||||||
"ctrl-k v": "markdown::OpenPreviewToTheSide",
|
|
||||||
"ctrl-shift-v": "markdown::OpenPreview",
|
|
||||||
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
|
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
|
||||||
"alt-.": "editor::GoToHunk",
|
"alt-.": "editor::GoToHunk",
|
||||||
"alt-,": "editor::GoToPreviousHunk"
|
"alt-,": "editor::GoToPreviousHunk"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && extension == md",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-k v": "markdown::OpenPreviewToTheSide",
|
||||||
|
"ctrl-shift-v": "markdown::OpenPreview"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && extension == svg",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-k v": "svg::OpenPreviewToTheSide",
|
||||||
|
"ctrl-shift-v": "svg::OpenPreview"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full",
|
"context": "Editor && mode == full",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
|
|
@ -545,11 +545,25 @@
|
||||||
"cmd-k r": "editor::RevealInFileManager",
|
"cmd-k r": "editor::RevealInFileManager",
|
||||||
"cmd-k p": "editor::CopyPath",
|
"cmd-k p": "editor::CopyPath",
|
||||||
"cmd-\\": "pane::SplitRight",
|
"cmd-\\": "pane::SplitRight",
|
||||||
"cmd-k v": "markdown::OpenPreviewToTheSide",
|
|
||||||
"cmd-shift-v": "markdown::OpenPreview",
|
|
||||||
"ctrl-cmd-c": "editor::DisplayCursorNames"
|
"ctrl-cmd-c": "editor::DisplayCursorNames"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && extension == md",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"cmd-k v": "markdown::OpenPreviewToTheSide",
|
||||||
|
"cmd-shift-v": "markdown::OpenPreview"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && extension == svg",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"cmd-k v": "svg::OpenPreviewToTheSide",
|
||||||
|
"cmd-shift-v": "svg::OpenPreview"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && mode == full",
|
"context": "Editor && mode == full",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use auto_update::AutoUpdater;
|
use auto_update::AutoUpdater;
|
||||||
use client::proto::UpdateNotification;
|
use client::proto::UpdateNotification;
|
||||||
use editor::{Editor, MultiBuffer};
|
use editor::{Editor, MultiBuffer};
|
||||||
use gpui::{App, Context, DismissEvent, Entity, SharedString, Window, actions, prelude::*};
|
use gpui::{App, Context, DismissEvent, Entity, Window, actions, prelude::*};
|
||||||
use http_client::HttpClient;
|
use http_client::HttpClient;
|
||||||
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
|
||||||
use release_channel::{AppVersion, ReleaseChannel};
|
use release_channel::{AppVersion, ReleaseChannel};
|
||||||
|
@ -94,7 +94,6 @@ fn view_release_notes_locally(
|
||||||
|
|
||||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||||
|
|
||||||
let tab_content = Some(SharedString::from(body.title.to_string()));
|
|
||||||
let editor = cx.new(|cx| {
|
let editor = cx.new(|cx| {
|
||||||
Editor::for_multibuffer(buffer, Some(project), window, cx)
|
Editor::for_multibuffer(buffer, Some(project), window, cx)
|
||||||
});
|
});
|
||||||
|
@ -105,7 +104,6 @@ fn view_release_notes_locally(
|
||||||
editor,
|
editor,
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
language_registry,
|
language_registry,
|
||||||
tab_content,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,10 +17,9 @@ use ui::prelude::*;
|
||||||
use workspace::item::{Item, ItemHandle};
|
use workspace::item::{Item, ItemHandle};
|
||||||
use workspace::{Pane, Workspace};
|
use workspace::{Pane, Workspace};
|
||||||
|
|
||||||
use crate::OpenPreviewToTheSide;
|
|
||||||
use crate::markdown_elements::ParsedMarkdownElement;
|
use crate::markdown_elements::ParsedMarkdownElement;
|
||||||
use crate::{
|
use crate::{
|
||||||
OpenFollowingPreview, OpenPreview,
|
OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide,
|
||||||
markdown_elements::ParsedMarkdown,
|
markdown_elements::ParsedMarkdown,
|
||||||
markdown_parser::parse_markdown,
|
markdown_parser::parse_markdown,
|
||||||
markdown_renderer::{RenderContext, render_markdown_block},
|
markdown_renderer::{RenderContext, render_markdown_block},
|
||||||
|
@ -36,7 +35,6 @@ pub struct MarkdownPreviewView {
|
||||||
contents: Option<ParsedMarkdown>,
|
contents: Option<ParsedMarkdown>,
|
||||||
selected_block: usize,
|
selected_block: usize,
|
||||||
list_state: ListState,
|
list_state: ListState,
|
||||||
tab_content_text: Option<SharedString>,
|
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
parsing_markdown_task: Option<Task<Result<()>>>,
|
parsing_markdown_task: Option<Task<Result<()>>>,
|
||||||
mode: MarkdownPreviewMode,
|
mode: MarkdownPreviewMode,
|
||||||
|
@ -173,7 +171,6 @@ impl MarkdownPreviewView {
|
||||||
editor,
|
editor,
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
language_registry,
|
language_registry,
|
||||||
None,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
@ -192,7 +189,6 @@ impl MarkdownPreviewView {
|
||||||
editor,
|
editor,
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
language_registry,
|
language_registry,
|
||||||
None,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
@ -203,7 +199,6 @@ impl MarkdownPreviewView {
|
||||||
active_editor: Entity<Editor>,
|
active_editor: Entity<Editor>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
tab_content_text: Option<SharedString>,
|
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Workspace>,
|
cx: &mut Context<Workspace>,
|
||||||
) -> Entity<Self> {
|
) -> Entity<Self> {
|
||||||
|
@ -324,7 +319,6 @@ impl MarkdownPreviewView {
|
||||||
workspace: workspace.clone(),
|
workspace: workspace.clone(),
|
||||||
contents: None,
|
contents: None,
|
||||||
list_state,
|
list_state,
|
||||||
tab_content_text,
|
|
||||||
language_registry,
|
language_registry,
|
||||||
parsing_markdown_task: None,
|
parsing_markdown_task: None,
|
||||||
image_cache: RetainAllImageCache::new(cx),
|
image_cache: RetainAllImageCache::new(cx),
|
||||||
|
@ -405,12 +399,6 @@ impl MarkdownPreviewView {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
let tab_content = editor.read(cx).tab_content_text(0, cx);
|
|
||||||
|
|
||||||
if self.tab_content_text.is_none() {
|
|
||||||
self.tab_content_text = Some(format!("Preview {}", tab_content).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.active_editor = Some(EditorState {
|
self.active_editor = Some(EditorState {
|
||||||
editor,
|
editor,
|
||||||
_subscription: subscription,
|
_subscription: subscription,
|
||||||
|
@ -547,21 +535,28 @@ impl Focusable for MarkdownPreviewView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
impl EventEmitter<()> for MarkdownPreviewView {}
|
||||||
pub enum PreviewEvent {}
|
|
||||||
|
|
||||||
impl EventEmitter<PreviewEvent> for MarkdownPreviewView {}
|
|
||||||
|
|
||||||
impl Item for MarkdownPreviewView {
|
impl Item for MarkdownPreviewView {
|
||||||
type Event = PreviewEvent;
|
type Event = ();
|
||||||
|
|
||||||
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
||||||
Some(Icon::new(IconName::FileDoc))
|
Some(Icon::new(IconName::FileDoc))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
|
||||||
self.tab_content_text
|
self.active_editor
|
||||||
.clone()
|
.as_ref()
|
||||||
|
.and_then(|editor_state| {
|
||||||
|
let buffer = editor_state.editor.read(cx).buffer().read(cx);
|
||||||
|
let buffer = buffer.as_singleton()?;
|
||||||
|
let file = buffer.read(cx).file()?;
|
||||||
|
let local_file = file.as_local()?;
|
||||||
|
local_file
|
||||||
|
.abs_path(cx)
|
||||||
|
.file_name()
|
||||||
|
.map(|name| format!("Preview {}", name.to_string_lossy()).into())
|
||||||
|
})
|
||||||
.unwrap_or_else(|| SharedString::from("Markdown Preview"))
|
.unwrap_or_else(|| SharedString::from("Markdown Preview"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
20
crates/svg_preview/Cargo.toml
Normal file
20
crates/svg_preview/Cargo.toml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
[package]
|
||||||
|
name = "svg_preview"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/svg_preview.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
editor.workspace = true
|
||||||
|
file_icons.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
|
ui.workspace = true
|
||||||
|
workspace.workspace = true
|
||||||
|
workspace-hack.workspace = true
|
1
crates/svg_preview/LICENSE-GPL
Symbolic link
1
crates/svg_preview/LICENSE-GPL
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../LICENSE-GPL
|
19
crates/svg_preview/src/svg_preview.rs
Normal file
19
crates/svg_preview/src/svg_preview.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use gpui::{App, actions};
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
pub mod svg_preview_view;
|
||||||
|
|
||||||
|
actions!(
|
||||||
|
svg,
|
||||||
|
[OpenPreview, OpenPreviewToTheSide, OpenFollowingPreview]
|
||||||
|
);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut App) {
|
||||||
|
cx.observe_new(|workspace: &mut Workspace, window, cx| {
|
||||||
|
let Some(window) = window else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
crate::svg_preview_view::SvgPreviewView::register(workspace, window, cx);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
323
crates/svg_preview/src/svg_preview_view.rs
Normal file
323
crates/svg_preview/src/svg_preview_view.rs
Normal file
|
@ -0,0 +1,323 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use editor::{Editor, EditorEvent};
|
||||||
|
use file_icons::FileIcons;
|
||||||
|
use gpui::{
|
||||||
|
App, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageSource, IntoElement,
|
||||||
|
ParentElement, Render, Resource, RetainAllImageCache, Styled, Subscription, WeakEntity, Window,
|
||||||
|
div, img,
|
||||||
|
};
|
||||||
|
use ui::prelude::*;
|
||||||
|
use workspace::item::Item;
|
||||||
|
use workspace::{Pane, Workspace};
|
||||||
|
|
||||||
|
use crate::{OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide};
|
||||||
|
|
||||||
|
pub struct SvgPreviewView {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
svg_path: Option<PathBuf>,
|
||||||
|
image_cache: Entity<RetainAllImageCache>,
|
||||||
|
_editor_subscription: Subscription,
|
||||||
|
_workspace_subscription: Option<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub enum SvgPreviewMode {
|
||||||
|
/// The preview will always show the contents of the provided editor.
|
||||||
|
Default,
|
||||||
|
/// The preview will "follow" the last active editor of an SVG file.
|
||||||
|
Follow,
|
||||||
|
}
|
||||||
|
|
||||||
|
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(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) {
|
||||||
|
if Self::is_svg_file(&editor, cx) {
|
||||||
|
let view = Self::create_svg_view(
|
||||||
|
SvgPreviewMode::Default,
|
||||||
|
workspace,
|
||||||
|
editor.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
workspace.active_pane().update(cx, |pane, cx| {
|
||||||
|
if let Some(existing_view_idx) =
|
||||||
|
Self::find_existing_preview_item_idx(pane, &editor, 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, window, cx| {
|
||||||
|
if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) {
|
||||||
|
if Self::is_svg_file(&editor, cx) {
|
||||||
|
let editor_clone = editor.clone();
|
||||||
|
let view = Self::create_svg_view(
|
||||||
|
SvgPreviewMode::Default,
|
||||||
|
workspace,
|
||||||
|
editor_clone,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
let pane = workspace
|
||||||
|
.find_pane_in_direction(workspace::SplitDirection::Right, cx)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
workspace.split_pane(
|
||||||
|
workspace.active_pane().clone(),
|
||||||
|
workspace::SplitDirection::Right,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
pane.update(cx, |pane, cx| {
|
||||||
|
if let Some(existing_view_idx) =
|
||||||
|
Self::find_existing_preview_item_idx(pane, &editor, cx)
|
||||||
|
{
|
||||||
|
pane.activate_item(existing_view_idx, true, true, window, cx);
|
||||||
|
} else {
|
||||||
|
pane.add_item(Box::new(view), false, false, None, window, cx)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
workspace.register_action(move |workspace, _: &OpenFollowingPreview, window, cx| {
|
||||||
|
if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) {
|
||||||
|
if Self::is_svg_file(&editor, cx) {
|
||||||
|
let view = Self::create_svg_view(
|
||||||
|
SvgPreviewMode::Follow,
|
||||||
|
workspace,
|
||||||
|
editor,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
workspace.active_pane().update(cx, |pane, cx| {
|
||||||
|
pane.add_item(Box::new(view), true, true, None, window, cx)
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_existing_preview_item_idx(
|
||||||
|
pane: &Pane,
|
||||||
|
editor: &Entity<Editor>,
|
||||||
|
cx: &App,
|
||||||
|
) -> Option<usize> {
|
||||||
|
let editor_path = Self::get_svg_path(editor, cx);
|
||||||
|
pane.items_of_type::<SvgPreviewView>()
|
||||||
|
.find(|view| {
|
||||||
|
let view_read = view.read(cx);
|
||||||
|
view_read.svg_path.is_some() && view_read.svg_path == editor_path
|
||||||
|
})
|
||||||
|
.and_then(|view| pane.index_for_item(&view))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolve_active_item_as_svg_editor(
|
||||||
|
workspace: &Workspace,
|
||||||
|
cx: &mut Context<Workspace>,
|
||||||
|
) -> Option<Entity<Editor>> {
|
||||||
|
let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
|
||||||
|
|
||||||
|
if Self::is_svg_file(&editor, cx) {
|
||||||
|
Some(editor)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_svg_view(
|
||||||
|
mode: SvgPreviewMode,
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
editor: Entity<Editor>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Workspace>,
|
||||||
|
) -> Entity<SvgPreviewView> {
|
||||||
|
let workspace_handle = workspace.weak_handle();
|
||||||
|
SvgPreviewView::new(mode, editor, workspace_handle, window, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(
|
||||||
|
mode: SvgPreviewMode,
|
||||||
|
active_editor: Entity<Editor>,
|
||||||
|
workspace_handle: WeakEntity<Workspace>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Workspace>,
|
||||||
|
) -> Entity<Self> {
|
||||||
|
cx.new(|cx| {
|
||||||
|
let svg_path = Self::get_svg_path(&active_editor, cx);
|
||||||
|
let image_cache = RetainAllImageCache::new(cx);
|
||||||
|
|
||||||
|
let subscription = cx.subscribe_in(
|
||||||
|
&active_editor,
|
||||||
|
window,
|
||||||
|
|this: &mut SvgPreviewView, _editor, event: &EditorEvent, window, cx| {
|
||||||
|
match event {
|
||||||
|
EditorEvent::Saved => {
|
||||||
|
// Remove cached image to force reload
|
||||||
|
if let Some(svg_path) = &this.svg_path {
|
||||||
|
let resource = Resource::Path(svg_path.clone().into());
|
||||||
|
this.image_cache.update(cx, |cache, cx| {
|
||||||
|
cache.remove(&resource, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Subscribe to workspace active item changes to follow SVG files
|
||||||
|
let workspace_subscription = if mode == SvgPreviewMode::Follow {
|
||||||
|
workspace_handle.upgrade().map(|workspace_handle| {
|
||||||
|
cx.subscribe_in(
|
||||||
|
&workspace_handle,
|
||||||
|
window,
|
||||||
|
|this: &mut SvgPreviewView,
|
||||||
|
workspace,
|
||||||
|
event: &workspace::Event,
|
||||||
|
_window,
|
||||||
|
cx| {
|
||||||
|
match event {
|
||||||
|
workspace::Event::ActiveItemChanged => {
|
||||||
|
let workspace_read = workspace.read(cx);
|
||||||
|
if let Some(active_item) = workspace_read.active_item(cx) {
|
||||||
|
if let Some(editor_entity) =
|
||||||
|
active_item.downcast::<Editor>()
|
||||||
|
{
|
||||||
|
if Self::is_svg_file(&editor_entity, cx) {
|
||||||
|
let new_path =
|
||||||
|
Self::get_svg_path(&editor_entity, cx);
|
||||||
|
if this.svg_path != new_path {
|
||||||
|
this.svg_path = new_path;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
svg_path,
|
||||||
|
image_cache,
|
||||||
|
_editor_subscription: subscription,
|
||||||
|
_workspace_subscription: workspace_subscription,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_svg_file<C>(editor: &Entity<Editor>, cx: &C) -> bool
|
||||||
|
where
|
||||||
|
C: std::borrow::Borrow<App>,
|
||||||
|
{
|
||||||
|
let app = cx.borrow();
|
||||||
|
let buffer = editor.read(app).buffer().read(app);
|
||||||
|
if let Some(buffer) = buffer.as_singleton() {
|
||||||
|
if let Some(file) = buffer.read(app).file() {
|
||||||
|
return file
|
||||||
|
.path()
|
||||||
|
.extension()
|
||||||
|
.and_then(|ext| ext.to_str())
|
||||||
|
.map(|ext| ext.eq_ignore_ascii_case("svg"))
|
||||||
|
.unwrap_or(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_svg_path<C>(editor: &Entity<Editor>, cx: &C) -> Option<PathBuf>
|
||||||
|
where
|
||||||
|
C: std::borrow::Borrow<App>,
|
||||||
|
{
|
||||||
|
let app = cx.borrow();
|
||||||
|
let buffer = editor.read(app).buffer().read(app).as_singleton()?;
|
||||||
|
let file = buffer.read(app).file()?;
|
||||||
|
let local_file = file.as_local()?;
|
||||||
|
Some(local_file.abs_path(app))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for SvgPreviewView {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.id("SvgPreview")
|
||||||
|
.key_context("SvgPreview")
|
||||||
|
.track_focus(&self.focus_handle(cx))
|
||||||
|
.size_full()
|
||||||
|
.bg(cx.theme().colors().editor_background)
|
||||||
|
.flex()
|
||||||
|
.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.child(if let Some(svg_path) = &self.svg_path {
|
||||||
|
img(ImageSource::from(svg_path.clone()))
|
||||||
|
.image_cache(&self.image_cache)
|
||||||
|
.max_w_full()
|
||||||
|
.max_h_full()
|
||||||
|
.with_fallback(|| {
|
||||||
|
div()
|
||||||
|
.p_4()
|
||||||
|
.child("Failed to load SVG file")
|
||||||
|
.into_any_element()
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
} else {
|
||||||
|
div().p_4().child("No SVG file selected").into_any_element()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Focusable for SvgPreviewView {
|
||||||
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<()> for SvgPreviewView {}
|
||||||
|
|
||||||
|
impl Item for SvgPreviewView {
|
||||||
|
type Event = ();
|
||||||
|
|
||||||
|
fn tab_icon(&self, _window: &Window, cx: &App) -> Option<Icon> {
|
||||||
|
// Use the same icon as SVG files in the file tree
|
||||||
|
self.svg_path
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|svg_path| FileIcons::get_icon(svg_path, cx))
|
||||||
|
.map(Icon::from_path)
|
||||||
|
.or_else(|| Some(Icon::new(IconName::Image)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||||
|
self.svg_path
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|svg_path| svg_path.file_name())
|
||||||
|
.map(|name| name.to_string_lossy())
|
||||||
|
.map(|name| format!("Preview {}", name).into())
|
||||||
|
.unwrap_or_else(|| "SVG Preview".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||||
|
Some("svg preview: open")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_item_events(_event: &Self::Event, _f: impl FnMut(workspace::item::ItemEvent)) {}
|
||||||
|
}
|
|
@ -85,6 +85,7 @@ libc.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
markdown.workspace = true
|
markdown.workspace = true
|
||||||
markdown_preview.workspace = true
|
markdown_preview.workspace = true
|
||||||
|
svg_preview.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
migrator.workspace = true
|
migrator.workspace = true
|
||||||
mimalloc = { version = "0.1", optional = true }
|
mimalloc = { version = "0.1", optional = true }
|
||||||
|
|
|
@ -582,6 +582,7 @@ pub fn main() {
|
||||||
jj_ui::init(cx);
|
jj_ui::init(cx);
|
||||||
feedback::init(cx);
|
feedback::init(cx);
|
||||||
markdown_preview::init(cx);
|
markdown_preview::init(cx);
|
||||||
|
svg_preview::init(cx);
|
||||||
welcome::init(cx);
|
welcome::init(cx);
|
||||||
settings_ui::init(cx);
|
settings_ui::init(cx);
|
||||||
extensions_ui::init(cx);
|
extensions_ui::init(cx);
|
||||||
|
|
|
@ -4323,6 +4323,7 @@ mod tests {
|
||||||
"search",
|
"search",
|
||||||
"snippets",
|
"snippets",
|
||||||
"supermaven",
|
"supermaven",
|
||||||
|
"svg",
|
||||||
"tab_switcher",
|
"tab_switcher",
|
||||||
"task",
|
"task",
|
||||||
"terminal",
|
"terminal",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
mod markdown_preview;
|
mod preview;
|
||||||
mod repl_menu;
|
mod repl_menu;
|
||||||
|
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use editor::actions::{
|
use editor::actions::{
|
||||||
AddSelectionAbove, AddSelectionBelow, CodeActionSource, DuplicateLineDown, GoToDiagnostic,
|
AddSelectionAbove, AddSelectionBelow, CodeActionSource, DuplicateLineDown, GoToDiagnostic,
|
||||||
|
@ -571,7 +572,7 @@ impl Render for QuickActionBar {
|
||||||
.id("quick action bar")
|
.id("quick action bar")
|
||||||
.gap(DynamicSpacing::Base01.rems(cx))
|
.gap(DynamicSpacing::Base01.rems(cx))
|
||||||
.children(self.render_repl_menu(cx))
|
.children(self.render_repl_menu(cx))
|
||||||
.children(self.render_toggle_markdown_preview(self.workspace.clone(), cx))
|
.children(self.render_preview_button(self.workspace.clone(), cx))
|
||||||
.children(search_button)
|
.children(search_button)
|
||||||
.when(
|
.when(
|
||||||
AgentSettings::get_global(cx).enabled && AgentSettings::get_global(cx).button,
|
AgentSettings::get_global(cx).enabled && AgentSettings::get_global(cx).button,
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
use gpui::{AnyElement, Modifiers, WeakEntity};
|
|
||||||
use markdown_preview::{
|
|
||||||
OpenPreview, OpenPreviewToTheSide, markdown_preview_view::MarkdownPreviewView,
|
|
||||||
};
|
|
||||||
use ui::{IconButtonShape, Tooltip, prelude::*, text_for_keystroke};
|
|
||||||
use workspace::Workspace;
|
|
||||||
|
|
||||||
use super::QuickActionBar;
|
|
||||||
|
|
||||||
impl QuickActionBar {
|
|
||||||
pub fn render_toggle_markdown_preview(
|
|
||||||
&self,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Option<AnyElement> {
|
|
||||||
let mut active_editor_is_markdown = false;
|
|
||||||
|
|
||||||
if let Some(workspace) = self.workspace.upgrade() {
|
|
||||||
workspace.update(cx, |workspace, cx| {
|
|
||||||
active_editor_is_markdown =
|
|
||||||
MarkdownPreviewView::resolve_active_item_as_markdown_editor(workspace, cx)
|
|
||||||
.is_some();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if !active_editor_is_markdown {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let alt_click = gpui::Keystroke {
|
|
||||||
key: "click".into(),
|
|
||||||
modifiers: Modifiers::alt(),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let button = IconButton::new("toggle-markdown-preview", IconName::Eye)
|
|
||||||
.shape(IconButtonShape::Square)
|
|
||||||
.icon_size(IconSize::Small)
|
|
||||||
.style(ButtonStyle::Subtle)
|
|
||||||
.tooltip(move |window, cx| {
|
|
||||||
Tooltip::with_meta(
|
|
||||||
"Preview Markdown",
|
|
||||||
Some(&markdown_preview::OpenPreview),
|
|
||||||
format!("{} to open in a split", text_for_keystroke(&alt_click, cx)),
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.on_click(move |_, window, cx| {
|
|
||||||
if let Some(workspace) = workspace.upgrade() {
|
|
||||||
workspace.update(cx, |_, cx| {
|
|
||||||
if window.modifiers().alt {
|
|
||||||
window.dispatch_action(Box::new(OpenPreviewToTheSide), cx);
|
|
||||||
} else {
|
|
||||||
window.dispatch_action(Box::new(OpenPreview), cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Some(button.into_any_element())
|
|
||||||
}
|
|
||||||
}
|
|
95
crates/zed/src/zed/quick_action_bar/preview.rs
Normal file
95
crates/zed/src/zed/quick_action_bar/preview.rs
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
use gpui::{AnyElement, Modifiers, WeakEntity};
|
||||||
|
use markdown_preview::{
|
||||||
|
OpenPreview as MarkdownOpenPreview, OpenPreviewToTheSide as MarkdownOpenPreviewToTheSide,
|
||||||
|
markdown_preview_view::MarkdownPreviewView,
|
||||||
|
};
|
||||||
|
use svg_preview::{
|
||||||
|
OpenPreview as SvgOpenPreview, OpenPreviewToTheSide as SvgOpenPreviewToTheSide,
|
||||||
|
svg_preview_view::SvgPreviewView,
|
||||||
|
};
|
||||||
|
use ui::{IconButtonShape, Tooltip, prelude::*, text_for_keystroke};
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use super::QuickActionBar;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum PreviewType {
|
||||||
|
Markdown,
|
||||||
|
Svg,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuickActionBar {
|
||||||
|
pub fn render_preview_button(
|
||||||
|
&self,
|
||||||
|
workspace_handle: WeakEntity<Workspace>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Option<AnyElement> {
|
||||||
|
let mut preview_type = None;
|
||||||
|
|
||||||
|
if let Some(workspace) = self.workspace.upgrade() {
|
||||||
|
workspace.update(cx, |workspace, cx| {
|
||||||
|
if MarkdownPreviewView::resolve_active_item_as_markdown_editor(workspace, cx)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
preview_type = Some(PreviewType::Markdown);
|
||||||
|
} else if SvgPreviewView::resolve_active_item_as_svg_editor(workspace, cx).is_some()
|
||||||
|
{
|
||||||
|
preview_type = Some(PreviewType::Svg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let alt_click = gpui::Keystroke {
|
||||||
|
key: "click".into(),
|
||||||
|
modifiers: Modifiers::alt(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let button = IconButton::new(button_id, IconName::Eye)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.style(ButtonStyle::Subtle)
|
||||||
|
.tooltip(move |window, cx| {
|
||||||
|
Tooltip::with_meta(
|
||||||
|
tooltip_text,
|
||||||
|
Some(open_action_for_tooltip),
|
||||||
|
format!("{} to open in a split", text_for_keystroke(&alt_click, cx)),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.on_click(move |_, window, cx| {
|
||||||
|
if let Some(workspace) = workspace_handle.upgrade() {
|
||||||
|
workspace.update(cx, |_, cx| {
|
||||||
|
if window.modifiers().alt {
|
||||||
|
window.dispatch_action(open_to_side_action.boxed_clone(), cx);
|
||||||
|
} else {
|
||||||
|
window.dispatch_action(open_action.boxed_clone(), cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(button.into_any_element())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue