File Undo
approvedby Jeff1024
Undo recent file deletions and moves (renames). - This plugin has not been manually reviewed by Obsidian staff.
A1 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.
For plugin developers
Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.