0
|
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
|