import { useRef, useEffect, useCallback, memo } from 'react' import { useEditorTabSwap, getH1TextFromBlocks } from '../hooks/useEditorTabSwap' import { useHeadingTitleSync } from '../hooks/useHeadingTitleSync' import { useCreateBlockNote } from '@blocknote/react' import '@blocknote/mantine/style.css' import { uploadImageFile } from '../hooks/useImageDrop' import type { VaultEntry, GitCommit, NoteStatus } from '../types' import type { FrontmatterValue } from './Inspector' import { ResizeHandle } from './ResizeHandle' import { TabBar } from './TabBar' import { useDiffMode } from '../hooks/useDiffMode' import { useRawMode } from '../hooks/useRawMode' import { useEditorFocus } from '../hooks/useEditorFocus' import { EditorRightPanel } from './EditorRightPanel' import { EditorContent } from './EditorContent' import { schema } from './editorSchema' import './Editor.css' import './EditorTheme.css' interface Tab { entry: VaultEntry content: string } interface EditorProps { tabs: Tab[] activeTabPath: string | null entries: VaultEntry[] onSwitchTab: (path: string) => void onCloseTab: (path: string) => void onReorderTabs?: (fromIndex: number, toIndex: number) => void onNavigateWikilink: (target: string) => void onLoadDiff?: (path: string) => Promise onLoadDiffAtCommit?: (path: string, commitHash: string) => Promise getNoteStatus?: (path: string) => NoteStatus onCreateNote?: () => void inspectorCollapsed: boolean onToggleInspector: () => void inspectorWidth: number onInspectorResize: (delta: number) => void inspectorEntry: VaultEntry | null inspectorContent: string | null allContent: Record gitHistory: GitCommit[] onUpdateFrontmatter?: (path: string, key: string, value: FrontmatterValue) => Promise onDeleteProperty?: (path: string, key: string) => Promise onAddProperty?: (path: string, key: string, value: FrontmatterValue) => Promise showAIChat?: boolean onToggleAIChat?: () => void vaultPath?: string onTrashNote?: (path: string) => void onRestoreNote?: (path: string) => void onArchiveNote?: (path: string) => void onUnarchiveNote?: (path: string) => void onRenameTab?: (path: string, newTitle: string) => void onContentChange?: (path: string, content: string) => void onSave?: () => void /** Called when H1→title sync updates the title (debounced). */ onTitleSync?: (path: string, newTitle: string) => void canGoBack?: boolean canGoForward?: boolean onGoBack?: () => void onGoForward?: () => void leftPanelsCollapsed?: boolean isDarkTheme?: boolean /** Mutable ref that Editor registers its raw-mode toggle into, for command palette access. */ rawToggleRef?: React.MutableRefObject<() => void> } function useEditorModeExclusion({ diffMode, rawMode, handleToggleDiff, handleToggleRaw, rawToggleRef, }: { diffMode: boolean rawMode: boolean handleToggleDiff: () => void | Promise handleToggleRaw: () => void rawToggleRef?: React.MutableRefObject<() => void> }) { const handleToggleDiffExclusive = useCallback(async () => { if (!diffMode && rawMode) handleToggleRaw() await handleToggleDiff() }, [diffMode, rawMode, handleToggleDiff, handleToggleRaw]) const handleToggleRawExclusive = useCallback(() => { if (!rawMode && diffMode) handleToggleDiff() handleToggleRaw() }, [rawMode, diffMode, handleToggleDiff, handleToggleRaw]) useEffect(() => { if (rawToggleRef) rawToggleRef.current = handleToggleRawExclusive }, [rawToggleRef, handleToggleRawExclusive]) return { handleToggleDiffExclusive, handleToggleRawExclusive } } function EditorEmptyState() { return (

Select a note to start editing

Cmd+P to search · Cmd+N to create
) } export const Editor = memo(function Editor({ tabs, activeTabPath, entries, onSwitchTab, onCloseTab, onReorderTabs, onNavigateWikilink, onLoadDiff, onLoadDiffAtCommit, getNoteStatus, onCreateNote, inspectorCollapsed, onToggleInspector, inspectorWidth, onInspectorResize, inspectorEntry, inspectorContent, allContent, gitHistory, onUpdateFrontmatter, onDeleteProperty, onAddProperty, showAIChat, onToggleAIChat, vaultPath, onTrashNote, onRestoreNote, onArchiveNote, onUnarchiveNote, onRenameTab, onContentChange, onSave, onTitleSync, canGoBack, canGoForward, onGoBack, onGoForward, leftPanelsCollapsed, isDarkTheme, rawToggleRef, }: EditorProps) { const vaultPathRef = useRef(vaultPath) useEffect(() => { vaultPathRef.current = vaultPath }, [vaultPath]) const editor = useCreateBlockNote({ schema, uploadFile: (file: File) => uploadImageFile(file, vaultPathRef.current), }) const activeTab = tabs.find((t) => t.entry.path === activeTabPath) ?? null const { syncActiveRef, onH1Changed, onManualRename } = useHeadingTitleSync({ activeTabPath, currentTitle: activeTab?.entry.title ?? null, onTitleSync: onTitleSync ?? (() => {}), }) const { handleEditorChange, editorMountedRef } = useEditorTabSwap({ tabs, activeTabPath, editor, onContentChange, onH1Change: onH1Changed, syncActiveRef, }) useEditorFocus(editor, editorMountedRef) const handleRenameTabWithSync = useCallback((path: string, newTitle: string) => { const h1Text = getH1TextFromBlocks(editor.document) onManualRename(newTitle, h1Text) onRenameTab?.(path, newTitle) }, [editor, onManualRename, onRenameTab]) const { diffMode, diffContent, diffLoading, handleToggleDiff, handleViewCommitDiff } = useDiffMode({ activeTabPath, onLoadDiff, onLoadDiffAtCommit, }) const { rawMode, handleToggleRaw } = useRawMode({ activeTabPath }) const { handleToggleDiffExclusive, handleToggleRawExclusive } = useEditorModeExclusion({ diffMode, rawMode, handleToggleDiff, handleToggleRaw, rawToggleRef, }) const isLoadingNewTab = activeTabPath !== null && !activeTab const activeStatus = activeTab ? getNoteStatus?.(activeTab.entry.path) ?? 'clean' : 'clean' const showDiffToggle = !!(activeTab && (diffMode || activeStatus === 'modified')) const showRightPanel = !!(showAIChat || !inspectorCollapsed) return (
{tabs.length === 0 ? : } {showRightPanel && }
) })