Add SVG preview (#32694)

Ron Harel created

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.

Change summary

Cargo.lock                                              |  13 
Cargo.toml                                              |   2 
assets/keymaps/default-linux.json                       |  18 
assets/keymaps/default-macos.json                       |  18 
crates/auto_update_ui/src/auto_update_ui.rs             |   4 
crates/markdown_preview/src/markdown_preview_view.rs    |  37 
crates/svg_preview/Cargo.toml                           |  20 
crates/svg_preview/LICENSE-GPL                          |   1 
crates/svg_preview/src/svg_preview.rs                   |  19 
crates/svg_preview/src/svg_preview_view.rs              | 323 +++++++++++
crates/zed/Cargo.toml                                   |   1 
crates/zed/src/main.rs                                  |   1 
crates/zed/src/zed.rs                                   |   1 
crates/zed/src/zed/quick_action_bar.rs                  |   5 
crates/zed/src/zed/quick_action_bar/markdown_preview.rs |  63 --
crates/zed/src/zed/quick_action_bar/preview.rs          |  95 +++
16 files changed, 528 insertions(+), 93 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -15526,6 +15526,18 @@ version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb"
 
+[[package]]
+name = "svg_preview"
+version = "0.1.0"
+dependencies = [
+ "editor",
+ "file_icons",
+ "gpui",
+ "ui",
+ "workspace",
+ "workspace-hack",
+]
+
 [[package]]
 name = "svgtypes"
 version = "0.15.3"
@@ -20021,6 +20033,7 @@ dependencies = [
  "snippet_provider",
  "snippets_ui",
  "supermaven",
+ "svg_preview",
  "sysinfo",
  "tab_switcher",
  "task",

Cargo.toml 🔗

@@ -95,6 +95,7 @@ members = [
     "crates/markdown_preview",
     "crates/media",
     "crates/menu",
+    "crates/svg_preview",
     "crates/migrator",
     "crates/mistral",
     "crates/multi_buffer",
@@ -304,6 +305,7 @@ lmstudio = { path = "crates/lmstudio" }
 lsp = { path = "crates/lsp" }
 markdown = { path = "crates/markdown" }
 markdown_preview = { path = "crates/markdown_preview" }
+svg_preview = { path = "crates/svg_preview" }
 media = { path = "crates/media" }
 menu = { path = "crates/menu" }
 migrator = { path = "crates/migrator" }

assets/keymaps/default-linux.json 🔗

@@ -491,13 +491,27 @@
       "ctrl-k r": "editor::RevealInFileManager",
       "ctrl-k p": "editor::CopyPath",
       "ctrl-\\": "pane::SplitRight",
-      "ctrl-k v": "markdown::OpenPreviewToTheSide",
-      "ctrl-shift-v": "markdown::OpenPreview",
       "ctrl-alt-shift-c": "editor::DisplayCursorNames",
       "alt-.": "editor::GoToHunk",
       "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",
     "bindings": {

assets/keymaps/default-macos.json 🔗

@@ -545,11 +545,25 @@
       "cmd-k r": "editor::RevealInFileManager",
       "cmd-k p": "editor::CopyPath",
       "cmd-\\": "pane::SplitRight",
-      "cmd-k v": "markdown::OpenPreviewToTheSide",
-      "cmd-shift-v": "markdown::OpenPreview",
       "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",
     "use_key_equivalents": true,

crates/auto_update_ui/src/auto_update_ui.rs 🔗

@@ -1,7 +1,7 @@
 use auto_update::AutoUpdater;
 use client::proto::UpdateNotification;
 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 markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
 use release_channel::{AppVersion, ReleaseChannel};
@@ -94,7 +94,6 @@ fn view_release_notes_locally(
 
                             let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
 
-                            let tab_content = Some(SharedString::from(body.title.to_string()));
                             let editor = cx.new(|cx| {
                                 Editor::for_multibuffer(buffer, Some(project), window, cx)
                             });
@@ -105,7 +104,6 @@ fn view_release_notes_locally(
                                     editor,
                                     workspace_handle,
                                     language_registry,
-                                    tab_content,
                                     window,
                                     cx,
                                 );

crates/markdown_preview/src/markdown_preview_view.rs 🔗

@@ -17,10 +17,9 @@ use ui::prelude::*;
 use workspace::item::{Item, ItemHandle};
 use workspace::{Pane, Workspace};
 
-use crate::OpenPreviewToTheSide;
 use crate::markdown_elements::ParsedMarkdownElement;
 use crate::{
-    OpenFollowingPreview, OpenPreview,
+    OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide,
     markdown_elements::ParsedMarkdown,
     markdown_parser::parse_markdown,
     markdown_renderer::{RenderContext, render_markdown_block},
@@ -36,7 +35,6 @@ pub struct MarkdownPreviewView {
     contents: Option<ParsedMarkdown>,
     selected_block: usize,
     list_state: ListState,
-    tab_content_text: Option<SharedString>,
     language_registry: Arc<LanguageRegistry>,
     parsing_markdown_task: Option<Task<Result<()>>>,
     mode: MarkdownPreviewMode,
@@ -173,7 +171,6 @@ impl MarkdownPreviewView {
             editor,
             workspace_handle,
             language_registry,
-            None,
             window,
             cx,
         )
@@ -192,7 +189,6 @@ impl MarkdownPreviewView {
             editor,
             workspace_handle,
             language_registry,
-            None,
             window,
             cx,
         )
@@ -203,7 +199,6 @@ impl MarkdownPreviewView {
         active_editor: Entity<Editor>,
         workspace: WeakEntity<Workspace>,
         language_registry: Arc<LanguageRegistry>,
-        tab_content_text: Option<SharedString>,
         window: &mut Window,
         cx: &mut Context<Workspace>,
     ) -> Entity<Self> {
@@ -324,7 +319,6 @@ impl MarkdownPreviewView {
                 workspace: workspace.clone(),
                 contents: None,
                 list_state,
-                tab_content_text,
                 language_registry,
                 parsing_markdown_task: None,
                 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 {
             editor,
             _subscription: subscription,
@@ -547,21 +535,28 @@ impl Focusable for MarkdownPreviewView {
     }
 }
 
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum PreviewEvent {}
-
-impl EventEmitter<PreviewEvent> for MarkdownPreviewView {}
+impl EventEmitter<()> for MarkdownPreviewView {}
 
 impl Item for MarkdownPreviewView {
-    type Event = PreviewEvent;
+    type Event = ();
 
     fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
         Some(Icon::new(IconName::FileDoc))
     }
 
-    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
-        self.tab_content_text
-            .clone()
+    fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
+        self.active_editor
+            .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"))
     }
 

crates/svg_preview/Cargo.toml 🔗

@@ -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

crates/svg_preview/src/svg_preview.rs 🔗

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

crates/svg_preview/src/svg_preview_view.rs 🔗

@@ -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)) {}
+}

crates/zed/Cargo.toml 🔗

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

crates/zed/src/main.rs 🔗

@@ -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);

crates/zed/src/zed.rs 🔗

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

crates/zed/src/zed/quick_action_bar.rs 🔗

@@ -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,

crates/zed/src/zed/quick_action_bar/markdown_preview.rs 🔗

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

crates/zed/src/zed/quick_action_bar/preview.rs 🔗

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