README file from
GithubA1 File Undo
Overview & Purpose
Obsidian plugin (manifest.json: id file-undo, name File Undo, v1.0.1, minAppVersion 0.15.0, isDesktopOnly false). Sole runtime is main.js exporting A1FileUndoPlugin. Records vault rename/move and delete (files and folders; listeners do not filter by type) into in-memory undoStack / redoStack and restores them from the command palette. The only persisted key is maxHistory (default 200) in data.json. onload always resets both stacks and ignoreEventsUntil; history does not survive reload or disable. No onunload.
Scope Boundaries
| Component | Responsible For | MUST NOT Contain |
|---|---|---|
A1FileUndoPlugin (main.js) |
loadSettings / saveSettings; stacks and ignoreEventsUntil; vault listeners; commands; restore I/O |
Editor/content undo; Obsidian trash-mode policy; UI beyond Notice; persisting stacks |
A1FileUndoSettingTab (main.js) |
Tab "A1 File Undo Settings"; clamp maxHistory 10–999; persist; FIFO-trim undoStack |
Trimming redoStack; vault rename/delete; restore |
Vault rename / delete handlers (onload) |
Record payloads; skip .trash/ and an open ignore window; redoStack = []; FIFO-trim undo |
Performing restore |
Key Invariants
undoLastAction/redoLastActionassignignoreEventsUntil = Date.now() + 1000infinallyafter awaited I/O, not before. Avault.renamerestore can emitrenameduring theawait; that event is skipped only if a previous window is still open. (Why chosen over wrapping I/O or source-tagged events: Obsidian vault events do not distinguish plugin vs user origin; this file sets the flag infinally.)- Record never pushes paths under
.trash/(rename also checksoldPath). Delete restore uses adapter path.trash/+action.name(basename), not the original full path. (Why chosen over treating trash as a first-class action or storing a unique trash location:.trash/is the restore source, and the recorder never saves a trash path.) - Any recorded user rename/delete sets
redoStack = []. SettingsonChangetrimsundoStackonly. (Why chosen over a branching history or trimming both stacks: linear undo/redo matches editor semantics; redo is not a FIFO log.)
Numbered Data Flow
onload()→loadSettings()(Object.assign({}, DEFAULT_SETTINGS, await this.loadData()));undoStack = [],redoStack = [],ignoreEventsUntil = 0;addSettingTab(A1FileUndoSettingTab);registerEventonapp.vaultrename/delete;addCommand×3;console.log("A1 File Undo Plugin loaded!").- Rename listener
(file, oldPath): return ifDate.now() < ignoreEventsUntilorfile.path/oldPathstarts with.trash/; else push{type:'rename', newPath:file.path, oldPath},redoStack = [],shift()whilelength > settings.maxHistory. - Delete listener
(file): same ignore; skip iffile.pathstarts with.trash/; push{type:'delete', path:file.path, name:file.name}; clear redo; FIFO-trim. - Empty undo/redo: Notice and return (no ignore window). Otherwise pop first, then I/O. Missing file/trash: Notice; the popped action is not re-queued;
finallystill opens the 1s window. - Rename undo:
getAbstractFileByPath(action.newPath)thenvault.rename(file, action.oldPath). Delete undo: ifadapter.exists(".trash/"+action.name),mkdirmissing parent ofaction.path,adapter.renametrash → original path. Success:redoStack.push(action)+ Notice.catch/finallysame as redo (step 6). - Rename redo:
getAbstractFileByPath(action.oldPath)thenvault.rename(file, action.newPath). Delete redo: ifadapter.exists(action.path),adapter.renameto.trash/+action.name(does notmkdir.trash). Success:undoStack.push(action)+ Notice.catch:console.error+ Notice.finally:ignoreEventsUntil = Date.now() + 1000. dump-commands: Nodefs.writeFileSyncofapp.commands.commandstocommands_dump.txtatadapter.getBasePath().
Side-effects API
| Method | Signature | Side-Effects |
|---|---|---|
onload |
async onload() |
Load settings; reset stacks/ignore; register events/commands/tab; console.log |
loadSettings |
async loadSettings() |
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()) |
saveSettings |
async saveSettings() |
await this.saveData(this.settings) (settings only, never stacks) |
A1FileUndoSettingTab.display |
display() |
Build UI. Mutations in text onChange: parseInt (NaN→200), clamp 10–999, saveSettings(), undoStack.shift() while over limit |
| rename listener | (file, oldPath) => void |
Push undo; redoStack = []; FIFO trim |
| delete listener | (file) => void |
Push undo; redoStack = []; FIFO trim |
undoLastAction |
async undoLastAction() |
Pop undo; vault.rename or adapter mkdir/rename from .trash/<name>; push redo on success; Notices; finally ignore 1s |
redoLastAction |
async redoLastAction() |
Pop redo; vault.rename or adapter rename to .trash/<name>; push undo on success; Notices; finally ignore 1s |
| dump-commands | async () => void |
Node fs.writeFileSync vault-root commands_dump.txt; Notice "Commands dumped to commands_dump.txt" |
Recipes
- Enable — Community plugins → File Undo (
file-undo). Bind hotkeys in Settings → Hotkeys;addCommandregisters none. History is empty until the next vault rename/delete in this session. - Undo a move — Command palette "Undo last file move or deletion" (
id: undo-last-file-action) →A1FileUndoPlugin.undoLastActioninmain.js(rename branch). Notices use prefixesA1:/A1 Error:. - Undo a deletion — Same command, delete branch: restores
.trash/<file.name>toaction.pathvia adapter (creates parent dirs if missing). Requires Obsidian Deleted files = vault.trashfolder, not system/OS trash. Same-basename entries in.trash/collide. - Redo — "Redo last undone file move or deletion" (
id: redo-last-file-action) →redoLastAction. Delete redo does not create.trash/if missing. - Cap history — Settings → A1 File Undo Settings → Max History Length (placeholder 200, min 10, max 999).
display()onChangeclamps,saveSettings(), trimsundoStackonly. Reload/disable clears both stacks. - Dump command list — "A1 Debug: Dump all commands" (
dump-commands) writescommands_dump.txtat the vault base path (Nodefs/ Electron;isDesktopOnlyis false). MarkedTEMPORARY DIAGNOSTIC COMMANDin source; still registered.