import { useEffect, useRef } from 'react' import { EditorState } from '@codemirror/state' import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter } from '@codemirror/view' import { defaultKeymap, history, historyKeymap } from '@codemirror/commands' import { markdown } from '@codemirror/lang-markdown' import { syntaxHighlighting, defaultHighlightStyle, bracketMatching } from '@codemirror/language' import { oneDark } from '@codemirror/theme-one-dark' import { livePreview } from './livePreview' import { frontmatterHide, findFrontmatter } from './frontmatterHide' import { wikilinks } from './wikilinks' import type { VaultEntry } from '../types' import './Editor.css' interface Tab { entry: VaultEntry content: string } interface EditorProps { tabs: Tab[] activeTabPath: string | null onSwitchTab: (path: string) => void onCloseTab: (path: string) => void onNavigateWikilink: (target: string) => void } const editorTheme = EditorView.theme({ '&': { height: '100%', fontSize: '14px', }, '.cm-scroller': { fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', padding: '16px 0', }, '.cm-content': { padding: '0 24px', maxWidth: '800px', }, '.cm-gutters': { background: '#0f0f1a', border: 'none', color: '#444', }, '.cm-activeLineGutter': { background: '#1a1a2e', }, '.cm-activeLine': { background: '#1a1a2e', }, '.cm-cursor': { borderLeftColor: '#e0e0e0', }, '.cm-selectionBackground': { background: '#2a2a5a !important', }, '&.cm-focused .cm-selectionBackground': { background: '#2a2a5a !important', }, }) export function Editor({ tabs, activeTabPath, onSwitchTab, onCloseTab, onNavigateWikilink }: EditorProps) { const containerRef = useRef(null) const viewRef = useRef(null) const navigateRef = useRef(onNavigateWikilink) navigateRef.current = onNavigateWikilink const activeTab = tabs.find((t) => t.entry.path === activeTabPath) ?? null // Create/destroy editor view when active tab changes useEffect(() => { if (!containerRef.current || !activeTab) return // If view already exists for this tab, skip if (viewRef.current) { viewRef.current.destroy() viewRef.current = null } // Place cursor after frontmatter so it starts hidden const fmRange = findFrontmatter(activeTab.content) const initialCursor = fmRange ? fmRange[1] : 0 const state = EditorState.create({ doc: activeTab.content, selection: { anchor: initialCursor }, extensions: [ lineNumbers(), highlightActiveLine(), highlightActiveLineGutter(), history(), bracketMatching(), markdown(), syntaxHighlighting(defaultHighlightStyle, { fallback: true }), oneDark, editorTheme, livePreview(), frontmatterHide(), wikilinks((target) => navigateRef.current(target)), keymap.of([...defaultKeymap, ...historyKeymap]), EditorView.lineWrapping, ], }) const view = new EditorView({ state, parent: containerRef.current, }) viewRef.current = view return () => { view.destroy() viewRef.current = null } // Re-create when active tab path changes OR when tab data becomes available // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeTabPath, activeTab?.content]) if (tabs.length === 0) { return (

Select a note to start editing

) } return (
{tabs.map((tab) => (
onSwitchTab(tab.entry.path)} > {tab.entry.title}
))}
) }