Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
6777164
Add support for doctest checking and running
winniederidder May 21, 2022
55da1d4
Add support for more advanced change listeners
winniederidder May 21, 2022
b0555b1
Clear timeout when useful
winniederidder May 21, 2022
fc669ab
Caching issue somewhere?
winniederidder May 22, 2022
97b6a61
Finally fix detecting doctests
winniederidder May 22, 2022
424dc63
Fix rebase
winniederidder May 22, 2022
8238c35
Fix buttons
winniederidder May 26, 2022
2f61af6
Allow run_modes dynamic key
winniederidder May 26, 2022
8967d4c
Use editable div instead of textarea
winniederidder May 22, 2022
60153f8
Make focusing work as expected
winniederidder May 22, 2022
66f5254
First version of using CodeMirror to handle user input in batch mode
winniederidder May 22, 2022
b9686bf
Fix and add translations
winniederidder May 26, 2022
aeb4262
Use es6 to allow working with Gutters
winniederidder May 26, 2022
33a8ee2
Highlights and checkmarks for used input
winniederidder May 26, 2022
6a43063
Add base class
winniederidder May 26, 2022
5621bd2
Autoformat and first fixes
winniederidder May 26, 2022
9a72833
Use batch editor
winniederidder May 26, 2022
fa8b09d
Move onChange to base
winniederidder May 26, 2022
90d653c
Rebase
winniederidder May 26, 2022
6216084
Rehighlight on render for translations
winniederidder May 26, 2022
9c0e995
Docs and fixes
winniederidder May 26, 2022
ec7d824
Cleanup and fix tests
winniederidder May 26, 2022
8d4ce2f
Add entrypoint to configure theme
winniederidder May 26, 2022
ebd211e
Cleanup and allow configuring input height
winniederidder May 27, 2022
cee74bb
Add support for making lines read-only
winniederidder May 27, 2022
20f55dd
Bump @codemirror/lint from 0.20.2 to 0.20.3
dependabot[bot] May 30, 2022
dce2eb2
Bump @typescript-eslint/eslint-plugin from 5.25.0 to 5.26.0
dependabot[bot] May 30, 2022
e0d4db7
Bump eslint-plugin-jest from 26.2.2 to 26.4.5
dependabot[bot] May 30, 2022
29f4a68
Bump @codemirror/autocomplete from 0.20.1 to 0.20.3
dependabot[bot] May 30, 2022
1bdc907
Bump typescript from 4.6.4 to 4.7.2
dependabot[bot] May 30, 2022
81d31d5
Bump @codemirror/view from 0.20.6 to 0.20.7
dependabot[bot] May 30, 2022
25047a3
Bump @typescript-eslint/parser from 5.25.0 to 5.26.0
dependabot[bot] May 30, 2022
e0c66bb
Allow translating CodeMirror
winniederidder May 30, 2022
f4d9405
Create button css class
winniederidder May 30, 2022
aabb935
Style CodeMirror elements
winniederidder May 30, 2022
d49d790
Improve styling
winniederidder May 30, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ yarn-debug.log*
yarn-error.log*

.eslintcache

.vscode
# script outputs
translationIssues.txt

