This commit is contained in:
Chris Wanstrath 2025-09-21 22:13:21 -07:00
parent b19c87b5d6
commit c397cda8af

View File

@ -5,16 +5,19 @@ import { focusInput } from "./focus.js"
const INDENT_SIZE = 2 const INDENT_SIZE = 2
export function initEditor() { export function initEditor() {
document.addEventListener("input", adjustHeight) document.addEventListener("input", handleAdjustHeight)
focusTextareaOnCreation() focusTextareaOnCreation()
} }
function adjustHeight(e: Event) { function handleAdjustHeight(e: Event) {
const target = e.target as HTMLElement const target = e.target as HTMLElement
if (target?.matches(".editor")) { if (target?.matches(".editor"))
target.style.height = "auto" adjustHeight(target as HTMLTextAreaElement)
target.style.height = target.scrollHeight + "px" }
}
function adjustHeight(editor: HTMLTextAreaElement) {
editor.style.height = "auto"
editor.style.height = editor.scrollHeight + "px"
} }
function focusTextareaOnCreation() { function focusTextareaOnCreation() {
@ -33,25 +36,75 @@ function focusTextareaOnCreation() {
} }
function keydownHandler(e: KeyboardEvent) { function keydownHandler(e: KeyboardEvent) {
const target = e.target as HTMLTextAreaElement const editor = e.target as HTMLTextAreaElement
if (e.key === "Tab") { if (e.key === "Tab") {
e.preventDefault() e.preventDefault()
if (e.shiftKey) if (e.shiftKey)
removeTab(target) removeTab(editor)
else else
insertTab(target) insertTab(editor)
} else if (e.ctrlKey && e.key === "c") { } else if (e.ctrlKey && e.key === "c") {
focusInput() focusInput()
} else if ((e.ctrlKey && e.key === "s") || (e.ctrlKey && e.key === "Enter")) { } else if ((e.ctrlKey && e.key === "s") || (e.ctrlKey && e.key === "Enter")) {
e.preventDefault() e.preventDefault()
send({ send({
id: target.dataset.path, id: editor.dataset.path,
type: "save-file", type: "save-file",
data: target.value 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) { function insertTab(editor: HTMLTextAreaElement) {
const start = editor.selectionStart const start = editor.selectionStart
const end = editor.selectionEnd const end = editor.selectionEnd
@ -73,3 +126,83 @@ function removeTab(editor: HTMLTextAreaElement) {
editor.selectionEnd = end - 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)
}