Something went wrong. Try again.
A local-first note taking app
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118import * 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<DocumentHandle> { return this.openLocalDocument(entryId); }
async createDocument(options?: { id?: string }): Promise<DocumentHandle> { 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<DocumentHandle> { // 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, }), }; }}