2023-05-05 14:59:52 -07:00
|
|
|
import { clearScreen, fillRect } from "./window.ts";
|
2023-05-09 08:19:20 -07:00
|
|
|
import { CHAR, font } from "./font.ts";
|
2023-05-08 21:39:08 -07:00
|
|
|
import { drawText, measureText } from "./builtins.ts";
|
2023-05-05 14:59:52 -07:00
|
|
|
import { COLOR } from "./colors.ts";
|
2023-05-06 14:49:46 -07:00
|
|
|
import { getCodeSheet, setSheet } from "./sheet.ts";
|
2023-05-06 10:54:27 -07:00
|
|
|
import { K, ctrlKeyDown, getKeyboardString, keyPressed, shiftKeyDown } from "./keyboard.ts";
|
|
|
|
import { clipboard, tokenize } from "./deps.ts";
|
2023-05-08 21:48:16 -07:00
|
|
|
import { getBuiltins } from "./runcode.ts";
|
2023-05-06 14:49:46 -07:00
|
|
|
import { page } from "./viewsheets.ts";
|
2023-05-08 22:20:58 -07:00
|
|
|
import { mouseDown, mouseHeld, mousePos } from "./mouse.ts";
|
2023-05-05 14:59:52 -07:00
|
|
|
|
2023-05-06 11:35:02 -07:00
|
|
|
const historyDebounceFrames = 20;
|
|
|
|
|
2023-05-08 21:39:08 -07:00
|
|
|
const fontHeight = font.height;
|
|
|
|
|
2023-05-08 22:13:01 -07:00
|
|
|
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",
|
2023-05-09 23:06:09 -07:00
|
|
|
"NaN",
|
|
|
|
"Infinity",
|
2023-05-08 22:13:01 -07:00
|
|
|
];
|
|
|
|
const operator = [
|
|
|
|
"&&",
|
|
|
|
"||",
|
|
|
|
"??",
|
|
|
|
"--",
|
|
|
|
"++",
|
|
|
|
".",
|
|
|
|
"?.",
|
|
|
|
"<",
|
|
|
|
"<=",
|
|
|
|
">",
|
|
|
|
">=",
|
|
|
|
"!=",
|
|
|
|
"!==",
|
|
|
|
"==",
|
|
|
|
"===",
|
|
|
|
"+",
|
|
|
|
"-",
|
|
|
|
"%",
|
|
|
|
"&",
|
|
|
|
"|",
|
|
|
|
"^",
|
|
|
|
"/",
|
|
|
|
"*",
|
|
|
|
"**",
|
|
|
|
"<<",
|
|
|
|
">>",
|
|
|
|
">>>",
|
|
|
|
"=",
|
|
|
|
"+=",
|
|
|
|
"-=",
|
|
|
|
"%=",
|
|
|
|
"&=",
|
|
|
|
"|=",
|
|
|
|
"^=",
|
|
|
|
"/=",
|
|
|
|
"*=",
|
|
|
|
"**=",
|
|
|
|
"<<=",
|
|
|
|
">>=",
|
|
|
|
">>>=",
|
|
|
|
"!",
|
|
|
|
"?",
|
|
|
|
"~",
|
|
|
|
"...",
|
|
|
|
];
|
|
|
|
const punctuation = [
|
|
|
|
"(",
|
|
|
|
")",
|
|
|
|
"[",
|
|
|
|
"]",
|
|
|
|
"{",
|
|
|
|
"}",
|
|
|
|
".",
|
|
|
|
":",
|
|
|
|
";",
|
|
|
|
",",
|
|
|
|
];
|
|
|
|
|
|
|
|
const builtinColor = COLOR.BLUE;
|
|
|
|
const keywordColor = COLOR.PURPLE;
|
|
|
|
const operatorColor = COLOR.CYAN;
|
|
|
|
const valueColor = COLOR.ORANGE;
|
|
|
|
const stringColor = COLOR.GREEN;
|
|
|
|
const regexColor = COLOR.PINK;
|
|
|
|
const punctuationColor = COLOR.LIGHTGRAY;
|
|
|
|
const commentColor = COLOR.DARKGREEN;
|
|
|
|
const identifierColor = COLOR.YELLOW;
|
|
|
|
const invalidColor = COLOR.RED;
|
|
|
|
|
|
|
|
const caretColor = COLOR.WHITE;
|
|
|
|
const selectionColor = COLOR.DARKBLUE;
|
|
|
|
|
|
|
|
const backgroundColor = COLOR.DARKERBLUE;
|
|
|
|
|
|
|
|
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,
|
|
|
|
}
|
|
|
|
|
2023-05-08 21:39:08 -07:00
|
|
|
const transformForCopy = (text: string) => {
|
2023-05-09 08:19:20 -07:00
|
|
|
text = text.replaceAll(CHAR.UP, "⬆️");
|
|
|
|
text = text.replaceAll(CHAR.LEFT, "⬅️");
|
|
|
|
text = text.replaceAll(CHAR.DOWN, "⬇️");
|
|
|
|
text = text.replaceAll(CHAR.RIGHT, "➡️");
|
2023-05-08 21:39:08 -07:00
|
|
|
return text;
|
|
|
|
}
|
|
|
|
|
|
|
|
const transformForPaste = (text: string) => {
|
|
|
|
let newstr = "";
|
2023-05-09 08:19:20 -07:00
|
|
|
text = text.replaceAll("⬆️", CHAR.UP);
|
|
|
|
text = text.replaceAll("⬅️", CHAR.LEFT);
|
|
|
|
text = text.replaceAll("⬇️", CHAR.DOWN);
|
|
|
|
text = text.replaceAll("➡️", CHAR.RIGHT);
|
2023-05-08 21:39:08 -07:00
|
|
|
for (const char of text) {
|
|
|
|
if (char in font.chars) {
|
|
|
|
newstr += char;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return newstr;
|
|
|
|
}
|
|
|
|
|
2023-05-05 14:59:52 -07:00
|
|
|
const state = {
|
2023-05-08 23:14:01 -07:00
|
|
|
doubleClickTimer: 0,
|
2023-05-08 22:13:01 -07:00
|
|
|
history: [] as Array<{code: string, anchor: number, focus: number}>,
|
2023-05-06 11:35:02 -07:00
|
|
|
historyDebounce: 0,
|
|
|
|
historyIndex: 0,
|
|
|
|
undo() {
|
2023-05-06 15:24:29 -07:00
|
|
|
console.log('undoing');
|
2023-05-06 11:35:02 -07:00
|
|
|
if (this.historyIndex === this.history.length && this.historyDebounce > 0) {
|
|
|
|
this.snapshot();
|
|
|
|
}
|
2023-05-08 21:48:16 -07:00
|
|
|
console.log('historyIndex', this.historyIndex);
|
2023-05-06 11:35:02 -07:00
|
|
|
if (this.historyIndex > 0) {
|
|
|
|
this.historyIndex -= 1;
|
|
|
|
const snap = this.history[this.historyIndex];
|
2023-05-08 21:48:16 -07:00
|
|
|
console.log('historyIndex', this.historyIndex);
|
2023-05-06 11:35:02 -07:00
|
|
|
this.code = snap.code;
|
|
|
|
this.setSelection(snap.anchor, snap.focus);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
redo() {
|
2023-05-06 15:24:29 -07:00
|
|
|
console.log('redoing');
|
2023-05-06 11:35:02 -07:00
|
|
|
if (this.historyIndex < this.history.length-1) {
|
|
|
|
this.historyIndex += 1;
|
|
|
|
const snap = this.history[this.historyIndex];
|
|
|
|
this.code = snap.code;
|
|
|
|
this.setSelection(snap.anchor, snap.focus);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
snapshot() {
|
2023-05-06 15:24:29 -07:00
|
|
|
const snap = {
|
2023-05-06 11:35:02 -07:00
|
|
|
code: this.code,
|
|
|
|
anchor: this.anchor,
|
|
|
|
focus: this.focus,
|
2023-05-06 15:24:29 -07:00
|
|
|
};
|
|
|
|
this.history.push(snap);
|
|
|
|
console.log('took snapshot', this.historyIndex, snap);
|
2023-05-06 11:35:02 -07:00
|
|
|
},
|
|
|
|
startSnapping() {
|
2023-05-06 15:24:29 -07:00
|
|
|
console.log('start snapping', this.historyIndex);
|
2023-05-08 22:13:01 -07:00
|
|
|
if (this.historyDebounce <= 0) {
|
|
|
|
this.historyIndex += 1;
|
|
|
|
}
|
2023-05-06 11:35:02 -07:00
|
|
|
if (this.history.length > this.historyIndex) {
|
|
|
|
this.history.length = this.historyIndex;
|
|
|
|
}
|
|
|
|
this.historyDebounce = historyDebounceFrames;
|
|
|
|
},
|
2023-05-08 23:14:01 -07:00
|
|
|
wordMode: false,
|
2023-05-05 14:59:52 -07:00
|
|
|
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;},
|
2023-05-08 21:39:08 -07:00
|
|
|
get focusPixelX() {return indexToRect(this.code, this.focus).x;},
|
|
|
|
get focusPixelY() {return indexToRect(this.code, this.focus).y;},
|
|
|
|
get anchorPixelX() {return indexToRect(this.code, this.anchor).x;},
|
|
|
|
get anchorPixelY() {return indexToRect(this.code, this.anchor).y;},
|
2023-05-05 14:59:52 -07:00
|
|
|
isCollapsed() {
|
|
|
|
return this.anchor === this.focus;
|
|
|
|
},
|
|
|
|
clampInRange(n: number) {
|
|
|
|
return Math.max(0, Math.min(n, this.code.length))
|
|
|
|
},
|
2023-05-08 23:14:01 -07:00
|
|
|
findNearestWordBoundaryLeft(index: number) {
|
|
|
|
if (index === this.code.length-1) {
|
|
|
|
return index;
|
|
|
|
}
|
|
|
|
const words1 = this.code.slice(0, index+1).split(/\b/g);
|
|
|
|
if (words1[words1.length-1].length === 1) {
|
|
|
|
return index;
|
|
|
|
}
|
|
|
|
const words = this.code.slice(0, index).split(/\b/g);
|
|
|
|
if (!words.length) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
return index-words[words.length-1].length;
|
|
|
|
},
|
|
|
|
findNearestWordBoundaryRight(index: number) {
|
|
|
|
if (index === 0) {
|
|
|
|
return index;
|
|
|
|
}
|
|
|
|
const words1 = this.code.slice(index-1).split(/\b/g);
|
|
|
|
if (words1[0].length === 1) {
|
|
|
|
return index;
|
|
|
|
}
|
|
|
|
const words = this.code.slice(index).split(/\b/g);
|
|
|
|
if (!words.length) {
|
|
|
|
return this.code.length;
|
|
|
|
}
|
|
|
|
return index+words[0].length;
|
|
|
|
},
|
2023-05-05 14:59:52 -07:00
|
|
|
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);
|
|
|
|
}
|
2023-05-08 23:14:01 -07:00
|
|
|
this.anchor = this.clampInRange(anchor);
|
2023-05-05 14:59:52 -07:00
|
|
|
this.focus = this.clampInRange(focus);
|
2023-05-08 23:14:01 -07:00
|
|
|
if (this.wordMode) {
|
|
|
|
console.log('word mode', this.anchor, this.focus, this.findNearestWordBoundaryLeft(this.anchor), this.findNearestWordBoundaryRight(this.focus));
|
|
|
|
if (this.anchor <= this.focus) {
|
|
|
|
this.anchor = this.findNearestWordBoundaryLeft(this.anchor);
|
|
|
|
this.focus = this.findNearestWordBoundaryRight(this.focus);
|
|
|
|
} else {
|
|
|
|
this.anchor = this.findNearestWordBoundaryRight(this.anchor);
|
|
|
|
this.focus = this.findNearestWordBoundaryLeft(this.focus);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.anchor = this.clampInRange(this.anchor);
|
|
|
|
this.focus = this.clampInRange(this.focus);
|
2023-05-05 14:59:52 -07:00
|
|
|
},
|
|
|
|
setFocus(focus: number | {x: number, y: number}) {
|
|
|
|
if (typeof focus !== "number") {
|
|
|
|
focus = gridToIndex(this.code, focus.x, focus.y);
|
|
|
|
}
|
|
|
|
this.focus = this.clampInRange(focus);
|
2023-05-08 23:14:01 -07:00
|
|
|
if (this.wordMode) {
|
|
|
|
if (this.anchor <= this.focus) {
|
|
|
|
this.anchor = this.findNearestWordBoundaryLeft(this.anchor);
|
|
|
|
this.focus = this.findNearestWordBoundaryRight(this.focus);
|
|
|
|
} else {
|
|
|
|
this.anchor = this.findNearestWordBoundaryRight(this.anchor);
|
|
|
|
this.focus = this.findNearestWordBoundaryLeft(this.focus);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.focus = this.clampInRange(this.focus);
|
2023-05-05 14:59:52 -07:00
|
|
|
},
|
|
|
|
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);
|
2023-05-06 11:35:02 -07:00
|
|
|
this.startSnapping();
|
2023-05-05 14:59:52 -07:00
|
|
|
},
|
2023-05-06 12:18:37 -07:00
|
|
|
toggleComment() {
|
|
|
|
const lines = this.code.split("\n");
|
|
|
|
const {focusX, focusY, anchorX, anchorY} = this;
|
|
|
|
const lineInSelection = (i: number) => i >= Math.min(focusY, anchorY) && i <= Math.max(focusY, anchorY);
|
|
|
|
const allLinesAreCommented = lines.every((line, i) => {
|
|
|
|
if (lineInSelection(i) && !line.trim().startsWith("// ")) {
|
|
|
|
return false;
|
|
|
|
} else {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
const newLines = lines.map((line, i) => {
|
|
|
|
if (lineInSelection(i)) {
|
|
|
|
if (allLinesAreCommented) {
|
|
|
|
return line.slice(3);
|
|
|
|
} else {
|
|
|
|
return "// "+line;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return line;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.code = newLines.join("\n");
|
|
|
|
const shiftBy = allLinesAreCommented ? -3 : 3;
|
|
|
|
this.setSelection({x: anchorX+shiftBy, y: anchorY}, {x: focusX+shiftBy, y: focusY});
|
|
|
|
this.startSnapping();
|
|
|
|
},
|
2023-05-05 14:59:52 -07:00
|
|
|
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});
|
2023-05-06 11:35:02 -07:00
|
|
|
this.startSnapping();
|
2023-05-05 14:59:52 -07:00
|
|
|
},
|
|
|
|
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});
|
2023-05-06 11:35:02 -07:00
|
|
|
this.startSnapping();
|
2023-05-05 14:59:52 -07:00
|
|
|
},
|
|
|
|
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);
|
2023-05-06 11:35:02 -07:00
|
|
|
this.startSnapping();
|
2023-05-05 14:59:52 -07:00
|
|
|
}
|
|
|
|
} 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);
|
2023-05-06 11:35:02 -07:00
|
|
|
this.startSnapping();
|
2023-05-05 14:59:52 -07:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.insertText("");
|
|
|
|
}
|
|
|
|
},
|
2023-05-06 10:54:27 -07:00
|
|
|
async copy() {
|
|
|
|
const {code, anchor, focus} = this;
|
|
|
|
const selected = code.slice(Math.min(anchor,focus), Math.max(anchor,focus));
|
2023-05-08 21:39:08 -07:00
|
|
|
await clipboard.writeText(transformForCopy(selected));
|
2023-05-06 10:54:27 -07:00
|
|
|
},
|
|
|
|
async cut() {
|
|
|
|
await this.copy();
|
|
|
|
this.insertText("");
|
|
|
|
},
|
|
|
|
async paste() {
|
2023-05-08 21:39:08 -07:00
|
|
|
this.insertText(transformForPaste(await clipboard.readText()));
|
2023-05-06 10:54:27 -07:00
|
|
|
},
|
2023-05-05 20:08:54 -07:00
|
|
|
scrollToCursor() {
|
2023-05-08 21:39:08 -07:00
|
|
|
const {focusY, scrollY, scrollX, focus} = this;
|
2023-05-05 20:08:54 -07:00
|
|
|
const fh = fontHeight + 1;
|
2023-05-08 21:39:08 -07:00
|
|
|
const rect = indexToRect(this.code, focus);
|
2023-05-05 20:08:54 -07:00
|
|
|
if (focusY*fh < scrollY) {
|
|
|
|
this.scrollY = focusY*fh;
|
|
|
|
}
|
|
|
|
if (focusY*fh > scrollY+112-fh) {
|
|
|
|
this.scrollY = focusY*fh-112+fh;
|
|
|
|
}
|
2023-05-08 21:39:08 -07:00
|
|
|
if (rect.x < scrollX) {
|
|
|
|
this.scrollX = rect.x;
|
2023-05-05 20:08:54 -07:00
|
|
|
}
|
2023-05-08 21:39:08 -07:00
|
|
|
if (rect.x+rect.w > scrollX+128) {
|
|
|
|
this.scrollX = rect.x-128+rect.w+1;
|
2023-05-05 20:08:54 -07:00
|
|
|
}
|
|
|
|
},
|
2023-05-06 11:45:59 -07:00
|
|
|
currentIndentation() {
|
|
|
|
const lines = this.code.slice(0, this.focus).split("\n");
|
|
|
|
const line = lines[lines.length-1];
|
|
|
|
const match = line.match(/^\s*/);
|
|
|
|
if (!match) {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
return match[0];
|
|
|
|
},
|
2023-05-05 14:59:52 -07:00
|
|
|
get code() {
|
2023-05-06 14:49:46 -07:00
|
|
|
return getCodeSheet(page.activeSheet);
|
2023-05-05 14:59:52 -07:00
|
|
|
},
|
|
|
|
set code(val) {
|
2023-05-06 14:49:46 -07:00
|
|
|
setSheet(page.activeSheet, "code", val);
|
2023-05-05 14:59:52 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2023-05-07 14:05:52 -07:00
|
|
|
return lines.slice(0, y).join("\n").length+Math.min(x, lines[y].length)+(y === 0 ? 0 : 1);
|
2023-05-05 14:59:52 -07:00
|
|
|
}
|
|
|
|
|
2023-05-08 21:39:08 -07:00
|
|
|
const indexToRect = (str: string, index: number) => {
|
|
|
|
const linesUpTo = str.slice(0,index).split("\n");
|
|
|
|
let extra = 0;
|
|
|
|
if (linesUpTo[linesUpTo.length-1].length > 0) {
|
|
|
|
extra = 1;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
x: measureText(linesUpTo[linesUpTo.length-1]) + extra,
|
|
|
|
y: (fontHeight + 1)*(linesUpTo.length - 1),
|
|
|
|
w: measureText(str[index] ?? "\n"),
|
|
|
|
h: fontHeight+1,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const pixelToIndex = (str: string, x: number, y: number) => {
|
|
|
|
const lines = str.split("\n");
|
|
|
|
if (y < 0) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
if (y >= (fontHeight+1)*lines.length) {
|
|
|
|
return str.length;
|
|
|
|
}
|
|
|
|
const yy = Math.floor(y/(fontHeight+1));
|
|
|
|
const prefix = lines.slice(0, yy).join("\n").length+(yy === 0 ? 0 : 1);
|
|
|
|
const line = lines[yy];
|
|
|
|
let j = 0;
|
2023-05-08 22:20:58 -07:00
|
|
|
while (measureText(line.slice(0, j)) < x && j < line.length) {
|
2023-05-08 21:39:08 -07:00
|
|
|
j+=1;
|
|
|
|
}
|
2023-05-08 22:20:58 -07:00
|
|
|
if (measureText(line) < x) {
|
|
|
|
j+=1;
|
|
|
|
}
|
|
|
|
return prefix + Math.max(0, j-1);
|
2023-05-08 21:39:08 -07:00
|
|
|
}
|
|
|
|
|
2023-05-06 10:54:27 -07:00
|
|
|
const update = async () => {
|
2023-05-08 21:39:08 -07:00
|
|
|
const { focus } = state;
|
2023-05-08 22:13:01 -07:00
|
|
|
if (state.history.length === 0) {
|
|
|
|
state.snapshot();
|
|
|
|
}
|
2023-05-06 11:35:02 -07:00
|
|
|
if (state.historyDebounce > 0) {
|
|
|
|
state.historyDebounce -= 1;
|
|
|
|
if (state.historyDebounce <= 0) {
|
|
|
|
state.snapshot();
|
|
|
|
}
|
|
|
|
}
|
2023-05-08 23:14:01 -07:00
|
|
|
if (state.doubleClickTimer > 0) {
|
|
|
|
state.doubleClickTimer -= 1;
|
|
|
|
}
|
2023-05-06 11:35:02 -07:00
|
|
|
|
2023-05-08 23:14:01 -07:00
|
|
|
if (mouseDown() && !shiftKeyDown()) {
|
|
|
|
if (state.doubleClickTimer > 0) {
|
|
|
|
state.wordMode = true;
|
|
|
|
} else {
|
|
|
|
state.doubleClickTimer = 10;
|
|
|
|
}
|
2023-05-08 22:20:58 -07:00
|
|
|
const {x, y} = mousePos();
|
2023-05-09 08:21:34 -07:00
|
|
|
state.setSelection(pixelToIndex(state.code, x+state.scrollX, y+state.scrollY-8));
|
|
|
|
state.scrollToCursor();
|
2023-05-08 22:20:58 -07:00
|
|
|
} else if (mouseHeld()) {
|
|
|
|
const {x, y} = mousePos();
|
2023-05-09 08:21:34 -07:00
|
|
|
state.setFocus(pixelToIndex(state.code, x+state.scrollX, y+state.scrollY-8));
|
|
|
|
state.scrollToCursor();
|
2023-05-08 23:14:01 -07:00
|
|
|
} else {
|
|
|
|
state.wordMode = false;
|
2023-05-08 22:20:58 -07:00
|
|
|
}
|
|
|
|
|
2023-05-05 14:59:52 -07:00
|
|
|
const keyboardString = getKeyboardString();
|
|
|
|
if (keyboardString) {
|
|
|
|
state.insertText(keyboardString);
|
2023-05-05 20:08:54 -07:00
|
|
|
state.scrollToCursor();
|
2023-05-05 14:59:52 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (keyPressed(K.ENTER)) {
|
2023-05-06 11:45:59 -07:00
|
|
|
state.insertText("\n"+state.currentIndentation());
|
2023-05-05 20:08:54 -07:00
|
|
|
state.scrollToCursor();
|
2023-05-05 14:59:52 -07:00
|
|
|
}
|
|
|
|
if (keyPressed(K.TAB)) {
|
|
|
|
if (!shiftKeyDown()) {
|
|
|
|
if (state.isCollapsed()) {
|
|
|
|
state.insertText("\t");
|
|
|
|
} else {
|
|
|
|
state.indent("\t");
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
state.outdent(/^(\t| )/);
|
|
|
|
}
|
2023-05-05 20:08:54 -07:00
|
|
|
state.scrollToCursor();
|
2023-05-05 14:59:52 -07:00
|
|
|
}
|
|
|
|
if (keyPressed(K.BACKSPACE)) {
|
|
|
|
state.backspace();
|
2023-05-05 20:08:54 -07:00
|
|
|
state.scrollToCursor();
|
2023-05-05 14:59:52 -07:00
|
|
|
}
|
|
|
|
if (keyPressed(K.DELETE)) {
|
|
|
|
state.delete();
|
2023-05-05 20:08:54 -07:00
|
|
|
state.scrollToCursor();
|
2023-05-05 14:59:52 -07:00
|
|
|
}
|
|
|
|
if (keyPressed(K.ARROW_RIGHT)) {
|
|
|
|
if (shiftKeyDown()) {
|
|
|
|
state.setFocus(focus+1);
|
|
|
|
} else {
|
|
|
|
state.setSelection(focus+1);
|
|
|
|
}
|
2023-05-05 20:08:54 -07:00
|
|
|
state.scrollToCursor();
|
2023-05-05 14:59:52 -07:00
|
|
|
}
|
|
|
|
if (keyPressed(K.ARROW_LEFT)) {
|
|
|
|
if (shiftKeyDown()) {
|
|
|
|
state.setFocus(focus-1);
|
|
|
|
} else {
|
|
|
|
state.setSelection(focus-1);
|
|
|
|
}
|
2023-05-05 20:08:54 -07:00
|
|
|
state.scrollToCursor();
|
2023-05-05 14:59:52 -07:00
|
|
|
}
|
|
|
|
if (keyPressed(K.ARROW_DOWN)) {
|
2023-05-08 21:39:08 -07:00
|
|
|
const rect = indexToRect(state.code, focus);
|
|
|
|
const newIndex = pixelToIndex(state.code, rect.x, rect.y+rect.h+1+1);
|
2023-05-05 14:59:52 -07:00
|
|
|
if (shiftKeyDown()) {
|
2023-05-08 21:39:08 -07:00
|
|
|
state.setFocus(newIndex);
|
2023-05-05 14:59:52 -07:00
|
|
|
} else {
|
2023-05-08 21:39:08 -07:00
|
|
|
state.setSelection(newIndex);
|
2023-05-05 14:59:52 -07:00
|
|
|
}
|
2023-05-05 20:08:54 -07:00
|
|
|
state.scrollToCursor();
|
2023-05-05 14:59:52 -07:00
|
|
|
}
|
|
|
|
if (keyPressed(K.ARROW_UP)) {
|
2023-05-08 21:39:08 -07:00
|
|
|
const rect = indexToRect(state.code, focus);
|
|
|
|
const newIndex = pixelToIndex(state.code, rect.x, rect.y-1-1);
|
2023-05-05 14:59:52 -07:00
|
|
|
if (shiftKeyDown()) {
|
2023-05-08 21:39:08 -07:00
|
|
|
state.setFocus(newIndex);
|
2023-05-05 14:59:52 -07:00
|
|
|
} else {
|
2023-05-08 21:39:08 -07:00
|
|
|
state.setSelection(newIndex);
|
2023-05-05 14:59:52 -07:00
|
|
|
}
|
2023-05-05 20:08:54 -07:00
|
|
|
state.scrollToCursor();
|
2023-05-05 14:59:52 -07:00
|
|
|
}
|
2023-05-06 10:54:27 -07:00
|
|
|
if (keyPressed("C") && ctrlKeyDown()) {
|
|
|
|
await state.copy();
|
2023-05-06 11:35:02 -07:00
|
|
|
state.scrollToCursor();
|
2023-05-06 10:54:27 -07:00
|
|
|
}
|
|
|
|
if (keyPressed("X") && ctrlKeyDown()) {
|
|
|
|
await state.cut();
|
2023-05-06 11:35:02 -07:00
|
|
|
state.scrollToCursor();
|
2023-05-06 10:54:27 -07:00
|
|
|
}
|
|
|
|
if (keyPressed("V") && ctrlKeyDown()) {
|
|
|
|
await state.paste();
|
2023-05-06 11:35:02 -07:00
|
|
|
state.scrollToCursor();
|
|
|
|
}
|
|
|
|
if (keyPressed("Z") && ctrlKeyDown()) {
|
|
|
|
if (shiftKeyDown()) {
|
|
|
|
state.redo();
|
|
|
|
} else {
|
|
|
|
state.undo();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (keyPressed("Y") && ctrlKeyDown()) {
|
|
|
|
state.redo();
|
2023-05-06 10:54:27 -07:00
|
|
|
}
|
2023-05-06 12:18:37 -07:00
|
|
|
if (keyPressed("/") && ctrlKeyDown()) {
|
|
|
|
state.toggleComment();
|
|
|
|
}
|
2023-05-05 14:59:52 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const draw = () => {
|
|
|
|
clearScreen();
|
2023-05-08 22:13:01 -07:00
|
|
|
const {
|
|
|
|
scrollX,
|
|
|
|
scrollY,
|
|
|
|
anchor,
|
|
|
|
focus,
|
|
|
|
code,
|
|
|
|
} = state;
|
|
|
|
const x = 0;
|
|
|
|
const y = 8;
|
|
|
|
const w = 128;
|
|
|
|
const h = 112;
|
|
|
|
fillRect(x, y, w, h, backgroundColor);
|
|
|
|
if (anchor !== focus) {
|
|
|
|
for (let i = Math.min(anchor, focus); i < Math.max(anchor, focus); i++) {
|
|
|
|
const sel = indexToRect(code, i);
|
|
|
|
fillRect(x+sel.x-scrollX, y+sel.y-scrollY, sel.w+2, sel.h, selectionColor);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const rect = indexToRect(code, focus);
|
|
|
|
fillRect(x+rect.x-scrollX, y+rect.y-scrollY, 1, rect.h, caretColor);
|
|
|
|
|
|
|
|
const builtins = Object.keys(getBuiltins());
|
|
|
|
const tokens = [...tokenize(code)];
|
|
|
|
let cx = 0;
|
|
|
|
let cy = 0;
|
|
|
|
tokens.forEach((token) => {
|
|
|
|
if (token.type === "LineTerminatorSequence") {
|
|
|
|
cx=0;
|
|
|
|
cy+=fontHeight+1;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const lines = token.value.split("\n");
|
|
|
|
lines.forEach((line, i) => {
|
|
|
|
let color = tokenColors[token.type];
|
2023-05-09 23:06:09 -07:00
|
|
|
if (builtins.includes(token.value)) {
|
|
|
|
color = builtinColor;
|
|
|
|
}
|
2023-05-08 22:13:01 -07:00
|
|
|
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(1+x+cx-scrollX, 1+y+cy-scrollY, line, color);
|
|
|
|
if (i === lines.length-1) {
|
|
|
|
cx += measureText(line)+1;
|
|
|
|
} else {
|
|
|
|
cx=0;
|
|
|
|
cy+=fontHeight+1;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
})
|
2023-05-05 14:59:52 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export const codetab = {
|
|
|
|
update,
|
|
|
|
draw,
|
|
|
|
}
|