1
0
silverbullet/plugs/editor/outline.ts
2023-12-16 14:33:08 +01:00

226 lines
6.5 KiB
TypeScript

import { editor } from "$sb/syscalls.ts";
export async function moveItemUp() {
const cursorPos = await editor.getCursor();
const text = await editor.getText();
try {
const currentItemBounds = determineItemBounds(text, cursorPos);
const previousItemBounds = determineItemBounds(
text,
currentItemBounds.from - 1,
currentItemBounds.indentLevel,
);
if (currentItemBounds.from === previousItemBounds.from) {
throw new Error("Already at the top");
}
const newText =
ensureNewLine(text.slice(currentItemBounds.from, currentItemBounds.to)) +
text.slice(previousItemBounds.from, previousItemBounds.to);
const newCursorPos = (cursorPos - currentItemBounds.from) +
previousItemBounds.from;
await editor.dispatch({
changes: [
{
from: previousItemBounds.from,
to: currentItemBounds.to,
insert: newText,
},
],
selection: {
anchor: newCursorPos,
},
});
} catch (e: any) {
await editor.flashNotification(e.message, "error");
}
}
export async function moveItemDown() {
const cursorPos = await editor.getCursor();
const text = await editor.getText();
try {
const currentItemBounds = determineItemBounds(text, cursorPos);
const nextItemBounds = determineItemBounds(
text,
currentItemBounds.to + 1,
currentItemBounds.indentLevel,
);
if (currentItemBounds.from === nextItemBounds.from) {
throw new Error("Already at the bottom");
}
const nextItemText = ensureNewLine(
text.slice(nextItemBounds.from, nextItemBounds.to),
);
const newText = nextItemText +
text.slice(currentItemBounds.from, currentItemBounds.to);
const newCursorPos = (cursorPos - currentItemBounds.from) +
currentItemBounds.from + nextItemText.length;
await editor.dispatch({
changes: [
{
from: currentItemBounds.from,
to: nextItemBounds.to,
insert: newText,
},
],
selection: {
anchor: newCursorPos,
},
});
} catch (e: any) {
await editor.flashNotification(e.message, "error");
}
}
export async function indentItem() {
const cursorPos = await editor.getCursor();
const text = await editor.getText();
try {
const currentItemBounds = determineItemBounds(text, cursorPos);
const itemText = text.slice(currentItemBounds.from, currentItemBounds.to);
const newText = itemText.split("\n").map((line) =>
line ? " " + line : line
).join("\n");
const preText = text.slice(currentItemBounds.from, cursorPos);
const newCursorPos = cursorPos + preText.split("\n").length * 2;
await editor.dispatch({
changes: [
{
from: currentItemBounds.from,
to: currentItemBounds.to,
insert: newText,
},
],
selection: {
anchor: newCursorPos,
},
});
} catch (e: any) {
await editor.flashNotification(e.message, "error");
}
}
export async function outdentItem() {
const cursorPos = await editor.getCursor();
const text = await editor.getText();
try {
const currentItemBounds = determineItemBounds(text, cursorPos);
const itemText = text.slice(currentItemBounds.from, currentItemBounds.to);
if (!itemText.startsWith(" ")) {
throw new Error("Cannot outdent further");
}
const newText = itemText.split("\n").map((line) =>
line.startsWith(" ") ? line.substring(2) : line
).join("\n");
const preText = text.slice(currentItemBounds.from, cursorPos);
const newCursorPos = cursorPos - preText.split("\n").length * 2;
await editor.dispatch({
changes: [
{
from: currentItemBounds.from,
to: currentItemBounds.to,
insert: newText,
},
],
selection: {
anchor: newCursorPos,
},
});
} catch (e: any) {
await editor.flashNotification(e.message, "error");
}
}
function ensureNewLine(s: string) {
if (!s.endsWith("\n")) {
return s + "\n";
} else {
return s;
}
}
function determineItemBounds(
text: string,
pos: number,
minIndentLevel?: number,
): { from: number; to: number; indentLevel: number } {
// Find the start of the item marked with a bullet
let currentItemStart = pos;
let indentLevel = 0;
while (true) {
while (currentItemStart > 0 && text[currentItemStart - 1] !== "\n") {
currentItemStart--;
}
// Check if the line is a bullet and determine the indent level
indentLevel = 0;
while (text[currentItemStart + indentLevel] === " ") {
indentLevel++;
}
if (minIndentLevel !== undefined && indentLevel < minIndentLevel) {
throw new Error("No item found at minimum indent level");
}
if (minIndentLevel !== undefined && indentLevel > minIndentLevel) {
// Not at the desired indent level yet, let's go up another line
currentItemStart--;
if (currentItemStart <= 0) {
// We've reached the top of the document, no bullet found
throw new Error("No item found");
}
continue;
}
if (["-", "*"].includes(text[currentItemStart + indentLevel])) {
// This is a bullet line, found it, let's break out of this loop
break;
} else {
// Not a bullet line, let's go up another line
currentItemStart--;
if (currentItemStart <= 0) {
// We've reached the top of the document, no bullet found
throw new Error("No item found");
}
}
}
// Ok, so at this point we have determine the starting point of our item
// Relevant variables are currentItemStart and indentLevel
// Now let's find the end point
let currentItemEnd = currentItemStart + 1;
while (true) {
// Let's traverse to the end of the line
while (currentItemEnd < text.length && text[currentItemEnd - 1] !== "\n") {
currentItemEnd++;
}
// Check the indent level of the next line
let nextIndentLevel = 0;
while (text[currentItemEnd + nextIndentLevel] === " ") {
nextIndentLevel++;
}
if (nextIndentLevel <= indentLevel) {
// This is a line indentend less than the current item, found it, let's break out of this loop
break;
} else {
// Not a bullet line, let's go up another line
currentItemEnd++;
if (currentItemEnd >= text.length) {
// End of the document, mark this as the end of the item
currentItemEnd = text.length - 1;
break;
}
}
}
return {
from: currentItemStart,
to: currentItemEnd,
indentLevel,
};
}