Mercurial > obsidian-remember-file-state
view src/main.ts @ 2:f3297d90329d
Various fixes:
- Fix calls to Object.values()
- Use transactions to set the CodeMirror editor selection
- Don't restore state asynchronously
- Remove access to Workspace.activeLeaf
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Wed, 09 Feb 2022 22:59:42 -0800 |
parents | 7975d7c73f8a |
children | 114d7e6d2633 |
line wrap: on
line source
import { Editor, MarkdownView, OpenViewState, Plugin, View } from 'obsidian'; import { EditorState, 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 = Object.values(this._viewUninstallers); console.debug(`Unregistering ${uninstallers.length} view callbacks`); uninstallers.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.getActiveViewOfType(MarkdownView); this.registerOnUnloadFile(activeView); if (!this._suppressNextFileOpen) { 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 restoreFileState(file: TFile, view: View) { 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); var transaction = view.editor.cm.state.update({ selection: EditorSelection.fromJSON(stateData.selection)}) view.editor.cm.dispatch(transaction); } } 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 ); }; }