import { scrollback } from "./dom.js" import { send } from "./websocket.js" import { focusInput } from "./focus.js" const INDENT_SIZE = 2 export function initEditor() { document.addEventListener("input", handleAdjustHeight) focusTextareaOnCreation() } function handleAdjustHeight(e: Event) { const target = e.target as HTMLElement if (target?.matches(".editor")) adjustHeight(target as HTMLTextAreaElement) } function adjustHeight(editor: HTMLTextAreaElement) { editor.style.height = "auto" editor.style.height = editor.scrollHeight + "px" } function focusTextareaOnCreation() { const observer = new MutationObserver(mutations => { for (const m of mutations) for (const node of Array.from(m.addedNodes)) if (node instanceof HTMLElement && node.childNodes[0] instanceof HTMLElement && node.childNodes[0].matches("textarea")) { const editor = node.childNodes[0] editor.focus() editor.addEventListener("keydown", keydownHandler) return } }) observer.observe(scrollback, { childList: true }) } function keydownHandler(e: KeyboardEvent) { const editor = e.target as HTMLTextAreaElement if (e.key === "Tab") { e.preventDefault() if (e.shiftKey) removeTab(editor) else insertTab(editor) } else if (e.ctrlKey && e.key === "c") { focusInput() } else if ((e.ctrlKey && e.key === "s") || (e.ctrlKey && e.key === "Enter")) { e.preventDefault() send({ id: editor.dataset.path, type: "save-file", data: editor.value }) } else if (e.key === "{") { if (editor.selectionStart !== editor.selectionEnd) { insertAroundSelection(editor, '{', '}') e.preventDefault() } else { setTimeout(() => insertAfterCaret(editor, "}"), 0) } } else if (e.key === "}" && isNextChar(editor, '}')) { moveOneRight(editor) e.preventDefault() } else if (e.key === "[") { if (editor.selectionStart !== editor.selectionEnd) { insertAroundSelection(editor, '[', ']') e.preventDefault() } else { setTimeout(() => insertAfterCaret(editor, "]"), 0) } } else if (e.key === "]" && isNextChar(editor, ']')) { moveOneRight(editor) e.preventDefault() } else if (e.key === "(") { if (editor.selectionStart !== editor.selectionEnd) { insertAroundSelection(editor, '(', ')') e.preventDefault() } else { setTimeout(() => insertAfterCaret(editor, ")"), 0) } } else if (e.key === ")" && isNextChar(editor, ')')) { moveOneRight(editor) e.preventDefault() } else if (e.key === '"') { if (isNextChar(editor, '"')) { moveOneRight(editor) e.preventDefault() } else if (editor.selectionStart !== editor.selectionEnd) { insertAroundSelection(editor, '"', '"') e.preventDefault() } else { setTimeout(() => insertAfterCaret(editor, '"'), 0) } } else if (e.key === "Enter") { indentNewlineForBraces(e, editor) } } function moveOneRight(editor: HTMLTextAreaElement) { const pos = editor.selectionStart editor.selectionStart = editor.selectionEnd = pos + 1 } function insertTab(editor: HTMLTextAreaElement) { const start = editor.selectionStart const end = editor.selectionEnd const lineStart = editor.value.lastIndexOf("\n", start - 1) + 1 editor.value = editor.value.slice(0, lineStart) + " " + editor.value.slice(lineStart) editor.selectionStart = start + INDENT_SIZE editor.selectionEnd = end + INDENT_SIZE } function removeTab(editor: HTMLTextAreaElement) { const start = editor.selectionStart const end = editor.selectionEnd const lineStart = editor.value.lastIndexOf("\n", start - 1) + 1 if (editor.value.slice(lineStart, lineStart + 2) === " ") { editor.value = editor.value.slice(0, lineStart) + editor.value.slice(lineStart + 2) editor.selectionStart = start - INDENT_SIZE editor.selectionEnd = end - INDENT_SIZE } } function insertAfterCaret(editor: HTMLTextAreaElement, char: string) { const pos = editor.selectionStart editor.value = editor.value.slice(0, pos) + char + editor.value.slice(pos) editor.selectionStart = editor.selectionEnd = pos } function isNextChar(editor: HTMLTextAreaElement, char: string): boolean { return editor.value[editor.selectionStart] === char } function indentNewlineForBraces(e: KeyboardEvent, editor: HTMLTextAreaElement) { e.preventDefault() if (isBetween(editor, "{", "}") || isBetween(editor, "[", "]")) { setTimeout(() => insertMoreIndentedNewline(editor), 0) } else { setTimeout(() => insertIndentedNewline(editor), 0) } } function isBetween(editor: HTMLTextAreaElement, start: string, end: string): boolean { const pos = editor.selectionStart const val = editor.value if (pos <= 0 || pos >= val.length) return false return val[pos - 1] === start && val[pos] === end } function insertIndentedNewline(editor: HTMLTextAreaElement) { const pos = editor.selectionStart const before = editor.value.slice(0, pos) const prevLineStart = before.lastIndexOf("\n", pos - 1) + 1 const prevLine = before.slice(prevLineStart) let leading = 0 while (prevLine[leading] === " ") leading++ const indent = " ".repeat(leading) const insert = "\n" + indent editor.value = editor.value.slice(0, pos) + insert + editor.value.slice(pos) const newPos = pos + insert.length editor.selectionStart = editor.selectionEnd = newPos adjustHeight(editor) } function insertAroundSelection(editor: HTMLTextAreaElement, before: string, after: string) { const start = editor.selectionStart const end = editor.selectionEnd editor.value = editor.value.slice(0, start) + before + editor.value.slice(start, end) + after + editor.value.slice(end) } function insertMoreIndentedNewline(editor: HTMLTextAreaElement) { const pos = editor.selectionStart const before = editor.value.slice(0, pos) const prevLineStart = before.lastIndexOf("\n", pos - 1) + 1 const prevLine = before.slice(prevLineStart) let leading = 0 while (prevLine[leading] === " ") leading++ const oldIndent = " ".repeat(leading) const newIndent = " ".repeat(leading + INDENT_SIZE) const insert = "\n" + newIndent + "\n" + oldIndent editor.value = editor.value.slice(0, pos) + insert + editor.value.slice(pos) const newPos = pos + insert.length editor.selectionStart = editor.selectionEnd = newPos - 1 - oldIndent.length adjustHeight(editor) }