209 lines
6.3 KiB
TypeScript
209 lines
6.3 KiB
TypeScript
import { scrollback } from "./dom"
|
|
import { send } from "./websocket"
|
|
import { focusInput } from "./focus"
|
|
|
|
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)
|
|
}
|