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