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:
Ron Harel 2025-06-27 12:08:05 +03:00 committed by GitHub
parent 6c46e1129d
commit e6bc1308af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 528 additions and 93 deletions

View file

@ -85,6 +85,7 @@ libc.workspace = true
log.workspace = true
markdown.workspace = true
markdown_preview.workspace = true
svg_preview.workspace = true
menu.workspace = true
migrator.workspace = true
mimalloc = { version = "0.1", optional = true }

View file

@ -582,6 +582,7 @@ pub fn main() {
jj_ui::init(cx);
feedback::init(cx);
markdown_preview::init(cx);
svg_preview::init(cx);
welcome::init(cx);
settings_ui::init(cx);
extensions_ui::init(cx);

View file

@ -4323,6 +4323,7 @@ mod tests {
"search",
"snippets",
"supermaven",
"svg",
"tab_switcher",
"task",
"terminal",

View file

@ -1,5 +1,6 @@
mod markdown_preview;
mod preview;
mod repl_menu;
use agent_settings::AgentSettings;
use editor::actions::{
AddSelectionAbove, AddSelectionBelow, CodeActionSource, DuplicateLineDown, GoToDiagnostic,
@ -571,7 +572,7 @@ impl Render for QuickActionBar {
.id("quick action bar")
.gap(DynamicSpacing::Base01.rems(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)
.when(
AgentSettings::get_global(cx).enabled && AgentSettings::get_global(cx).button,

View file

@ -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())
}
}

View 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())
}
}