use std::any::TypeId;use std::borrow::Cow;use std::cmp::min;use std::ops::Range;use std::path::{Path, PathBuf};use std::sync::Arc;use std::time::Duration; use anyhow::{Context as _, Result};use editor::items::open_resolved_target;use editor::scroll::Autoscroll;use editor::{Editor, EditorEvent, MultiBufferOffset, SelectionEffects};use gpui::{ App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageSource, InteractiveElement, IntoElement, IsZero, Pixels, Render, Resource, RetainAllImageCache, ScrollHandle, SharedString, SharedUri, Subscription, Task, WeakEntity, Window, point, px,};use language::{Buffer, LanguageRegistry};use markdown::{ CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownFont, MarkdownOptions, MarkdownStyle,};use project::search::SearchQuery;use project::{Project, ProjectPath};use settings::{SeedQuerySetting, Settings, update_settings_file};use theme::{SystemAppearance, Theme, ThemeRegistry};use theme_settings::ThemeSettings;use ui::utils::WithRemSize;use ui::{ContextMenu, LinkPreview, WithScrollbar, prelude::*, right_click_menu};use util::{ ResultExt, markdown::{source_position_from_fragment, split_local_url_fragment}, paths::PathWithPosition,};use workspace::item::{Item, ItemBufferKind, ItemHandle, SaveOptions, SerializableItem};use workspace::notifications::{NotifyResultExt, NotifyTaskExt};use workspace::path_link::{PathMatching, resolve_open_target};use workspace::searchable::{ Direction, SearchEvent, SearchOptions, SearchToken, SearchableItem, SearchableItemHandle,};use workspace::{ItemId, Pane, SaveIntent, Workspace, WorkspaceId, delete_unloaded_items};use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize}; use crate::markdown_preview_settings::MarkdownPreviewSettings;use crate::{ CloseAndReturnToEditor, OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide, ScrollDown, ScrollDownByItem,};use crate::{ScrollPageDown, ScrollPageUp, ScrollToBottom, ScrollToTop, ScrollUp, ScrollUpByItem}; const REPARSE_DEBOUNCE: Duration = Duration::from_millis(200); pub struct MarkdownPreviewView { workspace: WeakEntity<Workspace>, active_editor: Option<EditorState>, focus_handle: FocusHandle, markdown: Entity<Markdown>, _markdown_subscription: Subscription, active_source_index: Option<usize>, scroll_handle: ScrollHandle, image_cache: Entity<RetainAllImageCache>, base_directory: Option<PathBuf>, pending_update_task: Option<Task<Result<()>>>, hovered_url: Option<SharedString>, mode: MarkdownPreviewMode,} #[derive(Clone, Copy, Debug, PartialEq)]pub enum MarkdownPreviewMode { /// The preview will always show the contents of the provided editor. Default, /// The preview will "follow" the currently active editor. Follow,} impl MarkdownPreviewMode { fn to_db(self) -> i64 { match self { Self::Default => 0, Self::Follow => 1, } } fn from_db(value: i64) -> Self { match value { 1 => Self::Follow, _ => Self::Default, } }} struct EditorState { editor: Entity<Editor>, _subscription: Subscription,} #[derive(Clone, Copy, Debug)]pub enum MarkdownPreviewEvent { SourceEditorChanged, SourceFileHandleChanged,} enum PreviewLinkTarget { Heading(SharedString), Position { row: u32, column: u32 },} 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 pane = workspace.active_pane().clone(); Self::open_preview_in_pane(workspace, editor, pane, window, cx); } }); workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, window, cx| { if let Some(editor) = Self::resolve_active_item_as_markdown_editor(workspace, cx) { let pane = workspace.active_pane().clone(); Self::open_preview_to_the_side_of_pane(workspace, editor, pane, window, cx); } }); 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, window, cx); workspace.active_pane().update(cx, |pane, cx| { pane.add_item(Box::new(view.clone()), true, true, None, window, cx) }); } cx.notify(); } }); } pub fn open_preview_in_pane( workspace: &mut Workspace, editor: Entity<Editor>, pane: Entity<Pane>, window: &mut Window, cx: &mut Context<Workspace>, ) { Self::activate_or_add_preview(workspace, editor, pane, true, window, cx); } pub fn open_preview_to_the_side_of_pane( workspace: &mut Workspace, editor: Entity<Editor>, origin_pane: Entity<Pane>, window: &mut Window, cx: &mut Context<Workspace>, ) { let target_pane = workspace.adjacent_pane_of(&origin_pane, window, cx); Self::activate_or_add_preview(workspace, editor.clone(), target_pane, false, window, cx); editor.focus_handle(cx).focus(window, cx); } fn activate_or_add_preview( workspace: &mut Workspace, editor: Entity<Editor>, pane: Entity<Pane>, focus: bool, window: &mut Window, cx: &mut Context<Workspace>, ) { let existing_view_idx = Self::find_existing_independent_preview_item_idx(pane.read(cx), &editor, cx); if let Some(existing_view_idx) = existing_view_idx { pane.update(cx, |pane, cx| { pane.activate_item(existing_view_idx, focus, focus, window, cx); }); } else { let view = Self::create_markdown_view(workspace, editor, window, cx); pane.update(cx, |pane, cx| { pane.add_item(Box::new(view), focus, focus, None, window, cx) }); } cx.notify(); } fn find_existing_independent_preview_item_idx( pane: &Pane, editor: &Entity<Editor>, cx: &App, ) -> Option<usize> { let target_buffer = editor.read(cx).buffer().read(cx).as_singleton()?; pane.items_of_type::<MarkdownPreviewView>() .find(|view| { // Only look for independent (Default mode) previews, not Follow previews. // Match by buffer entity rather than editor entity so the lookup survives // workspace restoration, where the preview's bound editor may differ from // the editor the user is currently invoking the action on even though both // wrap the same source buffer. view.read(cx).mode == MarkdownPreviewMode::Default && view.read(cx).is_previewing(&target_buffer, cx) }) .and_then(|view| pane.index_for_item(&view)) } fn is_previewing(&self, buffer: &Entity<Buffer>, cx: &App) -> bool { self.active_editor.as_ref().is_some_and(|state| { state .editor .read(cx) .buffer() .read(cx) .as_singleton() .as_ref() == Some(buffer) }) } pub fn resolve_active_item_as_markdown_editor( workspace: &Workspace, cx: &mut Context<Workspace>, ) -> Option<Entity<Editor>> { if let Some(editor) = workspace .active_item(cx) .and_then(|item| item.act_as::<Editor>(cx)) && Self::is_markdown_file(&editor, cx) { return Some(editor); } None } pub fn create_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(); MarkdownPreviewView::new( MarkdownPreviewMode::Default, editor, workspace_handle, language_registry, 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(); MarkdownPreviewView::new( MarkdownPreviewMode::Follow, editor, workspace_handle, language_registry, window, cx, ) } pub fn new( mode: MarkdownPreviewMode, active_editor: Entity<Editor>, workspace: WeakEntity<Workspace>, language_registry: Arc<LanguageRegistry>, window: &mut Window, cx: &mut App, ) -> Entity<Self> { cx.new(|cx| { let markdown = cx.new(|cx| { Markdown::new_with_options( SharedString::default(), Some(language_registry), None, MarkdownOptions { parse_html: true, render_mermaid_diagrams: true, parse_heading_slugs: true, render_metadata_blocks: true, ..Default::default() }, cx, ) }); let mut this = Self { active_editor: None, focus_handle: cx.focus_handle(), workspace: workspace.clone(), _markdown_subscription: cx.observe( &markdown, |this: &mut Self, _: Entity<Markdown>, cx| { this.sync_active_root_block(cx); }, ), markdown, active_source_index: None, scroll_handle: ScrollHandle::new(), image_cache: RetainAllImageCache::new(cx), base_directory: None, pending_update_task: None, hovered_url: None, mode, }; this.set_editor(active_editor, window, cx); match mode { MarkdownPreviewMode::Follow => { if let Some(workspace) = &workspace.upgrade() { cx.observe_in(workspace, window, |this, workspace, window, cx| { let item = workspace.read(cx).active_item(cx); this.workspace_updated(item, window, cx); }) .detach(); } else { log::error!("Failed to listen to workspace updates"); } } MarkdownPreviewMode::Default => { // After workspace restoration the bound editor may be an orphan that // wraps the right buffer but isn't the canonical Editor instance in // any pane. Re-binding to the workspace's editor for our buffer is // what restores cursor-driven scroll sync — `SelectionsChanged` only // fires from the editor the user actually interacts with. // // Subscribing to `workspace::Event` (rather than `observe`) keeps the // rebind check off the cursor-move hot path; `observe` would fire on // every workspace `cx.notify`. if let Some(workspace) = &workspace.upgrade() { cx.subscribe_in(workspace, window, Self::on_workspace_event) .detach(); } } } this }) } fn workspace_updated( &mut self, active_item: Option<Box<dyn ItemHandle>>, window: &mut Window, cx: &mut Context<Self>, ) { if let Some(item) = active_item && item.item_id() != cx.entity_id() && let Some(editor) = item.act_as::<Editor>(cx) && Self::is_markdown_file(&editor, cx) { self.set_editor(editor, window, cx); } } pub fn is_markdown_path(path: impl AsRef<Path>, languages: &Arc<LanguageRegistry>) -> bool { languages .language_for_file_path(path.as_ref()) .is_some_and(|language_id| { languages .available_language_for_name("Markdown") .is_some_and(|markdown| markdown.id() == language_id) }) } pub fn open_for_project_path( project_path: ProjectPath, workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>, ) { 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<Editor>, cx: &App) -> bool { let buffer = editor.read(cx).buffer().read(cx); if let Some(buffer) = buffer.as_singleton() && let Some(language) = buffer.read(cx).language() { return language.name() == "Markdown"; } false } fn set_editor(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) { if let Some(active) = &self.active_editor && active.editor == editor { return; } let had_active_editor = self.active_editor.is_some(); let subscription = cx.subscribe_in( &editor, window, |this, editor, event: &EditorEvent, window, cx| { match event { EditorEvent::Edited { .. } | EditorEvent::BufferEdited { .. } | EditorEvent::DirtyChanged | EditorEvent::BuffersEdited { .. } => { this.update_markdown_from_active_editor(true, false, window, cx); } EditorEvent::FileHandleChanged => { this.base_directory = Self::get_folder_for_active_editor(editor.read(cx), cx); this.update_markdown_from_active_editor(false, false, window, cx); cx.emit(MarkdownPreviewEvent::SourceFileHandleChanged); } EditorEvent::SelectionsChanged { .. } => { let (selection_start, editor_is_focused) = editor.update(cx, |editor, cx| { let index = Self::selected_source_index(editor, cx); let focused = editor.focus_handle(cx).is_focused(window); (index, focused) }); if let Some(selection_start) = selection_start { this.sync_preview_to_source_index( selection_start, editor_is_focused, cx, ); cx.notify(); } } _ => {} }; }, ); self.base_directory = Self::get_folder_for_active_editor(editor.read(cx), cx); self.hovered_url = None; self.active_editor = Some(EditorState { editor, _subscription: subscription, }); self.update_markdown_from_active_editor(false, true, window, cx); if had_active_editor { cx.emit(MarkdownPreviewEvent::SourceEditorChanged); } } fn on_workspace_event( &mut self, workspace: &Entity<Workspace>, event: &workspace::Event, window: &mut Window, cx: &mut Context<Self>, ) { if !matches!( event, workspace::Event::ItemAdded { .. } | workspace::Event::ItemRemoved { .. } ) { return; } let candidate = self.find_canonical_editor(workspace.read(cx), cx); if let Some(editor) = candidate && self .active_editor .as_ref() .is_none_or(|s| s.editor != editor) { self.set_editor(editor, window, cx);Lines 1–500 of 3464. Load more
use std::any::TypeId; use std::borrow::Cow; use std::cmp::min; use std::ops::Range; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; use anyhow::{Context as _, Result}; use editor::items::open_resolved_target; use editor::scroll::Autoscroll; use editor::{Editor, EditorEvent, MultiBufferOffset, SelectionEffects}; use gpui::{ App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageSource, InteractiveElement, IntoElement, IsZero, Pixels, Render, Resource, RetainAllImageCache, ScrollHandle, SharedString, SharedUri, Subscription, Task, WeakEntity, Window, point, px, }; use language::{Buffer, LanguageRegistry}; use markdown::{ CodeBlockRenderer, CopyButtonVisibility, Markdown, MarkdownElement, MarkdownFont, MarkdownOptions, MarkdownStyle, }; use project::search::SearchQuery; use project::{Project, ProjectPath}; use settings::{SeedQuerySetting, Settings, update_settings_file}; use theme::{SystemAppearance, Theme, ThemeRegistry}; use theme_settings::ThemeSettings; use ui::utils::WithRemSize; use ui::{ContextMenu, LinkPreview, WithScrollbar, prelude::*, right_click_menu}; use util::{ ResultExt, markdown::{source_position_from_fragment, split_local_url_fragment}, paths::PathWithPosition, }; use workspace::item::{Item, ItemBufferKind, ItemHandle, SaveOptions, SerializableItem}; use workspace::notifications::{NotifyResultExt, NotifyTaskExt}; use workspace::path_link::{PathMatching, resolve_open_target}; use workspace::searchable::{ Direction, SearchEvent, SearchOptions, SearchToken, SearchableItem, SearchableItemHandle, }; use workspace::{ItemId, Pane, SaveIntent, Workspace, WorkspaceId, delete_unloaded_items}; use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize}; use crate::markdown_preview_settings::MarkdownPreviewSettings; use crate::{ CloseAndReturnToEditor, OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide, ScrollDown, ScrollDownByItem, }; use crate::{ScrollPageDown, ScrollPageUp, ScrollToBottom, ScrollToTop, ScrollUp, ScrollUpByItem}; const REPARSE_DEBOUNCE: Duration = Duration::from_millis(200); pub struct MarkdownPreviewView { workspace: WeakEntity<Workspace>, active_editor: Option<EditorState>, focus_handle: FocusHandle, markdown: Entity<Markdown>, _markdown_subscription: Subscription, active_source_index: Option<usize>, scroll_handle: ScrollHandle, image_cache: Entity<RetainAllImageCache>, base_directory: Option<PathBuf>, pending_update_task: Option<Task<Result<()>>>, hovered_url: Option<SharedString>, mode: MarkdownPreviewMode, } #[derive(Clone, Copy, Debug, PartialEq)] pub enum MarkdownPreviewMode { /// The preview will always show the contents of the provided editor. Default, /// The preview will "follow" the currently active editor. Follow, } impl MarkdownPreviewMode { fn to_db(self) -> i64 { match self { Self::Default => 0, Self::Follow => 1, } } fn from_db(value: i64) -> Self { match value { 1 => Self::Follow, _ => Self::Default, } } } struct EditorState { editor: Entity<Editor>, _subscription: Subscription, } #[derive(Clone, Copy, Debug)] pub enum MarkdownPreviewEvent { SourceEditorChanged, SourceFileHandleChanged, } enum PreviewLinkTarget { Heading(SharedString), Position { row: u32, column: u32 }, } 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 pane = workspace.active_pane().clone(); Self::open_preview_in_pane(workspace, editor, pane, window, cx); } }); workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, window, cx| { if let Some(editor) = Self::resolve_active_item_as_markdown_editor(workspace, cx) { let pane = workspace.active_pane().clone(); Self::open_preview_to_the_side_of_pane(workspace, editor, pane, window, cx); } }); 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, window, cx); workspace.active_pane().update(cx, |pane, cx| { pane.add_item(Box::new(view.clone()), true, true, None, window, cx) }); } cx.notify(); } }); } pub fn open_preview_in_pane( workspace: &mut Workspace, editor: Entity<Editor>, pane: Entity<Pane>, window: &mut Window, cx: &mut Context<Workspace>, ) { Self::activate_or_add_preview(workspace, editor, pane, true, window, cx); } pub fn open_preview_to_the_side_of_pane( workspace: &mut Workspace, editor: Entity<Editor>, origin_pane: Entity<Pane>, window: &mut Window, cx: &mut Context<Workspace>, ) { let target_pane = workspace.adjacent_pane_of(&origin_pane, window, cx); Self::activate_or_add_preview(workspace, editor.clone(), target_pane, false, window, cx); editor.focus_handle(cx).focus(window, cx); } fn activate_or_add_preview( workspace: &mut Workspace, editor: Entity<Editor>, pane: Entity<Pane>, focus: bool, window: &mut Window, cx: &mut Context<Workspace>, ) { let existing_view_idx = Self::find_existing_independent_preview_item_idx(pane.read(cx), &editor, cx); if let Some(existing_view_idx) = existing_view_idx { pane.update(cx, |pane, cx| { pane.activate_item(existing_view_idx, focus, focus, window, cx); }); } else { let view = Self::create_markdown_view(workspace, editor, window, cx); pane.update(cx, |pane, cx| { pane.add_item(Box::new(view), focus, focus, None, window, cx) }); } cx.notify(); } fn find_existing_independent_preview_item_idx( pane: &Pane, editor: &Entity<Editor>, cx: &App, ) -> Option<usize> { let target_buffer = editor.read(cx).buffer().read(cx).as_singleton()?; pane.items_of_type::<MarkdownPreviewView>() .find(|view| { // Only look for independent (Default mode) previews, not Follow previews. // Match by buffer entity rather than editor entity so the lookup survives // workspace restoration, where the preview's bound editor may differ from // the editor the user is currently invoking the action on even though both // wrap the same source buffer. view.read(cx).mode == MarkdownPreviewMode::Default && view.read(cx).is_previewing(&target_buffer, cx) }) .and_then(|view| pane.index_for_item(&view)) } fn is_previewing(&self, buffer: &Entity<Buffer>, cx: &App) -> bool { self.active_editor.as_ref().is_some_and(|state| { state .editor .read(cx) .buffer() .read(cx) .as_singleton() .as_ref() == Some(buffer) }) } pub fn resolve_active_item_as_markdown_editor( workspace: &Workspace, cx: &mut Context<Workspace>, ) -> Option<Entity<Editor>> { if let Some(editor) = workspace .active_item(cx) .and_then(|item| item.act_as::<Editor>(cx)) && Self::is_markdown_file(&editor, cx) { return Some(editor); } None } pub fn create_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(); MarkdownPreviewView::new( MarkdownPreviewMode::Default, editor, workspace_handle, language_registry, 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(); MarkdownPreviewView::new( MarkdownPreviewMode::Follow, editor, workspace_handle, language_registry, window, cx, ) } pub fn new( mode: MarkdownPreviewMode, active_editor: Entity<Editor>, workspace: WeakEntity<Workspace>, language_registry: Arc<LanguageRegistry>, window: &mut Window, cx: &mut App, ) -> Entity<Self> { cx.new(|cx| { let markdown = cx.new(|cx| { Markdown::new_with_options( SharedString::default(), Some(language_registry), None, MarkdownOptions { parse_html: true, render_mermaid_diagrams: true, parse_heading_slugs: true, render_metadata_blocks: true, ..Default::default() }, cx, ) }); let mut this = Self { active_editor: None, focus_handle: cx.focus_handle(), workspace: workspace.clone(), _markdown_subscription: cx.observe( &markdown, |this: &mut Self, _: Entity<Markdown>, cx| { this.sync_active_root_block(cx); }, ), markdown, active_source_index: None, scroll_handle: ScrollHandle::new(), image_cache: RetainAllImageCache::new(cx), base_directory: None, pending_update_task: None, hovered_url: None, mode, }; this.set_editor(active_editor, window, cx); match mode { MarkdownPreviewMode::Follow => { if let Some(workspace) = &workspace.upgrade() { cx.observe_in(workspace, window, |this, workspace, window, cx| { let item = workspace.read(cx).active_item(cx); this.workspace_updated(item, window, cx); }) .detach(); } else { log::error!("Failed to listen to workspace updates"); } } MarkdownPreviewMode::Default => { // After workspace restoration the bound editor may be an orphan that // wraps the right buffer but isn't the canonical Editor instance in // any pane. Re-binding to the workspace's editor for our buffer is // what restores cursor-driven scroll sync — `SelectionsChanged` only // fires from the editor the user actually interacts with. // // Subscribing to `workspace::Event` (rather than `observe`) keeps the // rebind check off the cursor-move hot path; `observe` would fire on // every workspace `cx.notify`. if let Some(workspace) = &workspace.upgrade() { cx.subscribe_in(workspace, window, Self::on_workspace_event) .detach(); } } } this }) } fn workspace_updated( &mut self, active_item: Option<Box<dyn ItemHandle>>, window: &mut Window, cx: &mut Context<Self>, ) { if let Some(item) = active_item && item.item_id() != cx.entity_id() && let Some(editor) = item.act_as::<Editor>(cx) && Self::is_markdown_file(&editor, cx) { self.set_editor(editor, window, cx); } } pub fn is_markdown_path(path: impl AsRef<Path>, languages: &Arc<LanguageRegistry>) -> bool { languages .language_for_file_path(path.as_ref()) .is_some_and(|language_id| { languages .available_language_for_name("Markdown") .is_some_and(|markdown| markdown.id() == language_id) }) } pub fn open_for_project_path( project_path: ProjectPath, workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>, ) { 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<Editor>, cx: &App) -> bool { let buffer = editor.read(cx).buffer().read(cx); if let Some(buffer) = buffer.as_singleton() && let Some(language) = buffer.read(cx).language() { return language.name() == "Markdown"; } false } fn set_editor(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) { if let Some(active) = &self.active_editor && active.editor == editor { return; } let had_active_editor = self.active_editor.is_some(); let subscription = cx.subscribe_in( &editor, window, |this, editor, event: &EditorEvent, window, cx| { match event { EditorEvent::Edited { .. } | EditorEvent::BufferEdited { .. } | EditorEvent::DirtyChanged | EditorEvent::BuffersEdited { .. } => { this.update_markdown_from_active_editor(true, false, window, cx); } EditorEvent::FileHandleChanged => { this.base_directory = Self::get_folder_for_active_editor(editor.read(cx), cx); this.update_markdown_from_active_editor(false, false, window, cx); cx.emit(MarkdownPreviewEvent::SourceFileHandleChanged); } EditorEvent::SelectionsChanged { .. } => { let (selection_start, editor_is_focused) = editor.update(cx, |editor, cx| { let index = Self::selected_source_index(editor, cx); let focused = editor.focus_handle(cx).is_focused(window); (index, focused) }); if let Some(selection_start) = selection_start { this.sync_preview_to_source_index( selection_start, editor_is_focused, cx, ); cx.notify(); } } _ => {} }; }, ); self.base_directory = Self::get_folder_for_active_editor(editor.read(cx), cx); self.hovered_url = None; self.active_editor = Some(EditorState { editor, _subscription: subscription, }); self.update_markdown_from_active_editor(false, true, window, cx); if had_active_editor { cx.emit(MarkdownPreviewEvent::SourceEditorChanged); } } fn on_workspace_event( &mut self, workspace: &Entity<Workspace>, event: &workspace::Event, window: &mut Window, cx: &mut Context<Self>, ) { if !matches!( event, workspace::Event::ItemAdded { .. } | workspace::Event::ItemRemoved { .. } ) { return; } let candidate = self.find_canonical_editor(workspace.read(cx), cx); if let Some(editor) = candidate && self .active_editor .as_ref() .is_none_or(|s| s.editor != editor) { self.set_editor(editor, window, cx);