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 | 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 | 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(); }