From db30c67ed254ee703399ea862cfc6acf33332b09 Mon Sep 17 00:00:00 2001 From: Eli Stark Date: Fri, 26 Jun 2026 08:30:51 -0400 Subject: [PATCH] project_panel: Add markdown preview context menu item (#57112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening a markdown file from the project panel used to require four steps: - click the file (opens raw editor) - right-click the editor tab - select "Open Markdown Preview" - then close the raw editor tab This PR shortcuts that to a single right-click (or keypress) from the project panel. Right-clicking a markdown file in the project panel now shows an **Open Markdown Preview** option. Selecting it opens the rendered markdown preview directly — no raw editor tab is left behind. The same shortcut used in the editor (Cmd+Shift+V on macOS, Ctrl+Shift+V on Windows/Linux) also works when focus is in the project panel. ## How it works - **Context menu**: `OpenMarkdownPreview` action is registered and surfaced in the right-click menu only for `.md`/`.markdown` files. - **Keybinding**: `cmd-shift-v` / `ctrl-shift-v` bound to `project_panel::OpenMarkdownPreview` in the `ProjectPanel` context, mirroring the existing editor binding. - **Loading**: The file is opened via `open_path_preview` with `focus_item: false, activate: false`, so the raw editor is loaded into the buffer system but never becomes the visible tab. - **Preview construction**: `MarkdownPreviewView::create_markdown_view` is called directly (no `dispatch_action` indirection, which would have been deferred and caused a race). The preview is added to the active pane and the raw editor tab is removed atomically in one synchronous pane update. - **Pre-existing tabs**: Before loading, all panes are checked for an existing item at the file's project path. If the file is already open in raw mode, `remove_item` is skipped — the existing tab is left untouched. Release Notes: - Added "Open Markdown Preview" context menu item to Project Panel markdown entries. --------- Co-authored-by: Smit Barmase Co-authored-by: Tom Houlé --- Cargo.lock | 1 + .../src/markdown_preview_view.rs | 40 ++++++++++++++++++- crates/project_panel/Cargo.toml | 1 + crates/project_panel/src/project_panel.rs | 36 ++++++++++++++++- 4 files changed, 76 insertions(+), 2 deletions(-) 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))