312 lines
7.0 KiB
TypeScript
312 lines
7.0 KiB
TypeScript
const demoCode = `# Highlighting Demo
|
|
# This is a comment
|
|
|
|
# Hover a define id anywhere to see it's definition
|
|
define a_def 10
|
|
|
|
# Hover HASH("String")'s to see computed crc32
|
|
# hover here vvvvvvvvvvvvvvvv
|
|
define a_hash HASH("This is a String")
|
|
|
|
# hover over an alias anywhere in the code
|
|
# to see it's definition
|
|
alias a_var r0
|
|
alias a_device d0
|
|
|
|
# instructions have Auto Completion,
|
|
# numeric logic types are identified on hover
|
|
s db 12 0
|
|
# ^^
|
|
# hover here
|
|
|
|
# Enums and their values are Known, Hover them!
|
|
# vvvvvvvvvvvvvvvvvv
|
|
move r2 LogicType.Temperature
|
|
push r2
|
|
|
|
# same with constants
|
|
# vvvv
|
|
move r3 pinf
|
|
|
|
# Labels are known
|
|
main:
|
|
l r1 dr15 RatioWater
|
|
move r2 100000.001
|
|
push r2
|
|
|
|
# Hover Hash Strings of Known prefab names
|
|
# to get their documentation
|
|
# vvvvvvvvvvvvvvv
|
|
move r0 HASH("AccessCardBlack")
|
|
push r0
|
|
beqzal r1 test
|
|
|
|
# -2045627372 is the crc32 hash of a SolarPanel,
|
|
# hover it to see the documentation!
|
|
# vvvvvvvvvv
|
|
move r1 -2045627372
|
|
jal test
|
|
move r1 $FF
|
|
push r1
|
|
beqzal 0 test
|
|
move r1 %1000
|
|
push r1
|
|
yield
|
|
j main
|
|
|
|
test:
|
|
add r15 r15 1
|
|
j ra
|
|
|
|
`;
|
|
|
|
import type { ICError } from "ic10emu_wasm";
|
|
|
|
export class Session extends EventTarget {
|
|
_programs: Map<number, string>;
|
|
_errors: Map<number, ICError[]>;
|
|
_activeIC: number;
|
|
_activeLines: Map<number, number>;
|
|
_activeLine: number;
|
|
_save_timeout?: ReturnType<typeof setTimeout>;
|
|
constructor() {
|
|
super();
|
|
this._programs = new Map();
|
|
this._errors = new Map();
|
|
this._save_timeout = undefined;
|
|
this._activeIC = 0;
|
|
this._activeLines = new Map();
|
|
this.loadFromFragment();
|
|
|
|
const that = this;
|
|
window.addEventListener("hashchange", (_event) => {
|
|
that.loadFromFragment();
|
|
});
|
|
}
|
|
|
|
get programs() {
|
|
return this._programs;
|
|
}
|
|
|
|
set programs(programs) {
|
|
this._programs = new Map([...programs]);
|
|
this._fireOnLoad();
|
|
}
|
|
|
|
get activeIC() {
|
|
return this._activeIC;
|
|
}
|
|
|
|
set activeIC(val: number) {
|
|
this._activeIC = val;
|
|
this.dispatchEvent(
|
|
new CustomEvent("session-active-ic", { detail: this.activeIC }),
|
|
);
|
|
}
|
|
|
|
onActiveIc(callback: EventListenerOrEventListenerObject) {
|
|
this.addEventListener("session-active-ic", callback);
|
|
}
|
|
|
|
get errors() {
|
|
return this._errors;
|
|
}
|
|
|
|
getActiveLine(id: number) {
|
|
return this._activeLines.get(id);
|
|
}
|
|
|
|
setActiveLine(id: number, line: number) {
|
|
const last = this._activeLines.get(id);
|
|
if (last !== line) {
|
|
this._activeLines.set(id, line);
|
|
this._fireOnActiveLine(id);
|
|
}
|
|
}
|
|
|
|
set activeLine(line: number) {
|
|
this._activeLine = line;
|
|
}
|
|
|
|
setProgramCode(id: number, code: string) {
|
|
this._programs.set(id, code);
|
|
this.save();
|
|
}
|
|
|
|
setProgramErrors(id: number, errors: ICError[]) {
|
|
this._errors.set(id, errors);
|
|
this._fireOnErrors([id]);
|
|
}
|
|
|
|
_fireOnErrors(ids: number[]) {
|
|
this.dispatchEvent(
|
|
new CustomEvent("session-errors", {
|
|
detail: ids,
|
|
}),
|
|
);
|
|
}
|
|
|
|
onErrors(callback: EventListenerOrEventListenerObject) {
|
|
this.addEventListener("session-errors", callback);
|
|
}
|
|
|
|
onLoad(callback: EventListenerOrEventListenerObject) {
|
|
this.addEventListener("session-load", callback);
|
|
}
|
|
|
|
_fireOnLoad() {
|
|
this.dispatchEvent(
|
|
new CustomEvent("session-load", {
|
|
detail: this,
|
|
}),
|
|
);
|
|
}
|
|
|
|
onActiveLine(callback: EventListenerOrEventListenerObject) {
|
|
this.addEventListener("active-line", callback);
|
|
}
|
|
|
|
_fireOnActiveLine(id: number) {
|
|
this.dispatchEvent(
|
|
new CustomEvent("active-line", {
|
|
detail: id,
|
|
}),
|
|
);
|
|
}
|
|
|
|
save() {
|
|
if (this._save_timeout) clearTimeout(this._save_timeout);
|
|
this._save_timeout = setTimeout(() => {
|
|
this.saveToFragment();
|
|
if (window.App!.vm) {
|
|
window.App!.vm.updateCode();
|
|
}
|
|
this._save_timeout = undefined;
|
|
}, 1000);
|
|
}
|
|
|
|
async saveToFragment() {
|
|
const toSave = { programs: Array.from(this._programs) };
|
|
const bytes = new TextEncoder().encode(JSON.stringify(toSave));
|
|
try {
|
|
const c_bytes = await compress(bytes);
|
|
const fragment = base64url_encode(c_bytes);
|
|
window.history.replaceState(null, "", `#${fragment}`);
|
|
} catch (e) {
|
|
console.log("Error compressing content fragment:", e);
|
|
return;
|
|
}
|
|
}
|
|
|
|
async loadFromFragment() {
|
|
const fragment = window.location.hash.slice(1);
|
|
if (fragment === "demo") {
|
|
this._programs = new Map([[0, demoCode]]);
|
|
this._fireOnLoad();
|
|
return;
|
|
}
|
|
if (fragment.length > 0) {
|
|
const c_bytes = base64url_decode(fragment);
|
|
const bytes = await decompressFragment(c_bytes);
|
|
if (bytes !== null) {
|
|
const txt = new TextDecoder().decode(bytes);
|
|
const data = getJson(txt);
|
|
if (data === null) {
|
|
// backwards compatible
|
|
this._programs = new Map([[0, txt]]);
|
|
this, this._fireOnLoad();
|
|
return;
|
|
}
|
|
try {
|
|
this._programs = new Map(data.programs);
|
|
this._fireOnLoad();
|
|
return;
|
|
} catch (e) {
|
|
console.log("Bad session data:", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
async function decompressFragment(c_bytes: ArrayBuffer) {
|
|
try {
|
|
const bytes = await decompress(c_bytes);
|
|
return bytes;
|
|
} catch (e) {
|
|
console.log("Error decompressing content fragment:", e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function getJson(value: any) {
|
|
try {
|
|
return JSON.parse(value);
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function* streamAsyncIterator(stream: ReadableStream) {
|
|
// Get a lock on the stream
|
|
const reader = stream.getReader();
|
|
|
|
try {
|
|
while (true) {
|
|
// Read from the stream
|
|
const { done, value } = await reader.read();
|
|
if (done) return;
|
|
yield value;
|
|
}
|
|
} finally {
|
|
reader.releaseLock();
|
|
}
|
|
}
|
|
|
|
function base64url_encode(buffer: ArrayBuffer) {
|
|
return btoa(
|
|
Array.from(new Uint8Array(buffer), (b) => String.fromCharCode(b)).join(""),
|
|
)
|
|
.replace(/\+/g, "-")
|
|
.replace(/\//g, "_")
|
|
.replace(/=+$/, "");
|
|
}
|
|
|
|
function base64url_decode(value: string): ArrayBuffer {
|
|
const m = value.length % 4;
|
|
return Uint8Array.from(
|
|
atob(
|
|
value
|
|
.replace(/-/g, "+")
|
|
.replace(/_/g, "/")
|
|
.padEnd(value.length + (m === 0 ? 0 : 4 - m), "="),
|
|
),
|
|
(c) => c.charCodeAt(0),
|
|
).buffer;
|
|
}
|
|
|
|
async function concatUintArrays(arrays: Uint8Array[]) {
|
|
const blob = new Blob(arrays);
|
|
const buffer = await blob.arrayBuffer();
|
|
return new Uint8Array(buffer);
|
|
}
|
|
|
|
async function compress(bytes: ArrayBuffer) {
|
|
const s = new Blob([bytes]).stream();
|
|
const cs = s.pipeThrough(new CompressionStream("deflate-raw"));
|
|
const chunks: Uint8Array[] = [];
|
|
for await (const chunk of streamAsyncIterator(cs)) {
|
|
chunks.push(chunk);
|
|
}
|
|
return await concatUintArrays(chunks);
|
|
}
|
|
|
|
async function decompress(bytes: ArrayBuffer) {
|
|
const s = new Blob([bytes]).stream();
|
|
const ds = s.pipeThrough(new DecompressionStream("deflate-raw"));
|
|
const chunks: Uint8Array[] = [];
|
|
for await (const chunk of streamAsyncIterator(ds)) {
|
|
chunks.push(chunk);
|
|
}
|
|
return await concatUintArrays(chunks);
|
|
}
|