@dodona/papyros
Version:
Scratchpad for multiple programming languages in the browser.
544 lines • 28.3 kB
JavaScript
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
var useValue = arguments.length > 2;
for (var i = 0; i < initializers.length; i++) {
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
}
return useValue ? value : void 0;
};
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
var _, done = false;
for (var i = decorators.length - 1; i >= 0; i--) {
var context = {};
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
if (kind === "accessor") {
if (result === void 0) continue;
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
if (_ = accept(result.get)) descriptor.get = _;
if (_ = accept(result.set)) descriptor.set = _;
if (_ = accept(result.init)) initializers.unshift(_);
}
else if (_ = accept(result)) {
if (kind === "field") initializers.unshift(_);
else descriptor[key] = _;
}
}
if (target) Object.defineProperty(target, contextIn.name, descriptor);
done = true;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { proxy } from "comlink";
import { RunMode } from "./Backend";
import { BackendEventType } from "./BackendEvent";
import { BackendManager } from "./BackendManager";
import { CodeEditor } from "./editor/CodeEditor";
import { addPapyrosPrefix, APPLICATION_STATE_TEXT_ID, CODE_BUTTONS_WRAPPER_ID, DEFAULT_EDITOR_DELAY, RUN_BUTTONS_WRAPPER_ID, STATE_SPINNER_ID, STOP_BTN_ID } from "./Constants";
import { InputManager, InputMode } from "./InputManager";
import { renderSpinningCircle } from "./util/HTMLShapes";
import { addListener, downloadResults, getElement, parseData, t } from "./util/Util";
import { appendClasses, Renderable, renderButton, renderWithOptions } from "./util/Rendering";
import { OutputManager } from "./OutputManager";
import { Debugger } from "./Debugger";
const MODE_ICONS = {
"debug": "<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"18px\" viewBox=\"0 0 24 24\" width=\"18px\" fill=\"currentColor\"><path d=\"M19 7H16.19C15.74 6.2 15.12 5.5 14.37 5L16 3.41L14.59 2L12.42 4.17C11.96 4.06 11.5 4 11 4S10.05 4.06 9.59 4.17L7.41 2L6 3.41L7.62 5C6.87 5.5 6.26 6.21 5.81 7H3V9H5.09C5.03 9.33 5 9.66 5 10V11H3V13H5V14C5 14.34 5.03 14.67 5.09 15H3V17H5.81C7.26 19.5 10.28 20.61 13 19.65V19C13 18.43 13.09 17.86 13.25 17.31C12.59 17.76 11.8 18 11 18C8.79 18 7 16.21 7 14V10C7 7.79 8.79 6 11 6S15 7.79 15 10V14C15 14.19 15 14.39 14.95 14.58C15.54 14.04 16.24 13.62 17 13.35V13H19V11H17V10C17 9.66 16.97 9.33 16.91 9H19V7M13 9V11H9V9H13M13 13V15H9V13H13M17 16V22L22 19L17 16Z\"></path></svg>",
"doctest": "<i class=\"mdi mdi-play\"></i>",
"run": "<i class=\"mdi mdi-play\"></i>"
};
const DEBUG_STOP_ICON = "<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"18px\" viewBox=\"0 0 24 24\" width=\"18px\" fill=\"currentColor\"><path d=\"M19 7H16.19C15.74 6.2 15.12 5.5 14.37 5L16 3.41L14.59 2L12.42 4.17C11.96 4.06 11.5 4 11 4S10.05 4.06 9.59 4.17L7.41 2L6 3.41L7.62 5C6.87 5.5 6.26 6.21 5.81 7H3V9H5.09C5.03 9.33 5 9.66 5 10V11H3V13H5V14C5 14.34 5.03 14.67 5.09 15H3V17H5.81C7.26 19.5 10.28 20.61 13 19.65V19C13 18.43 13.09 17.86 13.25 17.31C12.59 17.76 11.8 18 11 18C8.79 18 7 16.21 7 14V10C7 7.79 8.79 6 11 6S15 7.79 15 10V14C15 14.19 15 14.39 14.95 14.58C15.54 14.04 16.24 13.62 17 13.35V13H19V11H17V10C17 9.66 16.97 9.33 16.91 9H19V7M13 9V11H9V9H13M13 13V15H9V13H13M16 16H22V22H16V16Z\" /></svg>";
/**
* Enum representing the possible states while processing code
*/
export var RunState;
(function (RunState) {
RunState["Loading"] = "loading";
RunState["Running"] = "running";
RunState["AwaitingInput"] = "awaiting_input";
RunState["Stopping"] = "stopping";
RunState["Ready"] = "ready";
})(RunState || (RunState = {}));
/**
* Helper class to avoid code duplication when handling buttons
* It is an ordered array that does not allow duplicate ids
*/
class ButtonArray extends Array {
add(button, onClick) {
this.remove(button.id);
this.push({
id: button.id,
buttonHTML: renderButton(button),
onClick
});
}
remove(id) {
const existingIndex = this.findIndex(b => b.id === id);
if (existingIndex !== -1) {
this.splice(existingIndex, 1);
}
}
}
/*
* class function decorator that adds a delay,
* so that the function is only called after the delay has passed
*
* If it is called again before the delay has passed, the previous call is cancelled
*/
function delay(delay) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return function (original, context) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => original.apply(this, args), delay);
};
};
}
/**
* Helper component to manage and visualize the current RunState
*/
let CodeRunner = (() => {
var _a;
let _classSuper = Renderable;
let _instanceExtraInitializers = [];
let _renderCodeActionButtons_decorators;
let _renderUserButtons_decorators;
return _a = class CodeRunner extends _classSuper {
/**
* Construct a new RunStateManager with the given listeners
* @param {ProgrammingLanguage} programmingLanguage The language to use
* @param {InputMode} inputMode The input mode to use
*/
constructor(programmingLanguage, inputMode) {
super();
/**
* The currently used programming language
*/
this.programmingLanguage = __runInitializers(this, _instanceExtraInitializers);
// True while running or viewing with debugger
this._debugMode = false;
this.programmingLanguage = programmingLanguage;
this.editor = new CodeEditor(() => {
if (this.state === RunState.Ready) {
this.runCode();
}
});
this.inputManager = new InputManager((input) => __awaiter(this, void 0, void 0, function* () {
const backend = yield this.backend;
backend.writeMessage(input);
this.setState(RunState.Running);
}), inputMode);
this.outputManager = new OutputManager();
this.backend = Promise.resolve({});
this.userButtons = new ButtonArray();
this.runButtons = new ButtonArray();
this.updateRunButtons([RunMode.Debug]);
this.editor.onChange({
onChange: (code) => __awaiter(this, void 0, void 0, function* () {
const backend = yield this.backend;
const modes = yield backend.workerProxy.runModes(code);
this.updateRunButtons(modes);
this.renderCodeActionButtons();
}),
delay: DEFAULT_EDITOR_DELAY
});
BackendManager.subscribe(BackendEventType.Input, () => this.setState(RunState.AwaitingInput));
this.loadingPackages = [];
BackendManager.subscribe(BackendEventType.Loading, e => this.onLoad(e));
BackendManager.subscribe(BackendEventType.Start, e => this.onStart(e));
BackendManager.subscribe(BackendEventType.Stop, () => this.stop());
this.previousState = RunState.Ready;
this.runStartTime = new Date().getTime();
this.state = RunState.Ready;
this.traceViewer = new Debugger();
}
set debugMode(debugMode) {
this._debugMode = debugMode;
this.renderCodeActionButtons();
if (this.inputManager.getInputMode() === InputMode.Batch) {
const handler = this.inputManager.inputHandler;
handler.debugMode = debugMode;
}
this.outputManager.debugMode = debugMode;
this.editor.debugMode = debugMode;
if (!this._debugMode) {
this.traceViewer.reset();
this.outputManager.reset();
this.inputManager.inputHandler.reset();
}
this.dispatchEvent(new CustomEvent("debug-mode", { detail: debugMode }));
}
get debugMode() {
return this._debugMode;
}
/**
* Stops the current run and resets the state of the program
* Regular and debug output is cleared
* @return {Promise<void>} Returns when the program has been reset
*/
reset() {
return __awaiter(this, void 0, void 0, function* () {
if (![RunState.Ready, RunState.Loading].includes(this.state)) {
yield this.stop();
}
this.debugMode = false;
this.inputManager.inputHandler.reset();
this.outputManager.reset();
this.traceViewer.reset();
});
}
updateRunButtons(modes) {
this.runButtons = new ButtonArray();
this.addRunButton(RunMode.Run, "btn-primary");
modes.forEach(m => this.addRunButton(m));
}
addRunButton(mode, classNames = "btn-secondary") {
const id = addPapyrosPrefix(mode) + "-code-btn";
this.runButtons.add({
id: id,
buttonText: t(`Papyros.run_modes.${mode}`),
classNames,
icon: MODE_ICONS[mode]
}, () => this.runCode(mode));
}
/**
* Start the backend to enable running code
*/
start() {
return __awaiter(this, void 0, void 0, function* () {
this.setState(RunState.Loading);
const backend = BackendManager.getBackend(this.programmingLanguage);
this.editor.setProgrammingLanguage(this.programmingLanguage);
// Use a Promise to immediately enable running while downloading
// eslint-disable-next-line no-async-promise-executor
this.backend = new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
const workerProxy = backend.workerProxy;
yield workerProxy
// Allow passing messages between worker and main thread
.launch(proxy((e) => BackendManager.publish(e)), proxy(() => {
this.outputManager.onOverflow(null);
}));
this.editor.setLintingSource((view) => __awaiter(this, void 0, void 0, function* () {
const workerDiagnostics = yield workerProxy.lintCode(this.editor.getCode());
return workerDiagnostics.map(d => {
const fromline = view.state.doc.line(d.lineNr);
const toLine = view.state.doc.line(d.endLineNr);
const from = Math.min(fromline.from + d.columnNr, fromline.to);
const to = Math.min(toLine.from + d.endColumnNr, toLine.to);
return Object.assign(Object.assign({}, d), { from: from, to: to });
});
}));
return resolve(backend);
}));
this.editor.focus();
this.setState(RunState.Ready);
});
}
/**
* Interrupt the currently running code
* @return {Promise<void>} Returns when the code has been interrupted
*/
stop() {
return __awaiter(this, void 0, void 0, function* () {
this.setState(RunState.Stopping);
BackendManager.publish({
type: BackendEventType.End,
data: "User cancelled run", contentType: "text/plain"
});
const backend = yield this.backend;
yield backend.interrupt();
while (this.state === RunState.Stopping) {
yield new Promise(resolve => setTimeout(resolve, 100));
}
});
}
/**
* Set the used programming language to the given one to allow editing and running code
* @param {ProgrammingLanguage} programmingLanguage The language to use
*/
setProgrammingLanguage(programmingLanguage) {
return __awaiter(this, void 0, void 0, function* () {
if (this.programmingLanguage !== programmingLanguage) { // Expensive, so ensure it is needed
yield this.backend.then(b => b.interrupt());
this.programmingLanguage = programmingLanguage;
yield this.start();
}
});
}
provideFiles(inlinedFiles, hrefFiles) {
return __awaiter(this, void 0, void 0, function* () {
const fileNames = [...Object.keys(inlinedFiles), ...Object.keys(hrefFiles)];
if (fileNames.length === 0) {
return;
}
BackendManager.publish({ type: BackendEventType.Loading, data: JSON.stringify({
modules: fileNames,
status: "loading"
}) });
const backend = yield this.backend;
yield backend.workerProxy.provideFiles(inlinedFiles, hrefFiles);
});
}
/**
* @return {ProgrammingLanguage} The current programming language
*/
getProgrammingLanguage() {
return this.programmingLanguage;
}
/**
* Show or hide the spinning circle, representing a running animation
* @param {boolean} show Whether to show the spinner
*/
showSpinner(show) {
getElement(STATE_SPINNER_ID).style.display = show ? "" : "none";
}
/**
* Show the current state of the program to the user
* @param {RunState} state The current state of the run
* @param {string} message Optional message to indicate the state
*/
setState(state, message) {
var _b;
const stateElement = getElement(APPLICATION_STATE_TEXT_ID);
stateElement.innerText = message || t(`Papyros.states.${state}`);
(_b = stateElement.parentElement) === null || _b === void 0 ? void 0 : _b.classList.toggle("show", stateElement.innerText.length > 0);
if (state !== this.state) {
this.previousState = this.state;
this.state = state;
}
this.showSpinner(this.state !== RunState.Ready);
this.renderCodeActionButtons();
}
/**
* @return {RunState} The state of the current run
*/
getState() {
return this.state;
}
/**
* Remove a button from the internal button list. Requires a re-render to update
* @param {string} id Identifier of the button to remove
*/
removeButton(id) {
this.userButtons.remove(id);
this.renderUserButtons();
}
/**
* Add a button to display to the user
* @param {ButtonOptions} options Options for rendering the button
* @param {function} onClick Listener for click events on the button
*/
addButton(options, onClick) {
this.userButtons.add(options, onClick);
this.renderUserButtons();
}
/**
* Generate a button that the user can click to process code
* Can either run the code or interrupt it if already running
* @return {DynamicButton} A list of buttons to interact with the code according to the current state
*/
getCodeActionButtons() {
if (this.state === RunState.Ready) {
if (this.debugMode) {
return [{
id: "stop-debug-btn",
buttonHTML: renderButton({
id: "stop-debug-btn",
buttonText: t("Papyros.debug.stop"),
classNames: "btn-secondary",
icon: DEBUG_STOP_ICON
}),
onClick: () => this.debugMode = false
}];
}
else {
return this.runButtons;
}
}
else {
return [{
id: STOP_BTN_ID,
buttonHTML: renderButton({
id: STOP_BTN_ID,
buttonText: t("Papyros.stop"),
classNames: "btn-danger",
icon: "<i class=\"mdi mdi-stop\"></i>"
}),
onClick: () => this.stop()
}];
}
}
/**
* @param {DynamicButton[]} buttons The buttons to render
* @param {string} id The id of the element to render the buttons in
*/
renderButtons(buttons, id) {
getElement(id).innerHTML = buttons.map(b => b.buttonHTML).join("\n");
// Buttons are freshly added to the DOM, so attach listeners now
buttons.forEach(b => addListener(b.id, b.onClick, "click"));
}
renderCodeActionButtons() {
this.renderButtons(this.getCodeActionButtons(), RUN_BUTTONS_WRAPPER_ID);
}
renderUserButtons() {
this.renderButtons(this.userButtons, CODE_BUTTONS_WRAPPER_ID);
}
_render(options) {
appendClasses(options.statusPanelOptions, "_tw-border-solid _tw-border-gray-200 _tw-border-b-2 dark:_tw-border-dark-mode-content");
const rendered = renderWithOptions(options.statusPanelOptions, `
<div style="position: relative">
<div class="papyros-state-card cm-panels">
${renderSpinningCircle(STATE_SPINNER_ID, "_tw-border-gray-200 _tw-border-b-red-500")}
<div id="${APPLICATION_STATE_TEXT_ID}"></div>
</div>
</div>
<div class="_tw-items-center _tw-px-1 _tw-flex _tw-flex-row _tw-justify-between">
<div id="${RUN_BUTTONS_WRAPPER_ID}">
</div>
<div id="${CODE_BUTTONS_WRAPPER_ID}">
</div>
</div>`);
this.setState(this.state);
this.renderUserButtons();
this.inputManager.render(options.inputOptions);
this.outputManager.render(options.outputOptions);
this.editor.render(options.codeEditorOptions);
this.editor.setPanel(rendered);
// Set language again to update the placeholder
this.editor.setProgrammingLanguage(this.programmingLanguage);
this.traceViewer.render(options.traceOptions);
return rendered;
}
/**
* Execute the code in the editor
* @param {RunMode} mode The mode to run with
* @return {Promise<void>} Promise of running the code
*/
runCode(mode) {
return __awaiter(this, void 0, void 0, function* () {
this.debugMode = mode === RunMode.Debug;
const code = this.editor.getCode();
// Setup pre-run
this.setState(RunState.Loading);
// Ensure we go back to Loading after finishing any remaining installs
this.previousState = RunState.Loading;
BackendManager.publish({
type: BackendEventType.Start,
data: "StartClicked", contentType: "text/plain"
});
let interrupted = false;
let terminated = false;
const backend = yield this.backend;
this.runStartTime = new Date().getTime();
try {
yield backend.call(backend.workerProxy.runCode, code, mode);
}
catch (error) {
if (error.type === "InterruptError") {
// Error signaling forceful interrupt
interrupted = true;
terminated = true;
}
else {
BackendManager.publish({
type: BackendEventType.Error,
data: JSON.stringify(error),
contentType: "text/json"
});
BackendManager.publish({
type: BackendEventType.End,
data: "RunError", contentType: "text/plain"
});
}
}
finally {
if (this.state === RunState.Stopping) {
// Was interrupted, End message already published
interrupted = true;
}
if (terminated) {
yield this.start();
}
else if (yield backend.workerProxy.hasOverflow()) {
this.outputManager.onOverflow(() => __awaiter(this, void 0, void 0, function* () {
const backend = yield this.backend;
const overflowResults = (yield backend.workerProxy.getOverflow())
.map(e => e.data).join("");
downloadResults(overflowResults, "overflow-results.txt");
}));
}
this.setState(RunState.Ready, t(interrupted ? "Papyros.interrupted" : "Papyros.finished", { time: (new Date().getTime() - this.runStartTime) / 1000 }));
}
});
}
/**
* Callback to handle loading events
* @param {BackendEvent} e The loading event
*/
onLoad(e) {
const loadingData = parseData(e.data, e.contentType);
if (loadingData.status === "loading") {
loadingData.modules.forEach(m => {
if (!this.loadingPackages.includes(m)) {
this.loadingPackages.push(m);
}
});
}
else if (loadingData.status === "loaded") {
loadingData.modules.forEach(m => {
const index = this.loadingPackages.indexOf(m);
if (index !== -1) {
this.loadingPackages.splice(index, 1);
}
});
}
else { // failed
// If it is a true module, an Exception will be raised when running
// So this does not need to be handled here, as it is often an incomplete package-name
// that causes micropip to not find the correct wheel
this.loadingPackages = [];
}
if (this.loadingPackages.length > 0) {
const packageMessage = t("Papyros.loading", {
// limit amount of package names shown
packages: this.loadingPackages.slice(0, 3).join(", ")
});
this.setState(RunState.Loading, packageMessage);
}
else {
this.setState(this.previousState);
}
}
onStart(e) {
const startData = parseData(e.data, e.contentType);
if (startData.includes("RunCode")) {
this.runStartTime = new Date().getTime();
this.setState(RunState.Running);
}
}
},
(() => {
var _b;
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create((_b = _classSuper[Symbol.metadata]) !== null && _b !== void 0 ? _b : null) : void 0;
_renderCodeActionButtons_decorators = [delay(100)];
_renderUserButtons_decorators = [delay(100)];
__esDecorate(_a, null, _renderCodeActionButtons_decorators, { kind: "method", name: "renderCodeActionButtons", static: false, private: false, access: { has: obj => "renderCodeActionButtons" in obj, get: obj => obj.renderCodeActionButtons }, metadata: _metadata }, null, _instanceExtraInitializers);
__esDecorate(_a, null, _renderUserButtons_decorators, { kind: "method", name: "renderUserButtons", static: false, private: false, access: { has: obj => "renderUserButtons" in obj, get: obj => obj.renderUserButtons }, metadata: _metadata }, null, _instanceExtraInitializers);
if (_metadata) Object.defineProperty(_a, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
})(),
_a;
})();
export { CodeRunner };
//# sourceMappingURL=CodeRunner.js.map