3260 lines
442 KiB
JavaScript
3260 lines
442 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
var obsidian = require('obsidian');
|
||
|
var view = require('@codemirror/view');
|
||
|
var language = require('@codemirror/language');
|
||
|
var state = require('@codemirror/state');
|
||
|
|
||
|
/******************************************************************************
|
||
|
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());
|
||
|
});
|
||
|
}
|
||
|
|
||
|
class MoveCursorToPreviousUnfoldedLine {
|
||
|
constructor(root) {
|
||
|
this.root = root;
|
||
|
this.stopPropagation = false;
|
||
|
this.updated = false;
|
||
|
}
|
||
|
shouldStopPropagation() {
|
||
|
return this.stopPropagation;
|
||
|
}
|
||
|
shouldUpdate() {
|
||
|
return this.updated;
|
||
|
}
|
||
|
perform() {
|
||
|
const { root } = this;
|
||
|
if (!root.hasSingleCursor()) {
|
||
|
return;
|
||
|
}
|
||
|
const list = this.root.getListUnderCursor();
|
||
|
const cursor = this.root.getCursor();
|
||
|
const lines = list.getLinesInfo();
|
||
|
const lineNo = lines.findIndex((l) => {
|
||
|
return (cursor.ch === l.from.ch + list.getCheckboxLength() &&
|
||
|
cursor.line === l.from.line);
|
||
|
});
|
||
|
if (lineNo === 0) {
|
||
|
this.moveCursorToPreviousUnfoldedItem(root, cursor);
|
||
|
}
|
||
|
else if (lineNo > 0) {
|
||
|
this.moveCursorToPreviousNoteLine(root, lines, lineNo);
|
||
|
}
|
||
|
}
|
||
|
moveCursorToPreviousNoteLine(root, lines, lineNo) {
|
||
|
this.stopPropagation = true;
|
||
|
this.updated = true;
|
||
|
root.replaceCursor(lines[lineNo - 1].to);
|
||
|
}
|
||
|
moveCursorToPreviousUnfoldedItem(root, cursor) {
|
||
|
const prev = root.getListUnderLine(cursor.line - 1);
|
||
|
if (!prev) {
|
||
|
return;
|
||
|
}
|
||
|
this.stopPropagation = true;
|
||
|
this.updated = true;
|
||
|
if (prev.isFolded()) {
|
||
|
const foldRoot = prev.getTopFoldRoot();
|
||
|
const firstLineEnd = foldRoot.getLinesInfo()[0].to;
|
||
|
root.replaceCursor(firstLineEnd);
|
||
|
}
|
||
|
else {
|
||
|
root.replaceCursor(prev.getLastLineContentEnd());
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function getEditorFromState(state) {
|
||
|
const { editor } = state.field(obsidian.editorInfoField);
|
||
|
if (!editor) {
|
||
|
return null;
|
||
|
}
|
||
|
return new MyEditor(editor);
|
||
|
}
|
||
|
function foldInside(view, from, to) {
|
||
|
let found = null;
|
||
|
language.foldedRanges(view.state).between(from, to, (from, to) => {
|
||
|
if (!found || found.from > from)
|
||
|
found = { from, to };
|
||
|
});
|
||
|
return found;
|
||
|
}
|
||
|
class MyEditor {
|
||
|
constructor(e) {
|
||
|
this.e = e;
|
||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
this.view = this.e.cm;
|
||
|
}
|
||
|
getCursor() {
|
||
|
return this.e.getCursor();
|
||
|
}
|
||
|
getLine(n) {
|
||
|
return this.e.getLine(n);
|
||
|
}
|
||
|
lastLine() {
|
||
|
return this.e.lastLine();
|
||
|
}
|
||
|
listSelections() {
|
||
|
return this.e.listSelections();
|
||
|
}
|
||
|
getRange(from, to) {
|
||
|
return this.e.getRange(from, to);
|
||
|
}
|
||
|
replaceRange(replacement, from, to) {
|
||
|
return this.e.replaceRange(replacement, from, to);
|
||
|
}
|
||
|
setSelections(selections) {
|
||
|
this.e.setSelections(selections);
|
||
|
}
|
||
|
setValue(text) {
|
||
|
this.e.setValue(text);
|
||
|
}
|
||
|
getValue() {
|
||
|
return this.e.getValue();
|
||
|
}
|
||
|
offsetToPos(offset) {
|
||
|
return this.e.offsetToPos(offset);
|
||
|
}
|
||
|
posToOffset(pos) {
|
||
|
return this.e.posToOffset(pos);
|
||
|
}
|
||
|
fold(n) {
|
||
|
const { view } = this;
|
||
|
const l = view.lineBlockAt(view.state.doc.line(n + 1).from);
|
||
|
const range = language.foldable(view.state, l.from, l.to);
|
||
|
if (!range || range.from === range.to) {
|
||
|
return;
|
||
|
}
|
||
|
view.dispatch({ effects: [language.foldEffect.of(range)] });
|
||
|
}
|
||
|
unfold(n) {
|
||
|
const { view } = this;
|
||
|
const l = view.lineBlockAt(view.state.doc.line(n + 1).from);
|
||
|
const range = foldInside(view, l.from, l.to);
|
||
|
if (!range) {
|
||
|
return;
|
||
|
}
|
||
|
view.dispatch({ effects: [language.unfoldEffect.of(range)] });
|
||
|
}
|
||
|
getAllFoldedLines() {
|
||
|
const c = language.foldedRanges(this.view.state).iter();
|
||
|
const res = [];
|
||
|
while (c.value) {
|
||
|
res.push(this.offsetToPos(c.from).line);
|
||
|
c.next();
|
||
|
}
|
||
|
return res;
|
||
|
}
|
||
|
triggerOnKeyDown(e) {
|
||
|
view.runScopeHandlers(this.view, e, "editor");
|
||
|
}
|
||
|
getZoomRange() {
|
||
|
if (!window.ObsidianZoomPlugin) {
|
||
|
return null;
|
||
|
}
|
||
|
return window.ObsidianZoomPlugin.getZoomRange(this.e);
|
||
|
}
|
||
|
zoomOut() {
|
||
|
if (!window.ObsidianZoomPlugin) {
|
||
|
return;
|
||
|
}
|
||
|
window.ObsidianZoomPlugin.zoomOut(this.e);
|
||
|
}
|
||
|
zoomIn(line) {
|
||
|
if (!window.ObsidianZoomPlugin) {
|
||
|
return;
|
||
|
}
|
||
|
window.ObsidianZoomPlugin.zoomIn(this.e, line);
|
||
|
}
|
||
|
tryRefreshZoom(line) {
|
||
|
if (!window.ObsidianZoomPlugin) {
|
||
|
return;
|
||
|
}
|
||
|
if (window.ObsidianZoomPlugin.refreshZoom) {
|
||
|
window.ObsidianZoomPlugin.refreshZoom(this.e);
|
||
|
}
|
||
|
else {
|
||
|
window.ObsidianZoomPlugin.zoomIn(this.e, line);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function createKeymapRunCallback(config) {
|
||
|
const check = config.check || (() => true);
|
||
|
const { run } = config;
|
||
|
return (view) => {
|
||
|
const editor = getEditorFromState(view.state);
|
||
|
if (!check(editor)) {
|
||
|
return false;
|
||
|
}
|
||
|
const { shouldUpdate, shouldStopPropagation } = run(editor);
|
||
|
return shouldUpdate || shouldStopPropagation;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
class ArrowLeftAndCtrlArrowLeftBehaviourOverride {
|
||
|
constructor(plugin, settings, imeDetector, operationPerformer) {
|
||
|
this.plugin = plugin;
|
||
|
this.settings = settings;
|
||
|
this.imeDetector = imeDetector;
|
||
|
this.operationPerformer = operationPerformer;
|
||
|
this.check = () => {
|
||
|
return (this.settings.keepCursorWithinContent !== "never" &&
|
||
|
!this.imeDetector.isOpened());
|
||
|
};
|
||
|
this.run = (editor) => {
|
||
|
return this.operationPerformer.perform((root) => new MoveCursorToPreviousUnfoldedLine(root), editor);
|
||
|
};
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.registerEditorExtension(view.keymap.of([
|
||
|
{
|
||
|
key: "ArrowLeft",
|
||
|
run: createKeymapRunCallback({
|
||
|
check: this.check,
|
||
|
run: this.run,
|
||
|
}),
|
||
|
},
|
||
|
{
|
||
|
win: "c-ArrowLeft",
|
||
|
linux: "c-ArrowLeft",
|
||
|
run: createKeymapRunCallback({
|
||
|
check: this.check,
|
||
|
run: this.run,
|
||
|
}),
|
||
|
},
|
||
|
]));
|
||
|
});
|
||
|
}
|
||
|
unload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () { });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function cmpPos(a, b) {
|
||
|
return a.line - b.line || a.ch - b.ch;
|
||
|
}
|
||
|
function maxPos(a, b) {
|
||
|
return cmpPos(a, b) < 0 ? b : a;
|
||
|
}
|
||
|
function minPos(a, b) {
|
||
|
return cmpPos(a, b) < 0 ? a : b;
|
||
|
}
|
||
|
function isRangesIntersects(a, b) {
|
||
|
return cmpPos(a[1], b[0]) >= 0 && cmpPos(a[0], b[1]) <= 0;
|
||
|
}
|
||
|
function recalculateNumericBullets(root) {
|
||
|
function visit(parent) {
|
||
|
let index = 1;
|
||
|
for (const child of parent.getChildren()) {
|
||
|
if (/\d+\./.test(child.getBullet())) {
|
||
|
child.replateBullet(`${index++}.`);
|
||
|
}
|
||
|
visit(child);
|
||
|
}
|
||
|
}
|
||
|
visit(root);
|
||
|
}
|
||
|
let idSeq = 0;
|
||
|
class List {
|
||
|
constructor(root, indent, bullet, optionalCheckbox, spaceAfterBullet, firstLine, foldRoot) {
|
||
|
this.root = root;
|
||
|
this.indent = indent;
|
||
|
this.bullet = bullet;
|
||
|
this.optionalCheckbox = optionalCheckbox;
|
||
|
this.spaceAfterBullet = spaceAfterBullet;
|
||
|
this.foldRoot = foldRoot;
|
||
|
this.parent = null;
|
||
|
this.children = [];
|
||
|
this.notesIndent = null;
|
||
|
this.lines = [];
|
||
|
this.id = idSeq++;
|
||
|
this.lines.push(firstLine);
|
||
|
}
|
||
|
getID() {
|
||
|
return this.id;
|
||
|
}
|
||
|
getNotesIndent() {
|
||
|
return this.notesIndent;
|
||
|
}
|
||
|
setNotesIndent(notesIndent) {
|
||
|
if (this.notesIndent !== null) {
|
||
|
throw new Error(`Notes indent already provided`);
|
||
|
}
|
||
|
this.notesIndent = notesIndent;
|
||
|
}
|
||
|
addLine(text) {
|
||
|
if (this.notesIndent === null) {
|
||
|
throw new Error(`Unable to add line, notes indent should be provided first`);
|
||
|
}
|
||
|
this.lines.push(text);
|
||
|
}
|
||
|
replaceLines(lines) {
|
||
|
if (lines.length > 1 && this.notesIndent === null) {
|
||
|
throw new Error(`Unable to add line, notes indent should be provided first`);
|
||
|
}
|
||
|
this.lines = lines;
|
||
|
}
|
||
|
getLineCount() {
|
||
|
return this.lines.length;
|
||
|
}
|
||
|
getRoot() {
|
||
|
return this.root;
|
||
|
}
|
||
|
getChildren() {
|
||
|
return this.children.concat();
|
||
|
}
|
||
|
getLinesInfo() {
|
||
|
const startLine = this.root.getContentLinesRangeOf(this)[0];
|
||
|
return this.lines.map((row, i) => {
|
||
|
const line = startLine + i;
|
||
|
const startCh = i === 0 ? this.getContentStartCh() : this.notesIndent.length;
|
||
|
const endCh = startCh + row.length;
|
||
|
return {
|
||
|
text: row,
|
||
|
from: { line, ch: startCh },
|
||
|
to: { line, ch: endCh },
|
||
|
};
|
||
|
});
|
||
|
}
|
||
|
getLines() {
|
||
|
return this.lines.concat();
|
||
|
}
|
||
|
getFirstLineContentStart() {
|
||
|
const startLine = this.root.getContentLinesRangeOf(this)[0];
|
||
|
return {
|
||
|
line: startLine,
|
||
|
ch: this.getContentStartCh(),
|
||
|
};
|
||
|
}
|
||
|
getFirstLineContentStartAfterCheckbox() {
|
||
|
const startLine = this.root.getContentLinesRangeOf(this)[0];
|
||
|
return {
|
||
|
line: startLine,
|
||
|
ch: this.getContentStartCh() + this.getCheckboxLength(),
|
||
|
};
|
||
|
}
|
||
|
getLastLineContentEnd() {
|
||
|
const endLine = this.root.getContentLinesRangeOf(this)[1];
|
||
|
const endCh = this.lines.length === 1
|
||
|
? this.getContentStartCh() + this.lines[0].length
|
||
|
: this.notesIndent.length + this.lines[this.lines.length - 1].length;
|
||
|
return {
|
||
|
line: endLine,
|
||
|
ch: endCh,
|
||
|
};
|
||
|
}
|
||
|
getContentEndIncludingChildren() {
|
||
|
return this.getLastChild().getLastLineContentEnd();
|
||
|
}
|
||
|
getLastChild() {
|
||
|
let lastChild = this;
|
||
|
while (!lastChild.isEmpty()) {
|
||
|
lastChild = lastChild.getChildren().last();
|
||
|
}
|
||
|
return lastChild;
|
||
|
}
|
||
|
getContentStartCh() {
|
||
|
return this.indent.length + this.bullet.length + 1;
|
||
|
}
|
||
|
isFolded() {
|
||
|
if (this.foldRoot) {
|
||
|
return true;
|
||
|
}
|
||
|
if (this.parent) {
|
||
|
return this.parent.isFolded();
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
isFoldRoot() {
|
||
|
return this.foldRoot;
|
||
|
}
|
||
|
getTopFoldRoot() {
|
||
|
let tmp = this;
|
||
|
let foldRoot = null;
|
||
|
while (tmp) {
|
||
|
if (tmp.isFoldRoot()) {
|
||
|
foldRoot = tmp;
|
||
|
}
|
||
|
tmp = tmp.parent;
|
||
|
}
|
||
|
return foldRoot;
|
||
|
}
|
||
|
getLevel() {
|
||
|
if (!this.parent) {
|
||
|
return 0;
|
||
|
}
|
||
|
return this.parent.getLevel() + 1;
|
||
|
}
|
||
|
unindentContent(from, till) {
|
||
|
this.indent = this.indent.slice(0, from) + this.indent.slice(till);
|
||
|
if (this.notesIndent !== null) {
|
||
|
this.notesIndent =
|
||
|
this.notesIndent.slice(0, from) + this.notesIndent.slice(till);
|
||
|
}
|
||
|
for (const child of this.children) {
|
||
|
child.unindentContent(from, till);
|
||
|
}
|
||
|
}
|
||
|
indentContent(indentPos, indentChars) {
|
||
|
this.indent =
|
||
|
this.indent.slice(0, indentPos) +
|
||
|
indentChars +
|
||
|
this.indent.slice(indentPos);
|
||
|
if (this.notesIndent !== null) {
|
||
|
this.notesIndent =
|
||
|
this.notesIndent.slice(0, indentPos) +
|
||
|
indentChars +
|
||
|
this.notesIndent.slice(indentPos);
|
||
|
}
|
||
|
for (const child of this.children) {
|
||
|
child.indentContent(indentPos, indentChars);
|
||
|
}
|
||
|
}
|
||
|
getFirstLineIndent() {
|
||
|
return this.indent;
|
||
|
}
|
||
|
getBullet() {
|
||
|
return this.bullet;
|
||
|
}
|
||
|
getSpaceAfterBullet() {
|
||
|
return this.spaceAfterBullet;
|
||
|
}
|
||
|
getCheckboxLength() {
|
||
|
return this.optionalCheckbox.length;
|
||
|
}
|
||
|
replateBullet(bullet) {
|
||
|
this.bullet = bullet;
|
||
|
}
|
||
|
getParent() {
|
||
|
return this.parent;
|
||
|
}
|
||
|
addBeforeAll(list) {
|
||
|
this.children.unshift(list);
|
||
|
list.parent = this;
|
||
|
}
|
||
|
addAfterAll(list) {
|
||
|
this.children.push(list);
|
||
|
list.parent = this;
|
||
|
}
|
||
|
removeChild(list) {
|
||
|
const i = this.children.indexOf(list);
|
||
|
this.children.splice(i, 1);
|
||
|
list.parent = null;
|
||
|
}
|
||
|
addBefore(before, list) {
|
||
|
const i = this.children.indexOf(before);
|
||
|
this.children.splice(i, 0, list);
|
||
|
list.parent = this;
|
||
|
}
|
||
|
addAfter(before, list) {
|
||
|
const i = this.children.indexOf(before);
|
||
|
this.children.splice(i + 1, 0, list);
|
||
|
list.parent = this;
|
||
|
}
|
||
|
getPrevSiblingOf(list) {
|
||
|
const i = this.children.indexOf(list);
|
||
|
return i > 0 ? this.children[i - 1] : null;
|
||
|
}
|
||
|
getNextSiblingOf(list) {
|
||
|
const i = this.children.indexOf(list);
|
||
|
return i >= 0 && i < this.children.length ? this.children[i + 1] : null;
|
||
|
}
|
||
|
isEmpty() {
|
||
|
return this.children.length === 0;
|
||
|
}
|
||
|
print() {
|
||
|
let res = "";
|
||
|
for (let i = 0; i < this.lines.length; i++) {
|
||
|
res +=
|
||
|
i === 0
|
||
|
? this.indent + this.bullet + this.spaceAfterBullet
|
||
|
: this.notesIndent;
|
||
|
res += this.lines[i];
|
||
|
res += "\n";
|
||
|
}
|
||
|
for (const child of this.children) {
|
||
|
res += child.print();
|
||
|
}
|
||
|
return res;
|
||
|
}
|
||
|
clone(newRoot) {
|
||
|
const clone = new List(newRoot, this.indent, this.bullet, this.optionalCheckbox, this.spaceAfterBullet, "", this.foldRoot);
|
||
|
clone.id = this.id;
|
||
|
clone.lines = this.lines.concat();
|
||
|
clone.notesIndent = this.notesIndent;
|
||
|
for (const child of this.children) {
|
||
|
clone.addAfterAll(child.clone(newRoot));
|
||
|
}
|
||
|
return clone;
|
||
|
}
|
||
|
}
|
||
|
class Root {
|
||
|
constructor(start, end, selections) {
|
||
|
this.start = start;
|
||
|
this.end = end;
|
||
|
this.rootList = new List(this, "", "", "", "", "", false);
|
||
|
this.selections = [];
|
||
|
this.replaceSelections(selections);
|
||
|
}
|
||
|
getRootList() {
|
||
|
return this.rootList;
|
||
|
}
|
||
|
getContentRange() {
|
||
|
return [this.getContentStart(), this.getContentEnd()];
|
||
|
}
|
||
|
getContentStart() {
|
||
|
return Object.assign({}, this.start);
|
||
|
}
|
||
|
getContentEnd() {
|
||
|
return Object.assign({}, this.end);
|
||
|
}
|
||
|
getSelections() {
|
||
|
return this.selections.map((s) => ({
|
||
|
anchor: Object.assign({}, s.anchor),
|
||
|
head: Object.assign({}, s.head),
|
||
|
}));
|
||
|
}
|
||
|
hasSingleCursor() {
|
||
|
if (!this.hasSingleSelection()) {
|
||
|
return false;
|
||
|
}
|
||
|
const selection = this.selections[0];
|
||
|
return (selection.anchor.line === selection.head.line &&
|
||
|
selection.anchor.ch === selection.head.ch);
|
||
|
}
|
||
|
hasSingleSelection() {
|
||
|
return this.selections.length === 1;
|
||
|
}
|
||
|
getSelection() {
|
||
|
const selection = this.selections[this.selections.length - 1];
|
||
|
const from = selection.anchor.ch > selection.head.ch
|
||
|
? selection.head.ch
|
||
|
: selection.anchor.ch;
|
||
|
const to = selection.anchor.ch > selection.head.ch
|
||
|
? selection.anchor.ch
|
||
|
: selection.head.ch;
|
||
|
return Object.assign(Object.assign({}, selection), { from,
|
||
|
to });
|
||
|
}
|
||
|
getCursor() {
|
||
|
return Object.assign({}, this.selections[this.selections.length - 1].head);
|
||
|
}
|
||
|
replaceCursor(cursor) {
|
||
|
this.selections = [{ anchor: cursor, head: cursor }];
|
||
|
}
|
||
|
replaceSelections(selections) {
|
||
|
if (selections.length < 1) {
|
||
|
throw new Error(`Unable to create Root without selections`);
|
||
|
}
|
||
|
this.selections = selections;
|
||
|
}
|
||
|
getListUnderCursor() {
|
||
|
return this.getListUnderLine(this.getCursor().line);
|
||
|
}
|
||
|
getListUnderLine(line) {
|
||
|
if (line < this.start.line || line > this.end.line) {
|
||
|
return;
|
||
|
}
|
||
|
let result = null;
|
||
|
let index = this.start.line;
|
||
|
const visitArr = (ll) => {
|
||
|
for (const l of ll) {
|
||
|
const listFromLine = index;
|
||
|
const listTillLine = listFromLine + l.getLineCount() - 1;
|
||
|
if (line >= listFromLine && line <= listTillLine) {
|
||
|
result = l;
|
||
|
}
|
||
|
else {
|
||
|
index = listTillLine + 1;
|
||
|
visitArr(l.getChildren());
|
||
|
}
|
||
|
if (result !== null) {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
visitArr(this.rootList.getChildren());
|
||
|
return result;
|
||
|
}
|
||
|
getContentLinesRangeOf(list) {
|
||
|
let result = null;
|
||
|
let line = this.start.line;
|
||
|
const visitArr = (ll) => {
|
||
|
for (const l of ll) {
|
||
|
const listFromLine = line;
|
||
|
const listTillLine = listFromLine + l.getLineCount() - 1;
|
||
|
if (l === list) {
|
||
|
result = [listFromLine, listTillLine];
|
||
|
}
|
||
|
else {
|
||
|
line = listTillLine + 1;
|
||
|
visitArr(l.getChildren());
|
||
|
}
|
||
|
if (result !== null) {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
visitArr(this.rootList.getChildren());
|
||
|
return result;
|
||
|
}
|
||
|
getChildren() {
|
||
|
return this.rootList.getChildren();
|
||
|
}
|
||
|
print() {
|
||
|
let res = "";
|
||
|
for (const child of this.rootList.getChildren()) {
|
||
|
res += child.print();
|
||
|
}
|
||
|
return res.replace(/\n$/, "");
|
||
|
}
|
||
|
clone() {
|
||
|
const clone = new Root(Object.assign({}, this.start), Object.assign({}, this.end), this.getSelections());
|
||
|
clone.rootList = this.rootList.clone(clone);
|
||
|
return clone;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class DeleteTillPreviousLineContentEnd {
|
||
|
constructor(root) {
|
||
|
this.root = root;
|
||
|
this.stopPropagation = false;
|
||
|
this.updated = false;
|
||
|
}
|
||
|
shouldStopPropagation() {
|
||
|
return this.stopPropagation;
|
||
|
}
|
||
|
shouldUpdate() {
|
||
|
return this.updated;
|
||
|
}
|
||
|
perform() {
|
||
|
const { root } = this;
|
||
|
if (!root.hasSingleCursor()) {
|
||
|
return;
|
||
|
}
|
||
|
const list = root.getListUnderCursor();
|
||
|
const cursor = root.getCursor();
|
||
|
const lines = list.getLinesInfo();
|
||
|
const lineNo = lines.findIndex((l) => cursor.ch === l.from.ch && cursor.line === l.from.line);
|
||
|
if (lineNo === 0) {
|
||
|
this.mergeWithPreviousItem(root, cursor, list);
|
||
|
}
|
||
|
else if (lineNo > 0) {
|
||
|
this.mergeNotes(root, cursor, list, lines, lineNo);
|
||
|
}
|
||
|
}
|
||
|
mergeNotes(root, cursor, list, lines, lineNo) {
|
||
|
this.stopPropagation = true;
|
||
|
this.updated = true;
|
||
|
const prevLineNo = lineNo - 1;
|
||
|
root.replaceCursor({
|
||
|
line: cursor.line - 1,
|
||
|
ch: lines[prevLineNo].text.length + lines[prevLineNo].from.ch,
|
||
|
});
|
||
|
lines[prevLineNo].text += lines[lineNo].text;
|
||
|
lines.splice(lineNo, 1);
|
||
|
list.replaceLines(lines.map((l) => l.text));
|
||
|
}
|
||
|
mergeWithPreviousItem(root, cursor, list) {
|
||
|
if (root.getChildren()[0] === list && list.isEmpty()) {
|
||
|
return;
|
||
|
}
|
||
|
this.stopPropagation = true;
|
||
|
const prev = root.getListUnderLine(cursor.line - 1);
|
||
|
if (!prev) {
|
||
|
return;
|
||
|
}
|
||
|
const bothAreEmpty = prev.isEmpty() && list.isEmpty();
|
||
|
const prevIsEmptyAndSameLevel = prev.isEmpty() && !list.isEmpty() && prev.getLevel() === list.getLevel();
|
||
|
const listIsEmptyAndPrevIsParent = list.isEmpty() && prev.getLevel() === list.getLevel() - 1;
|
||
|
if (bothAreEmpty || prevIsEmptyAndSameLevel || listIsEmptyAndPrevIsParent) {
|
||
|
this.updated = true;
|
||
|
const parent = list.getParent();
|
||
|
const prevEnd = prev.getLastLineContentEnd();
|
||
|
if (!prev.getNotesIndent() && list.getNotesIndent()) {
|
||
|
prev.setNotesIndent(prev.getFirstLineIndent() +
|
||
|
list.getNotesIndent().slice(list.getFirstLineIndent().length));
|
||
|
}
|
||
|
const oldLines = prev.getLines();
|
||
|
const newLines = list.getLines();
|
||
|
oldLines[oldLines.length - 1] += newLines[0];
|
||
|
const resultLines = oldLines.concat(newLines.slice(1));
|
||
|
prev.replaceLines(resultLines);
|
||
|
parent.removeChild(list);
|
||
|
for (const c of list.getChildren()) {
|
||
|
list.removeChild(c);
|
||
|
prev.addAfterAll(c);
|
||
|
}
|
||
|
root.replaceCursor(prevEnd);
|
||
|
recalculateNumericBullets(root);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class BackspaceBehaviourOverride {
|
||
|
constructor(plugin, settings, imeDetector, operationPerformer) {
|
||
|
this.plugin = plugin;
|
||
|
this.settings = settings;
|
||
|
this.imeDetector = imeDetector;
|
||
|
this.operationPerformer = operationPerformer;
|
||
|
this.check = () => {
|
||
|
return (this.settings.keepCursorWithinContent !== "never" &&
|
||
|
!this.imeDetector.isOpened());
|
||
|
};
|
||
|
this.run = (editor) => {
|
||
|
return this.operationPerformer.perform((root) => new DeleteTillPreviousLineContentEnd(root), editor);
|
||
|
};
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.registerEditorExtension(view.keymap.of([
|
||
|
{
|
||
|
key: "Backspace",
|
||
|
run: createKeymapRunCallback({
|
||
|
check: this.check,
|
||
|
run: this.run,
|
||
|
}),
|
||
|
},
|
||
|
]));
|
||
|
});
|
||
|
}
|
||
|
unload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () { });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const BETTER_LISTS_BODY_CLASS = "outliner-plugin-better-lists";
|
||
|
class BetterListsStyles {
|
||
|
constructor(settings, obsidianSettings) {
|
||
|
this.settings = settings;
|
||
|
this.obsidianSettings = obsidianSettings;
|
||
|
this.updateBodyClass = () => {
|
||
|
const shouldExists = this.obsidianSettings.isDefaultThemeEnabled() &&
|
||
|
this.settings.betterListsStyles;
|
||
|
const exists = document.body.classList.contains(BETTER_LISTS_BODY_CLASS);
|
||
|
if (shouldExists && !exists) {
|
||
|
document.body.classList.add(BETTER_LISTS_BODY_CLASS);
|
||
|
}
|
||
|
if (!shouldExists && exists) {
|
||
|
document.body.classList.remove(BETTER_LISTS_BODY_CLASS);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.updateBodyClass();
|
||
|
this.updateBodyClassInterval = window.setInterval(() => {
|
||
|
this.updateBodyClass();
|
||
|
}, 1000);
|
||
|
});
|
||
|
}
|
||
|
unload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
clearInterval(this.updateBodyClassInterval);
|
||
|
document.body.classList.remove(BETTER_LISTS_BODY_CLASS);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class SelectAllContent {
|
||
|
constructor(root) {
|
||
|
this.root = root;
|
||
|
this.stopPropagation = false;
|
||
|
this.updated = false;
|
||
|
}
|
||
|
shouldStopPropagation() {
|
||
|
return this.stopPropagation;
|
||
|
}
|
||
|
shouldUpdate() {
|
||
|
return this.updated;
|
||
|
}
|
||
|
perform() {
|
||
|
const { root } = this;
|
||
|
if (!root.hasSingleSelection()) {
|
||
|
return;
|
||
|
}
|
||
|
const selection = root.getSelections()[0];
|
||
|
const [rootStart, rootEnd] = root.getContentRange();
|
||
|
const selectionFrom = minPos(selection.anchor, selection.head);
|
||
|
const selectionTo = maxPos(selection.anchor, selection.head);
|
||
|
if (selectionFrom.line < rootStart.line ||
|
||
|
selectionTo.line > rootEnd.line) {
|
||
|
return false;
|
||
|
}
|
||
|
if (selectionFrom.line === rootStart.line &&
|
||
|
selectionFrom.ch === rootStart.ch &&
|
||
|
selectionTo.line === rootEnd.line &&
|
||
|
selectionTo.ch === rootEnd.ch) {
|
||
|
return false;
|
||
|
}
|
||
|
const list = root.getListUnderCursor();
|
||
|
const contentStart = list.getFirstLineContentStartAfterCheckbox();
|
||
|
const contentEnd = list.getLastLineContentEnd();
|
||
|
if (selectionFrom.line < contentStart.line ||
|
||
|
selectionTo.line > contentEnd.line) {
|
||
|
return false;
|
||
|
}
|
||
|
this.stopPropagation = true;
|
||
|
this.updated = true;
|
||
|
if (selectionFrom.line === contentStart.line &&
|
||
|
selectionFrom.ch === contentStart.ch &&
|
||
|
selectionTo.line === contentEnd.line &&
|
||
|
selectionTo.ch === contentEnd.ch) {
|
||
|
// select whole list
|
||
|
root.replaceSelections([{ anchor: rootStart, head: rootEnd }]);
|
||
|
}
|
||
|
else {
|
||
|
// select whole line
|
||
|
root.replaceSelections([{ anchor: contentStart, head: contentEnd }]);
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class CtrlAAndCmdABehaviourOverride {
|
||
|
constructor(plugin, settings, imeDetector, operationPerformer) {
|
||
|
this.plugin = plugin;
|
||
|
this.settings = settings;
|
||
|
this.imeDetector = imeDetector;
|
||
|
this.operationPerformer = operationPerformer;
|
||
|
this.check = () => {
|
||
|
return (this.settings.overrideSelectAllBehaviour && !this.imeDetector.isOpened());
|
||
|
};
|
||
|
this.run = (editor) => {
|
||
|
return this.operationPerformer.perform((root) => new SelectAllContent(root), editor);
|
||
|
};
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.registerEditorExtension(view.keymap.of([
|
||
|
{
|
||
|
key: "c-a",
|
||
|
mac: "m-a",
|
||
|
run: createKeymapRunCallback({
|
||
|
check: this.check,
|
||
|
run: this.run,
|
||
|
}),
|
||
|
},
|
||
|
]));
|
||
|
});
|
||
|
}
|
||
|
unload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () { });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class DeleteTillNextLineContentStart {
|
||
|
constructor(root) {
|
||
|
this.root = root;
|
||
|
this.deleteTillPreviousLineContentEnd =
|
||
|
new DeleteTillPreviousLineContentEnd(root);
|
||
|
}
|
||
|
shouldStopPropagation() {
|
||
|
return this.deleteTillPreviousLineContentEnd.shouldStopPropagation();
|
||
|
}
|
||
|
shouldUpdate() {
|
||
|
return this.deleteTillPreviousLineContentEnd.shouldUpdate();
|
||
|
}
|
||
|
perform() {
|
||
|
const { root } = this;
|
||
|
if (!root.hasSingleCursor()) {
|
||
|
return;
|
||
|
}
|
||
|
const list = root.getListUnderCursor();
|
||
|
const cursor = root.getCursor();
|
||
|
const lines = list.getLinesInfo();
|
||
|
const lineNo = lines.findIndex((l) => cursor.ch === l.to.ch && cursor.line === l.to.line);
|
||
|
if (lineNo === lines.length - 1) {
|
||
|
const nextLine = lines[lineNo].to.line + 1;
|
||
|
const nextList = root.getListUnderLine(nextLine);
|
||
|
if (!nextList) {
|
||
|
return;
|
||
|
}
|
||
|
root.replaceCursor(nextList.getFirstLineContentStart());
|
||
|
this.deleteTillPreviousLineContentEnd.perform();
|
||
|
}
|
||
|
else if (lineNo >= 0) {
|
||
|
root.replaceCursor(lines[lineNo + 1].from);
|
||
|
this.deleteTillPreviousLineContentEnd.perform();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class DeleteBehaviourOverride {
|
||
|
constructor(plugin, settings, imeDetector, operationPerformer) {
|
||
|
this.plugin = plugin;
|
||
|
this.settings = settings;
|
||
|
this.imeDetector = imeDetector;
|
||
|
this.operationPerformer = operationPerformer;
|
||
|
this.check = () => {
|
||
|
return (this.settings.keepCursorWithinContent !== "never" &&
|
||
|
!this.imeDetector.isOpened());
|
||
|
};
|
||
|
this.run = (editor) => {
|
||
|
return this.operationPerformer.perform((root) => new DeleteTillNextLineContentStart(root), editor);
|
||
|
};
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.registerEditorExtension(view.keymap.of([
|
||
|
{
|
||
|
key: "Delete",
|
||
|
run: createKeymapRunCallback({
|
||
|
check: this.check,
|
||
|
run: this.run,
|
||
|
}),
|
||
|
},
|
||
|
]));
|
||
|
});
|
||
|
}
|
||
|
unload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () { });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class MoveListToDifferentPosition {
|
||
|
constructor(root, listToMove, placeToMove, whereToMove, defaultIndentChars) {
|
||
|
this.root = root;
|
||
|
this.listToMove = listToMove;
|
||
|
this.placeToMove = placeToMove;
|
||
|
this.whereToMove = whereToMove;
|
||
|
this.defaultIndentChars = defaultIndentChars;
|
||
|
this.stopPropagation = false;
|
||
|
this.updated = false;
|
||
|
}
|
||
|
shouldStopPropagation() {
|
||
|
return this.stopPropagation;
|
||
|
}
|
||
|
shouldUpdate() {
|
||
|
return this.updated;
|
||
|
}
|
||
|
perform() {
|
||
|
if (this.listToMove === this.placeToMove) {
|
||
|
return;
|
||
|
}
|
||
|
this.stopPropagation = true;
|
||
|
this.updated = true;
|
||
|
const cursorAnchor = this.calculateCursorAnchor();
|
||
|
this.moveList();
|
||
|
this.changeIndent();
|
||
|
this.restoreCursor(cursorAnchor);
|
||
|
recalculateNumericBullets(this.root);
|
||
|
}
|
||
|
calculateCursorAnchor() {
|
||
|
const cursorLine = this.root.getCursor().line;
|
||
|
const lines = [
|
||
|
this.listToMove.getFirstLineContentStart().line,
|
||
|
this.listToMove.getLastLineContentEnd().line,
|
||
|
this.placeToMove.getFirstLineContentStart().line,
|
||
|
this.placeToMove.getLastLineContentEnd().line,
|
||
|
];
|
||
|
const listStartLine = Math.min(...lines);
|
||
|
const listEndLine = Math.max(...lines);
|
||
|
if (cursorLine < listStartLine || cursorLine > listEndLine) {
|
||
|
return null;
|
||
|
}
|
||
|
const cursor = this.root.getCursor();
|
||
|
const cursorList = this.root.getListUnderLine(cursor.line);
|
||
|
const cursorListStart = cursorList.getFirstLineContentStart();
|
||
|
const lineDiff = cursor.line - cursorListStart.line;
|
||
|
const chDiff = cursor.ch - cursorListStart.ch;
|
||
|
return { cursorList, lineDiff, chDiff };
|
||
|
}
|
||
|
moveList() {
|
||
|
this.listToMove.getParent().removeChild(this.listToMove);
|
||
|
switch (this.whereToMove) {
|
||
|
case "before":
|
||
|
this.placeToMove
|
||
|
.getParent()
|
||
|
.addBefore(this.placeToMove, this.listToMove);
|
||
|
break;
|
||
|
case "after":
|
||
|
this.placeToMove
|
||
|
.getParent()
|
||
|
.addAfter(this.placeToMove, this.listToMove);
|
||
|
break;
|
||
|
case "inside":
|
||
|
this.placeToMove.addBeforeAll(this.listToMove);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
changeIndent() {
|
||
|
const oldIndent = this.listToMove.getFirstLineIndent();
|
||
|
const newIndent = this.whereToMove === "inside"
|
||
|
? this.placeToMove.getFirstLineIndent() + this.defaultIndentChars
|
||
|
: this.placeToMove.getFirstLineIndent();
|
||
|
this.listToMove.unindentContent(0, oldIndent.length);
|
||
|
this.listToMove.indentContent(0, newIndent);
|
||
|
}
|
||
|
restoreCursor(cursorAnchor) {
|
||
|
if (cursorAnchor) {
|
||
|
const cursorListStart = cursorAnchor.cursorList.getFirstLineContentStart();
|
||
|
this.root.replaceCursor({
|
||
|
line: cursorListStart.line + cursorAnchor.lineDiff,
|
||
|
ch: cursorListStart.ch + cursorAnchor.chDiff,
|
||
|
});
|
||
|
}
|
||
|
else {
|
||
|
// When you move a list, the screen scrolls to the cursor.
|
||
|
// It is better to move the cursor into the viewport than let the screen scroll.
|
||
|
this.root.replaceCursor(this.listToMove.getLastLineContentEnd());
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const BODY_CLASS = "outliner-plugin-dnd";
|
||
|
class DragAndDrop {
|
||
|
constructor(plugin, settings, obisidian, parser, operationPerformer) {
|
||
|
this.plugin = plugin;
|
||
|
this.settings = settings;
|
||
|
this.obisidian = obisidian;
|
||
|
this.parser = parser;
|
||
|
this.operationPerformer = operationPerformer;
|
||
|
this.preStart = null;
|
||
|
this.state = null;
|
||
|
this.handleSettingsChange = () => {
|
||
|
if (!isFeatureSupported()) {
|
||
|
return;
|
||
|
}
|
||
|
if (this.settings.dragAndDrop) {
|
||
|
document.body.classList.add(BODY_CLASS);
|
||
|
}
|
||
|
else {
|
||
|
document.body.classList.remove(BODY_CLASS);
|
||
|
}
|
||
|
};
|
||
|
this.handleMouseDown = (e) => {
|
||
|
if (!isFeatureSupported() ||
|
||
|
!this.settings.dragAndDrop ||
|
||
|
!isClickOnBullet(e)) {
|
||
|
return;
|
||
|
}
|
||
|
const view = getEditorViewFromHTMLElement(e.target);
|
||
|
if (!view) {
|
||
|
return;
|
||
|
}
|
||
|
e.preventDefault();
|
||
|
e.stopPropagation();
|
||
|
this.preStart = {
|
||
|
x: e.x,
|
||
|
y: e.y,
|
||
|
view,
|
||
|
};
|
||
|
};
|
||
|
this.handleMouseMove = (e) => {
|
||
|
if (this.preStart) {
|
||
|
this.startDragging();
|
||
|
}
|
||
|
if (this.state) {
|
||
|
this.detectAndDrawDropZone(e.x, e.y);
|
||
|
}
|
||
|
};
|
||
|
this.handleMouseUp = () => {
|
||
|
if (this.preStart) {
|
||
|
this.preStart = null;
|
||
|
}
|
||
|
if (this.state) {
|
||
|
this.stopDragging();
|
||
|
}
|
||
|
};
|
||
|
this.handleKeyDown = (e) => {
|
||
|
if (this.state && e.code === "Escape") {
|
||
|
this.cancelDragging();
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.registerEditorExtension([
|
||
|
draggingLinesStateField,
|
||
|
droppingLinesStateField,
|
||
|
]);
|
||
|
this.enableFeatureToggle();
|
||
|
this.createDropZone();
|
||
|
this.addEventListeners();
|
||
|
});
|
||
|
}
|
||
|
unload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.removeEventListeners();
|
||
|
this.removeDropZone();
|
||
|
this.disableFeatureToggle();
|
||
|
});
|
||
|
}
|
||
|
enableFeatureToggle() {
|
||
|
this.settings.onChange(this.handleSettingsChange);
|
||
|
this.handleSettingsChange();
|
||
|
}
|
||
|
disableFeatureToggle() {
|
||
|
this.settings.removeCallback(this.handleSettingsChange);
|
||
|
document.body.classList.remove(BODY_CLASS);
|
||
|
}
|
||
|
createDropZone() {
|
||
|
this.dropZone = document.createElement("div");
|
||
|
this.dropZone.classList.add("outliner-plugin-drop-zone");
|
||
|
this.dropZone.style.display = "none";
|
||
|
document.body.appendChild(this.dropZone);
|
||
|
}
|
||
|
removeDropZone() {
|
||
|
document.body.removeChild(this.dropZone);
|
||
|
this.dropZone = null;
|
||
|
}
|
||
|
addEventListeners() {
|
||
|
document.addEventListener("mousedown", this.handleMouseDown, {
|
||
|
capture: true,
|
||
|
});
|
||
|
document.addEventListener("mousemove", this.handleMouseMove);
|
||
|
document.addEventListener("mouseup", this.handleMouseUp);
|
||
|
document.addEventListener("keydown", this.handleKeyDown);
|
||
|
}
|
||
|
removeEventListeners() {
|
||
|
document.removeEventListener("mousedown", this.handleMouseDown, {
|
||
|
capture: true,
|
||
|
});
|
||
|
document.removeEventListener("mousemove", this.handleMouseMove);
|
||
|
document.removeEventListener("mouseup", this.handleMouseUp);
|
||
|
document.removeEventListener("keydown", this.handleKeyDown);
|
||
|
}
|
||
|
startDragging() {
|
||
|
const { x, y, view } = this.preStart;
|
||
|
this.preStart = null;
|
||
|
const editor = getEditorFromState(view.state);
|
||
|
const pos = editor.offsetToPos(view.posAtCoords({ x, y }));
|
||
|
const root = this.parser.parse(editor, pos);
|
||
|
const list = root.getListUnderLine(pos.line);
|
||
|
const state = new DragAndDropState(view, editor, root, list);
|
||
|
if (!state.hasDropVariants()) {
|
||
|
return;
|
||
|
}
|
||
|
this.state = state;
|
||
|
this.highlightDraggingLines();
|
||
|
}
|
||
|
detectAndDrawDropZone(x, y) {
|
||
|
this.state.calculateNearestDropVariant(x, y);
|
||
|
this.drawDropZone();
|
||
|
}
|
||
|
cancelDragging() {
|
||
|
this.state.dropVariant = null;
|
||
|
this.stopDragging();
|
||
|
}
|
||
|
stopDragging() {
|
||
|
this.unhightlightDraggingLines();
|
||
|
this.hideDropZone();
|
||
|
this.applyChanges();
|
||
|
this.state = null;
|
||
|
}
|
||
|
applyChanges() {
|
||
|
if (!this.state.dropVariant) {
|
||
|
return;
|
||
|
}
|
||
|
const { state } = this;
|
||
|
const { dropVariant, editor, root, list } = state;
|
||
|
const newRoot = this.parser.parse(editor, root.getContentStart());
|
||
|
if (!isSameRoots(root, newRoot)) {
|
||
|
new obsidian.Notice(`The item cannot be moved. The page content changed during the move.`, 5000);
|
||
|
return;
|
||
|
}
|
||
|
this.operationPerformer.eval(root, new MoveListToDifferentPosition(root, list, dropVariant.placeToMove, dropVariant.whereToMove, this.obisidian.getDefaultIndentChars()), editor);
|
||
|
}
|
||
|
highlightDraggingLines() {
|
||
|
const { state } = this;
|
||
|
const { list, editor, view } = state;
|
||
|
const lines = [];
|
||
|
const fromLine = list.getFirstLineContentStart().line;
|
||
|
const tillLine = list.getContentEndIncludingChildren().line;
|
||
|
for (let i = fromLine; i <= tillLine; i++) {
|
||
|
lines.push(editor.posToOffset({ line: i, ch: 0 }));
|
||
|
}
|
||
|
view.dispatch({
|
||
|
effects: [dndStarted.of(lines)],
|
||
|
});
|
||
|
document.body.classList.add("outliner-plugin-dragging");
|
||
|
}
|
||
|
unhightlightDraggingLines() {
|
||
|
document.body.classList.remove("outliner-plugin-dragging");
|
||
|
this.state.view.dispatch({
|
||
|
effects: [dndEnded.of()],
|
||
|
});
|
||
|
}
|
||
|
drawDropZone() {
|
||
|
const { state } = this;
|
||
|
const { view, editor, list, dropVariant } = state;
|
||
|
const width = Math.round(view.contentDOM.offsetWidth -
|
||
|
(dropVariant.left -
|
||
|
view.coordsAtPos(editor.posToOffset({
|
||
|
line: list.getFirstLineContentStart().line,
|
||
|
ch: 0,
|
||
|
})).left));
|
||
|
this.dropZone.style.display = "block";
|
||
|
this.dropZone.style.top = dropVariant.top + "px";
|
||
|
this.dropZone.style.left = dropVariant.left + "px";
|
||
|
this.dropZone.style.width = width + "px";
|
||
|
if (dropVariant.whereToMove === "before" &&
|
||
|
!this.dropZone.classList.contains("outliner-plugin-drop-zone-before")) {
|
||
|
this.dropZone.classList.remove("outliner-plugin-drop-zone-after");
|
||
|
this.dropZone.classList.add("outliner-plugin-drop-zone-before");
|
||
|
}
|
||
|
else if ((dropVariant.whereToMove === "after" ||
|
||
|
dropVariant.whereToMove === "inside") &&
|
||
|
!this.dropZone.classList.contains("outliner-plugin-drop-zone-after")) {
|
||
|
this.dropZone.classList.remove("outliner-plugin-drop-zone-before");
|
||
|
this.dropZone.classList.add("outliner-plugin-drop-zone-after");
|
||
|
}
|
||
|
const newParent = dropVariant.whereToMove === "inside"
|
||
|
? dropVariant.placeToMove
|
||
|
: dropVariant.placeToMove.getParent();
|
||
|
const newParentIsRootList = !newParent.getParent();
|
||
|
this.state.view.dispatch({
|
||
|
effects: [
|
||
|
dndMoved.of(newParentIsRootList
|
||
|
? null
|
||
|
: editor.posToOffset({
|
||
|
line: newParent.getFirstLineContentStart().line,
|
||
|
ch: 0,
|
||
|
})),
|
||
|
],
|
||
|
});
|
||
|
}
|
||
|
hideDropZone() {
|
||
|
this.dropZone.style.display = "none";
|
||
|
}
|
||
|
}
|
||
|
class DragAndDropState {
|
||
|
constructor(view, editor, root, list) {
|
||
|
this.view = view;
|
||
|
this.editor = editor;
|
||
|
this.root = root;
|
||
|
this.list = list;
|
||
|
this.dropVariants = new Map();
|
||
|
this.dropVariant = null;
|
||
|
this.collectDropVariants();
|
||
|
}
|
||
|
getDropVariants() {
|
||
|
return Array.from(this.dropVariants.values());
|
||
|
}
|
||
|
hasDropVariants() {
|
||
|
return this.dropVariants.size > 0;
|
||
|
}
|
||
|
calculateNearestDropVariant(x, y) {
|
||
|
const { view, editor } = this;
|
||
|
this.dropVariant = this.getDropVariants()
|
||
|
.map((v) => {
|
||
|
const { placeToMove } = v;
|
||
|
switch (v.whereToMove) {
|
||
|
case "before":
|
||
|
case "after":
|
||
|
v.left = Math.round(view.coordsAtPos(editor.posToOffset({
|
||
|
line: placeToMove.getFirstLineContentStart().line,
|
||
|
ch: placeToMove.getFirstLineIndent().length,
|
||
|
})).left);
|
||
|
break;
|
||
|
case "inside":
|
||
|
v.left = Math.round(view.coordsAtPos(editor.posToOffset({
|
||
|
line: placeToMove.getFirstLineContentStart().line,
|
||
|
ch: placeToMove.getFirstLineIndent().length,
|
||
|
})).left +
|
||
|
view.defaultCharacterWidth * 2);
|
||
|
break;
|
||
|
}
|
||
|
switch (v.whereToMove) {
|
||
|
case "before":
|
||
|
v.top = Math.round(view.coordsAtPos(editor.posToOffset(placeToMove.getFirstLineContentStart())).top);
|
||
|
break;
|
||
|
case "after":
|
||
|
case "inside":
|
||
|
v.top = Math.round(view.coordsAtPos(editor.posToOffset(placeToMove.getContentEndIncludingChildren())).top + view.defaultLineHeight);
|
||
|
break;
|
||
|
}
|
||
|
v.dist = Math.abs(Math.hypot(y - v.top, x - v.left));
|
||
|
return v;
|
||
|
})
|
||
|
.sort((a, b) => {
|
||
|
return a.dist - b.dist;
|
||
|
})
|
||
|
.first();
|
||
|
}
|
||
|
addDropVariant(v) {
|
||
|
this.dropVariants.set(`${v.line} ${v.level}`, v);
|
||
|
}
|
||
|
collectDropVariants() {
|
||
|
const visit = (lists) => {
|
||
|
for (const placeToMove of lists) {
|
||
|
const lineBefore = placeToMove.getFirstLineContentStart().line;
|
||
|
const lineAfter = placeToMove.getContentEndIncludingChildren().line + 1;
|
||
|
const level = placeToMove.getLevel();
|
||
|
this.addDropVariant({
|
||
|
line: lineBefore,
|
||
|
level,
|
||
|
left: 0,
|
||
|
top: 0,
|
||
|
dist: 0,
|
||
|
placeToMove,
|
||
|
whereToMove: "before",
|
||
|
});
|
||
|
this.addDropVariant({
|
||
|
line: lineAfter,
|
||
|
level,
|
||
|
left: 0,
|
||
|
top: 0,
|
||
|
dist: 0,
|
||
|
placeToMove,
|
||
|
whereToMove: "after",
|
||
|
});
|
||
|
if (placeToMove === this.list) {
|
||
|
continue;
|
||
|
}
|
||
|
if (placeToMove.isEmpty()) {
|
||
|
this.addDropVariant({
|
||
|
line: lineAfter,
|
||
|
level: level + 1,
|
||
|
left: 0,
|
||
|
top: 0,
|
||
|
dist: 0,
|
||
|
placeToMove,
|
||
|
whereToMove: "inside",
|
||
|
});
|
||
|
}
|
||
|
else {
|
||
|
visit(placeToMove.getChildren());
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
visit(this.root.getChildren());
|
||
|
}
|
||
|
}
|
||
|
const dndStarted = state.StateEffect.define({
|
||
|
map: (lines, change) => lines.map((l) => change.mapPos(l)),
|
||
|
});
|
||
|
const dndMoved = state.StateEffect.define({
|
||
|
map: (line, change) => (line !== null ? change.mapPos(line) : line),
|
||
|
});
|
||
|
const dndEnded = state.StateEffect.define();
|
||
|
const draggingLineDecoration = view.Decoration.line({
|
||
|
class: "outliner-plugin-dragging-line",
|
||
|
});
|
||
|
const droppingLineDecoration = view.Decoration.line({
|
||
|
class: "outliner-plugin-dropping-line",
|
||
|
});
|
||
|
const draggingLinesStateField = state.StateField.define({
|
||
|
create: () => view.Decoration.none,
|
||
|
update: (dndState, tr) => {
|
||
|
dndState = dndState.map(tr.changes);
|
||
|
for (const e of tr.effects) {
|
||
|
if (e.is(dndStarted)) {
|
||
|
dndState = dndState.update({
|
||
|
add: e.value.map((l) => draggingLineDecoration.range(l, l)),
|
||
|
});
|
||
|
}
|
||
|
if (e.is(dndEnded)) {
|
||
|
dndState = view.Decoration.none;
|
||
|
}
|
||
|
}
|
||
|
return dndState;
|
||
|
},
|
||
|
provide: (f) => view.EditorView.decorations.from(f),
|
||
|
});
|
||
|
const droppingLinesStateField = state.StateField.define({
|
||
|
create: () => view.Decoration.none,
|
||
|
update: (dndDroppingState, tr) => {
|
||
|
dndDroppingState = dndDroppingState.map(tr.changes);
|
||
|
for (const e of tr.effects) {
|
||
|
if (e.is(dndMoved)) {
|
||
|
dndDroppingState =
|
||
|
e.value === null
|
||
|
? view.Decoration.none
|
||
|
: view.Decoration.set(droppingLineDecoration.range(e.value, e.value));
|
||
|
}
|
||
|
if (e.is(dndEnded)) {
|
||
|
dndDroppingState = view.Decoration.none;
|
||
|
}
|
||
|
}
|
||
|
return dndDroppingState;
|
||
|
},
|
||
|
provide: (f) => view.EditorView.decorations.from(f),
|
||
|
});
|
||
|
function getEditorViewFromHTMLElement(e) {
|
||
|
while (e && !e.classList.contains("cm-editor")) {
|
||
|
e = e.parentElement;
|
||
|
}
|
||
|
if (!e) {
|
||
|
return null;
|
||
|
}
|
||
|
return view.EditorView.findFromDOM(e);
|
||
|
}
|
||
|
function isClickOnBullet(e) {
|
||
|
let el = e.target;
|
||
|
while (el) {
|
||
|
if (el.classList.contains("cm-formatting-list") ||
|
||
|
el.classList.contains("cm-fold-indicator") ||
|
||
|
el.classList.contains("task-list-item-checkbox")) {
|
||
|
return true;
|
||
|
}
|
||
|
el = el.parentElement;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
function isSameRoots(a, b) {
|
||
|
const [aStart, aEnd] = a.getContentRange();
|
||
|
const [bStart, bEnd] = b.getContentRange();
|
||
|
if (cmpPos(aStart, bStart) !== 0 || cmpPos(aEnd, bEnd) !== 0) {
|
||
|
return false;
|
||
|
}
|
||
|
return a.print() === b.print();
|
||
|
}
|
||
|
function isFeatureSupported() {
|
||
|
return obsidian.Platform.isDesktop;
|
||
|
}
|
||
|
|
||
|
class KeepCursorOutsideFoldedLines {
|
||
|
constructor(root) {
|
||
|
this.root = root;
|
||
|
this.stopPropagation = false;
|
||
|
this.updated = false;
|
||
|
}
|
||
|
shouldStopPropagation() {
|
||
|
return this.stopPropagation;
|
||
|
}
|
||
|
shouldUpdate() {
|
||
|
return this.updated;
|
||
|
}
|
||
|
perform() {
|
||
|
const { root } = this;
|
||
|
if (!root.hasSingleCursor()) {
|
||
|
return;
|
||
|
}
|
||
|
const cursor = root.getCursor();
|
||
|
const list = root.getListUnderCursor();
|
||
|
if (!list.isFolded()) {
|
||
|
return;
|
||
|
}
|
||
|
const foldRoot = list.getTopFoldRoot();
|
||
|
const firstLineEnd = foldRoot.getLinesInfo()[0].to;
|
||
|
if (cursor.line > firstLineEnd.line) {
|
||
|
this.updated = true;
|
||
|
this.stopPropagation = true;
|
||
|
root.replaceCursor(firstLineEnd);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class KeepCursorWithinListContent {
|
||
|
constructor(root) {
|
||
|
this.root = root;
|
||
|
this.stopPropagation = false;
|
||
|
this.updated = false;
|
||
|
}
|
||
|
shouldStopPropagation() {
|
||
|
return this.stopPropagation;
|
||
|
}
|
||
|
shouldUpdate() {
|
||
|
return this.updated;
|
||
|
}
|
||
|
perform() {
|
||
|
const { root } = this;
|
||
|
if (!root.hasSingleCursor()) {
|
||
|
return;
|
||
|
}
|
||
|
const cursor = root.getCursor();
|
||
|
const list = root.getListUnderCursor();
|
||
|
const contentStart = list.getFirstLineContentStartAfterCheckbox();
|
||
|
const linePrefix = contentStart.line === cursor.line
|
||
|
? contentStart.ch
|
||
|
: list.getNotesIndent().length;
|
||
|
if (cursor.ch < linePrefix) {
|
||
|
this.updated = true;
|
||
|
this.stopPropagation = true;
|
||
|
root.replaceCursor({
|
||
|
line: cursor.line,
|
||
|
ch: linePrefix,
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class EditorSelectionsBehaviourOverride {
|
||
|
constructor(plugin, settings, parser, operationPerformer) {
|
||
|
this.plugin = plugin;
|
||
|
this.settings = settings;
|
||
|
this.parser = parser;
|
||
|
this.operationPerformer = operationPerformer;
|
||
|
this.transactionExtender = (tr) => {
|
||
|
if (this.settings.keepCursorWithinContent === "never" || !tr.selection) {
|
||
|
return null;
|
||
|
}
|
||
|
const editor = getEditorFromState(tr.startState);
|
||
|
setTimeout(() => {
|
||
|
this.handleSelectionsChanges(editor);
|
||
|
}, 0);
|
||
|
return null;
|
||
|
};
|
||
|
this.handleSelectionsChanges = (editor) => {
|
||
|
const root = this.parser.parse(editor);
|
||
|
if (!root) {
|
||
|
return;
|
||
|
}
|
||
|
{
|
||
|
const { shouldStopPropagation } = this.operationPerformer.eval(root, new KeepCursorOutsideFoldedLines(root), editor);
|
||
|
if (shouldStopPropagation) {
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
this.operationPerformer.eval(root, new KeepCursorWithinListContent(root), editor);
|
||
|
};
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.registerEditorExtension(state.EditorState.transactionExtender.of(this.transactionExtender));
|
||
|
});
|
||
|
}
|
||
|
unload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () { });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const checkboxRe = `\\[[^\\[\\]]\\][ \t]`;
|
||
|
|
||
|
function isEmptyLineOrEmptyCheckbox(line) {
|
||
|
return line === "" || line === "[ ] ";
|
||
|
}
|
||
|
|
||
|
class CreateNewItem {
|
||
|
constructor(root, defaultIndentChars, getZoomRange) {
|
||
|
this.root = root;
|
||
|
this.defaultIndentChars = defaultIndentChars;
|
||
|
this.getZoomRange = getZoomRange;
|
||
|
this.stopPropagation = false;
|
||
|
this.updated = false;
|
||
|
}
|
||
|
shouldStopPropagation() {
|
||
|
return this.stopPropagation;
|
||
|
}
|
||
|
shouldUpdate() {
|
||
|
return this.updated;
|
||
|
}
|
||
|
perform() {
|
||
|
const { root } = this;
|
||
|
if (!root.hasSingleSelection()) {
|
||
|
return;
|
||
|
}
|
||
|
const selection = root.getSelection();
|
||
|
if (!selection || selection.anchor.line !== selection.head.line) {
|
||
|
return;
|
||
|
}
|
||
|
const list = root.getListUnderCursor();
|
||
|
const lines = list.getLinesInfo();
|
||
|
if (lines.length === 1 && isEmptyLineOrEmptyCheckbox(lines[0].text)) {
|
||
|
return;
|
||
|
}
|
||
|
const cursor = root.getCursor();
|
||
|
const lineUnderCursor = lines.find((l) => l.from.line === cursor.line);
|
||
|
if (cursor.ch < lineUnderCursor.from.ch) {
|
||
|
return;
|
||
|
}
|
||
|
const { oldLines, newLines } = lines.reduce((acc, line) => {
|
||
|
if (cursor.line > line.from.line) {
|
||
|
acc.oldLines.push(line.text);
|
||
|
}
|
||
|
else if (cursor.line === line.from.line) {
|
||
|
const left = line.text.slice(0, selection.from - line.from.ch);
|
||
|
const right = line.text.slice(selection.to - line.from.ch);
|
||
|
acc.oldLines.push(left);
|
||
|
acc.newLines.push(right);
|
||
|
}
|
||
|
else if (cursor.line < line.from.line) {
|
||
|
acc.newLines.push(line.text);
|
||
|
}
|
||
|
return acc;
|
||
|
}, {
|
||
|
oldLines: [],
|
||
|
newLines: [],
|
||
|
});
|
||
|
const codeBlockBacticks = oldLines.join("\n").split("```").length - 1;
|
||
|
const isInsideCodeblock = codeBlockBacticks > 0 && codeBlockBacticks % 2 !== 0;
|
||
|
if (isInsideCodeblock) {
|
||
|
return;
|
||
|
}
|
||
|
this.stopPropagation = true;
|
||
|
this.updated = true;
|
||
|
const zoomRange = this.getZoomRange.getZoomRange();
|
||
|
const listIsZoomingRoot = Boolean(zoomRange &&
|
||
|
list.getFirstLineContentStart().line >= zoomRange.from.line &&
|
||
|
list.getLastLineContentEnd().line <= zoomRange.from.line);
|
||
|
const hasChildren = !list.isEmpty();
|
||
|
const childIsFolded = list.isFoldRoot();
|
||
|
const endPos = list.getLastLineContentEnd();
|
||
|
const endOfLine = cursor.line === endPos.line && cursor.ch === endPos.ch;
|
||
|
const onChildLevel = listIsZoomingRoot || (hasChildren && !childIsFolded && endOfLine);
|
||
|
const indent = onChildLevel
|
||
|
? hasChildren
|
||
|
? list.getChildren()[0].getFirstLineIndent()
|
||
|
: list.getFirstLineIndent() + this.defaultIndentChars
|
||
|
: list.getFirstLineIndent();
|
||
|
const bullet = onChildLevel && hasChildren
|
||
|
? list.getChildren()[0].getBullet()
|
||
|
: list.getBullet();
|
||
|
const spaceAfterBullet = onChildLevel && hasChildren
|
||
|
? list.getChildren()[0].getSpaceAfterBullet()
|
||
|
: list.getSpaceAfterBullet();
|
||
|
const prefix = oldLines[0].match(checkboxRe) ? "[ ] " : "";
|
||
|
const newList = new List(list.getRoot(), indent, bullet, prefix, spaceAfterBullet, prefix + newLines.shift(), false);
|
||
|
if (newLines.length > 0) {
|
||
|
newList.setNotesIndent(list.getNotesIndent());
|
||
|
for (const line of newLines) {
|
||
|
newList.addLine(line);
|
||
|
}
|
||
|
}
|
||
|
if (onChildLevel) {
|
||
|
list.addBeforeAll(newList);
|
||
|
}
|
||
|
else {
|
||
|
if (!childIsFolded || !endOfLine) {
|
||
|
const children = list.getChildren();
|
||
|
for (const child of children) {
|
||
|
list.removeChild(child);
|
||
|
newList.addAfterAll(child);
|
||
|
}
|
||
|
}
|
||
|
list.getParent().addAfter(list, newList);
|
||
|
}
|
||
|
list.replaceLines(oldLines);
|
||
|
const newListStart = newList.getFirstLineContentStart();
|
||
|
root.replaceCursor({
|
||
|
line: newListStart.line,
|
||
|
ch: newListStart.ch + prefix.length,
|
||
|
});
|
||
|
recalculateNumericBullets(root);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class OutdentList {
|
||
|
constructor(root) {
|
||
|
this.root = root;
|
||
|
this.stopPropagation = false;
|
||
|
this.updated = false;
|
||
|
}
|
||
|
shouldStopPropagation() {
|
||
|
return this.stopPropagation;
|
||
|
}
|
||
|
shouldUpdate() {
|
||
|
return this.updated;
|
||
|
}
|
||
|
perform() {
|
||
|
const { root } = this;
|
||
|
if (!root.hasSingleCursor()) {
|
||
|
return;
|
||
|
}
|
||
|
this.stopPropagation = true;
|
||
|
const list = root.getListUnderCursor();
|
||
|
const parent = list.getParent();
|
||
|
const grandParent = parent.getParent();
|
||
|
if (!grandParent) {
|
||
|
return;
|
||
|
}
|
||
|
this.updated = true;
|
||
|
const listStartLineBefore = root.getContentLinesRangeOf(list)[0];
|
||
|
const indentRmFrom = parent.getFirstLineIndent().length;
|
||
|
const indentRmTill = list.getFirstLineIndent().length;
|
||
|
parent.removeChild(list);
|
||
|
grandParent.addAfter(parent, list);
|
||
|
list.unindentContent(indentRmFrom, indentRmTill);
|
||
|
const listStartLineAfter = root.getContentLinesRangeOf(list)[0];
|
||
|
const lineDiff = listStartLineAfter - listStartLineBefore;
|
||
|
const chDiff = indentRmTill - indentRmFrom;
|
||
|
const cursor = root.getCursor();
|
||
|
root.replaceCursor({
|
||
|
line: cursor.line + lineDiff,
|
||
|
ch: cursor.ch - chDiff,
|
||
|
});
|
||
|
recalculateNumericBullets(root);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class OutdentListIfItsEmpty {
|
||
|
constructor(root) {
|
||
|
this.root = root;
|
||
|
this.outdentList = new OutdentList(root);
|
||
|
}
|
||
|
shouldStopPropagation() {
|
||
|
return this.outdentList.shouldStopPropagation();
|
||
|
}
|
||
|
shouldUpdate() {
|
||
|
return this.outdentList.shouldUpdate();
|
||
|
}
|
||
|
perform() {
|
||
|
const { root } = this;
|
||
|
if (!root.hasSingleCursor()) {
|
||
|
return;
|
||
|
}
|
||
|
const list = root.getListUnderCursor();
|
||
|
const lines = list.getLines();
|
||
|
if (lines.length > 1 ||
|
||
|
!isEmptyLineOrEmptyCheckbox(lines[0]) ||
|
||
|
list.getLevel() === 1) {
|
||
|
return;
|
||
|
}
|
||
|
this.outdentList.perform();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class EnterBehaviourOverride {
|
||
|
constructor(plugin, settings, imeDetector, obsidianSettings, parser, operationPerformer) {
|
||
|
this.plugin = plugin;
|
||
|
this.settings = settings;
|
||
|
this.imeDetector = imeDetector;
|
||
|
this.obsidianSettings = obsidianSettings;
|
||
|
this.parser = parser;
|
||
|
this.operationPerformer = operationPerformer;
|
||
|
this.check = () => {
|
||
|
return this.settings.overrideEnterBehaviour && !this.imeDetector.isOpened();
|
||
|
};
|
||
|
this.run = (editor) => {
|
||
|
const root = this.parser.parse(editor);
|
||
|
if (!root) {
|
||
|
return {
|
||
|
shouldUpdate: false,
|
||
|
shouldStopPropagation: false,
|
||
|
};
|
||
|
}
|
||
|
{
|
||
|
const res = this.operationPerformer.eval(root, new OutdentListIfItsEmpty(root), editor);
|
||
|
if (res.shouldStopPropagation) {
|
||
|
return res;
|
||
|
}
|
||
|
}
|
||
|
{
|
||
|
const defaultIndentChars = this.obsidianSettings.getDefaultIndentChars();
|
||
|
const zoomRange = editor.getZoomRange();
|
||
|
const getZoomRange = {
|
||
|
getZoomRange: () => zoomRange,
|
||
|
};
|
||
|
const res = this.operationPerformer.eval(root, new CreateNewItem(root, defaultIndentChars, getZoomRange), editor);
|
||
|
if (res.shouldUpdate && zoomRange) {
|
||
|
editor.tryRefreshZoom(zoomRange.from.line);
|
||
|
}
|
||
|
return res;
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.registerEditorExtension(state.Prec.highest(view.keymap.of([
|
||
|
{
|
||
|
key: "Enter",
|
||
|
run: createKeymapRunCallback({
|
||
|
check: this.check,
|
||
|
run: this.run,
|
||
|
}),
|
||
|
},
|
||
|
])));
|
||
|
});
|
||
|
}
|
||
|
unload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () { });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function createEditorCallback(cb) {
|
||
|
return (editor) => {
|
||
|
const myEditor = new MyEditor(editor);
|
||
|
const shouldStopPropagation = cb(myEditor);
|
||
|
if (!shouldStopPropagation &&
|
||
|
window.event &&
|
||
|
window.event.type === "keydown") {
|
||
|
myEditor.triggerOnKeyDown(window.event);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
class ListsFoldingCommands {
|
||
|
constructor(plugin, obsidianSettings) {
|
||
|
this.plugin = plugin;
|
||
|
this.obsidianSettings = obsidianSettings;
|
||
|
this.fold = (editor) => {
|
||
|
return this.setFold(editor, "fold");
|
||
|
};
|
||
|
this.unfold = (editor) => {
|
||
|
return this.setFold(editor, "unfold");
|
||
|
};
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.addCommand({
|
||
|
id: "fold",
|
||
|
icon: "chevrons-down-up",
|
||
|
name: "Fold the list",
|
||
|
editorCallback: createEditorCallback(this.fold),
|
||
|
hotkeys: [
|
||
|
{
|
||
|
modifiers: ["Mod"],
|
||
|
key: "ArrowUp",
|
||
|
},
|
||
|
],
|
||
|
});
|
||
|
this.plugin.addCommand({
|
||
|
id: "unfold",
|
||
|
icon: "chevrons-up-down",
|
||
|
name: "Unfold the list",
|
||
|
editorCallback: createEditorCallback(this.unfold),
|
||
|
hotkeys: [
|
||
|
{
|
||
|
modifiers: ["Mod"],
|
||
|
key: "ArrowDown",
|
||
|
},
|
||
|
],
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
unload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () { });
|
||
|
}
|
||
|
setFold(editor, type) {
|
||
|
if (!this.obsidianSettings.getFoldSettings().foldIndent) {
|
||
|
new obsidian.Notice(`Unable to ${type} because folding is disabled. Please enable "Fold indent" in Obsidian settings.`, 5000);
|
||
|
return true;
|
||
|
}
|
||
|
const cursor = editor.getCursor();
|
||
|
if (type === "fold") {
|
||
|
editor.fold(cursor.line);
|
||
|
}
|
||
|
else {
|
||
|
editor.unfold(cursor.line);
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class IndentList {
|
||
|
constructor(root, defaultIndentChars) {
|
||
|
this.root = root;
|
||
|
this.defaultIndentChars = defaultIndentChars;
|
||
|
this.stopPropagation = false;
|
||
|
this.updated = false;
|
||
|
}
|
||
|
shouldStopPropagation() {
|
||
|
return this.stopPropagation;
|
||
|
}
|
||
|
shouldUpdate() {
|
||
|
return this.updated;
|
||
|
}
|
||
|
perform() {
|
||
|
const { root } = this;
|
||
|
if (!root.hasSingleCursor()) {
|
||
|
return;
|
||
|
}
|
||
|
this.stopPropagation = true;
|
||
|
const list = root.getListUnderCursor();
|
||
|
const parent = list.getParent();
|
||
|
const prev = parent.getPrevSiblingOf(list);
|
||
|
if (!prev) {
|
||
|
return;
|
||
|
}
|
||
|
this.updated = true;
|
||
|
const listStartLineBefore = root.getContentLinesRangeOf(list)[0];
|
||
|
const indentPos = list.getFirstLineIndent().length;
|
||
|
let indentChars = "";
|
||
|
if (indentChars === "" && !prev.isEmpty()) {
|
||
|
indentChars = prev
|
||
|
.getChildren()[0]
|
||
|
.getFirstLineIndent()
|
||
|
.slice(prev.getFirstLineIndent().length);
|
||
|
}
|
||
|
if (indentChars === "") {
|
||
|
indentChars = list
|
||
|
.getFirstLineIndent()
|
||
|
.slice(parent.getFirstLineIndent().length);
|
||
|
}
|
||
|
if (indentChars === "" && !list.isEmpty()) {
|
||
|
indentChars = list.getChildren()[0].getFirstLineIndent();
|
||
|
}
|
||
|
if (indentChars === "") {
|
||
|
indentChars = this.defaultIndentChars;
|
||
|
}
|
||
|
parent.removeChild(list);
|
||
|
prev.addAfterAll(list);
|
||
|
list.indentContent(indentPos, indentChars);
|
||
|
const listStartLineAfter = root.getContentLinesRangeOf(list)[0];
|
||
|
const lineDiff = listStartLineAfter - listStartLineBefore;
|
||
|
const cursor = root.getCursor();
|
||
|
root.replaceCursor({
|
||
|
line: cursor.line + lineDiff,
|
||
|
ch: cursor.ch + indentChars.length,
|
||
|
});
|
||
|
recalculateNumericBullets(root);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class MoveListDown {
|
||
|
constructor(root) {
|
||
|
this.root = root;
|
||
|
this.stopPropagation = false;
|
||
|
this.updated = false;
|
||
|
}
|
||
|
shouldStopPropagation() {
|
||
|
return this.stopPropagation;
|
||
|
}
|
||
|
shouldUpdate() {
|
||
|
return this.updated;
|
||
|
}
|
||
|
perform() {
|
||
|
const { root } = this;
|
||
|
if (!root.hasSingleCursor()) {
|
||
|
return;
|
||
|
}
|
||
|
this.stopPropagation = true;
|
||
|
const list = root.getListUnderCursor();
|
||
|
const parent = list.getParent();
|
||
|
const grandParent = parent.getParent();
|
||
|
const next = parent.getNextSiblingOf(list);
|
||
|
const listStartLineBefore = root.getContentLinesRangeOf(list)[0];
|
||
|
if (!next && grandParent) {
|
||
|
const newParent = grandParent.getNextSiblingOf(parent);
|
||
|
if (newParent) {
|
||
|
this.updated = true;
|
||
|
parent.removeChild(list);
|
||
|
newParent.addBeforeAll(list);
|
||
|
}
|
||
|
}
|
||
|
else if (next) {
|
||
|
this.updated = true;
|
||
|
parent.removeChild(list);
|
||
|
parent.addAfter(next, list);
|
||
|
}
|
||
|
if (!this.updated) {
|
||
|
return;
|
||
|
}
|
||
|
const listStartLineAfter = root.getContentLinesRangeOf(list)[0];
|
||
|
const lineDiff = listStartLineAfter - listStartLineBefore;
|
||
|
const cursor = root.getCursor();
|
||
|
root.replaceCursor({
|
||
|
line: cursor.line + lineDiff,
|
||
|
ch: cursor.ch,
|
||
|
});
|
||
|
recalculateNumericBullets(root);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class MoveListUp {
|
||
|
constructor(root) {
|
||
|
this.root = root;
|
||
|
this.stopPropagation = false;
|
||
|
this.updated = false;
|
||
|
}
|
||
|
shouldStopPropagation() {
|
||
|
return this.stopPropagation;
|
||
|
}
|
||
|
shouldUpdate() {
|
||
|
return this.updated;
|
||
|
}
|
||
|
perform() {
|
||
|
const { root } = this;
|
||
|
if (!root.hasSingleCursor()) {
|
||
|
return;
|
||
|
}
|
||
|
this.stopPropagation = true;
|
||
|
const list = root.getListUnderCursor();
|
||
|
const parent = list.getParent();
|
||
|
const grandParent = parent.getParent();
|
||
|
const prev = parent.getPrevSiblingOf(list);
|
||
|
const listStartLineBefore = root.getContentLinesRangeOf(list)[0];
|
||
|
if (!prev && grandParent) {
|
||
|
const newParent = grandParent.getPrevSiblingOf(parent);
|
||
|
if (newParent) {
|
||
|
this.updated = true;
|
||
|
parent.removeChild(list);
|
||
|
newParent.addAfterAll(list);
|
||
|
}
|
||
|
}
|
||
|
else if (prev) {
|
||
|
this.updated = true;
|
||
|
parent.removeChild(list);
|
||
|
parent.addBefore(prev, list);
|
||
|
}
|
||
|
if (!this.updated) {
|
||
|
return;
|
||
|
}
|
||
|
const listStartLineAfter = root.getContentLinesRangeOf(list)[0];
|
||
|
const lineDiff = listStartLineAfter - listStartLineBefore;
|
||
|
const cursor = root.getCursor();
|
||
|
root.replaceCursor({
|
||
|
line: cursor.line + lineDiff,
|
||
|
ch: cursor.ch,
|
||
|
});
|
||
|
recalculateNumericBullets(root);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class ListsMovementCommands {
|
||
|
constructor(plugin, obsidianSettings, operationPerformer) {
|
||
|
this.plugin = plugin;
|
||
|
this.obsidianSettings = obsidianSettings;
|
||
|
this.operationPerformer = operationPerformer;
|
||
|
this.moveListDown = (editor) => {
|
||
|
const { shouldStopPropagation } = this.operationPerformer.perform((root) => new MoveListDown(root), editor);
|
||
|
return shouldStopPropagation;
|
||
|
};
|
||
|
this.moveListUp = (editor) => {
|
||
|
const { shouldStopPropagation } = this.operationPerformer.perform((root) => new MoveListUp(root), editor);
|
||
|
return shouldStopPropagation;
|
||
|
};
|
||
|
this.indentList = (editor) => {
|
||
|
const { shouldStopPropagation } = this.operationPerformer.perform((root) => new IndentList(root, this.obsidianSettings.getDefaultIndentChars()), editor);
|
||
|
return shouldStopPropagation;
|
||
|
};
|
||
|
this.outdentList = (editor) => {
|
||
|
const { shouldStopPropagation } = this.operationPerformer.perform((root) => new OutdentList(root), editor);
|
||
|
return shouldStopPropagation;
|
||
|
};
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.addCommand({
|
||
|
id: "move-list-item-up",
|
||
|
icon: "arrow-up",
|
||
|
name: "Move list and sublists up",
|
||
|
editorCallback: createEditorCallback(this.moveListUp),
|
||
|
hotkeys: [
|
||
|
{
|
||
|
modifiers: ["Mod", "Shift"],
|
||
|
key: "ArrowUp",
|
||
|
},
|
||
|
],
|
||
|
});
|
||
|
this.plugin.addCommand({
|
||
|
id: "move-list-item-down",
|
||
|
icon: "arrow-down",
|
||
|
name: "Move list and sublists down",
|
||
|
editorCallback: createEditorCallback(this.moveListDown),
|
||
|
hotkeys: [
|
||
|
{
|
||
|
modifiers: ["Mod", "Shift"],
|
||
|
key: "ArrowDown",
|
||
|
},
|
||
|
],
|
||
|
});
|
||
|
this.plugin.addCommand({
|
||
|
id: "indent-list",
|
||
|
icon: "indent",
|
||
|
name: "Indent the list and sublists",
|
||
|
editorCallback: createEditorCallback(this.indentList),
|
||
|
hotkeys: [],
|
||
|
});
|
||
|
this.plugin.addCommand({
|
||
|
id: "outdent-list",
|
||
|
icon: "outdent",
|
||
|
name: "Outdent the list and sublists",
|
||
|
editorCallback: createEditorCallback(this.outdentList),
|
||
|
hotkeys: [],
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
unload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () { });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class DeleteTillCurrentLineContentStart {
|
||
|
constructor(root) {
|
||
|
this.root = root;
|
||
|
this.stopPropagation = false;
|
||
|
this.updated = false;
|
||
|
}
|
||
|
shouldStopPropagation() {
|
||
|
return this.stopPropagation;
|
||
|
}
|
||
|
shouldUpdate() {
|
||
|
return this.updated;
|
||
|
}
|
||
|
perform() {
|
||
|
const { root } = this;
|
||
|
if (!root.hasSingleCursor()) {
|
||
|
return;
|
||
|
}
|
||
|
this.stopPropagation = true;
|
||
|
this.updated = true;
|
||
|
const cursor = root.getCursor();
|
||
|
const list = root.getListUnderCursor();
|
||
|
const lines = list.getLinesInfo();
|
||
|
const lineNo = lines.findIndex((l) => l.from.line === cursor.line);
|
||
|
lines[lineNo].text = lines[lineNo].text.slice(cursor.ch - lines[lineNo].from.ch);
|
||
|
list.replaceLines(lines.map((l) => l.text));
|
||
|
root.replaceCursor(lines[lineNo].from);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class MetaBackspaceBehaviourOverride {
|
||
|
constructor(plugin, settings, imeDetector, operationPerformer) {
|
||
|
this.plugin = plugin;
|
||
|
this.settings = settings;
|
||
|
this.imeDetector = imeDetector;
|
||
|
this.operationPerformer = operationPerformer;
|
||
|
this.check = () => {
|
||
|
return (this.settings.keepCursorWithinContent !== "never" &&
|
||
|
!this.imeDetector.isOpened());
|
||
|
};
|
||
|
this.run = (editor) => {
|
||
|
return this.operationPerformer.perform((root) => new DeleteTillCurrentLineContentStart(root), editor);
|
||
|
};
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.registerEditorExtension(view.keymap.of([
|
||
|
{
|
||
|
mac: "m-Backspace",
|
||
|
run: createKeymapRunCallback({
|
||
|
check: this.check,
|
||
|
run: this.run,
|
||
|
}),
|
||
|
},
|
||
|
]));
|
||
|
});
|
||
|
}
|
||
|
unload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () { });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class ReleaseNotesModal extends obsidian.Modal {
|
||
|
constructor(plugin, title, content, cb) {
|
||
|
super(plugin.app);
|
||
|
this.plugin = plugin;
|
||
|
this.title = title;
|
||
|
this.content = content;
|
||
|
this.cb = cb;
|
||
|
}
|
||
|
onOpen() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.titleEl.setText(this.title);
|
||
|
obsidian.MarkdownRenderer.renderMarkdown(this.content, this.contentEl, "", this.plugin);
|
||
|
});
|
||
|
}
|
||
|
onClose() {
|
||
|
this.cb();
|
||
|
}
|
||
|
}
|
||
|
function compareReleases(a, b) {
|
||
|
const [aMajor, aMinor, aPatch] = a.split(".", 3).map(Number);
|
||
|
const [bMajor, bMinor, bPatch] = b.split(".", 3).map(Number);
|
||
|
if (aMajor === bMajor) {
|
||
|
if (aMinor === bMinor) {
|
||
|
return aPatch - bPatch;
|
||
|
}
|
||
|
return aMinor - bMinor;
|
||
|
}
|
||
|
return aMajor - bMajor;
|
||
|
}
|
||
|
function parseChangelog() {
|
||
|
const markdown = "## 4.5.0\n\n### Drag-and-Drop (Experimental)\n\nNow you can drag and drop items using your mouse! 🎉\n\nThis feature is experimental and is disabled by default. To enable this feature, open the plugin settings and turn on the `Drag-and-Drop (Experimental)` setting.\n\nIf you find a bug, please report the [issue](https://github.com/vslinko/obsidian-outliner/issues). Leave your other feedback [here](https://github.com/vslinko/obsidian-outliner/discussions/190).\n\n<img src=\"https://raw.githubusercontent.com/vslinko/obsidian-outliner/main/demo3.gif\" style=\"max-width: 100%\" />\n";
|
||
|
const releaseNotes = [];
|
||
|
let version;
|
||
|
let content = "";
|
||
|
for (const line of markdown.split("\n")) {
|
||
|
const versionHeaderMatches = /^#+\s+(\d+\.\d+\.\d+)$/.exec(line);
|
||
|
if (versionHeaderMatches) {
|
||
|
if (version && content.trim().length > 0) {
|
||
|
releaseNotes.push([version, content]);
|
||
|
}
|
||
|
version = versionHeaderMatches[1];
|
||
|
content = line;
|
||
|
content += "\n";
|
||
|
}
|
||
|
else {
|
||
|
content += line;
|
||
|
content += "\n";
|
||
|
}
|
||
|
}
|
||
|
if (version && content.trim().length > 0) {
|
||
|
releaseNotes.push([version, content]);
|
||
|
}
|
||
|
return releaseNotes;
|
||
|
}
|
||
|
class ReleaseNotesAnnouncement {
|
||
|
constructor(plugin, settings) {
|
||
|
this.plugin = plugin;
|
||
|
this.settings = settings;
|
||
|
this.modal = null;
|
||
|
this.showModal = (previousRelease = null) => {
|
||
|
let releaseNotes = "";
|
||
|
for (const [version, content] of parseChangelog()) {
|
||
|
if (compareReleases(version, previousRelease || "0.0.0") > 0) {
|
||
|
releaseNotes += content;
|
||
|
}
|
||
|
}
|
||
|
if (releaseNotes.trim().length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
const modalTitle = `Welcome to Obsidian Outliner ${"4.6.7"}`;
|
||
|
this.modal = new ReleaseNotesModal(this.plugin, modalTitle, releaseNotes, this.handleClose);
|
||
|
this.modal.open();
|
||
|
};
|
||
|
this.handleClose = () => __awaiter(this, void 0, void 0, function* () {
|
||
|
if (!this.modal) {
|
||
|
return;
|
||
|
}
|
||
|
this.settings.previousRelease = "4.6.7";
|
||
|
yield this.settings.save();
|
||
|
});
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.addCommand({
|
||
|
id: "show-release-notes",
|
||
|
name: "Show Release Notes",
|
||
|
callback: this.showModal,
|
||
|
});
|
||
|
this.showModal(this.settings.previousRelease);
|
||
|
});
|
||
|
}
|
||
|
unload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
if (!this.modal) {
|
||
|
return;
|
||
|
}
|
||
|
const modal = this.modal;
|
||
|
this.modal = null;
|
||
|
modal.close();
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class ObsidianOutlinerPluginSettingTab extends obsidian.PluginSettingTab {
|
||
|
constructor(app, plugin, settings) {
|
||
|
super(app, plugin);
|
||
|
this.settings = settings;
|
||
|
}
|
||
|
display() {
|
||
|
const { containerEl } = this;
|
||
|
containerEl.empty();
|
||
|
new obsidian.Setting(containerEl)
|
||
|
.setName("Stick the cursor to the content")
|
||
|
.setDesc("Don't let the cursor move to the bullet position.")
|
||
|
.addDropdown((dropdown) => {
|
||
|
dropdown
|
||
|
.addOptions({
|
||
|
never: "Never",
|
||
|
"bullet-only": "Stick cursor out of bullets",
|
||
|
"bullet-and-checkbox": "Stick cursor out of bullets and checkboxes",
|
||
|
})
|
||
|
.setValue(this.settings.keepCursorWithinContent)
|
||
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
||
|
this.settings.keepCursorWithinContent = value;
|
||
|
yield this.settings.save();
|
||
|
}));
|
||
|
});
|
||
|
new obsidian.Setting(containerEl)
|
||
|
.setName("Enhance the Tab key")
|
||
|
.setDesc("Make Tab and Shift-Tab behave the same as other outliners.")
|
||
|
.addToggle((toggle) => {
|
||
|
toggle
|
||
|
.setValue(this.settings.overrideTabBehaviour)
|
||
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
||
|
this.settings.overrideTabBehaviour = value;
|
||
|
yield this.settings.save();
|
||
|
}));
|
||
|
});
|
||
|
new obsidian.Setting(containerEl)
|
||
|
.setName("Enhance the Enter key")
|
||
|
.setDesc("Make the Enter key behave the same as other outliners.")
|
||
|
.addToggle((toggle) => {
|
||
|
toggle
|
||
|
.setValue(this.settings.overrideEnterBehaviour)
|
||
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
||
|
this.settings.overrideEnterBehaviour = value;
|
||
|
yield this.settings.save();
|
||
|
}));
|
||
|
});
|
||
|
new obsidian.Setting(containerEl)
|
||
|
.setName("Enhance the Ctrl+A or Cmd+A behavior")
|
||
|
.setDesc("Press the hotkey once to select the current list item. Press the hotkey twice to select the entire list.")
|
||
|
.addToggle((toggle) => {
|
||
|
toggle
|
||
|
.setValue(this.settings.overrideSelectAllBehaviour)
|
||
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
||
|
this.settings.overrideSelectAllBehaviour = value;
|
||
|
yield this.settings.save();
|
||
|
}));
|
||
|
});
|
||
|
new obsidian.Setting(containerEl)
|
||
|
.setName("Improve the style of your lists")
|
||
|
.setDesc("Styles are only compatible with built-in Obsidian themes and may not be compatible with other themes.")
|
||
|
.addToggle((toggle) => {
|
||
|
toggle
|
||
|
.setValue(this.settings.betterListsStyles)
|
||
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
||
|
this.settings.betterListsStyles = value;
|
||
|
yield this.settings.save();
|
||
|
}));
|
||
|
});
|
||
|
new obsidian.Setting(containerEl)
|
||
|
.setName("Draw vertical indentation lines")
|
||
|
.addToggle((toggle) => {
|
||
|
toggle.setValue(this.settings.verticalLines).onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
||
|
this.settings.verticalLines = value;
|
||
|
yield this.settings.save();
|
||
|
}));
|
||
|
});
|
||
|
new obsidian.Setting(containerEl)
|
||
|
.setName("Vertical indentation line click action")
|
||
|
.addDropdown((dropdown) => {
|
||
|
dropdown
|
||
|
.addOptions({
|
||
|
none: "None",
|
||
|
"zoom-in": "Zoom In",
|
||
|
"toggle-folding": "Toggle Folding",
|
||
|
})
|
||
|
.setValue(this.settings.verticalLinesAction)
|
||
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
||
|
this.settings.verticalLinesAction = value;
|
||
|
yield this.settings.save();
|
||
|
}));
|
||
|
});
|
||
|
new obsidian.Setting(containerEl)
|
||
|
.setName("Drag-and-Drop (Experimental)")
|
||
|
.addToggle((toggle) => {
|
||
|
toggle.setValue(this.settings.dragAndDrop).onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
||
|
this.settings.dragAndDrop = value;
|
||
|
yield this.settings.save();
|
||
|
}));
|
||
|
});
|
||
|
new obsidian.Setting(containerEl)
|
||
|
.setName("Debug mode")
|
||
|
.setDesc("Open DevTools (Command+Option+I or Control+Shift+I) to copy the debug logs.")
|
||
|
.addToggle((toggle) => {
|
||
|
toggle.setValue(this.settings.debug).onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
||
|
this.settings.debug = value;
|
||
|
yield this.settings.save();
|
||
|
}));
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
class SettingsTab {
|
||
|
constructor(plugin, settings) {
|
||
|
this.plugin = plugin;
|
||
|
this.settings = settings;
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.addSettingTab(new ObsidianOutlinerPluginSettingTab(this.plugin.app, this.plugin, this.settings));
|
||
|
});
|
||
|
}
|
||
|
unload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () { });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class ShiftTabBehaviourOverride {
|
||
|
constructor(plugin, imeDetector, settings, operationPerformer) {
|
||
|
this.plugin = plugin;
|
||
|
this.imeDetector = imeDetector;
|
||
|
this.settings = settings;
|
||
|
this.operationPerformer = operationPerformer;
|
||
|
this.check = () => {
|
||
|
return this.settings.overrideTabBehaviour && !this.imeDetector.isOpened();
|
||
|
};
|
||
|
this.run = (editor) => {
|
||
|
return this.operationPerformer.perform((root) => new OutdentList(root), editor);
|
||
|
};
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.registerEditorExtension(state.Prec.highest(view.keymap.of([
|
||
|
{
|
||
|
key: "s-Tab",
|
||
|
run: createKeymapRunCallback({
|
||
|
check: this.check,
|
||
|
run: this.run,
|
||
|
}),
|
||
|
},
|
||
|
])));
|
||
|
});
|
||
|
}
|
||
|
unload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () { });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class TabBehaviourOverride {
|
||
|
constructor(plugin, imeDetector, obsidianSettings, settings, operationPerformer) {
|
||
|
this.plugin = plugin;
|
||
|
this.imeDetector = imeDetector;
|
||
|
this.obsidianSettings = obsidianSettings;
|
||
|
this.settings = settings;
|
||
|
this.operationPerformer = operationPerformer;
|
||
|
this.check = () => {
|
||
|
return this.settings.overrideTabBehaviour && !this.imeDetector.isOpened();
|
||
|
};
|
||
|
this.run = (editor) => {
|
||
|
return this.operationPerformer.perform((root) => new IndentList(root, this.obsidianSettings.getDefaultIndentChars()), editor);
|
||
|
};
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.registerEditorExtension(state.Prec.highest(view.keymap.of([
|
||
|
{
|
||
|
key: "Tab",
|
||
|
run: createKeymapRunCallback({
|
||
|
check: this.check,
|
||
|
run: this.run,
|
||
|
}),
|
||
|
},
|
||
|
])));
|
||
|
});
|
||
|
}
|
||
|
unload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () { });
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const VERTICAL_LINES_BODY_CLASS = "outliner-plugin-vertical-lines";
|
||
|
class VerticalLinesPluginValue {
|
||
|
constructor(settings, obsidianSettings, parser, view) {
|
||
|
this.settings = settings;
|
||
|
this.obsidianSettings = obsidianSettings;
|
||
|
this.parser = parser;
|
||
|
this.view = view;
|
||
|
this.lineElements = [];
|
||
|
this.waitForEditor = () => {
|
||
|
const editor = getEditorFromState(this.view.state);
|
||
|
if (!editor) {
|
||
|
setTimeout(this.waitForEditor, 0);
|
||
|
return;
|
||
|
}
|
||
|
this.editor = editor;
|
||
|
this.scheduleRecalculate();
|
||
|
};
|
||
|
this.onScroll = (e) => {
|
||
|
const { scrollLeft, scrollTop } = e.target;
|
||
|
this.scroller.scrollTo(scrollLeft, scrollTop);
|
||
|
};
|
||
|
this.scheduleRecalculate = () => {
|
||
|
clearTimeout(this.scheduled);
|
||
|
this.scheduled = setTimeout(this.calculate, 0);
|
||
|
};
|
||
|
this.calculate = () => {
|
||
|
this.lines = [];
|
||
|
if (this.settings.verticalLines &&
|
||
|
this.obsidianSettings.isDefaultThemeEnabled() &&
|
||
|
this.view.viewportLineBlocks.length > 0 &&
|
||
|
this.view.visibleRanges.length > 0) {
|
||
|
const fromLine = this.editor.offsetToPos(this.view.viewport.from).line;
|
||
|
const toLine = this.editor.offsetToPos(this.view.viewport.to).line;
|
||
|
const lists = this.parser.parseRange(this.editor, fromLine, toLine);
|
||
|
for (const list of lists) {
|
||
|
this.lastLine = list.getContentEnd().line;
|
||
|
for (const c of list.getChildren()) {
|
||
|
this.recursive(c);
|
||
|
}
|
||
|
}
|
||
|
this.lines.sort((a, b) => a.top === b.top ? a.left - b.left : a.top - b.top);
|
||
|
}
|
||
|
this.updateDom();
|
||
|
};
|
||
|
this.onClick = (e) => {
|
||
|
e.preventDefault();
|
||
|
const line = this.lines[Number(e.target.dataset.index)];
|
||
|
switch (this.settings.verticalLinesAction) {
|
||
|
case "zoom-in":
|
||
|
this.zoomIn(line);
|
||
|
break;
|
||
|
case "toggle-folding":
|
||
|
this.toggleFolding(line);
|
||
|
break;
|
||
|
}
|
||
|
};
|
||
|
this.view.scrollDOM.addEventListener("scroll", this.onScroll);
|
||
|
this.settings.onChange(this.scheduleRecalculate);
|
||
|
this.prepareDom();
|
||
|
this.waitForEditor();
|
||
|
}
|
||
|
prepareDom() {
|
||
|
this.contentContainer = document.createElement("div");
|
||
|
this.contentContainer.classList.add("outliner-plugin-list-lines-content-container");
|
||
|
this.scroller = document.createElement("div");
|
||
|
this.scroller.classList.add("outliner-plugin-list-lines-scroller");
|
||
|
this.scroller.appendChild(this.contentContainer);
|
||
|
this.view.dom.appendChild(this.scroller);
|
||
|
}
|
||
|
update(update) {
|
||
|
if (update.docChanged ||
|
||
|
update.viewportChanged ||
|
||
|
update.geometryChanged ||
|
||
|
update.transactions.some((tr) => tr.reconfigured)) {
|
||
|
this.scheduleRecalculate();
|
||
|
}
|
||
|
}
|
||
|
getNextSibling(list) {
|
||
|
let listTmp = list;
|
||
|
let p = listTmp.getParent();
|
||
|
while (p) {
|
||
|
const nextSibling = p.getNextSiblingOf(listTmp);
|
||
|
if (nextSibling) {
|
||
|
return nextSibling;
|
||
|
}
|
||
|
listTmp = p;
|
||
|
p = listTmp.getParent();
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
recursive(list, parentCtx = {}) {
|
||
|
const children = list.getChildren();
|
||
|
if (children.length === 0) {
|
||
|
return;
|
||
|
}
|
||
|
const fromOffset = this.editor.posToOffset({
|
||
|
line: list.getFirstLineContentStart().line,
|
||
|
ch: list.getFirstLineIndent().length,
|
||
|
});
|
||
|
const nextSibling = this.getNextSibling(list);
|
||
|
const tillOffset = this.editor.posToOffset({
|
||
|
line: nextSibling
|
||
|
? nextSibling.getFirstLineContentStart().line - 1
|
||
|
: this.lastLine,
|
||
|
ch: 0,
|
||
|
});
|
||
|
let visibleFrom = this.view.visibleRanges[0].from;
|
||
|
let visibleTo = this.view.visibleRanges[this.view.visibleRanges.length - 1].to;
|
||
|
const zoomRange = this.editor.getZoomRange();
|
||
|
if (zoomRange) {
|
||
|
visibleFrom = Math.max(visibleFrom, this.editor.posToOffset(zoomRange.from));
|
||
|
visibleTo = Math.min(visibleTo, this.editor.posToOffset(zoomRange.to));
|
||
|
}
|
||
|
if (fromOffset > visibleTo || tillOffset < visibleFrom) {
|
||
|
return;
|
||
|
}
|
||
|
const coords = this.view.coordsAtPos(fromOffset, 1);
|
||
|
if (parentCtx.rootLeft === undefined) {
|
||
|
parentCtx.rootLeft = coords.left;
|
||
|
}
|
||
|
const left = Math.floor(coords.right - parentCtx.rootLeft);
|
||
|
const top = visibleFrom > 0 && fromOffset < visibleFrom
|
||
|
? -20
|
||
|
: this.view.lineBlockAt(fromOffset).top;
|
||
|
const bottom = tillOffset > visibleTo
|
||
|
? this.view.lineBlockAt(visibleTo - 1).bottom
|
||
|
: this.view.lineBlockAt(tillOffset).bottom;
|
||
|
const height = bottom - top;
|
||
|
if (height > 0 && !list.isFolded()) {
|
||
|
const nextSibling = list.getParent().getNextSiblingOf(list);
|
||
|
const hasNextSibling = !!nextSibling &&
|
||
|
this.editor.posToOffset(nextSibling.getFirstLineContentStart()) <=
|
||
|
visibleTo;
|
||
|
this.lines.push({
|
||
|
top,
|
||
|
left,
|
||
|
height: `calc(${height}px ${hasNextSibling ? "- 1.5em" : "- 2em"})`,
|
||
|
list,
|
||
|
});
|
||
|
}
|
||
|
for (const child of children) {
|
||
|
if (!child.isEmpty()) {
|
||
|
this.recursive(child, parentCtx);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
zoomIn(line) {
|
||
|
const editor = getEditorFromState(this.view.state);
|
||
|
editor.zoomIn(line.list.getFirstLineContentStart().line);
|
||
|
}
|
||
|
toggleFolding(line) {
|
||
|
const { list } = line;
|
||
|
if (list.isEmpty()) {
|
||
|
return;
|
||
|
}
|
||
|
let needToUnfold = true;
|
||
|
const linesToToggle = [];
|
||
|
for (const c of list.getChildren()) {
|
||
|
if (c.isEmpty()) {
|
||
|
continue;
|
||
|
}
|
||
|
if (!c.isFolded()) {
|
||
|
needToUnfold = false;
|
||
|
}
|
||
|
linesToToggle.push(c.getFirstLineContentStart().line);
|
||
|
}
|
||
|
const editor = getEditorFromState(this.view.state);
|
||
|
for (const l of linesToToggle) {
|
||
|
if (needToUnfold) {
|
||
|
editor.unfold(l);
|
||
|
}
|
||
|
else {
|
||
|
editor.fold(l);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
updateDom() {
|
||
|
const cmScroll = this.view.scrollDOM;
|
||
|
const cmContent = this.view.contentDOM;
|
||
|
const cmContentContainer = cmContent.parentElement;
|
||
|
const cmSizer = cmContentContainer.parentElement;
|
||
|
/**
|
||
|
* Obsidian can add additional elements into Content Manager.
|
||
|
* The most obvious case is the 'embedded-backlinks' core plugin that adds a menu inside a Content Manager.
|
||
|
* We must take heights of all of these elements into account
|
||
|
* to be able to calculate the correct size of lines' container.
|
||
|
*/
|
||
|
let cmSizerChildrenSumHeight = 0;
|
||
|
for (let i = 0; i < cmSizer.children.length; i++) {
|
||
|
cmSizerChildrenSumHeight += cmSizer.children[i].clientHeight;
|
||
|
}
|
||
|
this.scroller.style.top = cmScroll.offsetTop + "px";
|
||
|
this.contentContainer.style.height = cmSizerChildrenSumHeight + "px";
|
||
|
this.contentContainer.style.marginLeft =
|
||
|
cmContentContainer.offsetLeft + "px";
|
||
|
this.contentContainer.style.marginTop =
|
||
|
cmContent.firstElementChild.offsetTop - 24 + "px";
|
||
|
for (let i = 0; i < this.lines.length; i++) {
|
||
|
if (this.lineElements.length === i) {
|
||
|
const e = document.createElement("div");
|
||
|
e.classList.add("outliner-plugin-list-line");
|
||
|
e.dataset.index = String(i);
|
||
|
e.addEventListener("mousedown", this.onClick);
|
||
|
this.contentContainer.appendChild(e);
|
||
|
this.lineElements.push(e);
|
||
|
}
|
||
|
const l = this.lines[i];
|
||
|
const e = this.lineElements[i];
|
||
|
e.style.top = l.top + "px";
|
||
|
e.style.left = l.left + "px";
|
||
|
e.style.height = l.height;
|
||
|
e.style.display = "block";
|
||
|
}
|
||
|
for (let i = this.lines.length; i < this.lineElements.length; i++) {
|
||
|
const e = this.lineElements[i];
|
||
|
e.style.top = "0px";
|
||
|
e.style.left = "0px";
|
||
|
e.style.height = "0px";
|
||
|
e.style.display = "none";
|
||
|
}
|
||
|
}
|
||
|
destroy() {
|
||
|
this.settings.removeCallback(this.scheduleRecalculate);
|
||
|
this.view.scrollDOM.removeEventListener("scroll", this.onScroll);
|
||
|
this.view.dom.removeChild(this.scroller);
|
||
|
clearTimeout(this.scheduled);
|
||
|
}
|
||
|
}
|
||
|
class VerticalLines {
|
||
|
constructor(plugin, settings, obsidianSettings, parser) {
|
||
|
this.plugin = plugin;
|
||
|
this.settings = settings;
|
||
|
this.obsidianSettings = obsidianSettings;
|
||
|
this.parser = parser;
|
||
|
this.updateBodyClass = () => {
|
||
|
const shouldExists = this.obsidianSettings.isDefaultThemeEnabled() &&
|
||
|
this.settings.verticalLines;
|
||
|
const exists = document.body.classList.contains(VERTICAL_LINES_BODY_CLASS);
|
||
|
if (shouldExists && !exists) {
|
||
|
document.body.classList.add(VERTICAL_LINES_BODY_CLASS);
|
||
|
}
|
||
|
if (!shouldExists && exists) {
|
||
|
document.body.classList.remove(VERTICAL_LINES_BODY_CLASS);
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.updateBodyClass();
|
||
|
this.updateBodyClassInterval = window.setInterval(() => {
|
||
|
this.updateBodyClass();
|
||
|
}, 1000);
|
||
|
this.plugin.registerEditorExtension(view.ViewPlugin.define((view) => new VerticalLinesPluginValue(this.settings, this.obsidianSettings, this.parser, view)));
|
||
|
});
|
||
|
}
|
||
|
unload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
clearInterval(this.updateBodyClassInterval);
|
||
|
document.body.classList.remove(VERTICAL_LINES_BODY_CLASS);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class ChangesApplicator {
|
||
|
apply(editor, prevRoot, newRoot) {
|
||
|
const changes = this.calculateChanges(editor, prevRoot, newRoot);
|
||
|
if (changes) {
|
||
|
const { replacement, changeFrom, changeTo } = changes;
|
||
|
const { unfold, fold } = this.calculateFoldingOprations(prevRoot, newRoot, changeFrom, changeTo);
|
||
|
for (const line of unfold) {
|
||
|
editor.unfold(line);
|
||
|
}
|
||
|
editor.replaceRange(replacement, changeFrom, changeTo);
|
||
|
for (const line of fold) {
|
||
|
editor.fold(line);
|
||
|
}
|
||
|
}
|
||
|
editor.setSelections(newRoot.getSelections());
|
||
|
}
|
||
|
calculateChanges(editor, prevRoot, newRoot) {
|
||
|
const rootRange = prevRoot.getContentRange();
|
||
|
const oldString = editor.getRange(rootRange[0], rootRange[1]);
|
||
|
const newString = newRoot.print();
|
||
|
const changeFrom = Object.assign({}, rootRange[0]);
|
||
|
const changeTo = Object.assign({}, rootRange[1]);
|
||
|
let oldTmp = oldString;
|
||
|
let newTmp = newString;
|
||
|
while (true) {
|
||
|
const nlIndex = oldTmp.lastIndexOf("\n");
|
||
|
if (nlIndex < 0) {
|
||
|
break;
|
||
|
}
|
||
|
const oldLine = oldTmp.slice(nlIndex);
|
||
|
const newLine = newTmp.slice(-oldLine.length);
|
||
|
if (oldLine !== newLine) {
|
||
|
break;
|
||
|
}
|
||
|
oldTmp = oldTmp.slice(0, -oldLine.length);
|
||
|
newTmp = newTmp.slice(0, -oldLine.length);
|
||
|
const nlIndex2 = oldTmp.lastIndexOf("\n");
|
||
|
changeTo.ch =
|
||
|
nlIndex2 >= 0 ? oldTmp.length - nlIndex2 - 1 : oldTmp.length;
|
||
|
changeTo.line--;
|
||
|
}
|
||
|
while (true) {
|
||
|
const nlIndex = oldTmp.indexOf("\n");
|
||
|
if (nlIndex < 0) {
|
||
|
break;
|
||
|
}
|
||
|
const oldLine = oldTmp.slice(0, nlIndex + 1);
|
||
|
const newLine = newTmp.slice(0, oldLine.length);
|
||
|
if (oldLine !== newLine) {
|
||
|
break;
|
||
|
}
|
||
|
changeFrom.line++;
|
||
|
oldTmp = oldTmp.slice(oldLine.length);
|
||
|
newTmp = newTmp.slice(oldLine.length);
|
||
|
}
|
||
|
if (oldTmp === newTmp) {
|
||
|
return null;
|
||
|
}
|
||
|
return {
|
||
|
replacement: newTmp,
|
||
|
changeFrom,
|
||
|
changeTo,
|
||
|
};
|
||
|
}
|
||
|
calculateFoldingOprations(prevRoot, newRoot, changeFrom, changeTo) {
|
||
|
const changedRange = [changeFrom, changeTo];
|
||
|
const prevLists = getAllChildren(prevRoot);
|
||
|
const newLists = getAllChildren(newRoot);
|
||
|
const unfold = [];
|
||
|
const fold = [];
|
||
|
for (const prevList of prevLists.values()) {
|
||
|
if (!prevList.isFoldRoot()) {
|
||
|
continue;
|
||
|
}
|
||
|
const newList = newLists.get(prevList.getID());
|
||
|
if (!newList) {
|
||
|
continue;
|
||
|
}
|
||
|
const prevListRange = [
|
||
|
prevList.getFirstLineContentStart(),
|
||
|
prevList.getContentEndIncludingChildren(),
|
||
|
];
|
||
|
if (isRangesIntersects(prevListRange, changedRange)) {
|
||
|
unfold.push(prevList.getFirstLineContentStart().line);
|
||
|
fold.push(newList.getFirstLineContentStart().line);
|
||
|
}
|
||
|
}
|
||
|
unfold.sort((a, b) => b - a);
|
||
|
fold.sort((a, b) => b - a);
|
||
|
return { unfold, fold };
|
||
|
}
|
||
|
}
|
||
|
function getAllChildrenReduceFn(acc, child) {
|
||
|
acc.set(child.getID(), child);
|
||
|
child.getChildren().reduce(getAllChildrenReduceFn, acc);
|
||
|
return acc;
|
||
|
}
|
||
|
function getAllChildren(root) {
|
||
|
return root.getChildren().reduce(getAllChildrenReduceFn, new Map());
|
||
|
}
|
||
|
|
||
|
class IMEDetector {
|
||
|
constructor() {
|
||
|
this.composition = false;
|
||
|
this.onCompositionStart = () => {
|
||
|
this.composition = true;
|
||
|
};
|
||
|
this.onCompositionEnd = () => {
|
||
|
this.composition = false;
|
||
|
};
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
document.addEventListener("compositionstart", this.onCompositionStart);
|
||
|
document.addEventListener("compositionend", this.onCompositionEnd);
|
||
|
});
|
||
|
}
|
||
|
unload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
document.removeEventListener("compositionend", this.onCompositionEnd);
|
||
|
document.removeEventListener("compositionstart", this.onCompositionStart);
|
||
|
});
|
||
|
}
|
||
|
isOpened() {
|
||
|
return this.composition && obsidian.Platform.isDesktop;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class Logger {
|
||
|
constructor(settings) {
|
||
|
this.settings = settings;
|
||
|
}
|
||
|
log(method, ...args) {
|
||
|
if (!this.settings.debug) {
|
||
|
return;
|
||
|
}
|
||
|
console.info(method, ...args);
|
||
|
}
|
||
|
bind(method) {
|
||
|
return (...args) => this.log(method, ...args);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function getHiddenObsidianConfig(app) {
|
||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
|
return app.vault.config;
|
||
|
}
|
||
|
class ObsidianSettings {
|
||
|
constructor(app) {
|
||
|
this.app = app;
|
||
|
}
|
||
|
isLegacyEditorEnabled() {
|
||
|
const config = Object.assign({ legacyEditor: false }, getHiddenObsidianConfig(this.app));
|
||
|
return config.legacyEditor;
|
||
|
}
|
||
|
isDefaultThemeEnabled() {
|
||
|
const config = Object.assign({ cssTheme: "" }, getHiddenObsidianConfig(this.app));
|
||
|
return config.cssTheme === "";
|
||
|
}
|
||
|
getTabsSettings() {
|
||
|
return Object.assign({ useTab: true, tabSize: 4 }, getHiddenObsidianConfig(this.app));
|
||
|
}
|
||
|
getFoldSettings() {
|
||
|
return Object.assign({ foldIndent: true }, getHiddenObsidianConfig(this.app));
|
||
|
}
|
||
|
getDefaultIndentChars() {
|
||
|
const { useTab, tabSize } = this.getTabsSettings();
|
||
|
return useTab ? "\t" : new Array(tabSize).fill(" ").join("");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class OperationPerformer {
|
||
|
constructor(parser, changesApplicator) {
|
||
|
this.parser = parser;
|
||
|
this.changesApplicator = changesApplicator;
|
||
|
}
|
||
|
eval(root, op, editor) {
|
||
|
const prevRoot = root.clone();
|
||
|
op.perform();
|
||
|
if (op.shouldUpdate()) {
|
||
|
this.changesApplicator.apply(editor, prevRoot, root);
|
||
|
}
|
||
|
return {
|
||
|
shouldUpdate: op.shouldUpdate(),
|
||
|
shouldStopPropagation: op.shouldStopPropagation(),
|
||
|
};
|
||
|
}
|
||
|
perform(cb, editor, cursor = editor.getCursor()) {
|
||
|
const root = this.parser.parse(editor, cursor);
|
||
|
if (!root) {
|
||
|
return { shouldUpdate: false, shouldStopPropagation: false };
|
||
|
}
|
||
|
const op = cb(root);
|
||
|
return this.eval(root, op, editor);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const bulletSignRe = `(?:[-*+]|\\d+\\.)`;
|
||
|
const optionalCheckboxRe = `(?:${checkboxRe})?`;
|
||
|
const listItemWithoutSpacesRe = new RegExp(`^${bulletSignRe}( |\t)`);
|
||
|
const listItemRe = new RegExp(`^[ \t]*${bulletSignRe}( |\t)`);
|
||
|
const stringWithSpacesRe = new RegExp(`^[ \t]+`);
|
||
|
const parseListItemRe = new RegExp(`^([ \t]*)(${bulletSignRe})( |\t)(${optionalCheckboxRe})(.*)$`);
|
||
|
class Parser {
|
||
|
constructor(logger, settings) {
|
||
|
this.logger = logger;
|
||
|
this.settings = settings;
|
||
|
}
|
||
|
parseRange(editor, fromLine = 0, toLine = editor.lastLine()) {
|
||
|
const lists = [];
|
||
|
for (let i = fromLine; i <= toLine; i++) {
|
||
|
const line = editor.getLine(i);
|
||
|
if (i === fromLine || this.isListItem(line)) {
|
||
|
const list = this.parseWithLimits(editor, i, fromLine, toLine);
|
||
|
if (list) {
|
||
|
lists.push(list);
|
||
|
i = list.getContentEnd().line;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return lists;
|
||
|
}
|
||
|
parse(editor, cursor = editor.getCursor()) {
|
||
|
return this.parseWithLimits(editor, cursor.line, 0, editor.lastLine());
|
||
|
}
|
||
|
parseWithLimits(editor, parsingStartLine, limitFrom, limitTo) {
|
||
|
const d = this.logger.bind("parseList");
|
||
|
const error = (msg) => {
|
||
|
d(msg);
|
||
|
return null;
|
||
|
};
|
||
|
const line = editor.getLine(parsingStartLine);
|
||
|
let listLookingPos = null;
|
||
|
if (this.isListItem(line)) {
|
||
|
listLookingPos = parsingStartLine;
|
||
|
}
|
||
|
else if (this.isLineWithIndent(line)) {
|
||
|
let listLookingPosSearch = parsingStartLine - 1;
|
||
|
while (listLookingPosSearch >= 0) {
|
||
|
const line = editor.getLine(listLookingPosSearch);
|
||
|
if (this.isListItem(line)) {
|
||
|
listLookingPos = listLookingPosSearch;
|
||
|
break;
|
||
|
}
|
||
|
else if (this.isLineWithIndent(line)) {
|
||
|
listLookingPosSearch--;
|
||
|
}
|
||
|
else {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if (listLookingPos === null) {
|
||
|
return null;
|
||
|
}
|
||
|
let listStartLine = null;
|
||
|
let listStartLineLookup = listLookingPos;
|
||
|
while (listStartLineLookup >= 0) {
|
||
|
const line = editor.getLine(listStartLineLookup);
|
||
|
if (!this.isListItem(line) && !this.isLineWithIndent(line)) {
|
||
|
break;
|
||
|
}
|
||
|
if (this.isListItemWithoutSpaces(line)) {
|
||
|
listStartLine = listStartLineLookup;
|
||
|
if (listStartLineLookup <= limitFrom) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
listStartLineLookup--;
|
||
|
}
|
||
|
if (listStartLine === null) {
|
||
|
return null;
|
||
|
}
|
||
|
let listEndLine = listLookingPos;
|
||
|
let listEndLineLookup = listLookingPos;
|
||
|
while (listEndLineLookup <= editor.lastLine()) {
|
||
|
const line = editor.getLine(listEndLineLookup);
|
||
|
if (!this.isListItem(line) && !this.isLineWithIndent(line)) {
|
||
|
break;
|
||
|
}
|
||
|
if (!this.isEmptyLine(line)) {
|
||
|
listEndLine = listEndLineLookup;
|
||
|
}
|
||
|
if (listEndLineLookup >= limitTo) {
|
||
|
listEndLine = limitTo;
|
||
|
break;
|
||
|
}
|
||
|
listEndLineLookup++;
|
||
|
}
|
||
|
if (listStartLine > parsingStartLine || listEndLine < parsingStartLine) {
|
||
|
return null;
|
||
|
}
|
||
|
// if the last line contains only spaces and that's incorrect indent, then ignore the last line
|
||
|
// https://github.com/vslinko/obsidian-outliner/issues/368
|
||
|
if (listEndLine > listStartLine) {
|
||
|
const lastLine = editor.getLine(listEndLine);
|
||
|
if (lastLine.trim().length === 0) {
|
||
|
const prevLine = editor.getLine(listEndLine - 1);
|
||
|
const [, prevLineIndent] = /^(\s*)/.exec(prevLine);
|
||
|
if (!lastLine.startsWith(prevLineIndent)) {
|
||
|
listEndLine--;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
const root = new Root({ line: listStartLine, ch: 0 }, { line: listEndLine, ch: editor.getLine(listEndLine).length }, editor.listSelections().map((r) => ({
|
||
|
anchor: { line: r.anchor.line, ch: r.anchor.ch },
|
||
|
head: { line: r.head.line, ch: r.head.ch },
|
||
|
})));
|
||
|
let currentParent = root.getRootList();
|
||
|
let currentList = null;
|
||
|
let currentIndent = "";
|
||
|
const foldedLines = editor.getAllFoldedLines();
|
||
|
for (let l = listStartLine; l <= listEndLine; l++) {
|
||
|
const line = editor.getLine(l);
|
||
|
const matches = parseListItemRe.exec(line);
|
||
|
if (matches) {
|
||
|
const [, indent, bullet, spaceAfterBullet] = matches;
|
||
|
let [, , , , optionalCheckbox, content] = matches;
|
||
|
content = optionalCheckbox + content;
|
||
|
if (this.settings.keepCursorWithinContent !== "bullet-and-checkbox") {
|
||
|
optionalCheckbox = "";
|
||
|
}
|
||
|
const compareLength = Math.min(currentIndent.length, indent.length);
|
||
|
const indentSlice = indent.slice(0, compareLength);
|
||
|
const currentIndentSlice = currentIndent.slice(0, compareLength);
|
||
|
if (indentSlice !== currentIndentSlice) {
|
||
|
const expected = currentIndentSlice
|
||
|
.replace(/ /g, "S")
|
||
|
.replace(/\t/g, "T");
|
||
|
const got = indentSlice.replace(/ /g, "S").replace(/\t/g, "T");
|
||
|
return error(`Unable to parse list: expected indent "${expected}", got "${got}"`);
|
||
|
}
|
||
|
if (indent.length > currentIndent.length) {
|
||
|
currentParent = currentList;
|
||
|
currentIndent = indent;
|
||
|
}
|
||
|
else if (indent.length < currentIndent.length) {
|
||
|
while (currentParent.getFirstLineIndent().length >= indent.length &&
|
||
|
currentParent.getParent()) {
|
||
|
currentParent = currentParent.getParent();
|
||
|
}
|
||
|
currentIndent = indent;
|
||
|
}
|
||
|
const foldRoot = foldedLines.includes(l);
|
||
|
currentList = new List(root, indent, bullet, optionalCheckbox, spaceAfterBullet, content, foldRoot);
|
||
|
currentParent.addAfterAll(currentList);
|
||
|
}
|
||
|
else if (this.isLineWithIndent(line)) {
|
||
|
if (!currentList) {
|
||
|
return error(`Unable to parse list: expected list item, got empty line`);
|
||
|
}
|
||
|
const indentToCheck = currentList.getNotesIndent() || currentIndent;
|
||
|
if (line.indexOf(indentToCheck) !== 0) {
|
||
|
const expected = indentToCheck.replace(/ /g, "S").replace(/\t/g, "T");
|
||
|
const got = line
|
||
|
.match(/^[ \t]*/)[0]
|
||
|
.replace(/ /g, "S")
|
||
|
.replace(/\t/g, "T");
|
||
|
return error(`Unable to parse list: expected indent "${expected}", got "${got}"`);
|
||
|
}
|
||
|
if (!currentList.getNotesIndent()) {
|
||
|
const matches = line.match(/^[ \t]+/);
|
||
|
if (!matches || matches[0].length <= currentIndent.length) {
|
||
|
if (/^\s+$/.test(line)) {
|
||
|
continue;
|
||
|
}
|
||
|
return error(`Unable to parse list: expected some indent, got no indent`);
|
||
|
}
|
||
|
currentList.setNotesIndent(matches[0]);
|
||
|
}
|
||
|
currentList.addLine(line.slice(currentList.getNotesIndent().length));
|
||
|
}
|
||
|
else {
|
||
|
return error(`Unable to parse list: expected list item or note, got "${line}"`);
|
||
|
}
|
||
|
}
|
||
|
return root;
|
||
|
}
|
||
|
isEmptyLine(line) {
|
||
|
return line.length === 0;
|
||
|
}
|
||
|
isLineWithIndent(line) {
|
||
|
return stringWithSpacesRe.test(line);
|
||
|
}
|
||
|
isListItem(line) {
|
||
|
return listItemRe.test(line);
|
||
|
}
|
||
|
isListItemWithoutSpaces(line) {
|
||
|
return listItemWithoutSpacesRe.test(line);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const DEFAULT_SETTINGS = {
|
||
|
styleLists: true,
|
||
|
debug: false,
|
||
|
stickCursor: "bullet-and-checkbox",
|
||
|
betterEnter: true,
|
||
|
betterTab: true,
|
||
|
selectAll: true,
|
||
|
listLines: false,
|
||
|
listLineAction: "toggle-folding",
|
||
|
dndExperiment: false,
|
||
|
previousRelease: null,
|
||
|
};
|
||
|
class Settings {
|
||
|
constructor(storage) {
|
||
|
this.storage = storage;
|
||
|
this.callbacks = new Set();
|
||
|
}
|
||
|
get keepCursorWithinContent() {
|
||
|
// Adaptor for users migrating from older version of the plugin.
|
||
|
if (this.values.stickCursor === true) {
|
||
|
return "bullet-and-checkbox";
|
||
|
}
|
||
|
else if (this.values.stickCursor === false) {
|
||
|
return "never";
|
||
|
}
|
||
|
return this.values.stickCursor;
|
||
|
}
|
||
|
set keepCursorWithinContent(value) {
|
||
|
this.set("stickCursor", value);
|
||
|
}
|
||
|
get overrideTabBehaviour() {
|
||
|
return this.values.betterTab;
|
||
|
}
|
||
|
set overrideTabBehaviour(value) {
|
||
|
this.set("betterTab", value);
|
||
|
}
|
||
|
get overrideEnterBehaviour() {
|
||
|
return this.values.betterEnter;
|
||
|
}
|
||
|
set overrideEnterBehaviour(value) {
|
||
|
this.set("betterEnter", value);
|
||
|
}
|
||
|
get overrideSelectAllBehaviour() {
|
||
|
return this.values.selectAll;
|
||
|
}
|
||
|
set overrideSelectAllBehaviour(value) {
|
||
|
this.set("selectAll", value);
|
||
|
}
|
||
|
get betterListsStyles() {
|
||
|
return this.values.styleLists;
|
||
|
}
|
||
|
set betterListsStyles(value) {
|
||
|
this.set("styleLists", value);
|
||
|
}
|
||
|
get verticalLines() {
|
||
|
return this.values.listLines;
|
||
|
}
|
||
|
set verticalLines(value) {
|
||
|
this.set("listLines", value);
|
||
|
}
|
||
|
get verticalLinesAction() {
|
||
|
return this.values.listLineAction;
|
||
|
}
|
||
|
set verticalLinesAction(value) {
|
||
|
this.set("listLineAction", value);
|
||
|
}
|
||
|
get dragAndDrop() {
|
||
|
return this.values.dndExperiment;
|
||
|
}
|
||
|
set dragAndDrop(value) {
|
||
|
this.set("dndExperiment", value);
|
||
|
}
|
||
|
get debug() {
|
||
|
return this.values.debug;
|
||
|
}
|
||
|
set debug(value) {
|
||
|
this.set("debug", value);
|
||
|
}
|
||
|
get previousRelease() {
|
||
|
return this.values.previousRelease;
|
||
|
}
|
||
|
set previousRelease(value) {
|
||
|
this.set("previousRelease", value);
|
||
|
}
|
||
|
onChange(cb) {
|
||
|
this.callbacks.add(cb);
|
||
|
}
|
||
|
removeCallback(cb) {
|
||
|
this.callbacks.delete(cb);
|
||
|
}
|
||
|
reset() {
|
||
|
for (const [k, v] of Object.entries(DEFAULT_SETTINGS)) {
|
||
|
this.set(k, v);
|
||
|
}
|
||
|
}
|
||
|
load() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.values = Object.assign({}, DEFAULT_SETTINGS, yield this.storage.loadData());
|
||
|
});
|
||
|
}
|
||
|
save() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
yield this.storage.saveData(this.values);
|
||
|
});
|
||
|
}
|
||
|
set(key, value) {
|
||
|
this.values[key] = value;
|
||
|
for (const cb of this.callbacks) {
|
||
|
cb();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class ObsidianOutlinerPlugin extends obsidian.Plugin {
|
||
|
onload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
console.log(`Loading obsidian-outliner`);
|
||
|
yield this.prepareSettings();
|
||
|
this.obsidianSettings = new ObsidianSettings(this.app);
|
||
|
this.logger = new Logger(this.settings);
|
||
|
this.parser = new Parser(this.logger, this.settings);
|
||
|
this.changesApplicator = new ChangesApplicator();
|
||
|
this.operationPerformer = new OperationPerformer(this.parser, this.changesApplicator);
|
||
|
this.imeDetector = new IMEDetector();
|
||
|
yield this.imeDetector.load();
|
||
|
this.features = [
|
||
|
// service features
|
||
|
new ReleaseNotesAnnouncement(this, this.settings),
|
||
|
new SettingsTab(this, this.settings),
|
||
|
// general features
|
||
|
new ListsMovementCommands(this, this.obsidianSettings, this.operationPerformer),
|
||
|
new ListsFoldingCommands(this, this.obsidianSettings),
|
||
|
// features based on settings.keepCursorWithinContent
|
||
|
new EditorSelectionsBehaviourOverride(this, this.settings, this.parser, this.operationPerformer),
|
||
|
new ArrowLeftAndCtrlArrowLeftBehaviourOverride(this, this.settings, this.imeDetector, this.operationPerformer),
|
||
|
new BackspaceBehaviourOverride(this, this.settings, this.imeDetector, this.operationPerformer),
|
||
|
new MetaBackspaceBehaviourOverride(this, this.settings, this.imeDetector, this.operationPerformer),
|
||
|
new DeleteBehaviourOverride(this, this.settings, this.imeDetector, this.operationPerformer),
|
||
|
// features based on settings.overrideTabBehaviour
|
||
|
new TabBehaviourOverride(this, this.imeDetector, this.obsidianSettings, this.settings, this.operationPerformer),
|
||
|
new ShiftTabBehaviourOverride(this, this.imeDetector, this.settings, this.operationPerformer),
|
||
|
// features based on settings.overrideEnterBehaviour
|
||
|
new EnterBehaviourOverride(this, this.settings, this.imeDetector, this.obsidianSettings, this.parser, this.operationPerformer),
|
||
|
// features based on settings.overrideSelectAllBehaviour
|
||
|
new CtrlAAndCmdABehaviourOverride(this, this.settings, this.imeDetector, this.operationPerformer),
|
||
|
// features based on settings.betterListsStyles
|
||
|
new BetterListsStyles(this.settings, this.obsidianSettings),
|
||
|
// features based on settings.verticalLines
|
||
|
new VerticalLines(this, this.settings, this.obsidianSettings, this.parser),
|
||
|
// features based on settings.dragAndDrop
|
||
|
new DragAndDrop(this, this.settings, this.obsidianSettings, this.parser, this.operationPerformer),
|
||
|
];
|
||
|
for (const feature of this.features) {
|
||
|
yield feature.load();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
onunload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
console.log(`Unloading obsidian-outliner`);
|
||
|
yield this.imeDetector.unload();
|
||
|
for (const feature of this.features) {
|
||
|
yield feature.unload();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
prepareSettings() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.settings = new Settings(this);
|
||
|
yield this.settings.load();
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = ObsidianOutlinerPlugin;
|
||
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5qcyIsInNvdXJjZXMiOlsibm9kZV9tb2R1bGVzL3RzbGliL3RzbGliLmVzNi5qcyIsInNyYy9vcGVyYXRpb25zL01vdmVDdXJzb3JUb1ByZXZpb3VzVW5mb2xkZWRMaW5lLnRzIiwic3JjL2VkaXRvci9pbmRleC50cyIsInNyYy91dGlscy9jcmVhdGVLZXltYXBSdW5DYWxsYmFjay50cyIsInNyYy9mZWF0dXJlcy9BcnJvd0xlZnRBbmRDdHJsQXJyb3dMZWZ0QmVoYXZpb3VyT3ZlcnJpZGUudHMiLCJzcmMvcm9vdC9pbmRleC50cyIsInNyYy9vcGVyYXRpb25zL0RlbGV0ZVRpbGxQcmV2aW91c0xpbmVDb250ZW50RW5kLnRzIiwic3JjL2ZlYXR1cmVzL0JhY2tzcGFjZUJlaGF2aW91ck92ZXJyaWRlLnRzIiwic3JjL2ZlYXR1cmVzL0JldHRlckxpc3RzU3R5bGVzLnRzIiwic3JjL29wZXJhdGlvbnMvU2VsZWN0QWxsQ29udGVudC50cyIsInNyYy9mZWF0dXJlcy9DdHJsQUFuZENtZEFCZWhhdmlvdXJPdmVycmlkZS50cyIsInNyYy9vcGVyYXRpb25zL0RlbGV0ZVRpbGxOZXh0TGluZUNvbnRlbnRTdGFydC50cyIsInNyYy9mZWF0dXJlcy9EZWxldGVCZWhhdmlvdXJPdmVycmlkZS50cyIsInNyYy9vcGVyYXRpb25zL01vdmVMaXN0VG9EaWZmZXJlbnRQb3NpdGlvbi50cyIsInNyYy9mZWF0dXJlcy9EcmFnQW5kRHJvcC50cyIsInNyYy9vcGVyYXRpb25zL0tlZXBDdXJzb3JPdXRzaWRlRm9sZGVkTGluZXMudHMiLCJzcmMvb3BlcmF0aW9ucy9LZWVwQ3Vyc29yV2l0aGluTGlzdENvbnRlbnQudHMiLCJzcmMvZmVhdHVyZXMvRWRpdG9yU2VsZWN0aW9uc0JlaGF2aW91ck92ZXJyaWRlLnRzIiwic3JjL3V0aWxzL2NoZWNrYm94UmUudHMiLCJzcmMvdXRpbHMvaXNFbXB0eUxpbmVPckVtcHR5Q2hlY2tib3gudHMiLCJzcmMvb3BlcmF0aW9ucy9DcmVhdGVOZXdJdGVtLnRzIiwic3JjL29wZXJhdGlvbnMvT3V0ZGVudExpc3QudHMiLCJzcmMvb3BlcmF0aW9ucy9PdXRkZW50TGlzdElmSXRzRW1wdHkudHMiLCJzcmMvZmVhdHVyZXMvRW50ZXJCZWhhdmlvdXJPdmVycmlkZS50cyIsInNyYy91dGlscy9jcmVhdGVFZGl0b3JDYWxsYmFjay50cyIsInNyYy9mZWF0dXJlcy9MaXN0c0ZvbGRpbmdDb21tYW5kcy50cyIsInNyYy9vcGVyYXRpb25zL0luZGVudExpc3QudHMiLCJzcmMvb3BlcmF0aW9ucy9Nb3ZlTGlzdERvd24udHMiLCJzcmMvb3BlcmF0aW9ucy9Nb3ZlTGlzdFVwLnRzIiwic3JjL2ZlYXR1cmVzL0xpc3RzTW92ZW1lbnRDb21tYW5kcy50cyIsInNyYy9vcGVyYXRpb25zL0RlbGV0ZVRpbGxDdXJyZW50TGluZUNvbnRlbnRTdGFydC50cyIsInNyYy9mZWF0dXJlcy9NZXRhQmFja3NwYWNlQmVoYXZpb3VyT3ZlcnJpZGUudHMiLCJzcmMvZmVhdHVyZXMvUmVsZWFzZU5vdGVzQW5ub3VuY2VtZW50LnRzIiwic3JjL2ZlYXR1cmVzL1NldHRpbmdzVGFiLnRzIiwic3JjL2ZlYXR1cmVzL1NoaWZ0VGFiQmVoYXZpb3VyT3ZlcnJpZGUudHMiLCJzcmMvZmVhdHVyZXMvVGFiQmVoYXZpb3VyT3ZlcnJpZGUudHMiLCJzcmMvZmVhdHVyZXMvVmVydGljYWxMaW5lcy50cyIsInNyYy9zZXJ2aWNlcy9DaGFuZ2VzQXBwbGljYXRvci50cyIsInNyYy9zZXJ2aWNlcy9JTUVEZXRlY3Rvci50cyIsInNyYy9zZXJ2aWNlcy9Mb2dnZXIudHMiLCJzcmMvc2VydmljZXMvT2JzaWRpYW5TZXR0aW5ncy50cyIsInNyYy9zZXJ2aWNlcy9PcGVyYXRpb25QZXJmb3JtZXIudHMiLCJzcmMvc2VydmljZXMvUGFyc2VyLnRzIiwic3JjL3NlcnZpY2VzL1NldHRpbmdzLnRzIiwic3JjL09ic2lkaWFuT3V0bGluZXJQbHVnaW4udHMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKlxyXG5Db3B5cmlnaHQgKGMpIE1pY3Jvc29mdCBDb3Jwb3JhdGlvbi5cclxuXHJcblBlcm1pc3Npb24gdG8gdXNlLCBjb3B5LCBtb2RpZnksIGFuZC9vciBkaXN0cmlidXRlIHRoaXMgc29mdHdhcmUgZm9yIGFueVxyXG5wdXJwb3NlIHdpdGggb3Igd2l0aG91dCBmZWUgaXMgaGVyZWJ5IGdyYW50ZWQuXHJcblxyXG5USEUgU09GVFdBUkUgSVMgUFJPVklERUQgXCJBUyBJU1wiIEFORCBUSEUgQVVUSE9SIERJU0NMQUlNUyBBTEwgV0FSUkFOVElFUyBXSVRIXHJcblJFR0FSRCBUTyBUSElTIFNPRlRXQVJFIElOQ0xVRElORyBBTEwgSU1QTElFRCBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWVxyXG5BTkQgRklUTkVTUy4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFIEFVVEhPUiBCRSBMSUFCTEUgRk9SIEFOWSBTUEVDSUFMLCBESVJFQ1QsXHJcbklORElSRUNULCBPUiBDT05TRVFVRU5USUFMIERBTUFHRVMgT1IgQU5ZIERBTUFHRVMgV0hBVFNPRVZFUiBSRVNVTFRJTkcgRlJPTVxyXG5MT1NTIE9GIFVTRSwgREFUQSBPUiBQUk9GSVRTLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgTkVHTElHRU5DRSBPUlxyXG5PVEhFUiBUT1JUSU9VUyBBQ1RJT04sIEFSSVNJTkcgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgVVNFIE9SXHJcblBFUkZPUk1BTkNFIE9GIFRISVMgU09GVFdBUkUuXHJcbioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqICovXHJcbi8qIGdsb2JhbCBSZWZsZWN0LCBQcm9taXNlICovXHJcblxyXG52YXIgZXh0ZW5kU3RhdGljcyA9IGZ1bmN0aW9uKGQsIGIpIHtcclxuICAgIGV4dGVuZFN0YXRpY3MgPSBPYmplY3Quc2V0UHJvdG90eXBlT2YgfHxcclxuICAgICAgICAoeyBfX3Byb3RvX186IFtdIH0gaW5zdGFuY2VvZiBBcnJheSAmJiBmdW5jdGlvbiAoZCwgYikgeyBkLl9fcHJvdG9fXyA9IGI7IH0pIHx8XHJcbiAgICAgICAgZnVuY3Rpb24gKGQsIGIpIHsgZm9yICh2YXIgcCBpbiBiKSBpZiAoT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKGIsIHApKSBkW3BdID0gYltwXTsgfTtcclxuICAgIHJldHVybiBleHRlbmRTdGF0aWNzKGQsIGIpO1xyXG59O1xyXG5cclxuZXhwb3J0IGZ
|