Mercurial > obsidian-remember-file-state
comparison src/main.ts @ 43:7e981d54a055
Support state persistence between sessions
There is now an option to save/load file states on disk.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Wed, 20 Sep 2023 22:40:25 -0700 |
parents | aa9bc7754c5d |
children | fae202d7b3de |
comparison
equal
deleted
inserted
replaced
42:d52beb77d109 | 43:7e981d54a055 |
---|---|
5 Modal, | 5 Modal, |
6 OpenViewState, | 6 OpenViewState, |
7 Plugin, | 7 Plugin, |
8 TAbstractFile, | 8 TAbstractFile, |
9 TFile, | 9 TFile, |
10 Tasks, | |
10 View, | 11 View, |
11 WorkspaceLeaf | 12 WorkspaceLeaf |
12 } from 'obsidian'; | 13 } from 'obsidian'; |
13 | 14 |
14 import { | 15 import { |
67 // Default empty list of remembered file states. | 68 // Default empty list of remembered file states. |
68 const DEFAULT_DATA: RememberFileStatePluginData = { | 69 const DEFAULT_DATA: RememberFileStatePluginData = { |
69 rememberedFiles: {} | 70 rememberedFiles: {} |
70 }; | 71 }; |
71 | 72 |
73 // Where to save the states database. | |
74 const STATE_DB_PATH: string = '.obsidian/plugins/obsidian-remember-file-state/states.json'; | |
75 | |
72 // Simple warning message. | 76 // Simple warning message. |
73 class WarningModal extends Modal { | 77 class WarningModal extends Modal { |
74 title: string = ""; | 78 title: string = ""; |
75 message: string = ""; | 79 message: string = ""; |
76 | 80 |
107 | 111 |
108 await this.loadSettings(); | 112 await this.loadSettings(); |
109 | 113 |
110 this.data = Object.assign({}, DEFAULT_DATA); | 114 this.data = Object.assign({}, DEFAULT_DATA); |
111 | 115 |
116 await this.readStateDatabase(STATE_DB_PATH); | |
117 | |
112 this.registerEvent(this.app.workspace.on('file-open', this.onFileOpen)); | 118 this.registerEvent(this.app.workspace.on('file-open', this.onFileOpen)); |
119 this.registerEvent(this.app.workspace.on('quit', this.onAppQuit)); | |
113 this.registerEvent(this.app.vault.on('rename', this.onFileRename)); | 120 this.registerEvent(this.app.vault.on('rename', this.onFileRename)); |
114 this.registerEvent(this.app.vault.on('delete', this.onFileDelete)); | 121 this.registerEvent(this.app.vault.on('delete', this.onFileDelete)); |
115 | 122 |
116 this.app.workspace.getLeavesOfType("markdown").forEach( | 123 this.app.workspace.onLayoutReady(() => { this.onLayoutReady(); }); |
117 (leaf: WorkspaceLeaf) => { this.registerOnUnloadFile(leaf.view as MarkdownView); }); | |
118 | 124 |
119 const _this = this; | 125 const _this = this; |
120 var uninstall = around(this.app.workspace, { | 126 var uninstall = around(this.app.workspace, { |
121 openLinkText: function(next) { | 127 openLinkText: function(next) { |
122 return async function( | 128 return async function( |
182 | 188 |
183 async saveSettings() { | 189 async saveSettings() { |
184 await this.saveData(this.settings); | 190 await this.saveData(this.settings); |
185 } | 191 } |
186 | 192 |
193 private readonly onLayoutReady = function() { | |
194 this.app.workspace.getLeavesOfType("markdown").forEach( | |
195 (leaf: WorkspaceLeaf) => { | |
196 var view = leaf.view as MarkdownView; | |
197 | |
198 // On startup, assign unique IDs to views and register the | |
199 // unload callback to remember their state. | |
200 this.registerOnUnloadFile(view); | |
201 | |
202 // Also remember which file is opened in which view. | |
203 const viewId = this.getUniqueViewId(view as ViewWithID); | |
204 if (viewId != undefined) { | |
205 this._lastOpenFiles[viewId] = view.file.path; | |
206 } | |
207 | |
208 // Restore state for each opened pane on startup. | |
209 const existingFile = this.data.rememberedFiles[view.file.path]; | |
210 if (existingFile) { | |
211 const savedStateData = existingFile.stateData; | |
212 console.debug("RememberFileState: restoring saved state for:", view.file.path, savedStateData); | |
213 this.restoreState(savedStateData, view); | |
214 } | |
215 }); | |
216 } | |
217 | |
187 private readonly registerOnUnloadFile = function(view: MarkdownView) { | 218 private readonly registerOnUnloadFile = function(view: MarkdownView) { |
188 var filePath = view.file.path; | 219 var filePath = view.file.path; |
189 var viewId = this.getUniqueViewId(view as unknown as ViewWithID, true); | 220 var viewId = this.getUniqueViewId(view as unknown as ViewWithID, true); |
190 if (viewId in this._viewUninstallers) { | 221 if (viewId in this._viewUninstallers) { |
191 return; | 222 return; |
194 console.debug(`RememberFileState: registering callback on view ${viewId}`, filePath); | 225 console.debug(`RememberFileState: registering callback on view ${viewId}`, filePath); |
195 const _this = this; | 226 const _this = this; |
196 var uninstall = around(view, { | 227 var uninstall = around(view, { |
197 onUnloadFile: function(next) { | 228 onUnloadFile: function(next) { |
198 return async function (unloaded: TFile) { | 229 return async function (unloaded: TFile) { |
199 _this.rememberFileState(unloaded, this); | 230 _this.rememberFileState(this, unloaded); |
200 return await next.call(this, unloaded); | 231 return await next.call(this, unloaded); |
201 }; | 232 }; |
202 } | 233 } |
203 }); | 234 }); |
204 this._viewUninstallers[viewId] = uninstall; | 235 this._viewUninstallers[viewId] = uninstall; |
280 } catch (err) { | 311 } catch (err) { |
281 console.error("RememberFileState: couldn't restore file state: ", err); | 312 console.error("RememberFileState: couldn't restore file state: ", err); |
282 } | 313 } |
283 } | 314 } |
284 | 315 |
285 private readonly rememberFileState = async (file: TFile, view: MarkdownView): Promise<void> => { | 316 private readonly rememberFileState = async (view: MarkdownView, file?: TFile): Promise<void> => { |
286 const stateData = this.getState(view); | 317 const stateData = this.getState(view); |
318 | |
319 if (file === undefined) { | |
320 file = view.file; | |
321 } | |
287 var existingFile = this.data.rememberedFiles[file.path]; | 322 var existingFile = this.data.rememberedFiles[file.path]; |
288 if (existingFile) { | 323 if (existingFile) { |
289 existingFile.lastSavedTime = Date.now(); | 324 existingFile.lastSavedTime = Date.now(); |
290 existingFile.stateData = stateData; | 325 existingFile.stateData = stateData; |
291 } else { | 326 } else { |
339 var curView = leaf.view as MarkdownView; | 374 var curView = leaf.view as MarkdownView; |
340 if (curView != activeView && | 375 if (curView != activeView && |
341 curView.file.path == file.path && | 376 curView.file.path == file.path && |
342 this.getUniqueViewId(curView) >= 0 // Skip views that have never been activated. | 377 this.getUniqueViewId(curView) >= 0 // Skip views that have never been activated. |
343 ) { | 378 ) { |
344 console.debug(`FFFFOOOOOUNNNNDD!!!!! ${file.path}`, curView, activeView); | |
345 otherView = curView; | 379 otherView = curView; |
346 return false; // Stop iterating leaves. | 380 return false; // Stop iterating leaves. |
347 } | 381 } |
348 return true; | 382 return true; |
349 }, | 383 }, |
403 private readonly onFileDelete = async ( | 437 private readonly onFileDelete = async ( |
404 file: TAbstractFile, | 438 file: TAbstractFile, |
405 ): Promise<void> => { | 439 ): Promise<void> => { |
406 delete this.data.rememberedFiles[file.path]; | 440 delete this.data.rememberedFiles[file.path]; |
407 }; | 441 }; |
442 | |
443 private readonly onAppQuit = async (tasks: Tasks): Promise<void> => { | |
444 const _this = this; | |
445 tasks.addPromise( | |
446 _this.rememberAllOpenedFileStates() | |
447 .then(_this.writeStateDatabase(STATE_DB_PATH))); | |
448 } | |
449 | |
450 private readonly rememberAllOpenedFileStates = async(): Promise<void> => { | |
451 this.app.workspace.getLeavesOfType("markdown").forEach( | |
452 (leaf: WorkspaceLeaf) => { | |
453 const view = leaf.view as MarkdownView; | |
454 this.rememberFileState(view); | |
455 } | |
456 ); | |
457 } | |
458 | |
459 private readonly writeStateDatabase = async(path: string): Promise<void> => { | |
460 const fs = this.app.vault.adapter; | |
461 const jsonDb = JSON.stringify(this.data); | |
462 await fs.write(path, jsonDb); | |
463 } | |
464 | |
465 private readonly readStateDatabase = async(path: string): Promise<void> => { | |
466 const fs = this.app.vault.adapter; | |
467 if (await fs.exists(path)) { | |
468 const jsonDb = await fs.read(path); | |
469 this.data = JSON.parse(jsonDb); | |
470 const numLoaded = Object.keys(this.data.rememberedFiles).length; | |
471 console.debug(`RememberFileState: read ${numLoaded} record from state database.`); | |
472 } | |
473 } | |
408 } | 474 } |
409 | 475 |