import type { Stat, Vault, Workspace } from "obsidian"; import { MarkdownView, normalizePath } from "obsidian"; import type { CursorPosition, TextWithCursors } from "sync-client"; import { utils, type FileSystemOperations, type RelativePath } from "sync-client"; import { getSelectionsFromEditor } from "./views/cursors/get-selections-from-editor"; export class ObsidianFileSystemOperations implements FileSystemOperations { public constructor( private readonly vault: Vault, private readonly workspace: Workspace ) {} public async listFilesRecursively( root: RelativePath | undefined ): Promise { // Let's implement this by hand because vault.adapter.listAllFiles doesn't always return all files. const allFiles = []; const remainingFolders = [root ?? this.vault.getRoot().path]; // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition while (true) { const folder = remainingFolders.pop(); if (folder == undefined) { break; } // This would be a very bad idea to sync as it would mess with // the integrity of the sync database. if (folder.endsWith(".obsidian/plugins/vault-link/data.json")) { continue; } const files = await this.vault.adapter.list(normalizePath(folder)); allFiles.push(...files.files); remainingFolders.push(...files.folders); } return allFiles; } public async read(path: RelativePath): Promise { path = normalizePath(path); const view = this.workspace.getActiveViewOfType(MarkdownView); if (view?.file?.path === path) { return new TextEncoder().encode(view.editor.getValue()); } return new Uint8Array(await this.vault.adapter.readBinary(path)); } public async write(path: RelativePath, content: Uint8Array): Promise { path = normalizePath(path); const view = this.workspace.getActiveViewOfType(MarkdownView); if (view?.file?.path === path) { const position = view.editor.getCursor(); view.editor.setValue(new TextDecoder().decode(content)); view.editor.setCursor(position); return; } return this.vault.adapter.writeBinary( path, // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion content.buffer as ArrayBuffer ); } public async atomicUpdateText( path: RelativePath, updater: (current: TextWithCursors) => TextWithCursors ): Promise { path = normalizePath(path); const view = this.workspace.getActiveViewOfType(MarkdownView); if (view?.file?.path === path) { const text = view.editor.getValue(); const cursors: CursorPosition[] = getSelectionsFromEditor( view.editor ).flatMap(({ id, start: anchor, end: head }) => [ { id: 2 * id, position: anchor }, { id: 2 * id + 1, position: head } ]); const result = updater({ text, cursors }); if (result.text === text) { return text; } view.editor.setValue(result.text); const selections = []; for (let i = 0; i < result.cursors.length / 2; i++) { const from = result.cursors[2 * i]; const to = result.cursors[2 * i + 1]; const { line: fromLine, column: fromColumn } = utils.positionToLineAndColumn(result.text, from.position); const { line: toLine, column: toColumn } = utils.positionToLineAndColumn(result.text, to.position); selections.push({ anchor: { line: fromLine, ch: fromColumn }, head: { line: toLine, ch: toColumn } }); } view.editor.setSelections(selections); return result.text; } return this.vault.adapter.process( path, (text) => updater({ text, cursors: [] }).text ); } public async getFileSize(path: RelativePath): Promise { return (await this.statFile(path)).size; } public async getModificationTime(path: RelativePath): Promise { return new Date((await this.statFile(path)).mtime); } public async exists(path: RelativePath): Promise { return this.vault.adapter.exists(normalizePath(path)); } public async createDirectory(path: RelativePath): Promise { return this.vault.adapter.mkdir(normalizePath(path)); } public async delete(path: RelativePath): Promise { if (!(await this.vault.adapter.trashSystem(normalizePath(path)))) { return this.vault.adapter.remove(normalizePath(path)); } } public async rename( oldPath: RelativePath, newPath: RelativePath ): Promise { return this.vault.adapter.rename(oldPath, newPath); } private async statFile(path: string): Promise { const file = await this.vault.adapter.stat(normalizePath(path)); if (!file) { throw new Error(`File not found: ${path}`); } return file; } }