From ca58ca8641267c67dc6a7e67ee4009906b350bbb Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Fri, 1 Jul 2022 03:16:25 -0700 Subject: [PATCH] gh-84461: Improve WebAssembly in-browser demo (GH-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 (cherry picked from commit a8e333d79aa639417e496181bcbad2cb801a7a56) Co-authored-by: Trey Hunner --- Tools/wasm/.editorconfig | 7 +++ Tools/wasm/python.html | 109 +++++++++++++++++++++++++++++------- Tools/wasm/python.worker.js | 8 +-- 3 files changed, 99 insertions(+), 25 deletions(-) create mode 100644 Tools/wasm/.editorconfig diff --git a/Tools/wasm/.editorconfig b/Tools/wasm/.editorconfig new file mode 100644 index 00000000000000..da1aa6acaccc7e --- /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 c8d17488b2e70d..41cf5fcf6b6dfe 100644 --- a/Tools/wasm/python.html +++ b/Tools/wasm/python.html @@ -100,6 +100,7 @@ class WasmTerminal { constructor() { + this.inputBuffer = new BufferQueue(); this.input = '' this.resolveInput = null this.activeInput = false @@ -123,28 +124,47 @@ 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 @@ } } + 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 @@ 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 @@ 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 @@ 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 c3a8bdf7d2fc29..1b794608fffe7b 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)