Mercurial > obsidian-remember-file-state
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, |