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