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