diff options
author | Trey Hunner <trey@treyhunner.com> | 2022-07-01 09:52:58 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-07-01 09:52:58 (GMT) |
commit | a8e333d79aa639417e496181bcbad2cb801a7a56 (patch) | |
tree | d136e1ebf4dc07e41c52147b6cb1fe9a7bfd4a47 | |
parent | 5f2c91a343fb20f8c2fc78cbb3e68234bcba40a8 (diff) | |
download | cpython-a8e333d79aa639417e496181bcbad2cb801a7a56.zip cpython-a8e333d79aa639417e496181bcbad2cb801a7a56.tar.gz cpython-a8e333d79aa639417e496181bcbad2cb801a7a56.tar.bz2 |
gh-84461: Improve WebAssembly in-browser demo (#91879)
* Buffer standard input line-by-line
* Add non-root .editorconfig for JS & HTML indent
* Add support for clearing REPL with CTRL+L
* Support unicode in stdout and stderr
* Remove \r\n normalization
* Note that local .editorconfig file extends root
* Only normalize lone \r characters (convert to \n)
* Skip non-printable characters in buffered input
* Fix Safari bug (regex lookbehind not supported)
Co-authored-by: Christian Heimes <christian@python.org>
-rw-r--r-- | Tools/wasm/.editorconfig | 7 | ||||
-rw-r--r-- | Tools/wasm/python.html | 109 | ||||
-rw-r--r-- | Tools/wasm/python.worker.js | 8 |
3 files changed, 99 insertions, 25 deletions
diff --git a/Tools/wasm/.editorconfig b/Tools/wasm/.editorconfig new file mode 100644 index 0000000..da1aa6a --- /dev/null +++ b/Tools/wasm/.editorconfig @@ -0,0 +1,7 @@ +root = false # This extends the root .editorconfig + +[*.{html,js}] +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 diff --git a/Tools/wasm/python.html b/Tools/wasm/python.html index c8d1748..41cf5fc 100644 --- a/Tools/wasm/python.html +++ b/Tools/wasm/python.html @@ -100,6 +100,7 @@ class WorkerManager { class WasmTerminal { constructor() { + this.inputBuffer = new BufferQueue(); this.input = '' this.resolveInput = null this.activeInput = false @@ -123,28 +124,47 @@ class WasmTerminal { this.xterm.open(container); } - handleReadComplete(lastChar) { - this.resolveInput(this.input + lastChar) - this.activeInput = false - } - handleTermData = (data) => { - if (!this.activeInput) { - return - } const ord = data.charCodeAt(0); - let ofs; + data = data.replace(/\r(?!\n)/g, "\n") // Convert lone CRs to LF + // Handle pasted data + if (data.length > 1 && data.includes("\n")) { + let alreadyWrittenChars = 0; + // If line already had data on it, merge pasted data with it + if (this.input != '') { + this.inputBuffer.addData(this.input); + alreadyWrittenChars = this.input.length; + this.input = ''; + } + this.inputBuffer.addData(data); + // If input is active, write the first line + if (this.activeInput) { + let line = this.inputBuffer.nextLine(); + this.writeLine(line.slice(alreadyWrittenChars)); + this.resolveInput(line); + this.activeInput = false; + } + // When input isn't active, add to line buffer + } else if (!this.activeInput) { + // Skip non-printable characters + if (!(ord === 0x1b || ord == 0x7f || ord < 32)) { + this.inputBuffer.addData(data); + } // TODO: Handle ANSI escape sequences - if (ord === 0x1b) { + } else if (ord === 0x1b) { // Handle special characters } else if (ord < 32 || ord === 0x7f) { switch (data) { - case "\r": // ENTER + case "\x0c": // CTRL+L + this.clear(); + break; + case "\n": // ENTER case "\x0a": // CTRL+J case "\x0d": // CTRL+M - this.xterm.write('\r\n'); - this.handleReadComplete('\n'); + this.resolveInput(this.input + this.writeLine('\n')); + this.input = ''; + this.activeInput = false; break; case "\x7F": // BACKSPACE case "\x08": // CTRL+H @@ -157,6 +177,12 @@ class WasmTerminal { } } + writeLine(line) { + this.xterm.write(line.slice(0, -1)) + this.xterm.write('\r\n'); + return line; + } + handleCursorInsert(data) { this.input += data; this.xterm.write(data) @@ -176,9 +202,19 @@ class WasmTerminal { this.activeInput = true // Hack to allow stdout/stderr to finish before we figure out where input starts setTimeout(() => {this.inputStartCursor = this.xterm.buffer.active.cursorX}, 1) + // If line buffer has a line ready, send it immediately + if (this.inputBuffer.hasLineReady()) { + return new Promise((resolve, reject) => { + resolve(this.writeLine(this.inputBuffer.nextLine())); + this.activeInput = false; + }) + // If line buffer has an incomplete line, use it for the active line + } else if (this.inputBuffer.lastLineIsIncomplete()) { + // Hack to ensure cursor input start doesn't end up after user input + setTimeout(() => {this.handleCursorInsert(this.inputBuffer.nextLine())}, 1); + } return new Promise((resolve, reject) => { this.resolveInput = (value) => { - this.input = '' resolve(value) } }) @@ -188,9 +224,44 @@ class WasmTerminal { this.xterm.clear(); } - print(message) { - const normInput = message.replace(/[\r\n]+/g, "\n").replace(/\n/g, "\r\n"); - this.xterm.write(normInput); + print(charCode) { + let array = [charCode]; + if (charCode == 10) { + array = [13, 10]; // Replace \n with \r\n + } + this.xterm.write(new Uint8Array(array)); + } +} + +class BufferQueue { + constructor(xterm) { + this.buffer = [] + } + + isEmpty() { + return this.buffer.length == 0 + } + + lastLineIsIncomplete() { + return !this.isEmpty() && !this.buffer[this.buffer.length-1].endsWith("\n") + } + + hasLineReady() { + return !this.isEmpty() && this.buffer[0].endsWith("\n") + } + + addData(data) { + let lines = data.match(/.*(\n|$)/g) + if (this.lastLineIsIncomplete()) { + this.buffer[this.buffer.length-1] += lines.shift() + } + for (let line of lines) { + this.buffer.push(line) + } + } + + nextLine() { + return this.buffer.shift() } } @@ -202,8 +273,8 @@ window.onload = () => { terminal.open(document.getElementById('terminal')) const stdio = { - stdout: (s) => { terminal.print(s) }, - stderr: (s) => { terminal.print(s) }, + stdout: (charCode) => { terminal.print(charCode) }, + stderr: (charCode) => { terminal.print(charCode) }, stdin: async () => { return await terminal.prompt() } diff --git a/Tools/wasm/python.worker.js b/Tools/wasm/python.worker.js index c3a8bdf..1b79460 100644 --- a/Tools/wasm/python.worker.js +++ b/Tools/wasm/python.worker.js @@ -35,15 +35,11 @@ class StdinBuffer { } } -const stdoutBufSize = 128; -const stdoutBuf = new Int32Array() -let index = 0; - const stdout = (charCode) => { if (charCode) { postMessage({ type: 'stdout', - stdout: String.fromCharCode(charCode), + stdout: charCode, }) } else { console.log(typeof charCode, charCode) @@ -54,7 +50,7 @@ const stderr = (charCode) => { if (charCode) { postMessage({ type: 'stderr', - stderr: String.fromCharCode(charCode), + stderr: charCode, }) } else { console.log(typeof charCode, charCode) |