Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support debugging in the browser #109

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@codemirror/lang-python": "^0.19.2",
"@codemirror/language": "^0.19.5",
"@codemirror/panel": "^0.19.0",
"@codemirror/rangeset": "^0.19.9",
"@codemirror/rectangular-selection": "^0.19.1",
"@codemirror/search": "^0.19.3",
"@codemirror/state": "^0.19.6",
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.input_modes.switch_to_*",
"Papyros.input_placeholder.*"
"Papyros.input_placeholder.*",
"Papyros.debugging_command.*"
]
}
];
Expand Down
29 changes: 26 additions & 3 deletions src/Backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Channel, readMessage, uuidv4 } from "sync-message";
import { parseData } from "./util/Util";
import { CompletionContext, CompletionResult } from "@codemirror/autocomplete";
import { LogType, papyrosLog } from "./util/Logging";
import { DebuggingInputHandler } from "./input/DebuggingInputHandler";

export interface WorkerAutocompleteContext {
explicit: boolean;
Expand Down Expand Up @@ -71,19 +72,41 @@ export abstract class Backend {
* Results or Errors must be passed by using the onEvent-callback
* @param code The code to run
*/
protected abstract _runCodeInternal(code: string): Promise<any>;
protected abstract runCodeInternal(code: string): Promise<void>;

/**
* Executes the given code
* @param {string} code The code to run
* @param {string} runId The uuid for this execution
* @return {Promise<void>} Promise of execution
*/
async runCode(code: string, runId: number): Promise<any> {
async runCode(code: string, runId: number): Promise<void> {
this.runId = runId;
return await this._runCodeInternal(code);
return await this.runCodeInternal(code);
}

/**
* Run a piece of code in debug mode, allowing the user to figure out
* why things do or do not work
* @param {string} code The code to debug
* @param {number} runId The internal identifier for this code run
* @param {Set<number>} breakpoints The line numbers where the user put a breakpoint
* @return {Promise<void>} Promise of debugging
*/
debugCode(code: string, runId: number, breakpoints: Set<number>): Promise<void> {
this.runId = runId;
return this.debugCodeInternal(code, breakpoints);
}

/**
* Internal helper method that actually debugs the code
* Communication is done by using the onEvent-callback
* @param {string} code The code to debug
* @param {Set<number>} breakpoints The line numbers where the user put a breakpoint
* @return {Promise<void>} Promise of debugging
*/
protected abstract debugCodeInternal(code: string, breakpoints: Set<number>): Promise<any>;

/**
* Converts the context to a cloneable object containing useful properties
* to generate autocompletion suggestions with
Expand Down
43 changes: 30 additions & 13 deletions src/CodeEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import { rectangularSelection } from "@codemirror/rectangular-selection";
import { defaultHighlightStyle } from "@codemirror/highlight";
import { lintKeymap } from "@codemirror/lint";
import { showPanel } from "@codemirror/panel";
import { RenderOptions, renderWithOptions } from "./util/Util";

import { RenderOptions, renderWithOptions, t } from "./util/Util";
import { breakpoints } from "./extensions/Breakpoints";
/**
* Component that provides useful features to users writing code
*/
Expand Down Expand Up @@ -56,38 +56,55 @@ export class CodeEditor {
* Compartment to configure the autocompletion at runtime
*/
autocompletionCompartment: Compartment = new Compartment();
/**
* Indices of lines in the editor that have breakpoints
*/
readonly breakpointLines: Set<number>;

/**
* Construct a new CodeEditor
* @param {ProgrammingLanguage} language The used programming language
* @param {string} editorPlaceHolder The placeholder for the editor
* @param {string} initialCode The initial code to display
* @param {number} indentLength The length in spaces for the indent unit
*/
constructor(language: ProgrammingLanguage,
editorPlaceHolder: string, initialCode = "", indentLength = 4) {
constructor(initialCode = "", indentLength = 4) {
this.breakpointLines = new Set();
this.editorView = new EditorView(
{
state: EditorState.create({
doc: initialCode,
extensions:
[
this.languageCompartment.of(CodeEditor.getLanguageSupport(language)),
breakpoints((lineNr: number, active: boolean) =>
this.toggleBreakpoint(lineNr, active)),
this.languageCompartment.of([]),
this.autocompletionCompartment.of(
autocompletion()
),
this.indentCompartment.of(
indentUnit.of(CodeEditor.getIndentUnit(indentLength))
),
keymap.of([indentWithTab]),
this.placeholderCompartment.of(placeholder(editorPlaceHolder)),
this.placeholderCompartment.of([]),
this.panelCompartment.of(showPanel.of(null)),
...CodeEditor.getExtensions()
...CodeEditor.getDefaultExtensions()
]
})
});
}

/**
* Update the breakpoint status of the given line
* @param {number} lineNr The index of the line
* @param {boolean} active Whether the line has a breakpoint
*/
toggleBreakpoint(lineNr: number, active: boolean): void {
if (active) {
this.breakpointLines.add(lineNr);
} else {
this.breakpointLines.delete(lineNr);
}
}

/**
* Render the editor with the given options and panel
* @param {RenderOptions} options Options for rendering
Expand All @@ -110,15 +127,15 @@ export class CodeEditor {
* @param {CompletionSource} completionSource Function to generate autocomplete results
* @param {string} editorPlaceHolder Placeholder when empty
*/
setLanguage(language: ProgrammingLanguage, completionSource: CompletionSource,
editorPlaceHolder: string): void {
setLanguage(language: ProgrammingLanguage, completionSource: CompletionSource): void {
this.editorView.dispatch({
effects: [
this.languageCompartment.reconfigure(CodeEditor.getLanguageSupport(language)),
this.autocompletionCompartment.reconfigure(
autocompletion({ override: [completionSource] })
),
this.placeholderCompartment.reconfigure(placeholder(editorPlaceHolder))
this.placeholderCompartment.reconfigure(placeholder(t("Papyros.code_placeholder",
{ programmingLanguage: language })))
]
});
}
Expand Down Expand Up @@ -221,7 +238,7 @@ export class CodeEditor {
* - [indenting with tab](#commands.indentWithTab)
* @return {Array<Extension} Default extensions to use
*/
static getExtensions(): Array<Extension> {
static getDefaultExtensions(): Array<Extension> {
return [
lineNumbers(),
highlightActiveLineGutter(),
Expand Down
74 changes: 68 additions & 6 deletions src/RunStateManager.ts → src/CodeRunner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { APPLICATION_STATE_TEXT_ID, RUN_BTN_ID, STATE_SPINNER_ID, STOP_BTN_ID } from "./Constants";
import { Remote } from "comlink";
import { Backend } from "./Backend";
import { CodeEditor } from "./CodeEditor";
import {
APPLICATION_STATE_TEXT_ID, RUN_BTN_ID,
STATE_SPINNER_ID, STOP_BTN_ID, DEBUG_BTN_ID
} from "./Constants";
import { ProgrammingLanguage } from "./ProgrammingLanguage";
import { svgCircle } from "./util/HTMLShapes";
import {
addListener, ButtonOptions, renderButton,
Expand All @@ -23,10 +30,37 @@ export enum RunState {
Ready = "ready"
}

/**
* All listeners that should be configured externally
*/
interface ExternalClickListeners {
/* Callback for when the run button is clicked */
onRunClicked: () => void;
/* onStopClicked Callback for when the stop button is clicked */
onStopClicked: () => void;
/* onDebugClicked Callback for when the debug button is clicked */
onDebugClicked: () => void;
}
/**
* Helper component to manage and visualize the current RunState
*/
export class RunStateManager {
export class CodeRunner {
/**
* The currently used programming language
*/
programmingLanguage: ProgrammingLanguage;
/**
* The editor in which the code is written
*/
readonly editor: CodeEditor;
/**
* The backend that executes the code asynchronously
*/
backend: Remote<Backend>;
/**
* The identifier for the current run
*/
runId: number;
/**
* Current state of the program
*/
Expand All @@ -38,31 +72,58 @@ export class RunStateManager {

/**
* Construct a new RunStateManager with the given listeners
* @param {ProgrammingLanguage} programmingLanguage The language to use
* @param {Object} clickListeners
* @param {function} onRunClicked Callback for when the run button is clicked
* @param {function} onStopClicked Callback for when the stop button is clicked
* @param {function} onDebugClicked Callback for when the debug button is clicked
*/
constructor(onRunClicked: () => void, onStopClicked: () => void) {
constructor(programmingLanguage: ProgrammingLanguage,
clickListeners: ExternalClickListeners) {
this.programmingLanguage = programmingLanguage;
this.editor = new CodeEditor();
this.backend = {} as Remote<Backend>;
this.runId = 0;
this.buttons = [];
this.addButton({
id: RUN_BTN_ID,
buttonText: t("Papyros.run"),
extraClasses: "text-white bg-blue-500"
}, onRunClicked);
}, clickListeners.onRunClicked);
this.addButton({
id: STOP_BTN_ID,
buttonText: t("Papyros.stop"),
extraClasses: "text-white bg-red-500"
}, onStopClicked);
}, clickListeners.onStopClicked);
this.addButton({
id: DEBUG_BTN_ID,
buttonText: t("Papyros.debug"),
extraClasses: "text-white bg-green-500"
}, clickListeners.onDebugClicked);
this.state = RunState.Ready;
}

async setProgrammingLanguage(programmingLanguage: ProgrammingLanguage): Promise<void> {
this.editor.setLanguage(programmingLanguage,
async context => await this.backend.autocomplete(
Backend.convertCompletionContext(context)
));
}

/**
* Get the button to run the code
*/
get runButton(): HTMLButtonElement {
return getElement<HTMLButtonElement>(RUN_BTN_ID);
}

/**
* Get the button to debug the code
*/
get debugButton(): HTMLButtonElement {
return getElement<HTMLButtonElement>(DEBUG_BTN_ID);
}

/**
* Get the button to interrupt the code
*/
Expand All @@ -78,7 +139,6 @@ export class RunStateManager {
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
Expand All @@ -90,9 +150,11 @@ export class RunStateManager {
if (state === RunState.Ready) {
this.showSpinner(false);
this.runButton.disabled = false;
this.debugButton.disabled = false;
} else {
this.showSpinner(true);
this.runButton.disabled = true;
this.debugButton.disabled = true;
}
getElement(APPLICATION_STATE_TEXT_ID).innerText =
message || t(`Papyros.states.${state}`);
Expand Down
6 changes: 5 additions & 1 deletion src/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ProgrammingLanguage } from "./ProgrammingLanguage";
function addPapyrosPrefix(s: string): string {
return `__papyros-${s}`;
}
/* Default HT%M ids for various elements */
/* Default HTML ids for various elements */
export const MAIN_APP_ID = addPapyrosPrefix("papyros");
export const OUTPUT_TA_ID = addPapyrosPrefix("code-output-area");
export const INPUT_AREA_WRAPPER_ID = addPapyrosPrefix("code-input-area-wrapper");
Expand All @@ -18,10 +18,14 @@ 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 RUN_BTN_ID = addPapyrosPrefix("run-code-btn");
export const STOP_BTN_ID = addPapyrosPrefix("stop-btn");
export const DEBUG_BTN_ID = addPapyrosPrefix("debug-btn");
export const SEND_INPUT_BTN_ID = addPapyrosPrefix("send-input-btn");
export const SWITCH_INPUT_MODE_A_ID = addPapyrosPrefix("switch-input-mode");
export const DEBUGGING_INTERACTIVE_WRAPPER_ID = addPapyrosPrefix("debugging-interactive-wrapper");

export const EXAMPLE_SELECT_ID = addPapyrosPrefix("example-select");
export const LOCALE_SELECT_ID = addPapyrosPrefix("locale-select");
export const PROGRAMMING_LANGUAGE_SELECT_ID = addPapyrosPrefix("programming-language-select");
Expand Down
Loading