import { clearScreen, fillRect } from "./window.ts"; import { fontWidth, fontHeight } from "./font.ts"; import { drawText } from "./builtins.ts"; import { COLOR } from "./colors.ts"; import {getSheet, setSheet} from "./sheet.ts"; import { K, getKeyboardString, keyPressed, shiftKeyDown } from "./keyboard.ts"; import { tokenize } from "./deps.ts"; const state = { scrollX: 0, scrollY: 0, anchor: 0, focus: 0, get focusX() {return indexToGrid(this.code, this.focus).x;}, get focusY() {return indexToGrid(this.code, this.focus).y;}, get anchorX() {return indexToGrid(this.code, this.anchor).x;}, get anchorY() {return indexToGrid(this.code, this.anchor).y;}, isCollapsed() { return this.anchor === this.focus; }, clampInRange(n: number) { return Math.max(0, Math.min(n, this.code.length)) }, setSelection(anchor: number | {x: number, y: number}, focus?: number | {x: number, y: number}) { if (typeof anchor !== "number") { anchor = gridToIndex(this.code, anchor.x, anchor.y); } focus = focus ?? anchor; if (typeof focus !== "number") { focus = gridToIndex(this.code, focus.x, focus.y); } this.anchor = this.clampInRange(anchor), this.focus = this.clampInRange(focus); }, setFocus(focus: number | {x: number, y: number}) { if (typeof focus !== "number") { focus = gridToIndex(this.code, focus.x, focus.y); } this.focus = this.clampInRange(focus); }, insertText(text: string) { const {code, anchor, focus} = this; this.code = code.slice(0, Math.min(anchor, focus)) + text + code.slice(Math.max(anchor, focus)); this.setSelection(Math.min(anchor, focus) + text.length); }, indent(indentString: string) { const lines = this.code.split("\n"); const {focusX, focusY, anchorX, anchorY} = this; const newLines = lines.map((line, i) => { if (i >= Math.min(focusY, anchorY) && i <= Math.max(focusY, anchorY)) { return indentString+line; } else { return line; } }); this.code = newLines.join("\n"); this.setSelection({x: anchorX+1, y: anchorY}, {x: focusX+1, y: focusY}); }, outdent(outdentRegex: RegExp) { const lines = this.code.split("\n"); const {focusX, focusY, anchorX, anchorY} = this; const newLines = lines.map((line, i) => { const match = line.match(outdentRegex); if (i >= Math.min(focusY, anchorY) && i <= Math.max(focusY, anchorY) && match) { return line.slice(match[0].length); } else { return line; } }); this.code = newLines.join("\n"); this.setSelection({x: Math.max(0,anchorX-1), y: anchorY}, {x: Math.max(0,focusX-1), y: focusY}); }, backspace() { const {code, focus} = this; if (this.isCollapsed()) { if (focus > 0) { this.code = code.slice(0, focus-1) + code.slice(focus); this.setSelection(focus-1); } } else { this.insertText(""); } }, delete() { const {code, focus} = this; if (this.isCollapsed()) { if (focus < code.length) { this.code = code.slice(0, focus) + code.slice(1+focus); } } else { this.insertText(""); } }, scrollToCursor() { const {focusY, focusX, scrollY, scrollX} = this; const fh = fontHeight + 1; const fw = fontWidth; if (focusY*fh < scrollY) { this.scrollY = focusY*fh; } if (focusY*fh > scrollY+112-fh) { this.scrollY = focusY*fh-112+fh; } if (focusX*fw < scrollX) { this.scrollX = focusX*fw; } if (focusX*fw > scrollX+128-fw) { this.scrollX = focusX*fw-128+fw; } }, get code() { const {sheet_type, value} = getSheet(0); if (sheet_type !== "code") { throw "Trying to run a non-code sheet as code." } return value; }, set code(val) { setSheet(0, "code", val); } } const indexToGrid = (str: string, index: number) => { const linesUpTo = str.slice(0,index).split("\n"); return { x: linesUpTo[linesUpTo.length-1].length, y: linesUpTo.length - 1, } } const gridToIndex = (str: string, x: number, y: number) => { const lines = str.split("\n"); if (y < 0) { return 0; } if (y >= lines.length) { return str.length; } return lines.slice(0, y).join("\n").length+Math.min(x, lines[y].length)+1; } const keywords = [ "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do", "else", "export", "extends", "finally", "for", "function", "if", "import", "in", "instanceof", "new", "return", "super", "switch", "this", "throw", "try", "typeof", "var", "void", "while", "with", "let", "static", "yield", "await", "enum", "implements", "interface", "package", "private", "protected", "public", "=>", ]; const values = [ "false", "null", "true", "undefined", ]; const operator = [ "&&", "||", "??", "--", "++", ".", "?.", "<", "<=", ">", ">=", "!=", "!==", "==", "===", "+", "-", "%", "&", "|", "^", "/", "*", "**", "<<", ">>", ">>>", "=", "+=", "-=", "%=", "&=", "|=", "^=", "/=", "*=", "**=", "<<=", ">>=", ">>>=", "!", "?", "~", "...", ]; const punctuation = [ "(", ")", "[", "]", "{", "}", ".", ":", ";", ",", ]; const keywordColor = COLOR.PURPLE; const operatorColor = COLOR.CYAN; const valueColor = COLOR.ORANGE; const stringColor = COLOR.GREEN; const regexColor = stringColor; const punctuationColor = COLOR.WHITE; const commentColor = COLOR.GRAY; const identifierColor = COLOR.LIGHTGRAY; const invalidColor = COLOR.RED; const tokenColors = { "StringLiteral": stringColor, "NoSubstitutionTemplate": stringColor, "TemplateHead": stringColor, "TemplateMiddle": stringColor, "TemplateTail": stringColor, "RegularExpressionLiteral": regexColor, "MultiLineComment": commentColor, "SingleLineComment": commentColor, "IdentifierName": identifierColor, "PrivateIdentifier": identifierColor, "NumericLiteral": valueColor, "Punctuator": punctuationColor, "WhiteSpace": punctuationColor, "LineTerminatorSequence": punctuationColor, "Invalid": invalidColor, } const drawCodeField = (code: string, x: number, y: number, w: number, h: number) => { const { scrollX, scrollY, anchor, focus, } = state; const { x: focusX, y: focusY, } = indexToGrid(code, focus); const { x: anchorX, y: anchorY, } = indexToGrid(code, anchor); fillRect(x, y, w, h, COLOR.DARKBLUE); if (anchor === focus) { fillRect(x+focusX*fontWidth-scrollX, y+focusY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.RED); } else { // TODO: Draw this selection better fillRect(x+anchorX*fontWidth-scrollX, y+anchorY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.GREEN); fillRect(x+focusX*fontWidth-scrollX, y+focusY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.YELLOW); } // TODO: Add syntax highlighting use "npm:js-tokens" maybe? const tokens = [...tokenize(code)]; let cx = 0; let cy = 0; tokens.forEach((token) => { const lines = token.value.split("\n"); lines.forEach((line, i) => { let color = tokenColors[token.type]; if (keywords.includes(token.value)) { color = keywordColor; } if (values.includes(token.value)) { color = valueColor; } if (operator.includes(token.value)) { color = operatorColor; } if (punctuation.includes(token.value)) { color = punctuationColor; } drawText(x+cx-scrollX, 1+y+cy-scrollY, line, color); if (i === lines.length-1) { cx += fontWidth*line.length; } else { cx=0; cy+=fontHeight+1; } }); }) } const update = () => { const { focus, focusX, focusY} = state; const keyboardString = getKeyboardString(); if (keyboardString) { state.insertText(keyboardString); state.scrollToCursor(); } // TODO: Handle ctrl-C, ctrl-V, ctrl-X, ctrl-Z // TODO: Make ctrl-/ do commenting out (take inspiration from tab) if (keyPressed(K.ENTER)) { // TODO: Make this play nicely with indentation state.insertText("\n"); state.scrollToCursor(); } if (keyPressed(K.TAB)) { if (!shiftKeyDown()) { if (state.isCollapsed()) { state.insertText("\t"); } else { state.indent("\t"); } } else { state.outdent(/^(\t| )/); } state.scrollToCursor(); } if (keyPressed(K.BACKSPACE)) { state.backspace(); state.scrollToCursor(); } if (keyPressed(K.DELETE)) { state.delete(); state.scrollToCursor(); } if (keyPressed(K.ARROW_RIGHT)) { if (shiftKeyDown()) { state.setFocus(focus+1); } else { state.setSelection(focus+1); } state.scrollToCursor(); } if (keyPressed(K.ARROW_LEFT)) { if (shiftKeyDown()) { state.setFocus(focus-1); } else { state.setSelection(focus-1); } state.scrollToCursor(); } if (keyPressed(K.ARROW_DOWN)) { if (shiftKeyDown()) { state.setFocus({x: focusX, y: focusY+1}); } else { state.setSelection({x: focusX, y: focusY+1}); } state.scrollToCursor(); } if (keyPressed(K.ARROW_UP)) { if (shiftKeyDown()) { state.setFocus({x: focusX, y: focusY-1}); } else { state.setSelection({x: focusX, y: focusY-1}); } state.scrollToCursor(); } } const draw = () => { clearScreen(); drawCodeField(state.code, 0, 8, 128, 112); } export const codetab = { update, draw, }