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
editorDiv
outDiv
*/ 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; }