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, }; }