297 lines
67 KiB
JavaScript
297 lines
67 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_SETTINGS = {
|
||
|
dbFileName: '.obsidian/plugins/remember-cursor-position/cursor-positions.json',
|
||
|
delayAfterFileOpening: 100,
|
||
|
saveTimer: SAFE_DB_FLUSH_INTERVAL,
|
||
|
};
|
||
|
class RememberCursorPosition extends obsidian.Plugin {
|
||
|
constructor() {
|
||
|
super(...arguments);
|
||
|
this.loadingFile = false;
|
||
|
}
|
||
|
onload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
yield this.loadSettings();
|
||
|
try {
|
||
|
this.db = yield this.readDb();
|
||
|
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()));
|
||
|
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.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] = st;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
restoreEphemeralState() {
|
||
|
var _a, _b, _c;
|
||
|
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;
|
||
|
this.loadingFile = true;
|
||
|
if (this.lastLoadedFileName != fileName) {
|
||
|
this.lastEphemeralState = {};
|
||
|
this.lastLoadedFileName = fileName;
|
||
|
if (fileName) {
|
||
|
let st = this.db[fileName];
|
||
|
if (st) {
|
||
|
//waiting for load note
|
||
|
yield this.delay(this.settings.delayAfterFileOpening);
|
||
|
let scroll;
|
||
|
for (let i = 0; i < 20; i++) {
|
||
|
scroll = (_c = (_b = this.app.workspace.getActiveViewOfType(obsidian.MarkdownView)) === null || _b === void 0 ? void 0 : _b.currentMode) === null || _c === void 0 ? void 0 : _c.getScroll();
|
||
|
if (scroll !== null)
|
||
|
break;
|
||
|
yield this.delay(10);
|
||
|
}
|
||
|
//if note opened by link like [link](note.md#header), do not scroll it
|
||
|
// CHANGE this expression for compatability with the new PROPERTIES view after Obsidian Version 1.4
|
||
|
if (scroll === 0 || true) {
|
||
|
//force update scroll while note is loading
|
||
|
//todo: find better solution to wait for file loaded
|
||
|
for (let i = 0; i < 20; i++) {
|
||
|
this.setEphemeralState(st);
|
||
|
yield this.delay(10);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
this.lastEphemeralState = st;
|
||
|
}
|
||
|
this.loadingFile = false;
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
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);
|
||
|
}
|
||
|
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;
|
||
|
}
|
||
|
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.Setting(containerEl)
|
||
|
.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();
|
||
|
})));
|
||
|
new obsidian.Setting(containerEl)
|
||
|
.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)
|
||
|
.setValue(this.plugin.settings.delayAfterFileOpening)
|
||
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.settings.delayAfterFileOpening = value;
|
||
|
yield this.plugin.saveSettings();
|
||
|
})));
|
||
|
new obsidian.Setting(containerEl)
|
||
|
.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)
|
||
|
.setValue(this.plugin.settings.saveTimer)
|
||
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.settings.saveTimer = value;
|
||
|
yield this.plugin.saveSettings();
|
||
|
})));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = RememberCursorPosition;
|
||
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5qcyIsInNvdXJjZXMiOlsibm9kZV9tb2R1bGVzL3RzbGliL3RzbGliLmVzNi5qcyIsIm1haW4udHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKlxyXG5Db3B5cmlnaHQgKGMpIE1pY3Jvc29mdCBDb3Jwb3JhdGlvbi5cclxuXHJcblBlcm1pc3Npb24gdG8gdXNlLCBjb3B5LCBtb2RpZnksIGFuZC9vciBkaXN0cmlidXRlIHRoaXMgc29mdHdhcmUgZm9yIGFueVxyXG5wdXJwb3NlIHdpdGggb3Igd2l0aG91dCBmZWUgaXMgaGVyZWJ5IGdyYW50ZWQuXHJcblxyXG5USEUgU09GVFdBUkUgSVMgUFJPVklERUQgXCJBUyBJU1wiIEFORCBUSEUgQVVUSE9SIERJU0NMQUlNUyBBTEwgV0FSUkFOVElFUyBXSVRIXHJcblJFR0FSRCBUTyBUSElTIFNPRlRXQVJFIElOQ0xVRElORyBBTEwgSU1QTElFRCBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWVxyXG5BTkQgRklUTkVTUy4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFIEFVVEhPUiBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsXHJcbklORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTVxyXG5MT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUlxyXG5PVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SXHJcblBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuXHJcbioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqICovXHJcbi8qIGdsb2JhbCBSZWZsZWN0LCBQcm9taXNlLCBTdXBwcmVzc2VkRXJyb3IsIFN5bWJvbCAqL1xyXG5cclxudmFyIGV4dGVuZFN0YXRpY3MgPSBmdW5jdGlvbihkLCBiKSB7XHJcbiAgICBleHRlbmRTdGF0aWNzID0gT2JqZWN0LnNldFByb3RvdHlwZU9mIHx8XHJcbiAgICAgICAgKHsgX19wcm90b19fOiBbXSB9IGluc3RhbmNlb2YgQXJyYXkgJiYgZnVuY3Rpb24gKGQsIGIpIHsgZC5fX3Byb3RvX18gPSBiOyB9KSB8fFxyXG4gICAgICAgIGZ1bmN0aW9uIChkLCBiKSB7IGZvciAodmFyIHAgaW4gYikgaWYgKE9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbChiLCBwKSkgZFtwXSA9IGJbcF07IH07XHJcbiAgICByZXR1cm4gZXh0ZW5kU3RhdGljcyhkLCBiKTtcclxufTtcclxuXHJcbmV4cG9ydCBmdW5jdGlvbiBfX2V4dGVuZHMoZCwgYikge1xyXG4gICAgaWYgKHR5cGVvZiBiICE9PSBcImZ1bmN0aW9uXCIgJiYgYiAhPT0gbnVsbClcclxuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKFwiQ2xhc3MgZXh0ZW5kcyB2YWx1ZSBcIiArIFN0cmluZyhiKSArIFwiIGlzIG5vdCBhIGNvbnN0cnVjdG9yIG9yIG51bGxcIik7XHJcbiAgICBleHRlbmRTdGF0aWNzKGQsIGIpO1xyXG4gICAgZnVuY3Rpb24gX18oKSB7IHRoaXMuY29uc3RydWN0b3IgPSBkOyB9XHJcbiAgICBkLnByb3RvdHlwZSA9IGIgPT09IG51bGwgPyBPYmplY3QuY3JlYXRlKGIpIDogKF9fLnByb3RvdHlwZSA9IGIucHJvdG90eXBlLCBuZXcgX18oKSk7XHJcbn1cclxuXHJcbmV4cG9ydCB2YXIgX19hc3NpZ24gPSBmdW5jdGlvbigpIHtcclxuICAgIF9fYXNzaWduID0gT2JqZWN0LmFzc2lnbiB8fCBmdW5jdGlvbiBfX2Fzc2lnbih0KSB7XHJcbiAgICAgICAgZm9yICh2YXIgcywgaSA9IDEsIG4gPSBhcmd1bWVudHMubGVuZ3RoOyBpIDwgbjsgaSsrKSB7XHJcbiAgICAgICAgICAgIHMgPSBhcmd1bWVudHNbaV07XHJcbiAgICAgICAgICAgIGZvciAodmFyIHAgaW4gcykgaWYgKE9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbChzLCBwKSkgdFtwXSA9IHNbcF07XHJcbiAgICAgICAgfVxyXG4gICAgICAgIHJldHVybiB0O1xyXG4gICAgfVxyXG4gICAgcmV0dXJuIF9fYXNzaWduLmFwcGx5KHRoaXMsIGFyZ3VtZW50cyk7XHJcbn1cclxuXHJcbmV4cG9ydCBmdW5jdGlvbiBfX3Jlc3QocywgZSkge1xyXG4gICAgdmFyIHQgPSB7fTtcclxuICAgIGZvciAodmFyIHAgaW4gcykgaWYgKE9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbChzLCBwKSAmJiBlLmluZGV4T2YocCkgPCAwKVxyXG4gICAgICAgIHRbcF0gPSBzW3BdO1xyXG4gICAgaWYgKHMgIT0gbnVsbCAmJiB0eXBlb2YgT2JqZWN0LmdldE93blByb3BlcnR5U3ltYm9scyA9PT0gXCJmdW5jdGlvblwiKVxyXG4gICAgICAgIGZvciAodmFyIGkgPSAwLCBwID0gT2JqZWN0LmdldE93blByb3BlcnR5U3ltYm9scyhzKTsgaSA8IHAubGVuZ3RoOyBpKyspIHtcclxuICAgICAgICAgICAgaWYgKGUuaW5kZXhPZihwW2ldKSA8IDAgJiYgT2JqZWN0LnByb3RvdHlwZS5wcm9wZXJ0eUlzRW51bWVyYWJsZS5jYWxsKHMsIHBbaV0pKVxyXG4gICAgICAgICAgICAgICAgdFtwW2ldXSA9IHNbcFtpXV07XHJcbiAgICAgICAgfVxyXG4gICAgcmV0dXJuIHQ7XHJcbn1cclxuXHJcbmV4cG9ydCBmdW5jdGlvbiBfX2RlY29yYXRlKGRlY29yYXRvcnMsIHRhcmdldCwga2V5LCBkZXNjKSB7XHJcbiAgICB2YXIgYyA9IGFyZ3VtZW50cy5sZW5ndGgsIHIgPSBjIDwgMyA/IHRhcmdldCA6IGRlc2MgPT09IG51bGwgPyBkZXNjID0gT2JqZWN0LmdldE93blByb3BlcnR5RGVzY3JpcHRvcih0YXJnZXQsIGtleSkgOiBkZXNjLCBkO1xyXG4gICAgaWYgKHR5cGVvZiBSZWZsZWN0ID09PSBcIm9iamVjdFwiICYmIHR5cGVvZiBSZWZsZWN0LmRlY29yYXRlID09PSBcImZ1bmN0aW9uXCIpIHIgPSBSZWZsZWN0LmRlY29yYXRlKGRlY29yYXRvcnMsIHRhcmdldCwga2V5LCBkZXNjKTtcclxuICAgIGVsc2UgZm9yICh2YXIgaSA9IGRlY29yYXRvcnMubGVuZ3RoIC0gMTsgaSA+PSAwOyBpLS0pIGlmIChkID0gZGVjb3JhdG9
|