1
0
silverbullet/plugs/editor/outline.ts

291 lines
8.3 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);
let previousItemBounds: ReturnType<typeof determineItemBounds> | undefined;
try {
previousItemBounds = determineItemBounds(
text,
currentItemBounds.from - 1,
currentItemBounds.indentLevel,
);
if (currentItemBounds.from === previousItemBounds.from) {
throw new Error("Already at the top");
}
} catch {
// Ok, top of the list, let's find the previous item at any other indent level and adapt
previousItemBounds = determineItemBounds(
text,
currentItemBounds.from - 1,
);
}
let newPreviousText = text.slice(
previousItemBounds.from,
previousItemBounds.to,
);
// If the current item is embedded inside the previous item, we need to strip it out
if (
currentItemBounds.from >= previousItemBounds.from &&
currentItemBounds.to <= previousItemBounds.to
) {
newPreviousText =
text.slice(previousItemBounds.from, currentItemBounds.from) +
text.slice(currentItemBounds.to, previousItemBounds.to);
}
const newText =
ensureNewLine(text.slice(currentItemBounds.from, currentItemBounds.to)) +
newPreviousText;
const newCursorPos = (cursorPos - currentItemBounds.from) +
previousItemBounds.from;
// console.log("New replacement text", newText);
await editor.dispatch({
changes: [
{
from: Math.min(previousItemBounds.from, currentItemBounds.from),
to: Math.max(currentItemBounds.to, previousItemBounds.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);
let nextItemBounds: ReturnType<typeof determineItemBounds> | undefined;
try {
nextItemBounds = determineItemBounds(
text,
currentItemBounds.to + 1,
currentItemBounds.indentLevel,
);
if (currentItemBounds.from === nextItemBounds.from) {
throw new Error("Already at the bottom");
}
} catch {
nextItemBounds = determineItemBounds(
text,
currentItemBounds.to + 1,
undefined,
false,
);
}
if (currentItemBounds.to === nextItemBounds.to) {
throw new Error("Already at the bottom");
}
const nextItemText = ensureNewLine(
text.slice(nextItemBounds.from, nextItemBounds.to),
);
// console.log("Next item text", nextItemText);
const newText = nextItemText +
text.slice(currentItemBounds.from, currentItemBounds.to);
const newCursorPos = (cursorPos - currentItemBounds.from) +
currentItemBounds.from + nextItemText.length;
await editor.dispatch({
changes: [
{
from: Math.min(nextItemBounds.from, currentItemBounds.from),
to: Math.max(nextItemBounds.to, currentItemBounds.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,
withChildren = true,
): { 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++;
}
if (!withChildren) {
// We're not interested in the children, so let's stop here
break;
}
// 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,
};
}
export async function foldCommand() {
await editor.fold();
}
export async function unfoldCommand() {
await editor.unfold();
}
export async function toggleFoldCommand() {
await editor.toggleFold();
}
export async function foldAllCommand() {
await editor.foldAll();
}
export async function unfoldAllCommand() {
await editor.unfoldAll();
}