Files
BlueRoseNote/.obsidian/plugins/remember-cursor-position/main.js

405 lines
18 KiB
JavaScript

/*
THIS IS A GENERATED/BUNDLED FILE BY ROLLUP
if you want to view the source visit the plugins github repository
*/
'use strict';
var obsidian = require('obsidian');
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
};
const SAFE_DB_FLUSH_INTERVAL = 5000;
const DEFAULT_DB_FILENAME_LEGACY = '.obsidian/plugins/remember-cursor-position/cursor-positions.json';
const DEFAULT_SETTINGS = {
dbFileName: '',
delayAfterFileOpening: 100,
saveTimer: SAFE_DB_FLUSH_INTERVAL,
pruneOrphans: false,
maxAgeDays: 0,
maxCount: 0,
};
class RememberCursorPosition extends obsidian.Plugin {
constructor() {
super(...arguments);
this.loadedLeafIdList = [];
this.loadingFile = false;
}
onload() {
return __awaiter(this, void 0, void 0, function* () {
yield this.loadSettings();
try {
this.db = yield this.readDb();
this.pruneDb();
this.lastSavedDb = yield this.readDb();
}
catch (e) {
console.error("Remember Cursor Position plugin can\'t read database: " + e);
this.db = {};
this.lastSavedDb = {};
}
this.addSettingTab(new SettingTab(this.app, this));
this.registerEvent(this.app.workspace.on('file-open', (file) => this.restoreEphemeralState(file)));
this.registerEvent(this.app.workspace.on('quit', () => { this.writeDb(this.db); }));
this.registerEvent(this.app.vault.on('rename', (file, oldPath) => this.renameFile(file, oldPath)));
this.registerEvent(this.app.vault.on('delete', (file) => this.deleteFile(file)));
//todo: replace by scroll and mouse cursor move events
this.registerInterval(window.setInterval(() => this.checkEphemeralStateChanged(), 100));
this.saveTimerIntervalId = this.registerInterval(window.setInterval(() => this.writeDb(this.db), this.settings.saveTimer));
this.restoreEphemeralState();
});
}
renameFile(file, oldPath) {
let newName = file.path;
let oldName = oldPath;
this.db[newName] = this.db[oldName];
delete this.db[oldName];
}
deleteFile(file) {
let fileName = file.path;
delete this.db[fileName];
}
checkEphemeralStateChanged() {
var _a;
let fileName = (_a = this.app.workspace.getActiveFile()) === null || _a === void 0 ? void 0 : _a.path;
//waiting for load new file
if (!fileName || !this.lastLoadedFileName || fileName != this.lastLoadedFileName || this.loadingFile)
return;
let st = this.getEphemeralState();
if (!this.lastEphemeralState)
this.lastEphemeralState = st;
if (!isNaN(st.scroll) && !this.isEphemeralStatesEquals(st, this.lastEphemeralState)) {
this.saveEphemeralState(st);
this.lastEphemeralState = st;
}
}
isEphemeralStatesEquals(state1, state2) {
if (state1.cursor && !state2.cursor)
return false;
if (!state1.cursor && state2.cursor)
return false;
if (state1.cursor) {
if (state1.cursor.from.ch != state2.cursor.from.ch)
return false;
if (state1.cursor.from.line != state2.cursor.from.line)
return false;
if (state1.cursor.to.ch != state2.cursor.to.ch)
return false;
if (state1.cursor.to.line != state2.cursor.to.line)
return false;
}
if (state1.scroll && !state2.scroll)
return false;
if (!state1.scroll && state2.scroll)
return false;
if (state1.scroll && state1.scroll != state2.scroll)
return false;
return true;
}
saveEphemeralState(st) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
let fileName = (_a = this.app.workspace.getActiveFile()) === null || _a === void 0 ? void 0 : _a.path;
if (fileName && fileName == this.lastLoadedFileName) { //do not save if file changed or was not loaded
this.db[fileName] = Object.assign(Object.assign({}, st), { lastModified: Date.now() });
}
});
}
restoreEphemeralState(file) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
let fileName = (_a = this.app.workspace.getActiveFile()) === null || _a === void 0 ? void 0 : _a.path;
if (fileName && this.loadingFile && this.lastLoadedFileName == fileName) //if already started loading
return;
let activeLeaf = this.app.workspace.getMostRecentLeaf();
//@ts-ignore no-official-API
if (activeLeaf && this.loadedLeafIdList.includes(activeLeaf.id + ':' + activeLeaf.getViewState().state.file))
return;
this.loadedLeafIdList = [];
this.app.workspace.iterateAllLeaves((leaf) => {
if (leaf.getViewState().type === "markdown") {
//@ts-ignore no-official-API
this.loadedLeafIdList.push(leaf.id + ':' + leaf.getViewState().state.file);
}
});
this.loadingFile = true;
if (this.lastLoadedFileName != fileName) {
this.lastEphemeralState = {};
this.lastLoadedFileName = fileName;
let st;
if (fileName) {
st = this.db[fileName];
if (st) {
//waiting for load note
yield this.delay(this.settings.delayAfterFileOpening);
// Don't scroll when a link scrolls and highlights text
// i.e. if file is open by links like [link](note.md#header) and wikilinks
// See #10, #32, #46, #51
let containsFlashingSpan = this.app.workspace.containerEl.querySelector('.is-flashing');
if (!containsFlashingSpan) {
yield this.delay(10);
this.setEphemeralState(st);
}
}
}
this.lastEphemeralState = st;
}
this.loadingFile = false;
});
}
pruneDb() {
var _a;
const { pruneOrphans, maxAgeDays, maxCount } = this.settings;
if (pruneOrphans) {
for (const key of Object.keys(this.db)) {
if (!this.app.vault.getAbstractFileByPath(key)) {
delete this.db[key];
}
}
}
if (maxAgeDays > 0) {
const cutoff = Date.now() - maxAgeDays * 86400000;
for (const key of Object.keys(this.db)) {
if (((_a = this.db[key].lastModified) !== null && _a !== void 0 ? _a : 0) < cutoff) {
delete this.db[key];
}
}
}
if (maxCount > 0 && Object.keys(this.db).length > maxCount) {
const sorted = Object.entries(this.db)
.sort((a, b) => { var _a, _b; return ((_a = b[1].lastModified) !== null && _a !== void 0 ? _a : 0) - ((_b = a[1].lastModified) !== null && _b !== void 0 ? _b : 0); });
this.db = Object.fromEntries(sorted.slice(0, maxCount));
}
}
readDb() {
return __awaiter(this, void 0, void 0, function* () {
let db = {};
if (yield this.app.vault.adapter.exists(this.settings.dbFileName)) {
let data = yield this.app.vault.adapter.read(this.settings.dbFileName);
db = JSON.parse(data);
const now = Date.now();
for (const key of Object.keys(db)) {
if (db[key].lastModified === undefined) {
db[key].lastModified = now;
}
}
}
return db;
});
}
writeDb(db) {
return __awaiter(this, void 0, void 0, function* () {
//create folder for db file if not exist
let newParentFolder = this.settings.dbFileName.substring(0, this.settings.dbFileName.lastIndexOf("/"));
if (!(yield this.app.vault.adapter.exists(newParentFolder)))
this.app.vault.adapter.mkdir(newParentFolder);
if (JSON.stringify(this.db) !== JSON.stringify(this.lastSavedDb)) {
this.app.vault.adapter.write(this.settings.dbFileName, JSON.stringify(db));
this.lastSavedDb = JSON.parse(JSON.stringify(db));
}
});
}
getEphemeralState() {
// let state: EphemeralState = this.app.workspace.getActiveViewOfType(MarkdownView)?.getEphemeralState(); //doesn't work properly
var _a, _b, _c;
let state = {};
state.scroll = Number((_c = (_b = (_a = this.app.workspace.getActiveViewOfType(obsidian.MarkdownView)) === null || _a === void 0 ? void 0 : _a.currentMode) === null || _b === void 0 ? void 0 : _b.getScroll()) === null || _c === void 0 ? void 0 : _c.toFixed(4));
let editor = this.getEditor();
if (editor) {
let from = editor.getCursor("anchor");
let to = editor.getCursor("head");
if (from && to) {
state.cursor = {
from: {
ch: from.ch,
line: from.line
},
to: {
ch: to.ch,
line: to.line
}
};
}
}
return state;
}
setEphemeralState(state) {
const view = this.app.workspace.getActiveViewOfType(obsidian.MarkdownView);
if (state.cursor) {
let editor = this.getEditor();
if (editor) {
editor.setSelection(state.cursor.from, state.cursor.to);
}
}
if (view && state.scroll) {
view.setEphemeralState(state);
// view.previewMode.applyScroll(state.scroll);
// view.sourceMode.applyScroll(state.scroll);
}
}
getEditor() {
var _a;
return (_a = this.app.workspace.getActiveViewOfType(obsidian.MarkdownView)) === null || _a === void 0 ? void 0 : _a.editor;
}
loadSettings() {
return __awaiter(this, void 0, void 0, function* () {
let settings = Object.assign({}, DEFAULT_SETTINGS, yield this.loadData());
if ((settings === null || settings === void 0 ? void 0 : settings.saveTimer) < SAFE_DB_FLUSH_INTERVAL) {
settings.saveTimer = SAFE_DB_FLUSH_INTERVAL;
}
if (!settings.dbFileName || settings.dbFileName === DEFAULT_DB_FILENAME_LEGACY) {
settings.dbFileName = this.manifest.dir + '/cursor-positions.json';
}
this.settings = settings;
});
}
saveSettings() {
return __awaiter(this, void 0, void 0, function* () {
yield this.saveData(this.settings);
});
}
delay(ms) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise(resolve => setTimeout(resolve, ms));
});
}
}
class SettingTab extends obsidian.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
let { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Remember cursor position - Settings' });
new obsidian.SettingGroup(containerEl)
.addSetting((setting) => setting
.setName('Data file name')
.setDesc('Save positions to this file')
.addText((text) => text
.setPlaceholder('Example: cursor-positions.json')
.setValue(this.plugin.settings.dbFileName)
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.dbFileName = value;
yield this.plugin.saveSettings();
}))))
.addSetting((setting) => setting
.setName('Delay after opening a new note')
.setDesc("This plugin shouldn't scroll if you used a link to the note header like [link](note.md#header). If it did, then increase the delay until everything works. If you are not using links to page sections, set the delay to zero (slider to the left). Slider values: 0-300 ms (default value: 100 ms).")
.addSlider((text) => text
.setLimits(0, 300, 10)
.setDynamicTooltip()
.setValue(this.plugin.settings.delayAfterFileOpening)
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.delayAfterFileOpening = value;
yield this.plugin.saveSettings();
}))))
.addSetting((setting) => setting
.setName('Delay between saving the cursor position to file')
.setDesc("Useful for multi-device users. If you don't want to wait until closing Obsidian to the cursor position been saved.")
.addSlider((text) => text
.setLimits(SAFE_DB_FLUSH_INTERVAL, SAFE_DB_FLUSH_INTERVAL * 10, 10)
.setDynamicTooltip()
.setValue(this.plugin.settings.saveTimer)
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.saveTimer = value;
yield this.plugin.saveSettings();
window.clearInterval(this.plugin.saveTimerIntervalId);
this.plugin.saveTimerIntervalId = this.plugin.registerInterval(window.setInterval(() => this.plugin.writeDb(this.plugin.db), value));
}))));
const { pruneOrphans, maxAgeDays, maxCount } = this.plugin.settings;
const pruningEnabled = pruneOrphans || maxAgeDays > 0 || maxCount > 0;
const entryCount = Object.keys(this.plugin.db).length;
new obsidian.SettingGroup(containerEl)
.setHeading('Pruning')
.addSetting((setting) => setting
.setName('Remove entries for deleted or missing files')
.setDesc('On startup, remove saved positions for files that no longer exist in the vault. ' +
'Disable this if you use junctions, removable drives, or other setups where files may be temporarily unavailable.')
.addToggle((toggle) => toggle
.setValue(this.plugin.settings.pruneOrphans)
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.pruneOrphans = value;
yield this.plugin.saveSettings();
this.display();
}))))
.addSetting((setting) => setting
.setName('Remove entries older than')
.setDesc('On startup, remove saved positions for files that have not been visited within the selected period.')
.addDropdown((drop) => drop
.addOption('30', '30 days')
.addOption('60', '60 days')
.addOption('90', '90 days')
.addOption('365', '1 year')
.addOption('0', 'Never')
.setValue(String(this.plugin.settings.maxAgeDays))
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.maxAgeDays = Number(value);
yield this.plugin.saveSettings();
this.display();
}))))
.addSetting((setting) => setting
.setName('Maximum number of entries to keep')
.setDesc('On startup, if the number of saved positions exceeds this limit, the oldest entries are removed. Most-recently visited files are kept.')
.addDropdown((drop) => drop
.addOption('50', '50')
.addOption('100', '100')
.addOption('250', '250')
.addOption('500', '500')
.addOption('0', 'Never')
.setValue(String(this.plugin.settings.maxCount))
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
this.plugin.settings.maxCount = Number(value);
yield this.plugin.saveSettings();
this.display();
}))))
.addSetting((setting) => setting
.setName('Apply pruning rules')
.setDesc(`Currently tracking ${entryCount} ${entryCount === 1 ? 'entry' : 'entries'}. Pruning runs automatically on next reload; use this to apply immediately.`)
.addButton((btn) => {
btn.setButtonText('Prune now')
.setDisabled(!pruningEnabled);
if (pruningEnabled)
btn.setCta();
btn.onClick(() => __awaiter(this, void 0, void 0, function* () {
this.plugin.pruneDb();
yield this.plugin.writeDb(this.plugin.db);
this.display();
}));
}));
}
}
module.exports = RememberCursorPosition;
/* nosourcemap */