Mercurial > obsidian-remember-file-state
comparison src/main.ts @ 0:7975d7c73f8a 1.0.0
Initial commit
| author | Ludovic Chabant <ludovic@chabant.com> |
|---|---|
| date | Tue, 08 Feb 2022 21:40:33 -0800 |
| parents | |
| children | f3297d90329d |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:7975d7c73f8a |
|---|---|
| 1 import { | |
| 2 Editor, | |
| 3 MarkdownView, | |
| 4 OpenViewState, | |
| 5 Plugin, | |
| 6 View | |
| 7 } from 'obsidian'; | |
| 8 | |
| 9 import { | |
| 10 EditorSelection | |
| 11 } from '@codemirror/state'; | |
| 12 | |
| 13 import { | |
| 14 around | |
| 15 } from 'monkey-around'; | |
| 16 | |
| 17 import { | |
| 18 DEFAULT_SETTINGS, | |
| 19 RememberFileStatePluginSettings, | |
| 20 RememberFileStatePluginSettingTab | |
| 21 } from './settings'; | |
| 22 | |
| 23 // Interface for a file state. | |
| 24 interface RememberedFileState { | |
| 25 path: string; | |
| 26 lastSavedTime: number; | |
| 27 stateData: Object; | |
| 28 } | |
| 29 | |
| 30 // Interface for all currently remembered file states. | |
| 31 interface RememberFileStatePluginData { | |
| 32 rememberedFiles: RememberedFileState[]; | |
| 33 } | |
| 34 | |
| 35 // Default empty list of remembered file states. | |
| 36 const DEFAULT_DATA: RememberFileStatePluginData = { | |
| 37 rememberedFiles: [] | |
| 38 }; | |
| 39 | |
| 40 export default class RememberFileStatePlugin extends Plugin { | |
| 41 settings: RememberFileStatePluginSettings; | |
| 42 data: RememberFileStatePluginData; | |
| 43 | |
| 44 private _suppressNextFileOpen: boolean = false; | |
| 45 | |
| 46 private _viewUninstallers = {}; | |
| 47 private _globalUninstallers = []; | |
| 48 | |
| 49 async onload() { | |
| 50 console.log("Loading RememberFileState plugin"); | |
| 51 | |
| 52 await this.loadSettings(); | |
| 53 | |
| 54 this.data = Object.assign({}, DEFAULT_DATA); | |
| 55 | |
| 56 this.registerEvent(this.app.workspace.on('file-open', this.onFileOpen)); | |
| 57 this.registerEvent(this.app.vault.on('rename', this.onFileRename)); | |
| 58 this.registerEvent(this.app.vault.on('delete', this.onFileDelete)); | |
| 59 | |
| 60 this.app.workspace.getLeavesOfType("markdown").forEach( | |
| 61 (leaf) => { this.registerOnUnloadFile(leaf.view); }); | |
| 62 | |
| 63 const _this = this; | |
| 64 var uninstall = around(this.app.workspace, { | |
| 65 openLinkText: function(next) { | |
| 66 return async function( | |
| 67 linktext: string, sourcePath: string, | |
| 68 newLeaf?: boolean, openViewState?: OpenViewState) { | |
| 69 // When opening a link, we don't want to restore the | |
| 70 // scroll position/selection/etc because there's a | |
| 71 // good chance we want to show the file back at the | |
| 72 // top, or we're going straight to a specific block. | |
| 73 _this._suppressNextFileOpen = true; | |
| 74 return await next.call( | |
| 75 this, linktext, sourcePath, newLeaf, openViewState); | |
| 76 }; | |
| 77 } | |
| 78 }); | |
| 79 this._globalUninstallers.push(uninstall); | |
| 80 | |
| 81 this.addSettingTab(new RememberFileStatePluginSettingTab(this.app, this)); | |
| 82 } | |
| 83 | |
| 84 onunload() { | |
| 85 var uninstallers = this._viewUninstallers.values(); | |
| 86 console.debug(`Unregistering ${uninstallers.length} view callbacks`); | |
| 87 uninstallers.values().forEach((cb) => cb()); | |
| 88 this._viewUninstallers = {}; | |
| 89 | |
| 90 this._globalUninstallers.forEach((cb) => cb()); | |
| 91 } | |
| 92 | |
| 93 async loadSettings() { | |
| 94 this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); | |
| 95 } | |
| 96 | |
| 97 async saveSettings() { | |
| 98 await this.saveData(this.settings); | |
| 99 } | |
| 100 | |
| 101 private readonly registerOnUnloadFile = function(view) { | |
| 102 if (view in this._viewUninstallers) { | |
| 103 return; | |
| 104 } | |
| 105 | |
| 106 console.debug("Registering view callback"); | |
| 107 const _this = this; | |
| 108 var uninstall = around(view, { | |
| 109 onUnloadFile: function(next) { | |
| 110 return async function (unloaded: TFile) { | |
| 111 _this.rememberFileState(unloaded, view); | |
| 112 return await next.call(this, unloaded); | |
| 113 }; | |
| 114 } | |
| 115 }); | |
| 116 this._viewUninstallers[view] = uninstall; | |
| 117 | |
| 118 view.register(() => { | |
| 119 console.debug("Unregistering view callback"); | |
| 120 delete this._viewUninstallers[view]; | |
| 121 uninstall(); | |
| 122 }); | |
| 123 } | |
| 124 | |
| 125 private readonly onFileOpen = async ( | |
| 126 openedFile: TFile | |
| 127 ): Promise<void> => { | |
| 128 // If `openedFile` is null, it's because the last pane was closed | |
| 129 // and there is now an empty pane. | |
| 130 if (openedFile) { | |
| 131 var activeView = this.app.workspace.activeLeaf.view; | |
| 132 this.registerOnUnloadFile(activeView); | |
| 133 | |
| 134 if (!this._suppressNextFileOpen) { | |
| 135 await this.restoreFileState(openedFile, activeView); | |
| 136 } else { | |
| 137 this._suppressNextFileOpen = false; | |
| 138 } | |
| 139 } | |
| 140 } | |
| 141 | |
| 142 private readonly rememberFileState = async (file: TFile, view: View): Promise<void> => { | |
| 143 const scrollInfo = view.editor.getScrollInfo(); | |
| 144 const stateSelectionJSON = view.editor.cm.state.selection.toJSON(); | |
| 145 const stateData = {'scrollInfo': scrollInfo, 'selection': stateSelectionJSON}; | |
| 146 | |
| 147 var existingFile = this.data.rememberedFiles.find( | |
| 148 curFile => curFile.path == file.path | |
| 149 ); | |
| 150 | |
| 151 if (existingFile) { | |
| 152 existingFile.lastSavedTime = Date.now(); | |
| 153 existingFile.stateData = stateData; | |
| 154 } else { | |
| 155 let newFileState = { | |
| 156 path: file.path, | |
| 157 lastSavedTime: Date.now(), | |
| 158 stateData: stateData | |
| 159 }; | |
| 160 this.data.rememberedFiles.push(newFileState); | |
| 161 | |
| 162 // If we need to keep the number remembered files under a maximum, | |
| 163 // do it now. | |
| 164 this.forgetExcessFiles(); | |
| 165 } | |
| 166 console.debug("Remember file state for:", file.path); | |
| 167 } | |
| 168 | |
| 169 private readonly restoreFileState = async (file: TFile, view: View): Promise<void> => { | |
| 170 const existingFile = this.data.rememberedFiles.find( | |
| 171 (curFile) => curFile.path === file.path | |
| 172 ); | |
| 173 if (existingFile) { | |
| 174 console.debug("Restoring file state for:", file.path); | |
| 175 const stateData = existingFile.stateData; | |
| 176 view.editor.scrollTo(stateData.scrollInfo.left, stateData.scrollInfo.top); | |
| 177 view.editor.cm.state.selection = EditorSelection.fromJSON(stateData.selection); | |
| 178 } | |
| 179 } | |
| 180 | |
| 181 private forgetExcessFiles() { | |
| 182 const keepMax = this.settings.rememberMaxFiles; | |
| 183 if (keepMax <= 0) { | |
| 184 return; | |
| 185 } | |
| 186 | |
| 187 this.data.rememberedFiles.sort((a, b) => a.lastSavedTime < b.lastSavedTime); | |
| 188 | |
| 189 if (this.data.rememberedFiles.length > keepMax) { | |
| 190 this.data.rememberedFiles.splice(keepMax); | |
| 191 } | |
| 192 } | |
| 193 | |
| 194 private readonly onFileRename = async ( | |
| 195 file: TAbstractFile, | |
| 196 oldPath: string, | |
| 197 ): Promise<void> => { | |
| 198 const existingFile = this.data.rememberedFiles.find( | |
| 199 (curFile) => curFile.path === oldPath | |
| 200 ); | |
| 201 if (existingFile) { | |
| 202 existingFile.path = file.path; | |
| 203 } | |
| 204 }; | |
| 205 | |
| 206 private readonly onFileDelete = async ( | |
| 207 file: TAbstractFile, | |
| 208 ): Promise<void> => { | |
| 209 this.data.rememberedFiles = this.data.rememberedFiles.filter( | |
| 210 (curFile) => curFile.path !== file.path | |
| 211 ); | |
| 212 }; | |
| 213 } | |
| 214 |
