kill stores.runtimeLoaded and many other stores (#850)

As the title stays, the main goal of the branch is to kill the infamous runtimeLoaded global store and all the complications, problems and bugs caused by the fact that in many places we needed to ensure/wait that the global runtime was properly set before being able to execute code.

The core idea is that runtime is never a global object and that it's passed around explicitly, which means that when a function receives it, it is guaranteed to be initialized&ready.

This caused a bit of complications in pybutton.ts, pyinputbox.ts and pyrepl.ts, because they indirectly want to call runtime.run from connectedCallback, which is the only place where we cannot explicitly pass the runtime because it's automatically called by the browser.
But also, it is also a sign of a bad design, because it were entirely possible that connectedCallback was called before the runtime was ready, which probably caused many bugs, see e.g. #673 and #747.

The solution to is use dependency injection and create the class later on: so instead of having a global PyButton class which relies on a global runtime (whose state is uncertain) we have a make_PyButton function which takes a runtime and make a PyButton class which is tied to that specific runtime (whose state is certainly ready, because we call make_PyButton only when we know that the runtime is ready).
Similar for PyInputBox and PyRepl.

Other highlights: thanks to this, I could kill the also infamous runAfterRuntimeInitialized and a couple of smelly lines which used setTimeout to "wait" for the runtime.

While I was at it, I also called a lot of other stores which were completely unused and where probably leftovers from a past universe.
This commit is contained in:
Antonio Cuni
2022-10-17 10:31:57 +02:00
committed by GitHub
parent fe708c9fb4
commit beb3aa1574
15 changed files with 483 additions and 554 deletions

View File

@@ -7,195 +7,200 @@ import { defaultKeymap } from '@codemirror/commands';
import { oneDarkTheme } from '@codemirror/theme-one-dark';
import { addClasses, htmlDecode } from '../utils';
import { BaseEvalElement } from './base';
import type { Runtime } from '../runtime';
import { getLogger } from '../logger';
const logger = getLogger('py-repl');
export function make_PyRepl(runtime: Runtime) {
function createCmdHandler(el: PyRepl): StateCommand {
// Creates a codemirror cmd handler that calls the el.evaluate when an event
// triggers that specific cmd
return () => {
void el.evaluate();
return true;
};
}
let initialTheme: string;
function getEditorTheme(el: BaseEvalElement): string {
return initialTheme || (initialTheme = el.getAttribute('theme'));
}
export class PyRepl extends BaseEvalElement {
editor: EditorView;
editorNode: HTMLElement;
constructor() {
super();
// add an extra div where we can attach the codemirror editor
this.editorNode = document.createElement('div');
addClasses(this.editorNode, ['editor-box']);
this.shadow.appendChild(this.wrapper);
function createCmdHandler(el: PyRepl): StateCommand {
// Creates a codemirror cmd handler that calls the el.evaluate when an event
// triggers that specific cmd
return () => {
void el.evaluate(runtime);
return true;
};
}
connectedCallback() {
this.checkId();
this.code = htmlDecode(this.innerHTML);
this.innerHTML = '';
const languageConf = new Compartment();
let initialTheme: string;
function getEditorTheme(el: BaseEvalElement): string {
return initialTheme || (initialTheme = el.getAttribute('theme'));
}
const extensions = [
indentUnit.of(" "),
basicSetup,
languageConf.of(python()),
keymap.of([
...defaultKeymap,
{ key: 'Ctrl-Enter', run: createCmdHandler(this) },
{ key: 'Shift-Enter', run: createCmdHandler(this) },
]),
];
class PyRepl extends BaseEvalElement {
editor: EditorView;
editorNode: HTMLElement;
if (getEditorTheme(this) === 'dark') {
extensions.push(oneDarkTheme);
constructor() {
super();
// add an extra div where we can attach the codemirror editor
this.editorNode = document.createElement('div');
addClasses(this.editorNode, ['editor-box']);
this.shadow.appendChild(this.wrapper);
}
this.editor = new EditorView({
doc: this.code.trim(),
extensions,
parent: this.editorNode,
});
connectedCallback() {
this.checkId();
this.code = htmlDecode(this.innerHTML);
this.innerHTML = '';
const languageConf = new Compartment();
const mainDiv = document.createElement('div');
addClasses(mainDiv, ['py-repl-box']);
const extensions = [
indentUnit.of(" "),
basicSetup,
languageConf.of(python()),
keymap.of([
...defaultKeymap,
{ key: 'Ctrl-Enter', run: createCmdHandler(this) },
{ key: 'Shift-Enter', run: createCmdHandler(this) },
]),
];
// Styles that we use to hide the labels whilst also keeping it accessible for screen readers
const labelStyle = 'overflow:hidden; display:block; width:1px; height:1px';
if (getEditorTheme(this) === 'dark') {
extensions.push(oneDarkTheme);
}
// Code editor Label
this.editorNode.id = 'code-editor';
const editorLabel = document.createElement('label');
editorLabel.innerHTML = 'Python Script Area';
editorLabel.setAttribute('style', labelStyle);
editorLabel.htmlFor = 'code-editor';
this.editor = new EditorView({
doc: this.code.trim(),
extensions,
parent: this.editorNode,
});
mainDiv.append(editorLabel);
const mainDiv = document.createElement('div');
addClasses(mainDiv, ['py-repl-box']);
// add Editor to main PyScript div
mainDiv.appendChild(this.editorNode);
// Styles that we use to hide the labels whilst also keeping it accessible for screen readers
const labelStyle = 'overflow:hidden; display:block; width:1px; height:1px';
// Play Button
this.btnRun = document.createElement('button');
this.btnRun.id = 'btnRun';
this.btnRun.innerHTML =
'<svg id="" style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>';
addClasses(this.btnRun, ['absolute', 'repl-play-button']);
// Code editor Label
this.editorNode.id = 'code-editor';
const editorLabel = document.createElement('label');
editorLabel.innerHTML = 'Python Script Area';
editorLabel.setAttribute('style', labelStyle);
editorLabel.htmlFor = 'code-editor';
// Play Button Label
const btnLabel = document.createElement('label');
btnLabel.innerHTML = 'Python Script Run Button';
btnLabel.setAttribute('style', labelStyle);
btnLabel.htmlFor = 'btnRun';
mainDiv.append(editorLabel);
this.editorNode.appendChild(btnLabel);
this.editorNode.appendChild(this.btnRun);
// add Editor to main PyScript div
mainDiv.appendChild(this.editorNode);
this.btnRun.addEventListener('click', () => {
void this.evaluate();
});
// Play Button
this.btnRun = document.createElement('button');
this.btnRun.id = 'btnRun';
this.btnRun.innerHTML =
'<svg id="" style="height:20px;width:20px;vertical-align:-.125em;transform-origin:center;overflow:visible;color:green" viewBox="0 0 384 512" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg"><g transform="translate(192 256)" transform-origin="96 0"><g transform="translate(0,0) scale(1,1)"><path d="M361 215C375.3 223.8 384 239.3 384 256C384 272.7 375.3 288.2 361 296.1L73.03 472.1C58.21 482 39.66 482.4 24.52 473.9C9.377 465.4 0 449.4 0 432V80C0 62.64 9.377 46.63 24.52 38.13C39.66 29.64 58.21 29.99 73.03 39.04L361 215z" fill="currentColor" transform="translate(-192 -256)"></path></g></g></svg>';
addClasses(this.btnRun, ['absolute', 'repl-play-button']);
if (!this.id) {
logger.warn(
"WARNING: <py-repl> defined without an id. <py-repl> should always have an id, otherwise multiple <py-repl> in the same page will not work!"
);
}
// Play Button Label
const btnLabel = document.createElement('label');
btnLabel.innerHTML = 'Python Script Run Button';
btnLabel.setAttribute('style', labelStyle);
btnLabel.htmlFor = 'btnRun';
if (!this.hasAttribute('exec-id')) {
this.setAttribute('exec-id', '1');
}
this.editorNode.appendChild(btnLabel);
this.editorNode.appendChild(this.btnRun);
if (!this.hasAttribute('root')) {
this.setAttribute('root', this.id);
}
this.btnRun.addEventListener('click', () => {
void this.evaluate(runtime);
});
if (this.hasAttribute('output')) {
this.errorElement = this.outputElement = document.getElementById(this.getAttribute('output'));
} else {
if (this.hasAttribute('std-out')) {
this.outputElement = document.getElementById(this.getAttribute('std-out'));
if (!this.id) {
logger.warn(
"WARNING: <py-repl> defined without an id. <py-repl> should always have an id, otherwise multiple <py-repl> in the same page will not work!"
);
}
if (!this.hasAttribute('exec-id')) {
this.setAttribute('exec-id', '1');
}
if (!this.hasAttribute('root')) {
this.setAttribute('root', this.id);
}
if (this.hasAttribute('output')) {
this.errorElement = this.outputElement = document.getElementById(this.getAttribute('output'));
} else {
// In this case neither output or std-out have been provided so we need
// to create a new output div to output to
this.outputElement = document.createElement('div');
this.outputElement.classList.add('output');
this.outputElement.hidden = true;
this.outputElement.id = this.id + '-' + this.getAttribute('exec-id');
if (this.hasAttribute('std-out')) {
this.outputElement = document.getElementById(this.getAttribute('std-out'));
} else {
// In this case neither output or std-out have been provided so we need
// to create a new output div to output to
this.outputElement = document.createElement('div');
this.outputElement.classList.add('output');
this.outputElement.hidden = true;
this.outputElement.id = this.id + '-' + this.getAttribute('exec-id');
// add the output div id if there's not output pre-defined
mainDiv.appendChild(this.outputElement);
}
this.errorElement = this.hasAttribute('std-err')
? document.getElementById(this.getAttribute('std-err'))
: this.outputElement;
}
this.appendChild(mainDiv);
this.editor.focus();
logger.debug(`element ${this.id} successfully connected`);
}
addToOutput(s: string): void {
this.outputElement.innerHTML += '<div>' + s + '</div>';
this.outputElement.hidden = false;
}
preEvaluate(): void {
this.setOutputMode("replace");
if(!this.appendOutput) {
this.outputElement.innerHTML = '';
}
}
postEvaluate(): void {
this.outputElement.hidden = false;
this.outputElement.style.display = 'block';
if (this.hasAttribute('auto-generate')) {
const allPyRepls = document.querySelectorAll(`py-repl[root='${this.getAttribute('root')}'][exec-id]`);
const lastRepl = allPyRepls[allPyRepls.length - 1];
const lastExecId = lastRepl.getAttribute('exec-id');
const nextExecId = parseInt(lastExecId) + 1;
const newPyRepl = document.createElement('py-repl');
newPyRepl.setAttribute('root', this.getAttribute('root'));
newPyRepl.id = this.getAttribute('root') + '-' + nextExecId.toString();
if(this.hasAttribute('auto-generate')) {
newPyRepl.setAttribute('auto-generate', '');
this.removeAttribute('auto-generate');
}
if(this.hasAttribute('output-mode')) {
newPyRepl.setAttribute('output-mode', this.getAttribute('output-mode'));
}
const addReplAttribute = (attribute: string) => {
if (this.hasAttribute(attribute)) {
newPyRepl.setAttribute(attribute, this.getAttribute(attribute));
// add the output div id if there's not output pre-defined
mainDiv.appendChild(this.outputElement);
}
};
addReplAttribute('output');
addReplAttribute('std-out');
addReplAttribute('std-err');
this.errorElement = this.hasAttribute('std-err')
? document.getElementById(this.getAttribute('std-err'))
: this.outputElement;
}
newPyRepl.setAttribute('exec-id', nextExecId.toString());
this.parentElement.appendChild(newPyRepl);
this.appendChild(mainDiv);
this.editor.focus();
logger.debug(`element ${this.id} successfully connected`);
}
addToOutput(s: string): void {
this.outputElement.innerHTML += '<div>' + s + '</div>';
this.outputElement.hidden = false;
}
preEvaluate(): void {
this.setOutputMode("replace");
if(!this.appendOutput) {
this.outputElement.innerHTML = '';
}
}
postEvaluate(): void {
this.outputElement.hidden = false;
this.outputElement.style.display = 'block';
if (this.hasAttribute('auto-generate')) {
const allPyRepls = document.querySelectorAll(`py-repl[root='${this.getAttribute('root')}'][exec-id]`);
const lastRepl = allPyRepls[allPyRepls.length - 1];
const lastExecId = lastRepl.getAttribute('exec-id');
const nextExecId = parseInt(lastExecId) + 1;
const newPyRepl = document.createElement('py-repl');
newPyRepl.setAttribute('root', this.getAttribute('root'));
newPyRepl.id = this.getAttribute('root') + '-' + nextExecId.toString();
if(this.hasAttribute('auto-generate')) {
newPyRepl.setAttribute('auto-generate', '');
this.removeAttribute('auto-generate');
}
if(this.hasAttribute('output-mode')) {
newPyRepl.setAttribute('output-mode', this.getAttribute('output-mode'));
}
const addReplAttribute = (attribute: string) => {
if (this.hasAttribute(attribute)) {
newPyRepl.setAttribute(attribute, this.getAttribute(attribute));
}
};
addReplAttribute('output');
addReplAttribute('std-out');
addReplAttribute('std-err');
newPyRepl.setAttribute('exec-id', nextExecId.toString());
this.parentElement.appendChild(newPyRepl);
}
}
getSourceFromElement(): string {
return this.editor.state.doc.toString();
}
}
getSourceFromElement(): string {
return this.editor.state.doc.toString();
}
return PyRepl
}