@dodona/papyros
Version:
Scratchpad for multiple programming languages in the browser.
239 lines • 9.67 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { State, stateProperty } from "@dodona/lit-state";
import { BackendManager } from "../../communication/BackendManager";
import { BackendEventType } from "../../communication/BackendEvent";
import { isValidFileName, parseData } from "../../util/Util";
import { RunState } from "./Runner";
import { ServiceWorkerInputError } from "./PapyrosErrors";
export var OutputType;
(function (OutputType) {
OutputType["stdout"] = "stdout";
OutputType["stderr"] = "stderr";
OutputType["img"] = "img";
OutputType["turtle"] = "turtle";
})(OutputType || (OutputType = {}));
export var InputMode;
(function (InputMode) {
InputMode["batch"] = "batch";
InputMode["interactive"] = "interactive";
})(InputMode || (InputMode = {}));
export const CODE_TAB = "code";
export const OUTPUT_TAB = "output";
export const TURTLE_TAB = "turtle";
export function parseFileEntries(data, contentType) {
const parsed = parseData(data, contentType);
return Object.entries(parsed).map(([name, { content, binary }]) => ({ name, content, binary }));
}
export class InputOutput extends State {
get inputBuffer() {
return this._inputBuffer;
}
set inputBuffer(value) {
this._inputBuffer = value;
if (this.nextBufferedLine !== undefined && this.inputMode === InputMode.batch && this.awaitingInput) {
this.provideInput(this.nextBufferedLine);
}
if (!this.papyros.debugger.active && this.papyros.runner.state === RunState.Ready) {
this.clearInputs();
}
}
get nextBufferedLine() {
const bufferedLines = this.inputBuffer.split("\n").slice(0, -1);
if (bufferedLines.length > this.inputs.length) {
return bufferedLines[this.inputs.length];
}
return undefined;
}
constructor(papyros) {
super();
this.inputs = [];
this.output = [];
this.hasTurtleOutput = false;
this.files = [];
this.activeEditorTab = CODE_TAB;
this.activeOutputTab = OUTPUT_TAB;
this.outputTabManuallySet = false;
this.prompt = "";
this.awaitingInput = false;
this.inputMode = InputMode.interactive;
this._inputBuffer = "";
this.papyros = papyros;
this.reset();
BackendManager.subscribe(BackendEventType.Start, () => this.reset());
BackendManager.subscribe(BackendEventType.Output, (e) => {
const data = parseData(e.data, e.contentType);
if (e.contentType && e.contentType.startsWith("image/")) {
this.logImage(data, e.contentType);
}
else {
this.logOutput(data);
}
});
BackendManager.subscribe(BackendEventType.Turtle, (e) => {
const data = parseData(e.data, e.contentType);
this.logTurtle(data, e.contentType);
});
BackendManager.subscribe(BackendEventType.Error, (e) => {
const data = parseData(e.data, e.contentType);
this.logError(data);
});
BackendManager.subscribe(BackendEventType.Input, (e) => {
if (this.nextBufferedLine !== undefined && this.inputMode === InputMode.batch) {
this.provideInput(this.nextBufferedLine);
return;
}
this.prompt = e.data || "";
this.awaitingInput = true;
});
BackendManager.subscribe(BackendEventType.End, () => {
this.awaitingInput = false;
// If the finished run produced no turtle output, drop the (stale) Turtle tab
// selection so the tab bar hides. A manual selection is preserved.
if (this.activeOutputTab === TURTLE_TAB && !this.hasTurtleOutput && !this.outputTabManuallySet) {
this.activeOutputTab = OUTPUT_TAB;
}
});
BackendManager.subscribe(BackendEventType.Files, (e) => {
this.files = parseFileEntries(e.data, e.contentType);
});
}
logError(error) {
var _a;
if (typeof error === "string") {
if (error.includes("service worker for reading input")) {
this.papyros.errorHandler(new ServiceWorkerInputError("Service worker for reading input was not available", { cause: error }));
}
}
else if ((_a = error.traceback) === null || _a === void 0 ? void 0 : _a.includes("service worker for reading input")) {
this.papyros.errorHandler(new ServiceWorkerInputError("Service worker for reading input was not available", { cause: error }));
}
this.output = [...this.output, { type: OutputType.stderr, content: error }];
}
logImage(imageData, contentType = "image/png") {
this.output = [...this.output, { type: OutputType.img, content: imageData, contentType }];
}
logTurtle(imageData, contentType = "image/svg+xml;base64") {
const isFirstSnapshot = !this.hasTurtleOutput;
this.output = [...this.output, { type: OutputType.turtle, content: imageData, contentType }];
this.hasTurtleOutput = true;
if (isFirstSnapshot && this.activeOutputTab === OUTPUT_TAB && !this.outputTabManuallySet) {
this.activeOutputTab = TURTLE_TAB;
}
}
selectOutputTab(tab) {
this.activeOutputTab = tab;
this.outputTabManuallySet = true;
}
logOutput(output) {
// lines have been merged to limit the number of events
// we split them again here, to simplify overflow detection
const lines = output.split("\n");
if (lines.length > 1) {
this.output = [
...this.output,
...lines.slice(0, -1).map((line) => ({ type: OutputType.stdout, content: line + "\n" })),
{ type: OutputType.stdout, content: lines[lines.length - 1] },
];
}
else {
this.output = [...this.output, { type: OutputType.stdout, content: output }];
}
}
provideInput(input) {
this.inputs = [...this.inputs, input];
this.papyros.runner.provideInput(input);
this.prompt = "";
this.awaitingInput = false;
}
removeFile(name) {
if (this.activeEditorTab === name) {
this.activeEditorTab = CODE_TAB;
}
this.files = this.files.filter((f) => f.name !== name);
}
addFile(name, content = "", binary = false) {
if (!isValidFileName(name) || this.files.some((f) => f.name === name)) {
return false;
}
this.files = [...this.files, { name, content, binary }];
this.activeEditorTab = name;
return true;
}
updateFileContent(name, content, binary) {
this.files = this.files.map((f) => (f.name === name ? Object.assign(Object.assign({}, f), { content, binary }) : f));
}
renameFile(oldName, newName) {
if (!isValidFileName(newName) ||
newName === oldName ||
!this.files.some((f) => f.name === oldName) ||
this.files.some((f) => f.name === newName)) {
return false;
}
this.files = this.files.map((f) => (f.name === oldName ? Object.assign(Object.assign({}, f), { name: newName }) : f));
if (this.activeEditorTab === oldName) {
this.activeEditorTab = newName;
}
return true;
}
upsertFile(name, content, binary) {
if (!this.addFile(name, content, binary)) {
this.updateFileContent(name, content, binary);
}
}
clearInputs() {
this.inputs = [];
}
reset() {
this.inputs = [];
this.output = [];
this.hasTurtleOutput = false;
this.prompt = "";
this.awaitingInput = false;
this.activeEditorTab = CODE_TAB;
// activeOutputTab is intentionally preserved across reruns: resetting it would make
// the tab bar flicker (Turtle → Output → Turtle) as a turtle rerun progresses. It is
// pruned back to OUTPUT_TAB on End when no turtle output was produced.
}
}
__decorate([
stateProperty
], InputOutput.prototype, "inputs", void 0);
__decorate([
stateProperty
], InputOutput.prototype, "output", void 0);
__decorate([
stateProperty
], InputOutput.prototype, "hasTurtleOutput", void 0);
__decorate([
stateProperty
], InputOutput.prototype, "files", void 0);
__decorate([
stateProperty
], InputOutput.prototype, "activeEditorTab", void 0);
__decorate([
stateProperty
], InputOutput.prototype, "activeOutputTab", void 0);
__decorate([
stateProperty
], InputOutput.prototype, "outputTabManuallySet", void 0);
__decorate([
stateProperty
], InputOutput.prototype, "prompt", void 0);
__decorate([
stateProperty
], InputOutput.prototype, "awaitingInput", void 0);
__decorate([
stateProperty
], InputOutput.prototype, "inputMode", void 0);
__decorate([
stateProperty
], InputOutput.prototype, "_inputBuffer", void 0);
__decorate([
stateProperty
], InputOutput.prototype, "inputBuffer", null);
//# sourceMappingURL=InputOutput.js.map