File Undo

approved

by Jeff1024

Undo recent file deletions and moves (renames). - This plugin has not been manually reviewed by Obsidian staff.

163 downloadsUpdated 1mo agoMIT

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

ComponentResponsible ForMUST NOT Contain
A1FileUndoPlugin (main.js)loadSettings / saveSettings; stacks and ignoreEventsUntil; vault listeners; commands; restore I/OEditor/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 undoStackTrimming redoStack; vault rename/delete; restore
Vault rename / delete handlers (onload)Record payloads; skip .trash/ and an open ignore window; redoStack = []; FIFO-trim undoPerforming restore

Key Invariants

  1. undoLastAction / redoLastAction assign ignoreEventsUntil = Date.now() + 1000 in finally after awaited I/O, not before. A vault.rename restore can emit rename during the await; 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 in finally.)
  2. Record never pushes paths under .trash/ (rename also checks oldPath). 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.)
  3. Any recorded user rename/delete sets redoStack = []. Settings onChange trims undoStack only. (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

  1. onload()loadSettings() (Object.assign({}, DEFAULT_SETTINGS, await this.loadData())); undoStack = [], redoStack = [], ignoreEventsUntil = 0; addSettingTab(A1FileUndoSettingTab); registerEvent on app.vault rename / delete; addCommand ×3; console.log("A1 File Undo Plugin loaded!").
  2. Rename listener (file, oldPath): return if Date.now() < ignoreEventsUntil or file.path / oldPath starts with .trash/; else push {type:'rename', newPath:file.path, oldPath}, redoStack = [], shift() while length > settings.maxHistory.
  3. Delete listener (file): same ignore; skip if file.path starts with .trash/; push {type:'delete', path:file.path, name:file.name}; clear redo; FIFO-trim.
  4. 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; finally still opens the 1s window.
  5. Rename undo: getAbstractFileByPath(action.newPath) then vault.rename(file, action.oldPath). Delete undo: if adapter.exists(".trash/"+action.name), mkdir missing parent of action.path, adapter.rename trash → original path. Success: redoStack.push(action) + Notice. catch / finally same as redo (step 6).
  6. Rename redo: getAbstractFileByPath(action.oldPath) then vault.rename(file, action.newPath). Delete redo: if adapter.exists(action.path), adapter.rename to .trash/+action.name (does not mkdir .trash). Success: undoStack.push(action) + Notice. catch: console.error + Notice. finally: ignoreEventsUntil = Date.now() + 1000.
  7. dump-commands: Node fs.writeFileSync of app.commands.commands to commands_dump.txt at adapter.getBasePath().

Side-effects API

MethodSignatureSide-Effects
onloadasync onload()Load settings; reset stacks/ignore; register events/commands/tab; console.log
loadSettingsasync loadSettings()this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData())
saveSettingsasync saveSettings()await this.saveData(this.settings) (settings only, never stacks)
A1FileUndoSettingTab.displaydisplay()Build UI. Mutations in text onChange: parseInt (NaN→200), clamp 10–999, saveSettings(), undoStack.shift() while over limit
rename listener(file, oldPath) => voidPush undo; redoStack = []; FIFO trim
delete listener(file) => voidPush undo; redoStack = []; FIFO trim
undoLastActionasync undoLastAction()Pop undo; vault.rename or adapter mkdir/rename from .trash/<name>; push redo on success; Notices; finally ignore 1s
redoLastActionasync redoLastAction()Pop redo; vault.rename or adapter rename to .trash/<name>; push undo on success; Notices; finally ignore 1s
dump-commandsasync () => voidNode fs.writeFileSync vault-root commands_dump.txt; Notice "Commands dumped to commands_dump.txt"

Recipes

  1. Enable — Community plugins → File Undo (file-undo). Bind hotkeys in Settings → Hotkeys; addCommand registers none. History is empty until the next vault rename/delete in this session.
  2. Undo a move — Command palette "Undo last file move or deletion" (id: undo-last-file-action) → A1FileUndoPlugin.undoLastAction in main.js (rename branch). Notices use prefixes A1: / A1 Error:.
  3. Undo a deletion — Same command, delete branch: restores .trash/<file.name> to action.path via adapter (creates parent dirs if missing). Requires Obsidian Deleted files = vault .trash folder, not system/OS trash. Same-basename entries in .trash/ collide.
  4. Redo — "Redo last undone file move or deletion" (id: redo-last-file-action) → redoLastAction. Delete redo does not create .trash/ if missing.
  5. Cap history — Settings → A1 File Undo Settings → Max History Length (placeholder 200, min 10, max 999). display() onChange clamps, saveSettings(), trims undoStack only. Reload/disable clears both stacks.
  6. Dump command list — "A1 Debug: Dump all commands" (dump-commands) writes commands_dump.txt at the vault base path (Node fs / Electron; isDesktopOnly is false). Marked TEMPORARY DIAGNOSTIC COMMAND in 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.