Mercurial > obsidian-remember-file-state
comparison src/main.ts @ 21:815b93d13e0f
Improve typescript compliance
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Fri, 18 Mar 2022 19:25:18 -0700 |
parents | a50ef39473b6 |
children | f7e0926c2500 |
comparison
equal
deleted
inserted
replaced
20:18ff216ce0c4 | 21:815b93d13e0f |
---|---|
1 import { | 1 import { |
2 Editor, | 2 App, |
3 Editor, | |
3 MarkdownView, | 4 MarkdownView, |
4 OpenViewState, | 5 OpenViewState, |
5 Plugin, | 6 Plugin, |
6 View | 7 TAbstractFile, |
8 TFile, | |
9 View, | |
10 WorkspaceLeaf | |
7 } from 'obsidian'; | 11 } from 'obsidian'; |
12 | |
13 import { | |
14 EditorView | |
15 } from '@codemirror/view'; | |
8 | 16 |
9 import { | 17 import { |
10 EditorState, | 18 EditorState, |
11 EditorSelection | 19 EditorSelection |
12 } from '@codemirror/state'; | 20 } from '@codemirror/state'; |
19 DEFAULT_SETTINGS, | 27 DEFAULT_SETTINGS, |
20 RememberFileStatePluginSettings, | 28 RememberFileStatePluginSettings, |
21 RememberFileStatePluginSettingTab | 29 RememberFileStatePluginSettingTab |
22 } from './settings'; | 30 } from './settings'; |
23 | 31 |
32 declare var app: App; | |
33 | |
34 // Interface for CM6 editor view | |
35 interface EditorWithCM6 extends Editor { | |
36 cm: EditorView | |
37 }; | |
38 | |
39 // View with unique ID | |
40 interface ViewWithID extends View { | |
41 __uniqueId: number | |
42 }; | |
43 | |
44 // Scroll info interface | |
45 interface ScrollInfo { | |
46 top: number, left: number | |
47 }; | |
48 | |
49 interface StateData { | |
50 selection: EditorSelection, | |
51 scrollInfo: ScrollInfo | |
52 }; | |
53 | |
24 // Interface for a file state. | 54 // Interface for a file state. |
25 interface RememberedFileState { | 55 interface RememberedFileState { |
26 path: string; | 56 path: string; |
27 lastSavedTime: number; | 57 lastSavedTime: number; |
28 stateData: Object; | 58 stateData: StateData; |
29 } | 59 } |
30 | 60 |
31 // Interface for all currently remembered file states. | 61 // Interface for all currently remembered file states. |
32 interface RememberFileStatePluginData { | 62 interface RememberFileStatePluginData { |
33 rememberedFiles: Record<string, RememberedFileState>; | 63 rememberedFiles: Record<string, RememberedFileState>; |
46 private _suppressNextFileOpen: boolean = false; | 76 private _suppressNextFileOpen: boolean = false; |
47 // Next unique ID to identify views without keeping references to them. | 77 // Next unique ID to identify views without keeping references to them. |
48 private _nextUniqueViewId: number = 0; | 78 private _nextUniqueViewId: number = 0; |
49 | 79 |
50 // Functions to unregister any monkey-patched view hooks on plugin unload. | 80 // Functions to unregister any monkey-patched view hooks on plugin unload. |
51 private _viewUninstallers = {}; | 81 private _viewUninstallers: Record<string, Function> = {}; |
52 // Functions to unregister any global callbacks on plugin unload. | 82 // Functions to unregister any global callbacks on plugin unload. |
53 private _globalUninstallers = []; | 83 private _globalUninstallers: Function[] = []; |
54 | 84 |
55 async onload() { | 85 async onload() { |
56 console.log("Loading RememberFileState plugin"); | 86 console.log("Loading RememberFileState plugin"); |
57 | 87 |
58 await this.loadSettings(); | 88 await this.loadSettings(); |
62 this.registerEvent(this.app.workspace.on('file-open', this.onFileOpen)); | 92 this.registerEvent(this.app.workspace.on('file-open', this.onFileOpen)); |
63 this.registerEvent(this.app.vault.on('rename', this.onFileRename)); | 93 this.registerEvent(this.app.vault.on('rename', this.onFileRename)); |
64 this.registerEvent(this.app.vault.on('delete', this.onFileDelete)); | 94 this.registerEvent(this.app.vault.on('delete', this.onFileDelete)); |
65 | 95 |
66 this.app.workspace.getLeavesOfType("markdown").forEach( | 96 this.app.workspace.getLeavesOfType("markdown").forEach( |
67 (leaf) => { this.registerOnUnloadFile(leaf.view); }); | 97 (leaf: WorkspaceLeaf) => { this.registerOnUnloadFile(leaf.view as MarkdownView); }); |
68 | 98 |
69 const _this = this; | 99 const _this = this; |
70 var uninstall = around(this.app.workspace, { | 100 var uninstall = around(this.app.workspace, { |
71 openLinkText: function(next) { | 101 openLinkText: function(next) { |
72 return async function( | 102 return async function( |
89 | 119 |
90 onunload() { | 120 onunload() { |
91 // Run view uninstallers on all current views. | 121 // Run view uninstallers on all current views. |
92 var numViews: number = 0; | 122 var numViews: number = 0; |
93 this.app.workspace.getLeavesOfType("markdown").forEach( | 123 this.app.workspace.getLeavesOfType("markdown").forEach( |
94 (leaf) => { | 124 (leaf: WorkspaceLeaf) => { |
95 const filePath = leaf.view.file.path; | 125 const filePath = (leaf.view as MarkdownView).file.path; |
96 const viewId = this.getUniqueViewId(leaf.view); | 126 const viewId = this.getUniqueViewId(leaf.view as ViewWithID); |
97 if (viewId != undefined) { | 127 if (viewId != undefined) { |
98 var uninstaller = this._viewUninstallers[viewId]; | 128 var uninstaller = this._viewUninstallers[viewId]; |
99 if (uninstaller) { | 129 if (uninstaller) { |
100 console.debug(`Uninstalling hooks for view ${viewId}`, filePath); | 130 console.debug(`Uninstalling hooks for view ${viewId}`, filePath); |
101 uninstaller(leaf.view); | 131 uninstaller(leaf.view); |
103 } else { | 133 } else { |
104 console.debug("Found markdown view without an uninstaller!", filePath); | 134 console.debug("Found markdown view without an uninstaller!", filePath); |
105 } | 135 } |
106 // Clear the ID so we don't get confused if the plugin | 136 // Clear the ID so we don't get confused if the plugin |
107 // is re-enabled later. | 137 // is re-enabled later. |
108 this.clearUniqueViewId(leaf.view); | 138 this.clearUniqueViewId(leaf.view as ViewWithID); |
109 } else { | 139 } else { |
110 console.debug("Found markdown view without an ID!", filePath); | 140 console.debug("Found markdown view without an ID!", filePath); |
111 } | 141 } |
112 }); | 142 }); |
113 console.debug(`Unregistered ${numViews} view callbacks`); | 143 console.debug(`Unregistered ${numViews} view callbacks`); |
123 | 153 |
124 async saveSettings() { | 154 async saveSettings() { |
125 await this.saveData(this.settings); | 155 await this.saveData(this.settings); |
126 } | 156 } |
127 | 157 |
128 private readonly registerOnUnloadFile = function(view) { | 158 private readonly registerOnUnloadFile = function(view: MarkdownView) { |
129 var filePath = view.file.path; | 159 var filePath = view.file.path; |
130 var viewId = this.getUniqueViewId(view, true); | 160 var viewId = this.getUniqueViewId(view as unknown as ViewWithID, true); |
131 if (viewId in this._viewUninstallers) { | 161 if (viewId in this._viewUninstallers) { |
132 console.debug(`View ${viewId} is already registered`, filePath); | 162 console.debug(`View ${viewId} is already registered`, filePath); |
133 return; | 163 return; |
134 } | 164 } |
135 | 165 |
147 | 177 |
148 view.register(() => { | 178 view.register(() => { |
149 // Don't hold a reference to this plugin here because this callback | 179 // Don't hold a reference to this plugin here because this callback |
150 // will outlive it if it gets deactivated. So let's find it, and | 180 // will outlive it if it gets deactivated. So let's find it, and |
151 // do nothing if we don't find it. | 181 // do nothing if we don't find it. |
152 var plugin = app.plugins.getPlugin("obsidian-remember-file-state"); | 182 // @ts-ignore |
183 var plugin: RememberFileStatePlugin = app.plugins.getPlugin("obsidian-remember-file-state"); | |
153 if (plugin) { | 184 if (plugin) { |
154 console.debug(`Unregistering view ${viewId} callback`, filePath); | 185 console.debug(`Unregistering view ${viewId} callback`, filePath); |
155 delete plugin._viewUninstallers[viewId]; | 186 delete plugin._viewUninstallers[viewId]; |
156 uninstall(); | 187 uninstall(); |
157 } else { | 188 } else { |
165 openedFile: TFile | 196 openedFile: TFile |
166 ): Promise<void> => { | 197 ): Promise<void> => { |
167 // If `openedFile` is null, it's because the last pane was closed | 198 // If `openedFile` is null, it's because the last pane was closed |
168 // and there is now an empty pane. | 199 // and there is now an empty pane. |
169 if (openedFile) { | 200 if (openedFile) { |
170 var activeView = this.app.workspace.getActiveViewOfType(MarkdownView); | 201 var activeView: MarkdownView = this.app.workspace.getActiveViewOfType(MarkdownView); |
171 if (activeView) { | 202 if (activeView) { |
172 this.registerOnUnloadFile(activeView); | 203 this.registerOnUnloadFile(activeView); |
173 | 204 |
174 if (!this._suppressNextFileOpen) { | 205 if (!this._suppressNextFileOpen) { |
175 this.restoreFileState(openedFile, activeView); | 206 this.restoreFileState(openedFile, activeView); |
179 | 210 |
180 this._suppressNextFileOpen = false; | 211 this._suppressNextFileOpen = false; |
181 } | 212 } |
182 } | 213 } |
183 | 214 |
184 private readonly rememberFileState = async (file: TFile, view: View): Promise<void> => { | 215 private readonly rememberFileState = async (file: TFile, view: MarkdownView): Promise<void> => { |
185 const scrollInfo = view.editor.getScrollInfo(); | 216 const scrollInfo = view.editor.getScrollInfo(); |
186 const stateSelection = view.editor.cm.state.selection; | 217 const cm6editor = view.editor as EditorWithCM6; |
218 const stateSelection: EditorSelection = cm6editor.cm.state.selection; | |
187 if (stateSelection == undefined) { | 219 if (stateSelection == undefined) { |
188 // Legacy editor is in use, let's ignore | 220 // Legacy editor is in use, let's ignore |
189 return; | 221 return; |
190 } | 222 } |
191 const stateSelectionJSON = stateSelection.toJSON(); | 223 const stateSelectionJSON = stateSelection.toJSON(); |
208 this.forgetExcessFiles(); | 240 this.forgetExcessFiles(); |
209 } | 241 } |
210 console.debug("Remember file state for:", file.path); | 242 console.debug("Remember file state for:", file.path); |
211 } | 243 } |
212 | 244 |
213 private readonly restoreFileState = function(file: TFile, view: View) { | 245 private readonly restoreFileState = function(file: TFile, view: MarkdownView) { |
214 const existingFile = this.data.rememberedFiles[file.path]; | 246 const existingFile = this.data.rememberedFiles[file.path]; |
215 if (existingFile) { | 247 if (existingFile) { |
216 console.debug("Restoring file state for:", file.path); | 248 console.debug("Restoring file state for:", file.path); |
217 const stateData = existingFile.stateData; | 249 const stateData = existingFile.stateData; |
218 view.editor.scrollTo(stateData.scrollInfo.left, stateData.scrollInfo.top); | 250 view.editor.scrollTo(stateData.scrollInfo.left, stateData.scrollInfo.top); |
219 var transaction = view.editor.cm.state.update({ | 251 const cm6editor = view.editor as EditorWithCM6; |
252 var transaction = cm6editor.cm.state.update({ | |
220 selection: EditorSelection.fromJSON(stateData.selection)}) | 253 selection: EditorSelection.fromJSON(stateData.selection)}) |
221 view.editor.cm.dispatch(transaction); | 254 |
255 cm6editor.cm.dispatch(transaction); | |
222 } | 256 } |
223 } | 257 } |
224 | 258 |
225 private readonly forgetExcessFiles = function() { | 259 private readonly forgetExcessFiles = function() { |
226 const keepMax = this.settings.rememberMaxFiles; | 260 const keepMax = this.settings.rememberMaxFiles; |
227 if (keepMax <= 0) { | 261 if (keepMax <= 0) { |
228 return; | 262 return; |
229 } | 263 } |
230 | 264 |
231 // Sort newer files first, older files last. | 265 // Sort newer files first, older files last. |
232 var filesData = Object.values(this.data.rememberedFiles); | 266 var filesData: RememberedFileState[] = Object.values(this.data.rememberedFiles); |
233 filesData.sort((a, b) => { | 267 filesData.sort((a, b) => { |
234 if (a.lastSavedTime > b.lastSavedTime) return -1; // a before b | 268 if (a.lastSavedTime > b.lastSavedTime) return -1; // a before b |
235 if (a.lastSavedTime < b.lastSavedTime) return 1; // b before a | 269 if (a.lastSavedTime < b.lastSavedTime) return 1; // b before a |
236 return 0; | 270 return 0; |
237 }); | 271 }); |
241 var fileData = filesData[i]; | 275 var fileData = filesData[i]; |
242 delete this.data.rememberedFiles[fileData.path]; | 276 delete this.data.rememberedFiles[fileData.path]; |
243 } | 277 } |
244 } | 278 } |
245 | 279 |
246 private readonly getUniqueViewId = function(view: View, autocreateId: boolean = false) { | 280 private readonly getUniqueViewId = function(view: ViewWithID, autocreateId: boolean = false) { |
247 if (view.__uniqueId == undefined) { | 281 if (view.__uniqueId == undefined) { |
248 if (!autocreateId) { | 282 if (!autocreateId) { |
249 return -1; | 283 return -1; |
250 } | 284 } |
251 view.__uniqueId = (this._nextUniqueViewId++); | 285 view.__uniqueId = (this._nextUniqueViewId++); |
252 return view.__uniqueId; | 286 return view.__uniqueId; |
253 } | 287 } |
254 return view.__uniqueId; | 288 return view.__uniqueId; |
255 } | 289 } |
256 | 290 |
257 private readonly clearUniqueViewId = function(view: View) { | 291 private readonly clearUniqueViewId = function(view: ViewWithID) { |
258 delete view["__uniqueId"]; | 292 delete view["__uniqueId"]; |
259 } | 293 } |
260 | 294 |
261 private readonly onFileRename = async ( | 295 private readonly onFileRename = async ( |
262 file: TAbstractFile, | 296 file: TAbstractFile, |
263 oldPath: string, | 297 oldPath: string, |
264 ): Promise<void> => { | 298 ): Promise<void> => { |
265 const existingFile = this.data.rememberedFiles[oldPath]; | 299 const existingFile: RememberedFileState = this.data.rememberedFiles[oldPath]; |
266 if (existingFile) { | 300 if (existingFile) { |
267 existingFile.path = file.path; | 301 existingFile.path = file.path; |
268 delete this.data.rememberedFiles[oldPath]; | 302 delete this.data.rememberedFiles[oldPath]; |
269 this.data.rememberedFiles[file.path] = existingFile; | 303 this.data.rememberedFiles[file.path] = existingFile; |
270 } | 304 } |