comparison src/main.ts @ 6:114d7e6d2633

Fix various issues around keeping references to editor objects - Don't keep references to view. Instead, generate unique view IDs and use that to find the correct uninstaller when needed. - Don't keep a reference to the plugin itself in registered callbacks on views because that could keep the plugin alive after it has been unloaded.
author Ludovic Chabant <ludovic@chabant.com>
date Mon, 14 Feb 2022 12:35:45 -0800
parents f3297d90329d
children b1cb0474bb18
comparison
equal deleted inserted replaced
5:e8300db708b6 6:114d7e6d2633
40 40
41 export default class RememberFileStatePlugin extends Plugin { 41 export default class RememberFileStatePlugin extends Plugin {
42 settings: RememberFileStatePluginSettings; 42 settings: RememberFileStatePluginSettings;
43 data: RememberFileStatePluginData; 43 data: RememberFileStatePluginData;
44 44
45 // Don't restore state on the next file being opened.
45 private _suppressNextFileOpen: boolean = false; 46 private _suppressNextFileOpen: boolean = false;
46 47 // Next unique ID to identify views without keeping references to them.
48 private _nextUniqueViewId: number = 0;
49
50 // Functions to unregister any monkey-patched view hooks on plugin unload.
47 private _viewUninstallers = {}; 51 private _viewUninstallers = {};
52 // Functions to unregister any global callbacks on plugin unload.
48 private _globalUninstallers = []; 53 private _globalUninstallers = [];
49 54
50 async onload() { 55 async onload() {
51 console.log("Loading RememberFileState plugin"); 56 console.log("Loading RememberFileState plugin");
52 57
81 86
82 this.addSettingTab(new RememberFileStatePluginSettingTab(this.app, this)); 87 this.addSettingTab(new RememberFileStatePluginSettingTab(this.app, this));
83 } 88 }
84 89
85 onunload() { 90 onunload() {
86 var uninstallers = Object.values(this._viewUninstallers); 91 // Run view uninstallers on all current views.
87 console.debug(`Unregistering ${uninstallers.length} view callbacks`); 92 var numViews: number = 0;
88 uninstallers.forEach((cb) => cb()); 93 this.app.workspace.getLeavesOfType("markdown").forEach(
94 (leaf) => {
95 const filePath = leaf.view.file.path;
96 const viewId = this.getUniqueViewId(leaf.view);
97 if (viewId != undefined) {
98 var uninstaller = this._viewUninstallers[viewId];
99 if (uninstaller) {
100 console.debug(`Uninstalling hooks for view ${viewId}`, filePath);
101 uninstaller(leaf.view);
102 ++numViews;
103 } else {
104 console.debug("Found markdown view without an uninstaller!", filePath);
105 }
106 // Clear the ID so we don't get confused if the plugin
107 // is re-enabled later.
108 this.clearUniqueViewId(leaf.view);
109 } else {
110 console.debug("Found markdown view without an ID!", filePath);
111 }
112 });
113 console.debug(`Unregistered ${numViews} view callbacks`);
89 this._viewUninstallers = {}; 114 this._viewUninstallers = {};
90 115
116 // Run global unhooks.
91 this._globalUninstallers.forEach((cb) => cb()); 117 this._globalUninstallers.forEach((cb) => cb());
92 } 118 }
93 119
94 async loadSettings() { 120 async loadSettings() {
95 this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 121 this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
98 async saveSettings() { 124 async saveSettings() {
99 await this.saveData(this.settings); 125 await this.saveData(this.settings);
100 } 126 }
101 127
102 private readonly registerOnUnloadFile = function(view) { 128 private readonly registerOnUnloadFile = function(view) {
103 if (view in this._viewUninstallers) { 129 var filePath = view.file.path;
130 var viewId = this.getUniqueViewId(view, true);
131 if (viewId in this._viewUninstallers) {
132 console.debug(`View ${viewId} is already registered`, filePath);
104 return; 133 return;
105 } 134 }
106 135
107 console.debug("Registering view callback"); 136 console.debug(`Registering callback on view ${viewId}`, filePath);
108 const _this = this; 137 const _this = this;
109 var uninstall = around(view, { 138 var uninstall = around(view, {
110 onUnloadFile: function(next) { 139 onUnloadFile: function(next) {
111 return async function (unloaded: TFile) { 140 return async function (unloaded: TFile) {
112 _this.rememberFileState(unloaded, view); 141 _this.rememberFileState(unloaded, view);
113 return await next.call(this, unloaded); 142 return await next.call(this, unloaded);
114 }; 143 };
115 } 144 }
116 }); 145 });
117 this._viewUninstallers[view] = uninstall; 146 this._viewUninstallers[viewId] = uninstall;
118 147
119 view.register(() => { 148 view.register(() => {
120 console.debug("Unregistering view callback"); 149 // Don't hold a reference to this plugin here because this callback
121 delete this._viewUninstallers[view]; 150 // will outlive it if it gets deactivated. So let's find it, and
122 uninstall(); 151 // do nothing if we don't find it.
152 var plugin = app.plugins.getPlugin("remember-file-state");
153 if (plugin) {
154 console.debug(`Unregistering view ${viewId} callback`, filePath);
155 delete plugin._viewUninstallers[viewId];
156 uninstall();
157 } else {
158 console.debug(
159 "Plugin remember-file-state has been unloaded, ignoring unregister");
160 }
123 }); 161 });
124 } 162 }
125 163
126 private readonly onFileOpen = async ( 164 private readonly onFileOpen = async (
127 openedFile: TFile 165 openedFile: TFile
165 this.forgetExcessFiles(); 203 this.forgetExcessFiles();
166 } 204 }
167 console.debug("Remember file state for:", file.path); 205 console.debug("Remember file state for:", file.path);
168 } 206 }
169 207
170 private restoreFileState(file: TFile, view: View) { 208 private readonly restoreFileState = function(file: TFile, view: View) {
171 const existingFile = this.data.rememberedFiles.find( 209 const existingFile = this.data.rememberedFiles.find(
172 (curFile) => curFile.path === file.path 210 (curFile) => curFile.path === file.path
173 ); 211 );
174 if (existingFile) { 212 if (existingFile) {
175 console.debug("Restoring file state for:", file.path); 213 console.debug("Restoring file state for:", file.path);
179 selection: EditorSelection.fromJSON(stateData.selection)}) 217 selection: EditorSelection.fromJSON(stateData.selection)})
180 view.editor.cm.dispatch(transaction); 218 view.editor.cm.dispatch(transaction);
181 } 219 }
182 } 220 }
183 221
184 private forgetExcessFiles() { 222 private readonly forgetExcessFiles = function() {
185 const keepMax = this.settings.rememberMaxFiles; 223 const keepMax = this.settings.rememberMaxFiles;
186 if (keepMax <= 0) { 224 if (keepMax <= 0) {
187 return; 225 return;
188 } 226 }
189 227
190 this.data.rememberedFiles.sort((a, b) => a.lastSavedTime < b.lastSavedTime); 228 this.data.rememberedFiles.sort((a, b) => a.lastSavedTime < b.lastSavedTime);
191 229
192 if (this.data.rememberedFiles.length > keepMax) { 230 if (this.data.rememberedFiles.length > keepMax) {
193 this.data.rememberedFiles.splice(keepMax); 231 this.data.rememberedFiles.splice(keepMax);
194 } 232 }
233 }
234
235 private readonly getUniqueViewId = function(view: View, autocreateId: boolean = false) {
236 if (view.__uniqueId == undefined) {
237 if (!autocreateId) {
238 return -1;
239 }
240 view.__uniqueId = (this._nextUniqueViewId++);
241 return view.__uniqueId;
242 }
243 return view.__uniqueId;
244 }
245
246 private readonly clearUniqueViewId = function(view: View) {
247 delete view["__uniqueId"];
195 } 248 }
196 249
197 private readonly onFileRename = async ( 250 private readonly onFileRename = async (
198 file: TAbstractFile, 251 file: TAbstractFile,
199 oldPath: string, 252 oldPath: string,