Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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.4.1",
"version": "0.4.3",
"private": false,
"homepage": ".",
"devDependencies": {
Expand Down
38 changes: 25 additions & 13 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,28 @@ import {
} from "./Constants";
import { Papyros, PapyrosConfig } from "./Papyros";
import { InputMode } from "./InputManager";
import { BatchInputHandler } from "./input/BatchInputHandler";
import { CodeMirrorEditor } from "./editor/CodeMirrorEditor";

const LOCAL_STORAGE_CODE_KEY = addPapyrosPrefix("previous-code");
const LOCAL_STORAGE_KEYS = {
code: addPapyrosPrefix("previous-code"),
input: addPapyrosPrefix("previous-batch-input")
};

function setUpEditor(editor: CodeMirrorEditor, storageKey: string): void {
const previousValue = window.localStorage.getItem(storageKey);
if (previousValue) {
editor.setText(previousValue);
}
editor.onChange(
{
onChange: (text: string) => {
window.localStorage.setItem(storageKey, text);
},
delay: 0
}
);
}

async function startPapyros(): Promise<void> {
// Retrieve initial locale and programming language from URL
Expand Down Expand Up @@ -40,19 +60,11 @@ async function startPapyros(): Promise<void> {
},
darkMode: darkMode
});
// Restore previous code if it existed
const previousCode = window.localStorage.getItem(LOCAL_STORAGE_CODE_KEY);
if (previousCode) {
papyros.setCode(previousCode);
setUpEditor(papyros.codeRunner.editor, LOCAL_STORAGE_KEYS.code);
const handler = papyros.codeRunner.inputManager.getInputHandler(InputMode.Batch);
if (handler instanceof BatchInputHandler) {
setUpEditor((handler as BatchInputHandler).batchEditor, LOCAL_STORAGE_KEYS.input);
}
papyros.codeRunner.editor.onChange(
{
onChange: (code: string) => {
window.localStorage.setItem(LOCAL_STORAGE_CODE_KEY, code);
},
delay: 0
}
);

await papyros.launch();
}
Expand Down
30 changes: 16 additions & 14 deletions src/InputManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from "./Constants";
import { BackendEvent, BackendEventType } from "./BackendEvent";
import {
addListener,
addListener, getElement,
} from "./util/Util";
import { InteractiveInputHandler } from "./input/InteractiveInputHandler";
import { UserInputHandler } from "./input/UserInputHandler";
Expand Down Expand Up @@ -38,6 +38,7 @@ export class InputManager extends Renderable<InputManagerRenderOptions> {

constructor(sendInput: (input: string) => void, inputMode: InputMode) {
super();
this.onUserInput = this.onUserInput.bind(this);
this.inputHandlers = this.buildInputHandlerMap();
this.inputMode = inputMode;
this.sendInput = sendInput;
Expand All @@ -50,9 +51,9 @@ export class InputManager extends Renderable<InputManagerRenderOptions> {

private buildInputHandlerMap(): Map<InputMode, UserInputHandler> {
const interactiveInputHandler: UserInputHandler =
new InteractiveInputHandler(() => this.onUserInput());
new InteractiveInputHandler(this.onUserInput);
const batchInputHandler: UserInputHandler =
new BatchInputHandler(() => this.onUserInput());
new BatchInputHandler(this.onUserInput);
return new Map([
[InputMode.Interactive, interactiveInputHandler],
[InputMode.Batch, batchInputHandler]
Expand All @@ -70,8 +71,12 @@ export class InputManager extends Renderable<InputManagerRenderOptions> {
this.inputHandler.toggle(true);
}

public getInputHandler(inputMode: InputMode): UserInputHandler {
return this.inputHandlers.get(inputMode)!;
}

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

public isWaiting(): boolean {
Expand Down Expand Up @@ -108,31 +113,28 @@ ${switchMode}`);
this.inputHandler.waitWithPrompt(this.waiting, this.prompt);
}

private onUserInput(): void {
if (this.inputHandler.hasNext()) {
const line = this.inputHandler.next();
this.sendInput(line);
this.waitWithPrompt(false);
} else {
this.waitWithPrompt(true, this.prompt);
}
private onUserInput(line: string): void {
this.sendInput(line);
this.waitWithPrompt(false);
}

/**
* Asynchronously handle an input request by prompting the user for input
* @param {BackendEvent} e Event containing the input data
*/
private onInputRequest(e: BackendEvent): void {
this.prompt = e.data;
this.onUserInput();
this.waitWithPrompt(true, e.data);
}

private onRunStart(): void {
// Prevent switching input mode during runs
getElement(SWITCH_INPUT_MODE_A_ID).hidden = true;
this.waitWithPrompt(false);
this.inputHandler.onRunStart();
}

private onRunEnd(): void {
getElement(SWITCH_INPUT_MODE_A_ID).hidden = false;
this.inputHandler.onRunEnd();
this.waitWithPrompt(false);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Papyros.css
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ Ensure the default browser behavior of the `hidden` attribute.
*/

[hidden] {
display: none;
display: none !important;
}

.cm-content,
Expand Down
4 changes: 2 additions & 2 deletions src/Translations.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const ENGLISH_TRANSLATION = {
"run_modes": {
"doctest": "Run doctests"
},
"used_input": "This line was used as input by your code.",
"used_input": "This line has already been used as input.",
"used_input_with_prompt": "This line was used as input for the following prompt: %{prompt}"
};

Expand Down Expand Up @@ -104,7 +104,7 @@ const DUTCH_TRANSLATION = {
"run_modes": {
"doctest": "Run doctests"
},
"used_input": "Deze regel werd gebruikt als invoer.",
"used_input": "Deze regel werd al gebruikt als invoer.",
"used_input_with_prompt": "Deze regel werd gebruikt als invoer voor de volgende vraag: %{prompt}"
};

Expand Down
68 changes: 56 additions & 12 deletions src/editor/BatchInputEditor.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import { CodeMirrorEditor } from "./CodeMirrorEditor";
import { UsedInputGutters, UsedInputGutterInfo } from "./Gutters";
import { ViewUpdate } from "@codemirror/view";

/**
* Editor to handle and highlight user input
* Arguments used to higlight lines in the Editor
*/
export class BatchInputEditor extends CodeMirrorEditor {
interface HighlightArgs {
/**
* Style classes used to highlight lines
* Whether the user's code is currently running and using input
*/
private static HIGHLIGHT_CLASSES = ["cm-activeLine"];
running: boolean;
/**
* Function to obtain gutter info per line (1-based indexing)
*/
getInfo: (lineInfo: number) => UsedInputGutterInfo;
}
/**
* Editor to handle and highlight user input
*/
export class BatchInputEditor extends CodeMirrorEditor {
/**
* Gutters to show which lines were used
*/
private usedInputGutters: UsedInputGutters;
private lastHighlightArgs?: HighlightArgs;

constructor() {
super(new Set([CodeMirrorEditor.PLACEHOLDER, CodeMirrorEditor.STYLE]), {
Expand All @@ -21,24 +32,57 @@ export class BatchInputEditor extends CodeMirrorEditor {
"dark:_tw-bg-dark-mode-bg", "dark:_tw-border-dark-mode-content",
"focus:_tw-outline-none", "focus:_tw-ring-1", "focus:_tw-ring-blue-500"],
minHeight: "10vh",
maxHeight: "20vh"
}
);
maxHeight: "20vh",
theme: {}
});
this.usedInputGutters = new UsedInputGutters();
this.addExtension(this.usedInputGutters.toExtension());
}

private getLastHighlightArgs(): HighlightArgs {
return this.lastHighlightArgs || {
running: false,
getInfo: lineNr => {
return {
lineNr,
on: false,
title: ""
};
}
};
}

protected override onViewUpdate(v: ViewUpdate): void {
super.onViewUpdate(v);
// Ensure that highlighting occurs after CodeMirrors internal update
// so that the style classes are not overwritten
setTimeout(() => {
this.highlight(this.getLastHighlightArgs());
}, 10);
}

/**
* Apply highlighting to the lines in the Editor
* @param {function(number): UsedInputGutterInfo} getInfo Function to obtain gutter
* info per line (1-based indexing)
* @param {HightlightArgs} args Arguments for highlighting
* @param {Array<string>} highlightClasses HTML classes to use for consumed lines
*/
public highlight(getInfo: (lineNr: number) => UsedInputGutterInfo): void {
public highlight(args: HighlightArgs,
highlightClasses = ["_tw-bg-slate-200", "dark:_tw-bg-slate-500"]): void {
this.lastHighlightArgs = args;
const {
running, getInfo
} = args;
let nextLineToUse = 0;
this.editorView.dom.querySelectorAll(".cm-line").forEach((line, i) => {
const info = getInfo(i + 1);
BatchInputEditor.HIGHLIGHT_CLASSES.forEach(c => {
line.classList.toggle(c, info.on);
if (info.on) {
nextLineToUse += 1;
}
line.classList.toggle("cm-activeLine", running && i === nextLineToUse);
highlightClasses.forEach(className => {
line.classList.toggle(className, i !== nextLineToUse && info.on);
});
line.setAttribute("contenteditable", "" + (!running || !info.on));
this.usedInputGutters.setMarker(this.editorView, info);
});
}
Expand Down
7 changes: 4 additions & 3 deletions src/editor/CodeEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ import {
} from "@codemirror/language";
import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
import { EditorState, Extension } from "@codemirror/state";
import { oneDark } from "@codemirror/theme-one-dark";
import { oneDarkHighlightStyle } from "@codemirror/theme-one-dark";
import {
EditorView, showPanel, lineNumbers, highlightActiveLineGutter,
highlightSpecialChars, drawSelection,
rectangularSelection, highlightActiveLine, keymap
} from "@codemirror/view";
import { Diagnostic, linter, lintGutter, lintKeymap } from "@codemirror/lint";
import { CodeMirrorEditor } from "./CodeMirrorEditor";
import { darkTheme } from "./DarkTheme";

/**
* Component that provides useful features to users writing code
Expand Down Expand Up @@ -76,9 +77,9 @@ export class CodeEditor extends CodeMirrorEditor {
public override setDarkMode(darkMode: boolean): void {
let styleExtensions: Extension = [];
if (darkMode) {
styleExtensions = oneDark;
styleExtensions = [darkTheme, syntaxHighlighting(oneDarkHighlightStyle)];
} else {
styleExtensions = syntaxHighlighting(defaultHighlightStyle, { fallback: true });
styleExtensions = syntaxHighlighting(defaultHighlightStyle);
}
this.reconfigure([CodeMirrorEditor.STYLE, styleExtensions]);
}
Expand Down
25 changes: 14 additions & 11 deletions src/editor/CodeMirrorEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Compartment, EditorState, Extension, StateEffect } from "@codemirror/st
import { EditorView, placeholder, ViewUpdate } from "@codemirror/view";
import { Renderable, RenderOptions, renderWithOptions } from "../util/Rendering";
import { StyleSpec } from "style-mod";
import { oneDark } from "@codemirror/theme-one-dark";
import { darkTheme } from "./DarkTheme";

/**
* Data structure containing common elements for styling
Expand Down Expand Up @@ -107,15 +107,16 @@ export abstract class CodeMirrorEditor extends Renderable {
state: EditorState.create({
extensions: [
configurableExtensions,
EditorView.updateListener.of((v: ViewUpdate) => {
if (v.docChanged) {
this.handleChange();
}
})
EditorView.updateListener.of(this.onViewUpdate.bind(this))
]
})
});
this.setStyling(styling);
}

protected onViewUpdate(v: ViewUpdate): void {
if (v.docChanged) {
this.handleChange();
}
}

/**
Expand Down Expand Up @@ -178,9 +179,7 @@ export abstract class CodeMirrorEditor extends Renderable {
public setDarkMode(darkMode: boolean): void {
let styleExtensions: Extension = [];
if (darkMode) {
styleExtensions = oneDark;
} else {
styleExtensions = [];
styleExtensions = [darkTheme];
}
this.reconfigure([CodeMirrorEditor.STYLE, styleExtensions]);
}
Expand All @@ -195,14 +194,18 @@ export abstract class CodeMirrorEditor extends Renderable {
CodeMirrorEditor.THEME,
EditorView.theme({
".cm-scroller": { overflow: "auto" },
"&": { maxHeight: this.styling.maxHeight, height: "100%" },
"&": {
"maxHeight": this.styling.maxHeight, "height": "100%",
"font-size": "14px" // use proper size to align gutters with editor
},
".cm-gutter,.cm-content": { minHeight: this.styling.minHeight },
...(this.styling.theme || {})
})
]);
}

protected override _render(options: RenderOptions): void {
this.setStyling(this.styling);
this.setDarkMode(options.darkMode || false);
const wrappingDiv = document.createElement("div");
wrappingDiv.classList.add(...this.styling.classes);
Expand Down
Loading