Allow multiple Markdown preview tabs (#32859)

ddoemonn created

Closes #32791


https://github.com/user-attachments/assets/8cb90e3d-ef7b-407f-b78b-7ba4ff6d8df2

Release Notes:
- Allowed multiple Markdown preview tabs

Change summary

crates/markdown_preview/src/markdown_preview.rs      |  5 
crates/markdown_preview/src/markdown_preview_view.rs | 76 ++++++++++++-
2 files changed, 74 insertions(+), 7 deletions(-)

Detailed changes

crates/markdown_preview/src/markdown_preview.rs 🔗

@@ -6,7 +6,10 @@ pub mod markdown_parser;
 pub mod markdown_preview_view;
 pub mod markdown_renderer;
 
-actions!(markdown, [OpenPreview, OpenPreviewToTheSide]);
+actions!(
+    markdown,
+    [OpenPreview, OpenPreviewToTheSide, OpenFollowingPreview]
+);
 
 pub fn init(cx: &mut App) {
     cx.observe_new(|workspace: &mut Workspace, window, cx| {

crates/markdown_preview/src/markdown_preview_view.rs 🔗

@@ -20,7 +20,7 @@ use workspace::{Pane, Workspace};
 use crate::OpenPreviewToTheSide;
 use crate::markdown_elements::ParsedMarkdownElement;
 use crate::{
-    OpenPreview,
+    OpenFollowingPreview, OpenPreview,
     markdown_elements::ParsedMarkdown,
     markdown_parser::parse_markdown,
     markdown_renderer::{RenderContext, render_markdown_block},
@@ -39,6 +39,7 @@ pub struct MarkdownPreviewView {
     tab_content_text: Option<SharedString>,
     language_registry: Arc<LanguageRegistry>,
     parsing_markdown_task: Option<Task<Result<()>>>,
+    mode: MarkdownPreviewMode,
 }
 
 #[derive(Clone, Copy, Debug, PartialEq)]
@@ -58,9 +59,11 @@ impl MarkdownPreviewView {
     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_markdown_editor(workspace, cx) {
-                let view = Self::create_markdown_view(workspace, editor, window, cx);
+                let view = Self::create_markdown_view(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) {
+                    if let Some(existing_view_idx) =
+                        Self::find_existing_independent_preview_item_idx(pane, &editor, cx)
+                    {
                         pane.activate_item(existing_view_idx, true, true, window, cx);
                     } else {
                         pane.add_item(Box::new(view.clone()), true, true, None, window, cx)
@@ -84,7 +87,9 @@ impl MarkdownPreviewView {
                         )
                     });
                 pane.update(cx, |pane, cx| {
-                    if let Some(existing_view_idx) = Self::find_existing_preview_item_idx(pane) {
+                    if let Some(existing_view_idx) =
+                        Self::find_existing_independent_preview_item_idx(pane, &editor, cx)
+                    {
                         pane.activate_item(existing_view_idx, true, true, window, cx);
                     } else {
                         pane.add_item(Box::new(view.clone()), false, false, None, window, cx)
@@ -94,11 +99,49 @@ impl MarkdownPreviewView {
                 cx.notify();
             }
         });
+
+        workspace.register_action(move |workspace, _: &OpenFollowingPreview, window, cx| {
+            if let Some(editor) = Self::resolve_active_item_as_markdown_editor(workspace, cx) {
+                // Check if there's already a following preview
+                let existing_follow_view_idx = {
+                    let active_pane = workspace.active_pane().read(cx);
+                    active_pane
+                        .items_of_type::<MarkdownPreviewView>()
+                        .find(|view| view.read(cx).mode == MarkdownPreviewMode::Follow)
+                        .and_then(|view| active_pane.index_for_item(&view))
+                };
+
+                if let Some(existing_follow_view_idx) = existing_follow_view_idx {
+                    workspace.active_pane().update(cx, |pane, cx| {
+                        pane.activate_item(existing_follow_view_idx, true, true, window, cx);
+                    });
+                } else {
+                    let view =
+                        Self::create_following_markdown_view(workspace, editor.clone(), window, cx);
+                    workspace.active_pane().update(cx, |pane, cx| {
+                        pane.add_item(Box::new(view.clone()), true, true, None, window, cx)
+                    });
+                }
+                cx.notify();
+            }
+        });
     }
 
-    fn find_existing_preview_item_idx(pane: &Pane) -> Option<usize> {
+    fn find_existing_independent_preview_item_idx(
+        pane: &Pane,
+        editor: &Entity<Editor>,
+        cx: &App,
+    ) -> Option<usize> {
         pane.items_of_type::<MarkdownPreviewView>()
-            .nth(0)
+            .find(|view| {
+                let view_read = view.read(cx);
+                // Only look for independent (Default mode) previews, not Follow previews
+                view_read.mode == MarkdownPreviewMode::Default
+                    && view_read
+                        .active_editor
+                        .as_ref()
+                        .is_some_and(|active_editor| active_editor.editor == *editor)
+            })
             .and_then(|view| pane.index_for_item(&view))
     }
 
@@ -122,6 +165,25 @@ impl MarkdownPreviewView {
         editor: Entity<Editor>,
         window: &mut Window,
         cx: &mut Context<Workspace>,
+    ) -> Entity<MarkdownPreviewView> {
+        let language_registry = workspace.project().read(cx).languages().clone();
+        let workspace_handle = workspace.weak_handle();
+        MarkdownPreviewView::new(
+            MarkdownPreviewMode::Default,
+            editor,
+            workspace_handle,
+            language_registry,
+            None,
+            window,
+            cx,
+        )
+    }
+
+    fn create_following_markdown_view(
+        workspace: &mut Workspace,
+        editor: Entity<Editor>,
+        window: &mut Window,
+        cx: &mut Context<Workspace>,
     ) -> Entity<MarkdownPreviewView> {
         let language_registry = workspace.project().read(cx).languages().clone();
         let workspace_handle = workspace.weak_handle();
@@ -266,6 +328,7 @@ impl MarkdownPreviewView {
                 language_registry,
                 parsing_markdown_task: None,
                 image_cache: RetainAllImageCache::new(cx),
+                mode,
             };
 
             this.set_editor(active_editor, window, cx);
@@ -343,6 +406,7 @@ 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());
         }