changeset 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 d52beb77d109
children fae202d7b3de
files src/main.ts src/settings.ts
diffstat 2 files changed, 84 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- a/src/main.ts	Wed Sep 20 17:19:46 2023 -0700
+++ b/src/main.ts	Wed Sep 20 22:40:25 2023 -0700
@@ -7,6 +7,7 @@
 	Plugin,
 	TAbstractFile,
 	TFile,
+	Tasks,
 	View,
 	WorkspaceLeaf
 } from 'obsidian';
@@ -69,6 +70,9 @@
 	rememberedFiles: {}
 };
 
+// Where to save the states database.
+const STATE_DB_PATH: string = '.obsidian/plugins/obsidian-remember-file-state/states.json';
+
 // Simple warning message.
 class WarningModal extends Modal {
 	title: string = "";
@@ -109,12 +113,14 @@
 
 		this.data = Object.assign({}, DEFAULT_DATA);
 
+		await this.readStateDatabase(STATE_DB_PATH);
+
 		this.registerEvent(this.app.workspace.on('file-open', this.onFileOpen));
+		this.registerEvent(this.app.workspace.on('quit', this.onAppQuit));
 		this.registerEvent(this.app.vault.on('rename', this.onFileRename));
 		this.registerEvent(this.app.vault.on('delete', this.onFileDelete));
 
-		this.app.workspace.getLeavesOfType("markdown").forEach(
-			(leaf: WorkspaceLeaf) => { this.registerOnUnloadFile(leaf.view as MarkdownView); });
+		this.app.workspace.onLayoutReady(() => { this.onLayoutReady(); });
 
 		const _this = this;
 		var uninstall = around(this.app.workspace, {
@@ -184,6 +190,31 @@
 		await this.saveData(this.settings);
 	}
 
+	private readonly onLayoutReady = function() {
+		this.app.workspace.getLeavesOfType("markdown").forEach(
+			(leaf: WorkspaceLeaf) => { 
+				var view = leaf.view as MarkdownView;
+
+				// On startup, assign unique IDs to views and register the
+				// unload callback to remember their state.
+				this.registerOnUnloadFile(view); 
+
+				// Also remember which file is opened in which view.
+				const viewId = this.getUniqueViewId(view as ViewWithID);
+				if (viewId != undefined) {
+					this._lastOpenFiles[viewId] = view.file.path;
+				}
+
+				// Restore state for each opened pane on startup.
+				const existingFile = this.data.rememberedFiles[view.file.path];
+				if (existingFile) {
+					const savedStateData = existingFile.stateData;
+					console.debug("RememberFileState: restoring saved state for:", view.file.path, savedStateData);
+					this.restoreState(savedStateData, view);
+				}
+			});
+	}
+
 	private readonly registerOnUnloadFile = function(view: MarkdownView) {
 		var filePath = view.file.path;
 		var viewId = this.getUniqueViewId(view as unknown as ViewWithID, true);
@@ -196,7 +227,7 @@
 		var uninstall = around(view, {
 			onUnloadFile: function(next) {
 				return async function (unloaded: TFile) {
-					_this.rememberFileState(unloaded, this);
+					_this.rememberFileState(this, unloaded);
 					return await next.call(this, unloaded);
 				};
 			}
@@ -282,8 +313,12 @@
 		}
 	}
 
-	private readonly rememberFileState = async (file: TFile, view: MarkdownView): Promise<void> => {
+	private readonly rememberFileState = async (view: MarkdownView, file?: TFile): Promise<void> => {
 		const stateData = this.getState(view);
+
+		if (file === undefined) {
+			file = view.file;
+		}
 		var existingFile = this.data.rememberedFiles[file.path];
 		if (existingFile) {
 			existingFile.lastSavedTime = Date.now();
@@ -341,7 +376,6 @@
 					curView.file.path == file.path &&
 					this.getUniqueViewId(curView) >= 0  // Skip views that have never been activated.
 				   ) {
-					console.debug(`FFFFOOOOOUNNNNDD!!!!! ${file.path}`, curView, activeView);
 					otherView = curView;
 					return false; // Stop iterating leaves.
 				}
@@ -405,5 +439,37 @@
 	): Promise<void> => {
 		delete this.data.rememberedFiles[file.path];
 	};
+
+	private readonly onAppQuit = async (tasks: Tasks): Promise<void> => {
+		const _this = this;
+		tasks.addPromise(
+			_this.rememberAllOpenedFileStates()
+			.then(_this.writeStateDatabase(STATE_DB_PATH)));
+	}
+
+	private readonly rememberAllOpenedFileStates = async(): Promise<void> => {
+		this.app.workspace.getLeavesOfType("markdown").forEach(
+			(leaf: WorkspaceLeaf) => { 
+				const view = leaf.view as MarkdownView;
+				this.rememberFileState(view);
+			}
+		);
+	}
+
+	private readonly writeStateDatabase = async(path: string): Promise<void> => {
+		const fs = this.app.vault.adapter;
+		const jsonDb = JSON.stringify(this.data);
+		await fs.write(path, jsonDb);
+	}
+
+	private readonly readStateDatabase = async(path: string): Promise<void> => {
+		const fs = this.app.vault.adapter;
+		if (await fs.exists(path)) {
+			const jsonDb = await fs.read(path);
+			this.data = JSON.parse(jsonDb);
+			const numLoaded = Object.keys(this.data.rememberedFiles).length;
+			console.debug(`RememberFileState: read ${numLoaded} record from state database.`);
+		}
+	}
 }
 
--- a/src/settings.ts	Wed Sep 20 17:19:46 2023 -0700
+++ b/src/settings.ts	Wed Sep 20 22:40:25 2023 -0700
@@ -8,12 +8,14 @@
 
 export interface RememberFileStatePluginSettings {
 	rememberMaxFiles: number;
+	persistStates: boolean;
 }
 
 export const DEFAULT_SETTINGS: RememberFileStatePluginSettings = {
 	// Matches the number of files Obsidian remembers the undo/redo 
 	// history for by default (at least as of 0.13.17).
-	rememberMaxFiles: 20 
+	rememberMaxFiles: 20,
+	persistStates: true
 }
 
 export class RememberFileStatePluginSettingTab extends PluginSettingTab {
@@ -41,5 +43,15 @@
 						await this.plugin.saveSettings();
 					}
 				}));
+
+		new Setting(containerEl)
+			.setName('Save states')
+			.setDesc('Whether to save the state of all open files to disk')
+			.addToggle(toggle => toggle
+				.setValue(this.plugin.settings.persistStates)
+				.onChange(async (value: boolean) => {
+					this.plugin.settings.persistStates = value;
+					await this.plugin.saveSettings();
+				}));
 	}
 }