import * as Automerge from '@automerge/automerge'; import { createAutomergeBinding } from '../../editors/automerge/automergeEditorBinding'; import type { DocumentHandle, DocumentProvider } from '../types'; import { basename, join } from '../../filesystem/vaultFs'; import type { FilesystemApi } from '../../filesystem/types'; import { isTextFile } from '../../vault/utils/fileType'; export type VaultDocumentProviderOptions = { fs: FilesystemApi; /** Absolute path to the vault root. Used for creating new untitled documents. */ rootPath: string; }; /** * Document provider for vault files. * Reads/writes files via IPC to the main process. * Uses an in-memory Automerge doc for editing (no CRDT persistence or sync). * Synced vaults are not yet implemented. */ export class VaultDocumentProvider implements DocumentProvider { readonly id = 'vault'; private fs: FilesystemApi; private rootPath: string; constructor(options: VaultDocumentProviderOptions) { this.fs = options.fs; this.rootPath = options.rootPath; } async openDocument(entryId: string): Promise { return this.openLocalDocument(entryId); } async createDocument(options?: { id?: string }): Promise { if (options?.id) { return this.openDocument(options.id); } // Create a new untitled file in the vault root. let fileName = 'Untitled.md'; let filePath = join(this.rootPath, fileName); let counter = 1; while (await this.fs.fileExists(filePath)) { fileName = `Untitled-${counter}.md`; filePath = join(this.rootPath, fileName); counter++; } await this.fs.writeFile(filePath, ''); return this.openLocalDocument(filePath); } private async openLocalDocument(filePath: string): Promise { // Vault editing is text-only for now. if (!isTextFile(filePath)) { throw new Error( `Cannot open non-text file "${basename(filePath)}" in the editor yet`, ); } let content = ''; try { content = await this.fs.readFile(filePath); } catch (error) { // File might not exist yet; that's okay for new files. if ( error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code !== 'ENOENT' ) { throw error; } } const name = basename(filePath); // Create an in-memory Automerge doc for editing. // Local vaults don't persist the CRDT; we just use it for the editor binding. let doc = Automerge.from<{ name: string; content: string }>({ name, content, }); const listeners = new Set<() => void>(); const applyChange = (fn: (d: typeof doc) => void) => { const newDoc = Automerge.change(doc, fn); if (newDoc !== doc) { doc = newDoc; // Write plain text back to disk via IPC (no CRDT persistence) this.fs.writeFile(filePath, doc.content).catch(() => {}); for (const listener of listeners) { listener(); } } }; return { id: filePath, loadPromise: Promise.resolve(), getTitle: () => name, subscribeTitle() { return () => {}; }, release: () => {}, getEditorBinding: () => createAutomergeBinding({ getDoc: () => doc, applyChange, subscribeToChanges: (cb) => { listeners.add(cb); return () => listeners.delete(cb); }, caretProvider: null, }), }; } }