diff --git a/Cargo.lock b/Cargo.lock index 7680a19c201..794ec8f1ce5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14057,6 +14057,7 @@ dependencies = [ "gpui", "itertools 0.14.0", "language", + "markdown_preview", "menu", "notifications", "pretty_assertions", diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index 55ba68ecb8d..1d69d829e70 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -28,6 +28,7 @@ use ui::utils::WithRemSize; use ui::{ContextMenu, WithScrollbar, prelude::*, right_click_menu}; use util::markdown::split_local_url_fragment; use workspace::item::{Item, ItemBufferKind, ItemHandle, SaveOptions, SerializableItem}; +use workspace::notifications::NotifyResultExt; use workspace::searchable::{ Direction, SearchEvent, SearchOptions, SearchToken, SearchableItem, SearchableItemHandle, }; @@ -208,7 +209,7 @@ impl MarkdownPreviewView { None } - fn create_markdown_view( + pub fn create_markdown_view( workspace: &mut Workspace, editor: Entity, window: &mut Window, @@ -337,6 +338,43 @@ impl MarkdownPreviewView { } } + pub fn is_markdown_path(path: impl AsRef) -> bool { + path.as_ref().extension().is_some_and(|ext| { + ext.eq_ignore_ascii_case("md") || ext.eq_ignore_ascii_case("markdown") + }) + } + + pub fn open_for_project_path( + project_path: ProjectPath, + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, + ) { + let open_buffer = workspace + .project() + .update(cx, |project, cx| project.open_buffer(project_path, cx)); + + cx.spawn_in(window, async move |workspace, mut cx| { + let Some(buffer) = open_buffer + .await + .notify_workspace_async_err(workspace.clone(), &mut cx) + else { + return; + }; + workspace + .update_in(cx, |workspace, window, cx| { + let project = workspace.project().clone(); + let editor = cx.new(|cx| Editor::for_buffer(buffer, Some(project), window, cx)); + let preview = Self::create_markdown_view(workspace, editor, window, cx); + workspace.active_pane().update(cx, |pane, cx| { + pane.add_item(Box::new(preview), true, true, None, window, cx); + }); + }) + .ok(); + }) + .detach(); + } + pub fn is_markdown_file(editor: &Entity, cx: &mut Context) -> bool { let buffer = editor.read(cx).buffer().read(cx); if let Some(buffer) = buffer.as_singleton() diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index d20b09107da..eac58314f08 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -45,6 +45,7 @@ client.workspace = true worktree.workspace = true workspace.workspace = true language.workspace = true +markdown_preview.workspace = true zed_actions.workspace = true telemetry.workspace = true notifications.workspace = true diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 7b1072849c0..da0cfe2789b 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -30,6 +30,7 @@ use gpui::{ point, px, size, transparent_white, uniform_list, }; use language::DiagnosticSeverity; +use markdown_preview::markdown_preview_view::MarkdownPreviewView; use menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious}; use notifications::status_toast::StatusToast; use project::{ @@ -405,6 +406,8 @@ actions!( Undo, /// Redoes the last undone file operation. Redo, + /// Opens a markdown preview for the selected file. + OpenMarkdownPreview, ] ); @@ -1091,6 +1094,7 @@ impl ProjectPanel { let is_remote = project.is_remote(); let is_collab = project.is_via_collab(); let is_local = project.is_local() || project.is_via_wsl_with_host_interop(cx); + let is_markdown = !is_dir && MarkdownPreviewView::is_markdown_path(&*entry.path); let settings = ProjectPanelSettings::get_global(cx); let visible_worktrees_count = project.visible_worktrees(cx).count(); @@ -1120,7 +1124,10 @@ impl ProjectPanel { let context_menu = ContextMenu::build(window, cx, |menu, _, cx| { menu.context(self.focus_handle.clone()).map(|menu| { if is_read_only { - menu.when(is_dir, |menu| { + menu.when(is_markdown, |menu| { + menu.action("Open Markdown Preview", Box::new(OpenMarkdownPreview)) + }) + .when(is_dir, |menu| { menu.action("Search Inside", Box::new(NewSearchInDirectory)) }) } else { @@ -1137,6 +1144,9 @@ impl ProjectPanel { menu.action("Open in Default App", Box::new(OpenWithSystem)) }) .action("Open in Terminal", Box::new(OpenInTerminal)) + .when(is_markdown, |menu| { + menu.action("Open Markdown Preview", Box::new(OpenMarkdownPreview)) + }) .when(is_dir, |menu| { menu.separator() .action("Find in Folder…", Box::new(NewSearchInDirectory)) @@ -1685,6 +1695,29 @@ impl ProjectPanel { ); } + fn open_markdown_preview( + &mut self, + _: &OpenMarkdownPreview, + window: &mut Window, + cx: &mut Context, + ) { + let Some((worktree, entry)) = self.selected_entry(cx) else { + return; + }; + if !entry.is_file() || !MarkdownPreviewView::is_markdown_path(&*entry.path) { + return; + } + let project_path = ProjectPath { + worktree_id: worktree.id(), + path: entry.path.clone(), + }; + self.workspace + .update(cx, |workspace, cx| { + MarkdownPreviewView::open_for_project_path(project_path, workspace, window, cx); + }) + .ok(); + } + fn open_internal( &mut self, allow_preview: bool, @@ -6769,6 +6802,7 @@ impl Render for ProjectPanel { .on_action(cx.listener(Self::open_permanent)) .on_action(cx.listener(Self::open_split_vertical)) .on_action(cx.listener(Self::open_split_horizontal)) + .on_action(cx.listener(Self::open_markdown_preview)) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::copy_path))