From e6bc1308af109a6a149f9aa003dc904f604c53f9 Mon Sep 17 00:00:00 2001 From: Ron Harel <55725807+ronharel02@users.noreply.github.com> Date: Fri, 27 Jun 2025 12:08:05 +0300 Subject: [PATCH] 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. --- 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 +- .../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 +- .../zed/quick_action_bar/markdown_preview.rs | 63 ---- .../zed/src/zed/quick_action_bar/preview.rs | 95 ++++++ 16 files changed, 528 insertions(+), 93 deletions(-) create mode 100644 crates/svg_preview/Cargo.toml create mode 120000 crates/svg_preview/LICENSE-GPL create mode 100644 crates/svg_preview/src/svg_preview.rs create mode 100644 crates/svg_preview/src/svg_preview_view.rs delete mode 100644 crates/zed/src/zed/quick_action_bar/markdown_preview.rs create mode 100644 crates/zed/src/zed/quick_action_bar/preview.rs diff --git a/Cargo.lock b/Cargo.lock index 7778b00ee7..19e105e9a3 100644 --- a/Cargo.lock +++ b/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", diff --git a/Cargo.toml b/Cargo.toml index 22377ccb40..4239fcf1e9 100644 --- a/Cargo.toml +++ b/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" } diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index e21005816b..525907a71a 100644 --- a/assets/keymaps/default-linux.json +++ b/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": { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 51f4ffe23f..121dbe93e0 100644 --- a/assets/keymaps/default-macos.json +++ b/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, diff --git a/crates/auto_update_ui/src/auto_update_ui.rs b/crates/auto_update_ui/src/auto_update_ui.rs index 30c1cddec2..afb135bc97 100644 --- a/crates/auto_update_ui/src/auto_update_ui.rs +++ b/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, ); diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index 40c1783482..bf1a1da572 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/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, selected_block: usize, list_state: ListState, - tab_content_text: Option, language_registry: Arc, parsing_markdown_task: Option>>, 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, workspace: WeakEntity, language_registry: Arc, - tab_content_text: Option, window: &mut Window, cx: &mut Context, ) -> Entity { @@ -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 for MarkdownPreviewView {} +impl EventEmitter<()> for MarkdownPreviewView {} impl Item for MarkdownPreviewView { - type Event = PreviewEvent; + type Event = (); fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { 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")) } diff --git a/crates/svg_preview/Cargo.toml b/crates/svg_preview/Cargo.toml new file mode 100644 index 0000000000..b783d7192c --- /dev/null +++ b/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 diff --git a/crates/svg_preview/LICENSE-GPL b/crates/svg_preview/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/svg_preview/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/svg_preview/src/svg_preview.rs b/crates/svg_preview/src/svg_preview.rs new file mode 100644 index 0000000000..cbee76be83 --- /dev/null +++ b/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(); +} diff --git a/crates/svg_preview/src/svg_preview_view.rs b/crates/svg_preview/src/svg_preview_view.rs new file mode 100644 index 0000000000..327856d749 --- /dev/null +++ b/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, + image_cache: Entity, + _editor_subscription: Subscription, + _workspace_subscription: Option, +} + +#[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.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, + cx: &App, + ) -> Option { + let editor_path = Self::get_svg_path(editor, cx); + pane.items_of_type::() + .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, + ) -> Option> { + let editor = workspace.active_item(cx)?.act_as::(cx)?; + + if Self::is_svg_file(&editor, cx) { + Some(editor) + } else { + None + } + } + + fn create_svg_view( + mode: SvgPreviewMode, + workspace: &mut Workspace, + editor: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + let workspace_handle = workspace.weak_handle(); + SvgPreviewView::new(mode, editor, workspace_handle, window, cx) + } + + pub fn new( + mode: SvgPreviewMode, + active_editor: Entity, + workspace_handle: WeakEntity, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + 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::() + { + 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(editor: &Entity, cx: &C) -> bool + where + C: std::borrow::Borrow, + { + 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(editor: &Entity, cx: &C) -> Option + where + C: std::borrow::Borrow, + { + 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) -> 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 { + // 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)) {} +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 534d79c6ac..4e426c3837 100644 --- a/crates/zed/Cargo.toml +++ b/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 } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 0e08b304f7..00a1f150ea 100644 --- a/crates/zed/src/main.rs +++ b/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); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 5ab4b672ae..ca1670227b 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4323,6 +4323,7 @@ mod tests { "search", "snippets", "supermaven", + "svg", "tab_switcher", "task", "terminal", diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 52c4ff3831..85e28c6ae8 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/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, diff --git a/crates/zed/src/zed/quick_action_bar/markdown_preview.rs b/crates/zed/src/zed/quick_action_bar/markdown_preview.rs deleted file mode 100644 index 44008f7110..0000000000 --- a/crates/zed/src/zed/quick_action_bar/markdown_preview.rs +++ /dev/null @@ -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, - cx: &mut Context, - ) -> Option { - 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()) - } -} diff --git a/crates/zed/src/zed/quick_action_bar/preview.rs b/crates/zed/src/zed/quick_action_bar/preview.rs new file mode 100644 index 0000000000..57775d31fd --- /dev/null +++ b/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, + cx: &mut Context, + ) -> Option { + 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, + Box::new(MarkdownOpenPreviewToTheSide) as Box, + &markdown_preview::OpenPreview as &dyn gpui::Action, + ), + PreviewType::Svg => ( + "toggle-svg-preview", + "Preview SVG", + Box::new(SvgOpenPreview) as Box, + Box::new(SvgOpenPreviewToTheSide) as Box, + &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()) + } +}