Mercurial > obsidian-remember-file-state
comparison 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 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:7975d7c73f8a |
---|---|
1 import { | |
2 Editor, | |
3 MarkdownView, | |
4 OpenViewState, | |
5 Plugin, | |
6 View | |
7 } from 'obsidian'; | |
8 | |
9 import { | |
10 EditorSelection | |
11 } from '@codemirror/state'; | |
12 | |
13 import { | |
14 around | |
15 } from 'monkey-around'; | |
16 | |
17 import { | |
18 DEFAULT_SETTINGS, | |
19 RememberFileStatePluginSettings, | |
20 RememberFileStatePluginSettingTab | |
21 } from './settings'; | |
22 | |
23 // Interface for a file state. | |
24 interface RememberedFileState { | |
25 path: string; | |
26 lastSavedTime: number; | |
27 stateData: Object; | |
28 } | |
29 | |
30 // Interface for all currently remembered file states. | |
31 interface RememberFileStatePluginData { | |
32 rememberedFiles: RememberedFileState[]; | |
33 } | |
34 | |
35 // Default empty list of remembered file states. | |
36 const DEFAULT_DATA: RememberFileStatePluginData = { | |
37 rememberedFiles: [] | |
38 }; | |
39 | |
40 export default class RememberFileStatePlugin extends Plugin { | |
41 settings: RememberFileStatePluginSettings; | |
42 data: RememberFileStatePluginData; | |
43 | |
44 private _suppressNextFileOpen: boolean = false; | |
45 | |
46 private _viewUninstallers = {}; | |
47 private _globalUninstallers = []; | |
48 | |
49 async onload() { | |
50 console.log("Loading RememberFileState plugin"); | |
51 | |
52 await this.loadSettings(); | |
53 | |
54 this.data = Object.assign({}, DEFAULT_DATA); | |
55 | |
56 this.registerEvent(this.app.workspace.on('file-open', this.onFileOpen)); | |
57 this.registerEvent(this.app.vault.on('rename', this.onFileRename)); | |
58 this.registerEvent(this.app.vault.on('delete', this.onFileDelete)); | |
59 | |
60 this.app.workspace.getLeavesOfType("markdown").forEach( | |
61 (leaf) => { this.registerOnUnloadFile(leaf.view); }); | |
62 | |
63 const _this = this; | |
64 var uninstall = around(this.app.workspace, { | |
65 openLinkText: function(next) { | |
66 return async function( | |
67 linktext: string, sourcePath: string, | |
68 newLeaf?: boolean, openViewState?: OpenViewState) { | |
69 // When opening a link, we don't want to restore the | |
70 // scroll position/selection/etc because there's a | |
71 // good chance we want to show the file back at the | |
72 // top, or we're going straight to a specific block. | |
73 _this._suppressNextFileOpen = true; | |
74 return await next.call( | |
75 this, linktext, sourcePath, newLeaf, openViewState); | |
76 }; | |
77 } | |
78 }); | |
79 this._globalUninstallers.push(uninstall); | |
80 | |
81 this.addSettingTab(new RememberFileStatePluginSettingTab(this.app, this)); | |
82 } | |
83 | |
84 onunload() { | |
85 var uninstallers = this._viewUninstallers.values(); | |
86 console.debug(`Unregistering ${uninstallers.length} view callbacks`); | |
87 uninstallers.values().forEach((cb) => cb()); | |
88 this._viewUninstallers = {}; | |
89 | |
90 this._globalUninstallers.forEach((cb) => cb()); | |
91 } | |
92 | |
93 async loadSettings() { | |
94 this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); | |
95 } | |
96 | |
97 async saveSettings() { | |
98 await this.saveData(this.settings); | |
99 } | |
100 | |
101 private readonly registerOnUnloadFile = function(view) { | |
102 if (view in this._viewUninstallers) { | |
103 return; | |
104 } | |
105 | |
106 console.debug("Registering view callback"); | |
107 const _this = this; | |
108 var uninstall = around(view, { | |
109 onUnloadFile: function(next) { | |
110 return async function (unloaded: TFile) { | |
111 _this.rememberFileState(unloaded, view); | |
112 return await next.call(this, unloaded); | |
113 }; | |
114 } | |
115 }); | |
116 this._viewUninstallers[view] = uninstall; | |
117 | |
118 view.register(() => { | |
119 console.debug("Unregistering view callback"); | |
120 delete this._viewUninstallers[view]; | |
121 uninstall(); | |
122 }); | |
123 } | |
124 | |
125 private readonly onFileOpen = async ( | |
126 openedFile: TFile | |
127 ): Promise<void> => { | |
128 // If `openedFile` is null, it's because the last pane was closed | |
129 // and there is now an empty pane. | |
130 if (openedFile) { | |
131 var activeView = this.app.workspace.activeLeaf.view; | |
132 this.registerOnUnloadFile(activeView); | |
133 | |
134 if (!this._suppressNextFileOpen) { | |
135 await this.restoreFileState(openedFile, activeView); | |
136 } else { | |
137 this._suppressNextFileOpen = false; | |
138 } | |
139 } | |
140 } | |
141 | |
142 private readonly rememberFileState = async (file: TFile, view: View): Promise<void> => { | |
143 const scrollInfo = view.editor.getScrollInfo(); | |
144 const stateSelectionJSON = view.editor.cm.state.selection.toJSON(); | |
145 const stateData = {'scrollInfo': scrollInfo, 'selection': stateSelectionJSON}; | |
146 | |
147 var existingFile = this.data.rememberedFiles.find( | |
148 curFile => curFile.path == file.path | |
149 ); | |
150 | |
151 if (existingFile) { | |
152 existingFile.lastSavedTime = Date.now(); | |
153 existingFile.stateData = stateData; | |
154 } else { | |
155 let newFileState = { | |
156 path: file.path, | |
157 lastSavedTime: Date.now(), | |
158 stateData: stateData | |
159 }; | |
160 this.data.rememberedFiles.push(newFileState); | |
161 | |
162 // If we need to keep the number remembered files under a maximum, | |
163 // do it now. | |
164 this.forgetExcessFiles(); | |
165 } | |
166 console.debug("Remember file state for:", file.path); | |
167 } | |
168 | |
169 private readonly restoreFileState = async (file: TFile, view: View): Promise<void> => { | |
170 const existingFile = this.data.rememberedFiles.find( | |
171 (curFile) => curFile.path === file.path | |
172 ); | |
173 if (existingFile) { | |
174 console.debug("Restoring file state for:", file.path); | |
175 const stateData = existingFile.stateData; | |
176 view.editor.scrollTo(stateData.scrollInfo.left, stateData.scrollInfo.top); | |
177 view.editor.cm.state.selection = EditorSelection.fromJSON(stateData.selection); | |
178 } | |
179 } | |
180 | |
181 private forgetExcessFiles() { | |
182 const keepMax = this.settings.rememberMaxFiles; | |
183 if (keepMax <= 0) { | |
184 return; | |
185 } | |
186 | |
187 this.data.rememberedFiles.sort((a, b) => a.lastSavedTime < b.lastSavedTime); | |
188 | |
189 if (this.data.rememberedFiles.length > keepMax) { | |
190 this.data.rememberedFiles.splice(keepMax); | |
191 } | |
192 } | |
193 | |
194 private readonly onFileRename = async ( | |
195 file: TAbstractFile, | |
196 oldPath: string, | |
197 ): Promise<void> => { | |
198 const existingFile = this.data.rememberedFiles.find( | |
199 (curFile) => curFile.path === oldPath | |
200 ); | |
201 if (existingFile) { | |
202 existingFile.path = file.path; | |
203 } | |
204 }; | |
205 | |
206 private readonly onFileDelete = async ( | |
207 file: TAbstractFile, | |
208 ): Promise<void> => { | |
209 this.data.rememberedFiles = this.data.rememberedFiles.filter( | |
210 (curFile) => curFile.path !== file.path | |
211 ); | |
212 }; | |
213 } | |
214 |