# Ignore output of local pip install when building the tar
src/workers/python/python_package/
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dodona/papyros",
"version": "0.3.8",
"version": "0.4.3",
"private": false,
"homepage": ".",
"devDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion scripts/ValidateTranslations.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ const checks = [
"Papyros.locales.*",
"Papyros.states.*",
"Papyros.switch_input_mode_to.*",
"Papyros.input_placeholder.*"
"Papyros.input_placeholder.*",
"Papyros.run_modes.*"
]
}
];
Expand Down
13 changes: 9 additions & 4 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ async function startPapyros(): Promise<void> {
standAlone: true,
programmingLanguage: language,
locale: locale,
inputMode: InputMode.Interactive,
inputMode: InputMode.Batch,
channelOptions: {
serviceWorkerName: DEFAULT_SERVICE_WORKER
}
Expand All @@ -45,9 +45,14 @@ async function startPapyros(): Promise<void> {
if (previousCode) {
papyros.setCode(previousCode);
}
papyros.codeRunner.editor.onChange(code => {
window.localStorage.setItem(LOCAL_STORAGE_CODE_KEY, code);
});
papyros.codeRunner.editor.onChange(
{
onChange: (code: string) => {
window.localStorage.setItem(LOCAL_STORAGE_CODE_KEY, code);
},
delay: 0
}
);

await papyros.launch();
}
Expand Down
18 changes: 17 additions & 1 deletion src/Backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ export interface WorkerDiagnostic {
message: string;
}

export interface RunMode {
mode: string;
active: boolean;
}

export abstract class Backend<Extras extends SyncExtras = SyncExtras> {
/**
* SyncExtras object that grants access to helpful methods
Expand Down Expand Up @@ -122,13 +127,24 @@ export abstract class Backend<Extras extends SyncExtras = SyncExtras> {
return Promise.resolve();
}

/**
* Determine whether the modes supported by this Backend are active
* @param {string} code The current code in the editor
* @return {Array<RunMode>} The run modes of this Backend
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public runModes(code: string): Array<RunMode> {
return [];
}

/**
* Executes the given code
* @param {Extras} extras Helper properties to run code
* @param {string} code The code to run
* @param {string} mode The mode to run the code in
* @return {Promise<void>} Promise of execution
*/
public abstract runCode(extras: Extras, code: string): Promise<void>;
public abstract runCode(extras: Extras, code: string, mode?: string): Promise<void>;

/**
* Converts the context to a cloneable object containing useful properties
Expand Down
78 changes: 59 additions & 19 deletions src/CodeRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { SyncClient } from "comsync";
import { Backend } from "./Backend";
import { BackendEvent, BackendEventType } from "./BackendEvent";
import { BackendManager } from "./BackendManager";
import { CodeEditor } from "./CodeEditor";
import { CodeEditor } from "./editor/CodeEditor";
import {
APPLICATION_STATE_TEXT_ID, RUN_BTN_ID,
addPapyrosPrefix,
APPLICATION_STATE_TEXT_ID, CODE_BUTTONS_WRAPPER_ID, DEFAULT_EDITOR_DELAY, RUN_BTN_ID,
STATE_SPINNER_ID, STOP_BTN_ID
} from "./Constants";
import { InputManager } from "./InputManager";
import { InputManager, InputManagerRenderOptions, InputMode } from "./InputManager";
import { ProgrammingLanguage } from "./ProgrammingLanguage";
import { renderSpinningCircle } from "./util/HTMLShapes";
import {
Expand All @@ -35,7 +36,7 @@ interface CodeRunnerRenderOptions {
/**
* Options for rendering the InputManager
*/
inputOptions: RenderOptions;
inputOptions: InputManagerRenderOptions;
/**
* Options for rendering the editor
*/
Expand Down Expand Up @@ -116,33 +117,55 @@ export class CodeRunner extends Renderable<CodeRunnerRenderOptions> {
/**
* 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: ProgrammingLanguage) {
constructor(programmingLanguage: ProgrammingLanguage, inputMode: InputMode) {
super();
this.programmingLanguage = programmingLanguage;
this.editor = new CodeEditor(() => {
if (this.state === RunState.Ready) {
this.runCode(this.editor.getCode());
this.runCode(this.editor.getText());
}
});
this.inputManager = new InputManager(async (input: string) => {
const backend = await this.backend;
backend.writeMessage(input);
this.setState(RunState.Running);
});
}, inputMode);
this.outputManager = new OutputManager();
this.backend = Promise.resolve({} as SyncClient<Backend>);
this.buttons = [];
this.addButton({
id: RUN_BTN_ID,
buttonText: t("Papyros.run"),
classNames: "_tw-text-white _tw-bg-blue-500"
}, () => this.runCode(this.editor.getCode()));
}, () => this.runCode(this.editor.getText()));
this.addButton({
id: STOP_BTN_ID,
buttonText: t("Papyros.stop"),
classNames: "_tw-text-white _tw-bg-red-500"
}, () => this.stop());
this.editor.onChange({
onChange: async code => {
const backend = await this.backend;
const modes = await backend.workerProxy.runModes(code);
modes.forEach(mode => {
const id = addPapyrosPrefix(mode.mode);
if (mode.active) {
this.addButton({
id: id,
buttonText: t(`Papyros.run_modes.${mode.mode}`),
classNames: "_tw-text-white _tw-bg-green-500"
}, () => this.runCode(this.editor.getText(), mode.mode));
} else {
this.removeButton(id);
}
});
this.renderButtons();
},
delay: DEFAULT_EDITOR_DELAY
});

BackendManager.subscribe(BackendEventType.Input,
() => this.setState(RunState.AwaitingInput));
this.loadingPackages = [];
Expand Down Expand Up @@ -177,7 +200,7 @@ export class CodeRunner extends Renderable<CodeRunnerRenderOptions> {
});
this.editor.setLintingSource(
async view => {
const workerDiagnostics = await workerProxy.lintCode(this.editor.getCode());
const workerDiagnostics = await workerProxy.lintCode(this.editor.getText());
return workerDiagnostics.map(d => {
const fromline = view.state.doc.line(d.lineNr);
const toLine = view.state.doc.line(d.endLineNr);
Expand Down Expand Up @@ -250,7 +273,11 @@ export class CodeRunner extends Renderable<CodeRunnerRenderOptions> {
* @param {string} message Optional message to indicate the state
*/
public setState(state: RunState, message?: string): void {
this.state = state;
if (state !== this.state) {
getElement(APPLICATION_STATE_TEXT_ID).innerText =
message || t(`Papyros.states.${state}`);
this.state = state;
}
this.stopButton.disabled = [RunState.Ready, RunState.Loading].includes(state);
if ([RunState.Ready, RunState.Loading].includes(state)) {
this.showSpinner(state == RunState.Loading);
Expand All @@ -259,41 +286,53 @@ export class CodeRunner extends Renderable<CodeRunnerRenderOptions> {
this.showSpinner(true);
this.runButton.disabled = true;
}
getElement(APPLICATION_STATE_TEXT_ID).innerText =
message || t(`Papyros.states.${state}`);
}

public getState(): RunState {
return this.state;
}

public removeButton(id: string): void {
const existingIndex = this.buttons.findIndex(b => b.id === id);
if (existingIndex !== -1) {
this.buttons.splice(existingIndex, 1);
}
}

/**
* 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
*/
public addButton(options: ButtonOptions, onClick: () => void): void {
this.removeButton(options.id);
this.buttons.push({
id: options.id,
buttonHTML: renderButton(options),
onClick: onClick
});
}

private renderButtons(): void {
getElement(CODE_BUTTONS_WRAPPER_ID).innerHTML =
this.buttons.map(b => b.buttonHTML).join("\n");
// Buttons are freshly added to the DOM, so attach listeners now
this.buttons.forEach(b => addListener(b.id, b.onClick, "click"));
// Ensure buttons are shown properly
this.setState(this.state);
}

protected override _render(options: CodeRunnerRenderOptions): HTMLElement {
const rendered = renderWithOptions(options.statusPanelOptions, `
<div class="_tw-grid _tw-grid-cols-2 _tw-items-center _tw-mx-1">
<div class="_tw-col-span-1 _tw-flex _tw-flex-row">
${this.buttons.map(b => b.buttonHTML).join("\n")}
<div id="${CODE_BUTTONS_WRAPPER_ID}" class="_tw-col-span-1 _tw-flex _tw-flex-row">
</div>
<div class="_tw-col-span-1 _tw-flex _tw-flex-row-reverse _tw-items-center">
<div id="${APPLICATION_STATE_TEXT_ID}"></div>
${renderSpinningCircle(STATE_SPINNER_ID, "_tw-border-gray-200 _tw-border-b-red-500")}
</div>
</div>`);
// Buttons are freshly added to the DOM, so attach listeners now
this.buttons.forEach(b => addListener(b.id, b.onClick, "click"));
this.setState(this.state);
this.renderButtons();
this.inputManager.render(options.inputOptions);
this.outputManager.render(options.outputOptions);
this.editor.render(options.codeEditorOptions);
Expand All @@ -305,9 +344,10 @@ export class CodeRunner extends Renderable<CodeRunnerRenderOptions> {

/**
* @param {string} code The code to run
* @param {string} mode The mode to run with
* @return {Promise<void>} Promise of running the code
*/
public async runCode(code: string): Promise<void> {
public async runCode(code: string, mode?: string): Promise<void> {
// Setup pre-run
this.setState(RunState.Running);
BackendManager.publish({
Expand All @@ -321,7 +361,7 @@ export class CodeRunner extends Renderable<CodeRunnerRenderOptions> {
const backend = await this.backend;
try {
await backend.call(
backend.workerProxy.runCode, code
backend.workerProxy.runCode, code, mode
);
} catch (error: any) {
if (error.type === "InterruptError") {
Expand Down
2 changes: 2 additions & 0 deletions src/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const EDITOR_WRAPPER_ID = addPapyrosPrefix("code-area");
export const PANEL_WRAPPER_ID = addPapyrosPrefix("code-status-panel");
export const STATE_SPINNER_ID = addPapyrosPrefix("state-spinner");
export const APPLICATION_STATE_TEXT_ID = addPapyrosPrefix("application-state-text");
export const CODE_BUTTONS_WRAPPER_ID = addPapyrosPrefix("code-buttons");
export const RUN_BTN_ID = addPapyrosPrefix("run-code-btn");
export const STOP_BTN_ID = addPapyrosPrefix("stop-btn");
export const SEND_INPUT_BTN_ID = addPapyrosPrefix("send-input-btn");
Expand All @@ -32,3 +33,4 @@ export const DARK_MODE_TOGGLE_ID = addPapyrosPrefix("toggle-dark-mode");
export const DEFAULT_PROGRAMMING_LANGUAGE = ProgrammingLanguage.Python;
export const DEFAULT_LOCALE = "nl";
export const DEFAULT_SERVICE_WORKER = "InputServiceWorker.js";
export const DEFAULT_EDITOR_DELAY = 750; // milliseconds
28 changes: 18 additions & 10 deletions src/InputManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { UserInputHandler } from "./input/UserInputHandler";
import { BatchInputHandler } from "./input/BatchInputHandler";
import { BackendManager } from "./BackendManager";
import { Renderable, RenderOptions, renderWithOptions } from "./util/Rendering";
import { EditorStyling } from "./editor/CodeMirrorEditor";

export enum InputMode {
Interactive = "interactive",
Expand All @@ -20,18 +21,25 @@ export enum InputMode {

export const INPUT_MODES = [InputMode.Batch, InputMode.Interactive];

export class InputManager extends Renderable {
export interface InputManagerRenderOptions extends RenderOptions {
/**
* Option to allow styling the editor area of the input handler
*/
inputStyling?: Partial<EditorStyling>;
}

export class InputManager extends Renderable<InputManagerRenderOptions> {
private inputMode: InputMode;
private inputHandlers: Map<InputMode, UserInputHandler>;
private waiting: boolean;
private prompt: string;

private sendInput: (input: string) => void;

constructor(sendInput: (input: string) => void) {
constructor(sendInput: (input: string) => void, inputMode: InputMode) {
super();
this.inputHandlers = this.buildInputHandlerMap();
this.inputMode = InputMode.Interactive;
this.inputMode = inputMode;
this.sendInput = sendInput;
this.waiting = false;
this.prompt = "";
Expand Down Expand Up @@ -62,15 +70,15 @@ export class InputManager extends Renderable {
this.inputHandler.toggle(true);
}

private get inputHandler(): UserInputHandler {
public get inputHandler(): UserInputHandler {
return this.inputHandlers.get(this.inputMode)!;
}

public isWaiting(): boolean {
return this.waiting;
}

protected override _render(options: RenderOptions): void {
protected override _render(options: InputManagerRenderOptions): void {
let switchMode = "";
const otherMode = this.inputMode === InputMode.Interactive ?
InputMode.Batch : InputMode.Interactive;
Expand All @@ -88,7 +96,8 @@ ${switchMode}`);

this.inputHandler.render({
parentElementId: USER_INPUT_WRAPPER_ID,
darkMode: options.darkMode
darkMode: options.darkMode,
inputStyling: options.inputStyling
});
this.inputHandler.waitWithPrompt(this.waiting, this.prompt);
}
Expand All @@ -99,7 +108,7 @@ ${switchMode}`);
this.inputHandler.waitWithPrompt(this.waiting, this.prompt);
}

private async onUserInput(): Promise<void> {
private onUserInput(): void {
if (this.inputHandler.hasNext()) {
const line = this.inputHandler.next();
this.sendInput(line);
Expand All @@ -112,11 +121,10 @@ ${switchMode}`);
/**
* Asynchronously handle an input request by prompting the user for input
* @param {BackendEvent} e Event containing the input data
* @return {Promise<void>} Promise of handling the request
*/
private async onInputRequest(e: BackendEvent): Promise<void> {
private onInputRequest(e: BackendEvent): void {
this.prompt = e.data;
return await this.onUserInput();
this.onUserInput();
}

private onRunStart(): void {
Expand Down
2 changes: 1 addition & 1 deletion src/Library.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BackendEvent } from "./BackendEvent";
import { CodeEditor } from "./CodeEditor";
import { CodeEditor } from "./editor/CodeEditor";
import { InputManager, InputMode } from "./InputManager";
import { FriendlyError, OutputManager } from "./OutputManager";
import { Papyros, PapyrosConfig, PapyrosRenderOptions } from "./Papyros";
Expand Down
Loading