import { basicSetup, EditorView } from 'codemirror';
import { python } from '@codemirror/lang-python';
import { indentUnit } from '@codemirror/language';
import { Compartment } from '@codemirror/state';
import { keymap, Command } from '@codemirror/view';
import { defaultKeymap } from '@codemirror/commands';
import { oneDarkTheme } from '@codemirror/theme-one-dark';
import { ensureUniqueId, htmlDecode } from '../utils';
import { pyExec } from '../pyexec';
import { getLogger } from '../logger';
import { InterpreterClient } from '../interpreter_client';
import type { PyScriptApp } from '../main';
import { Stdio } from '../stdio';
const logger = getLogger('py-repl');
const RUNBUTTON = ``;
export function make_PyRepl(interpreter: InterpreterClient, app: PyScriptApp) {
/* High level structure of py-repl DOM, and the corresponding JS names.
this
boxDiv
*/
class PyRepl extends HTMLElement {
outDiv: HTMLElement;
editor: EditorView;
stdout_manager: Stdio | null;
stderr_manager: Stdio | null;
connectedCallback() {
ensureUniqueId(this);
if (!this.hasAttribute('exec-id')) {
this.setAttribute('exec-id', '0');
}
if (!this.hasAttribute('root')) {
this.setAttribute('root', this.id);
}
const pySrc = htmlDecode(this.innerHTML).trim();
this.innerHTML = '';
this.editor = this.makeEditor(pySrc);
const boxDiv = this.makeBoxDiv();
this.appendChild(boxDiv);
this.editor.focus();
logger.debug(`element ${this.id} successfully connected`);
}
/** Create and configure the codemirror editor
*/
makeEditor(pySrc: string): EditorView {
const languageConf = new Compartment();
const extensions = [
indentUnit.of(' '),
basicSetup,
languageConf.of(python()),
keymap.of([
...defaultKeymap,
{ key: 'Ctrl-Enter', run: this.execute.bind(this) as Command, preventDefault: true },
{ key: 'Shift-Enter', run: this.execute.bind(this) as Command, preventDefault: true },
]),
];
if (this.getAttribute('theme') === 'dark') {
extensions.push(oneDarkTheme);
}
return new EditorView({
doc: pySrc,
extensions,
});
}
// ******** main entry point for py-repl DOM building **********
//
// The following functions are written in a top-down, depth-first
// order (so that the order of code roughly matches the order of
// execution)
makeBoxDiv(): HTMLElement {
const boxDiv = document.createElement('div');
boxDiv.className = 'py-repl-box';
const editorDiv = this.makeEditorDiv();
this.outDiv = this.makeOutDiv();
boxDiv.appendChild(editorDiv);
boxDiv.appendChild(this.outDiv);
return boxDiv;
}
makeEditorDiv(): HTMLElement {
const editorDiv = document.createElement('div');
editorDiv.className = 'py-repl-editor';
editorDiv.setAttribute('aria-label', 'Python Script Area');
editorDiv.appendChild(this.editor.dom);
const runButton = this.makeRunButton();
editorDiv.appendChild(runButton);
return editorDiv;
}
makeRunButton(): HTMLElement {
const runButton = document.createElement('button');
runButton.className = 'absolute py-repl-run-button';
runButton.innerHTML = RUNBUTTON;
runButton.setAttribute('aria-label', 'Python Script Run Button');
runButton.addEventListener('click', this.execute.bind(this) as (e: MouseEvent) => void);
return runButton;
}
makeOutDiv(): HTMLElement {
const outDiv = document.createElement('div');
outDiv.className = 'py-repl-output';
outDiv.id = this.id + '-repl-output';
return outDiv;
}
// ********************* execution logic *********************
/** Execute the python code written in the editor, and automatically
* display() the last evaluated expression
*/
async execute(): Promise {
const pySrc = this.getPySrc();
const outEl = this.outDiv;
// execute the python code
await app.plugins.beforePyReplExec({ interpreter: interpreter, src: pySrc, outEl: outEl, pyReplTag: this });
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { result } = await pyExec(interpreter, pySrc, outEl);
await app.plugins.afterPyReplExec({
interpreter: interpreter,
src: pySrc,
outEl: outEl,
pyReplTag: this,
result, // eslint-disable-line @typescript-eslint/no-unsafe-assignment
});
this.autogenerateMaybe();
}
getPySrc(): string {
return this.editor.state.doc.toString();
}
// XXX the autogenerate logic is very messy. We should redo it, and it
// should be the default.
autogenerateMaybe(): void {
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');
//Attributes to be copied from old REPL to auto-generated REPL
for (const attribute of ['root', 'output-mode', 'output', 'stderr']) {
const attr = this.getAttribute(attribute);
if (attr) {
newPyRepl.setAttribute(attribute, attr);
}
}
newPyRepl.id = this.getAttribute('root') + '-' + nextExecId.toString();
if (this.hasAttribute('auto-generate')) {
newPyRepl.setAttribute('auto-generate', '');
this.removeAttribute('auto-generate');
}
newPyRepl.setAttribute('exec-id', nextExecId.toString());
if (this.parentElement) {
this.parentElement.appendChild(newPyRepl);
}
}
}
}
return PyRepl;
}