diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main.ts	Tue Feb 08 21:40:33 2022 -0800
@@ -0,0 +1,214 @@
+import {
+	Editor, 
+	MarkdownView,
+	OpenViewState,
+	Plugin, 
+	View
+} from 'obsidian';
+
+import {
+	EditorSelection
+} from '@codemirror/state';
+
+import {
+	around
+} from 'monkey-around';
+
+import {
+	DEFAULT_SETTINGS,
+	RememberFileStatePluginSettings,
+	RememberFileStatePluginSettingTab
+} from './settings';
+
+// Interface for a file state.
+interface RememberedFileState {
+	path: string;
+	lastSavedTime: number;
+	stateData: Object;
+}
+
+// Interface for all currently remembered file states.
+interface RememberFileStatePluginData {
+	rememberedFiles: RememberedFileState[];
+}
+
+// Default empty list of remembered file states.
+const DEFAULT_DATA: RememberFileStatePluginData = {
+	rememberedFiles: []
+};
+
+export default class RememberFileStatePlugin extends Plugin {
+	settings: RememberFileStatePluginSettings;
+	data: RememberFileStatePluginData;
+
+	private _suppressNextFileOpen: boolean = false;
+
+	private _viewUninstallers = {};
+	private _globalUninstallers = [];
+
+	async onload() {
+		console.log("Loading RememberFileState plugin");
+
+		await this.loadSettings();
+
+		this.data = Object.assign({}, DEFAULT_DATA);
+
+		this.registerEvent(this.app.workspace.on('file-open', this.onFileOpen));
+		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) => { this.registerOnUnloadFile(leaf.view); });
+
+		const _this = this;
+		var uninstall = around(this.app.workspace, {
+			openLinkText: function(next) {
+				return async function(
+					linktext: string, sourcePath: string, 
+					newLeaf?: boolean, openViewState?: OpenViewState) {
+						// When opening a link, we don't want to restore the
+						// scroll position/selection/etc because there's a
+						// good chance we want to show the file back at the
+						// top, or we're going straight to a specific block.
+						_this._suppressNextFileOpen = true;
+						return await next.call(
+							this, linktext, sourcePath, newLeaf, openViewState);
+					};
+			}
+		});
+		this._globalUninstallers.push(uninstall);
+
+		this.addSettingTab(new RememberFileStatePluginSettingTab(this.app, this));
+	}
+
+	onunload() {
+		var uninstallers = this._viewUninstallers.values();
+		console.debug(`Unregistering ${uninstallers.length} view callbacks`);
+		uninstallers.values().forEach((cb) => cb());
+		this._viewUninstallers = {};
+
+		this._globalUninstallers.forEach((cb) => cb());
+	}
+
+	async loadSettings() {
+		this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
+	}
+
+	async saveSettings() {
+		await this.saveData(this.settings);
+	}
+
+	private readonly registerOnUnloadFile = function(view) {
+		if (view in this._viewUninstallers) {
+			return;
+		}
+
+		console.debug("Registering view callback");
+		const _this = this;
+		var uninstall = around(view, {
+			onUnloadFile: function(next) {
+				return async function (unloaded: TFile) {
+					_this.rememberFileState(unloaded, view);
+					return await next.call(this, unloaded);
+				};
+			}
+		});
+		this._viewUninstallers[view] = uninstall;
+
+		view.register(() => {
+			console.debug("Unregistering view callback");
+			delete this._viewUninstallers[view];
+			uninstall();
+		});
+	}
+
+	private readonly onFileOpen = async (
+		openedFile: TFile
+	): Promise<void> => {
+		// If `openedFile` is null, it's because the last pane was closed
+		// and there is now an empty pane.
+		if (openedFile) {
+			var activeView = this.app.workspace.activeLeaf.view;
+			this.registerOnUnloadFile(activeView);
+
+			if (!this._suppressNextFileOpen) {
+				await this.restoreFileState(openedFile, activeView);
+			} else {
+				this._suppressNextFileOpen = false;
+			}
+		}
+	}
+
+	private readonly rememberFileState = async (file: TFile, view: View): Promise<void> => {
+		const scrollInfo = view.editor.getScrollInfo();
+		const stateSelectionJSON = view.editor.cm.state.selection.toJSON();
+		const stateData = {'scrollInfo': scrollInfo, 'selection': stateSelectionJSON};
+
+		var existingFile = this.data.rememberedFiles.find(
+			curFile => curFile.path == file.path
+		);
+
+		if (existingFile) {
+			existingFile.lastSavedTime = Date.now();
+			existingFile.stateData = stateData;
+		} else {
+			let newFileState = {
+				path: file.path,
+				lastSavedTime: Date.now(),
+				stateData: stateData
+			};
+			this.data.rememberedFiles.push(newFileState);
+
+			// If we need to keep the number remembered files under a maximum,
+			// do it now.
+			this.forgetExcessFiles();
+		}
+		console.debug("Remember file state for:", file.path);
+	}
+
+	private readonly restoreFileState = async (file: TFile, view: View): Promise<void> => {
+		const existingFile = this.data.rememberedFiles.find(
+			(curFile) => curFile.path === file.path
+		);
+		if (existingFile) {
+			console.debug("Restoring file state for:", file.path);
+			const stateData = existingFile.stateData;
+			view.editor.scrollTo(stateData.scrollInfo.left, stateData.scrollInfo.top);
+			view.editor.cm.state.selection = EditorSelection.fromJSON(stateData.selection);
+		}
+	}
+
+	private forgetExcessFiles() {
+		const keepMax = this.settings.rememberMaxFiles;
+		if (keepMax <= 0) {
+			return;
+		}
+
+		this.data.rememberedFiles.sort((a, b) => a.lastSavedTime < b.lastSavedTime);
+
+		if (this.data.rememberedFiles.length > keepMax) {
+			this.data.rememberedFiles.splice(keepMax);
+		}
+	}
+
+	private readonly onFileRename = async (
+		file: TAbstractFile,
+		oldPath: string,
+	): Promise<void> => {
+		const existingFile = this.data.rememberedFiles.find(
+			(curFile) => curFile.path === oldPath
+		);
+		if (existingFile) {
+			existingFile.path = file.path;
+		}
+	};
+
+	private readonly onFileDelete = async (
+		file: TAbstractFile,
+	): Promise<void> => {
+		this.data.rememberedFiles = this.data.rememberedFiles.filter(
+			(curFile) => curFile.path !== file.path
+		);
+	};
+}
+