3464 lines · 121.8 KB · Rust
RawDownload
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.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t