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 snoop #116

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/state": "^0.20.0",
"@codemirror/theme-one-dark": "^0.20.0",
"@codemirror/view": "^0.20.3",
"ansi_up": "^5.1.0",
"comlink": "^4.3.1",
"comsync": "^0.0.8",
"escape-html": "^1.0.3",
Expand Down
9 changes: 7 additions & 2 deletions src/Backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export abstract class Backend<Extras extends SyncExtras = SyncExtras> {
* @param {function(BackendEvent):void} onEvent Callback for when events occur
* @return {Promise<void>} Promise of launching
*/
launch(
public launch(
onEvent: (e: BackendEvent) => void
): Promise<void> {
this.onEvent = (e: BackendEvent) => {
Expand All @@ -90,13 +90,18 @@ export abstract class Backend<Extras extends SyncExtras = SyncExtras> {
return Promise.resolve();
}

public runModes(): Array<string> {
return ["exec"];
}

/**
* 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 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
5 changes: 3 additions & 2 deletions src/BackendEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export enum BackendEventType {
Output = "output",
Sleep = "sleep",
Error = "error",
Interrupt = "interrupt"
Interrupt = "interrupt",
Debug = "debug"
}
/**
* All possible types for ease of iteration
Expand All @@ -17,7 +18,7 @@ export const BACKEND_EVENT_TYPES = [
BackendEventType.Start, BackendEventType.End,
BackendEventType.Input, BackendEventType.Output,
BackendEventType.Sleep, BackendEventType.Error,
BackendEventType.Interrupt
BackendEventType.Interrupt, BackendEventType.Debug
];
/**
* Interface for events used for communication between threads
Expand Down
16 changes: 7 additions & 9 deletions src/CodeEditor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable valid-jsdoc */
import { ProgrammingLanguage } from "./ProgrammingLanguage";
import { t } from "./util/Util";
import { Renderable, RenderOptions, appendClasses, renderWithOptions } from "./util/Rendering";
import { Renderable, RenderOptions, renderWithOptions } from "./util/Rendering";
import {
CompletionSource, autocompletion,
closeBrackets, closeBracketsKeymap, completionKeymap
Expand Down Expand Up @@ -38,11 +38,14 @@ const OPTIONS = [
Option.Autocompletion, Option.Linting,
Option.Style
];
export interface CodeEditorRenderOptions extends RenderOptions {
programmingLanguage: ProgrammingLanguage;
}

/**
* Component that provides useful features to users writing code
*/
export class CodeEditor extends Renderable {
export class CodeEditor extends Renderable<CodeEditorRenderOptions> {
/**
* Reference to the user interface of the editor
*/
Expand Down Expand Up @@ -88,20 +91,15 @@ export class CodeEditor extends Renderable {
});
}

/**
* Render the editor with the given options and panel
* @param {RenderOptions} options Options for rendering
* @param {HTMLElement} panel The panel to display at the bottom
* @return {HTMLElement} The rendered element
*/
protected override _render(options: RenderOptions): void {
protected override _render(options: CodeEditorRenderOptions): void {
let styleExtensions: Extension = [];
if (options.darkMode) {
styleExtensions = oneDark;
} else {
styleExtensions = syntaxHighlighting(defaultHighlightStyle, { fallback: true });
}
this.reconfigure([Option.Style, styleExtensions]);
this.setProgrammingLanguage(options.programmingLanguage);
// Ensure that the classes are added to a child of the parent so that
// dark mode classes are properly activated
// CodeMirror dom resets its classList, so that is not an option
Expand Down
112 changes: 62 additions & 50 deletions src/CodeRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { BackendEvent, BackendEventType } from "./BackendEvent";
import { BackendManager } from "./BackendManager";
import { CodeEditor } from "./CodeEditor";
import {
APPLICATION_STATE_TEXT_ID, OUTPUT_OVERFLOW_ID, RUN_BTN_ID,
STATE_SPINNER_ID, STOP_BTN_ID
APPLICATION_STATE_TEXT_ID,
STATE_SPINNER_ID, STOP_BTN_ID,
RUNNER_BUTTON_AREA_WRAPPER_ID, addPapyrosPrefix, OUTPUT_OVERFLOW_ID
} from "./Constants";
import { InputManager } from "./InputManager";
import { ProgrammingLanguage } from "./ProgrammingLanguage";
Expand All @@ -21,10 +22,17 @@ import {
renderButton, ButtonOptions, Renderable
} from "./util/Rendering";

export enum ButtonType {
Run = "run",
Stop = "stop",
Other = "other"
}

interface DynamicButton {
id: string;
buttonHTML: string;
onClick: () => void;
type: ButtonType;
}

interface CodeRunnerRenderOptions {
Expand Down Expand Up @@ -96,16 +104,6 @@ export class CodeRunner extends Renderable<CodeRunnerRenderOptions> {
});
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.addButton({
id: STOP_BTN_ID,
buttonText: t("Papyros.stop"),
classNames: "_tw-text-white _tw-bg-red-500"
}, () => this.stop());
BackendManager.subscribe(BackendEventType.Input,
() => this.setState(RunState.AwaitingInput));
this.state = RunState.Ready;
Expand All @@ -117,7 +115,20 @@ export class CodeRunner extends Renderable<CodeRunnerRenderOptions> {
async start(): Promise<void> {
this.setState(RunState.Loading);
const backend = BackendManager.getBackend(this.programmingLanguage);
this.editor.setProgrammingLanguage(this.programmingLanguage);
this.buttons = [];
(await backend.workerProxy.runModes()).forEach(mode => {
this.addButton({
id: addPapyrosPrefix(`run-${mode}-btn`),
buttonText: t(`Papyros.run_modes.${mode}`, { defaultValue: mode }),
classNames: "_tw-text-white _tw-bg-blue-500"
}, () => this.runCode(mode), ButtonType.Run);
});
this.addButton({
id: STOP_BTN_ID,
buttonText: t("Papyros.stop"),
classNames: "_tw-text-white _tw-bg-red-500"
}, () => this.stop(), ButtonType.Stop);
this.renderButtons();
// Use a Promise to immediately enable running while downloading
// eslint-disable-next-line no-async-promise-executor
this.backend = new Promise(async resolve => {
Expand All @@ -140,6 +151,7 @@ export class CodeRunner extends Renderable<CodeRunnerRenderOptions> {
});
return resolve(backend);
});
this.editor.setProgrammingLanguage(this.programmingLanguage);
this.editor.focus();
this.setState(RunState.Ready);
}
Expand Down Expand Up @@ -173,18 +185,10 @@ export class CodeRunner extends Renderable<CodeRunnerRenderOptions> {
return this.programmingLanguage;
}

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

/**
* Get the button to interrupt the code
*/
get stopButton(): HTMLButtonElement {
return getElement<HTMLButtonElement>(STOP_BTN_ID);
getButtons(type: ButtonType): Array<HTMLButtonElement> {
return this.buttons.filter(b => b.type === type)
.map(b => getElement<HTMLButtonElement>(b.id))
.filter(Boolean);
}

/**
Expand All @@ -202,14 +206,13 @@ export class CodeRunner extends Renderable<CodeRunnerRenderOptions> {
*/
setState(state: RunState, message?: string): void {
this.state = state;
this.stopButton.disabled = [RunState.Ready, RunState.Loading].includes(state);
if (state === RunState.Ready) {
this.showSpinner(false);
this.runButton.disabled = false;
} else {
this.showSpinner(true);
this.runButton.disabled = true;
}
this.getButtons(ButtonType.Stop).forEach(b => {
b.disabled = [RunState.Ready, RunState.Loading].includes(state);
});
this.getButtons(ButtonType.Run).forEach(b => {
b.disabled = ![RunState.Ready].includes(state);
});
this.showSpinner(![RunState.Ready].includes(state));
getElement(APPLICATION_STATE_TEXT_ID).innerText =
message || t(`Papyros.states.${state}`);
}
Expand All @@ -222,42 +225,51 @@ export class CodeRunner extends Renderable<CodeRunnerRenderOptions> {
* 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
* @param {ButtonType} type The type of the button
*/
addButton(options: ButtonOptions, onClick: () => void): void {
addButton(options: ButtonOptions, onClick: () => void, type: ButtonType): void {
this.buttons.push({
id: options.id,
buttonHTML: renderButton(options),
onClick: onClick
onClick: onClick,
type: type
});
}

protected override _render(options: CodeRunnerRenderOptions): HTMLElement {
const rendered = renderWithOptions(options.statusPanelOptions, `
private renderButtons(): void {
getElement(RUNNER_BUTTON_AREA_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"));
}

protected override _render(options: CodeRunnerRenderOptions): void {
const panel = 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>
<div class="_tw-col-span-1 _tw-flex _tw-flex-row-reverse _tw-items-center">
<div id="${RUNNER_BUTTON_AREA_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>
${spinningCircle(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.inputManager.render(options.inputOptions);
this.editor.render(options.codeEditorOptions);
this.editor.setPanel(rendered);
// Set language again to update the placeholder
this.editor.setProgrammingLanguage(this.programmingLanguage);
return rendered;
this.editor.render({
...options.codeEditorOptions,
programmingLanguage: this.programmingLanguage
});
this.editor.setPanel(panel);
this.renderButtons();
}

/**
* Run the code that is currently present in the editor
* @param {string} mode The mode to run the code in
* @return {Promise<void>} Promise of running the code
*/
async runCode(): Promise<void> {
async runCode(mode: string): Promise<void> {
// Setup pre-run
this.setState(RunState.Running);
BackendManager.publish({
Expand All @@ -272,7 +284,7 @@ export class CodeRunner extends Renderable<CodeRunnerRenderOptions> {
const backend = await this.backend;
try {
await backend.call(
backend.workerProxy.runCode, this.editor.getCode()
backend.workerProxy.runCode, this.editor.getCode(), mode
);
} catch (error: any) {
papyrosLog(LogType.Debug, "Error during code run", error);
Expand Down
4 changes: 2 additions & 2 deletions src/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ProgrammingLanguage } from "./ProgrammingLanguage";
* @param {string} s The value to add a prefix to
* @return {string} The value with an almost certainly unused prefix
*/
function addPapyrosPrefix(s: string): string {
export function addPapyrosPrefix(s: string): string {
return `__papyros-${s}`;
}
/* Default HTML ids for various elements */
Expand All @@ -18,9 +18,9 @@ export const INPUT_TA_ID = addPapyrosPrefix("code-input-area");
export const USER_INPUT_WRAPPER_ID = addPapyrosPrefix("user-input-wrapper");
export const EDITOR_WRAPPER_ID = addPapyrosPrefix("code-area");
export const PANEL_WRAPPER_ID = addPapyrosPrefix("code-status-panel");
export const RUNNER_BUTTON_AREA_WRAPPER_ID = addPapyrosPrefix("runner-button-area-wrapper");
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 SEND_INPUT_BTN_ID = addPapyrosPrefix("send-input-btn");
export const SWITCH_INPUT_MODE_A_ID = addPapyrosPrefix("switch-input-mode");
Expand Down
2 changes: 1 addition & 1 deletion src/InputManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class InputManager extends Renderable {
</a>`;

renderWithOptions(options, `
<div id="${USER_INPUT_WRAPPER_ID}" class="_tw-my-1">
<div id="${USER_INPUT_WRAPPER_ID}" class="_tw-my-1 papyros-font-family">
</div>
${switchMode}`);
addListener<InputMode>(SWITCH_INPUT_MODE_A_ID, im => this.setInputMode(im),
Expand Down
9 changes: 8 additions & 1 deletion src/OutputManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
RenderOptions, renderWithOptions
} from "./util/Rendering";
import { OUTPUT_AREA_ID, OUTPUT_OVERFLOW_ID } from "./Constants";
import AnsiUp from "ansi_up";

/**
* Shape of Error objects that are easy to interpret
Expand Down Expand Up @@ -51,14 +52,20 @@ export class OutputManager extends Renderable {
* Store the HTML that is rendered to restore when changing language/theme
*/
private content: Array<string>;
/**
* Allow converting ansi strings to proper HTML
*/
private ansiUp: AnsiUp;

constructor() {
super();
this.content = [];
BackendManager.subscribe(BackendEventType.Start, () => this.reset());
BackendManager.subscribe(BackendEventType.Output, e => this.showOutput(e));
BackendManager.subscribe(BackendEventType.Debug, e => this.showOutput(e));
BackendManager.subscribe(BackendEventType.Error, e => this.showError(e));
BackendManager.subscribe(BackendEventType.End, () => this.onRunEnd());
this.ansiUp = new AnsiUp();
}

/**
Expand Down Expand Up @@ -96,7 +103,7 @@ export class OutputManager extends Renderable {
.filter(line => !ignoreEmpty || line.trim().length > 0)
.join("\n");
}
return `<span class="${className}">${escapeHTML(spanText)}</span>`;
return `<span class="${className}">${this.ansiUp.ansi_to_html(spanText)}</span>`;
}

/**
Expand Down
7 changes: 4 additions & 3 deletions src/Papyros.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
removeSelection,
addListener, getElement, cleanCurrentUrl
} from "./util/Util";
import { RunState, CodeRunner } from "./CodeRunner";
import { RunState, CodeRunner, ButtonType } from "./CodeRunner";
import { getCodeForExample, getExampleNames } from "./examples/Examples";
import { OutputManager } from "./OutputManager";
import { AtomicsChannelOptions, makeChannel, ServiceWorkerChannelOptions } from "sync-message";
Expand Down Expand Up @@ -343,9 +343,10 @@ export class Papyros extends Renderable<PapyrosRenderOptions> {
* Add a button to the status panel within Papyros
* @param {ButtonOptions} options Options to render the button with
* @param {function} onClick Listener for click events on the button
* @param {ButtonType} type The type of the button
*/
addButton(options: ButtonOptions, onClick: () => void): void {
this.codeRunner.addButton(options, onClick);
addButton(options: ButtonOptions, onClick: () => void, type: ButtonType): void {
this.codeRunner.addButton(options, onClick, type);
}

/**
Expand Down
Loading