// src/plugin/views/markdown-view.tsx import type { ReactNode } from 'react' import { Profiler } from 'react' import { TextFileView } from './text-file-view' import { CM6EditorAdapter } from '../cm6-editor' import type { Editor } from '../editor' import type { WorkspaceLeaf } from '../workspace' import type { MarkdownFileInfo } from '../obsidian-cm6-stubs' import type { MarkdownSubView, HoverPopover } from '../obsidian-stubs' import { MarkdownPreviewView } from '../obsidian-stubs' import type { ReactItemView } from './item-view' import { MarkdownLeafBinding } from './markdown-leaf-binding' import { onRenderProfiler } from '../../lib/perf-profile' /** * View mode type. */ export type MarkdownViewModeType = 'source' | 'preview' /** * Internal WeakMaps for private data that should not appear in declarations. * Using WeakMap ensures: * 1. No visible members in the emitted .d.ts * 2. Proper garbage collection when MarkdownView instances are collected * 3. Private data access only within this module */ const modeMap = new WeakMap() const cm6EditorMap = new WeakMap() /** * Markdown document view. * Provides editor access and file reference. * * The editor is a CM6EditorAdapter. It starts unbound (no EditorView) * and is wired by host-only helper functions. * * Implements `MarkdownFileInfo` for compatibility with Obsidian plugins * that check for this interface on editor contexts. * * @public */ export class MarkdownView extends TextFileView implements MarkdownFileInfo, ReactItemView { /** * Editor instance — typed as abstract Editor for Obsidian-compatible API. * @public */ editor: Editor /** * The preview mode subview. * @public */ previewMode: MarkdownPreviewView /** * The current subview (source or preview). * @public */ currentMode: MarkdownSubView /** * Hover popover for link previews. * @public */ hoverPopover: HoverPopover | null = null constructor(leaf: WorkspaceLeaf) { super(leaf) const cm6Editor = new CM6EditorAdapter() cm6EditorMap.set(this, cm6Editor) modeMap.set(this, 'source') this.editor = cm6Editor // Initialize previewMode with a stub MarkdownPreviewView this.previewMode = new MarkdownPreviewView(this.containerEl) // currentMode starts as previewMode (or could be a source-mode stub) // For now, use previewMode as the default since it implements MarkdownSubView this.currentMode = this.previewMode } /** View type identifier */ getViewType(): string { return 'markdown' } /** Get current mode */ getMode(): MarkdownViewModeType { return modeMap.get(this) ?? 'source' } /** * Get view data (markdown content). * Returns the editor content when bound, otherwise the backing store. */ getViewData(): string { const cm6Editor = cm6EditorMap.get(this) if (cm6Editor?.isBound()) return cm6Editor.getValue() return this.data } /** Set view data. Updates the internal data store. */ setViewData(data: string, clear: boolean): void { if (clear) { this.clear() } this.data = data } /** Clear view. Resets the internal data store. */ clear(): void { this.data = '' } /** Show search in editor */ showSearch(_replace?: boolean): void { // Search implementation deferred } /** * @internal React component that owns the editor binding for this leaf. * Consumed by MainAreaViewContainer (#406) so MarkdownView participates in * the same content-type routing path as every other view. */ getReactComponent(): ReactNode { const profilerId = `MarkdownLeafBinding[${this.leaf.file?.path ?? this.leaf.id}]` return ( ) } } /** @internal Host-only mode bridge for the singleton editor architecture. */ export function setMarkdownViewMode(view: MarkdownView, mode: MarkdownViewModeType): void { modeMap.set(view, mode) } /** @internal Host-only CM6 bridge for the singleton editor architecture. */ export function attachMarkdownViewEditor(view: MarkdownView, adapter: CM6EditorAdapter): void { const editorView = adapter.getView() if (editorView) { ;(view.editor as CM6EditorAdapter).setView(editorView) } } /** @internal Host-only CM6 bridge cleanup for the singleton editor architecture. */ export function detachMarkdownViewEditor(view: MarkdownView): void { ;(view.editor as CM6EditorAdapter).clearView() }