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>
This commit is contained in:
Trey Hunner 2022-07-01 02:52:58 -07:00 committed by GitHub
parent 5f2c91a343
commit a8e333d79a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 99 additions and 25 deletions

7
Tools/wasm/.editorconfig Normal file
View file

@ -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

View file

@ -100,6 +100,7 @@ class WorkerManager {
class WasmTerminal { class WasmTerminal {
constructor() { constructor() {
this.inputBuffer = new BufferQueue();
this.input = '' this.input = ''
this.resolveInput = null this.resolveInput = null
this.activeInput = false this.activeInput = false
@ -123,28 +124,47 @@ class WasmTerminal {
this.xterm.open(container); this.xterm.open(container);
} }
handleReadComplete(lastChar) {
this.resolveInput(this.input + lastChar)
this.activeInput = false
}
handleTermData = (data) => { handleTermData = (data) => {
if (!this.activeInput) {
return
}
const ord = data.charCodeAt(0); 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 // TODO: Handle ANSI escape sequences
if (ord === 0x1b) { } else if (ord === 0x1b) {
// Handle special characters // Handle special characters
} else if (ord < 32 || ord === 0x7f) { } else if (ord < 32 || ord === 0x7f) {
switch (data) { switch (data) {
case "\r": // ENTER case "\x0c": // CTRL+L
this.clear();
break;
case "\n": // ENTER
case "\x0a": // CTRL+J case "\x0a": // CTRL+J
case "\x0d": // CTRL+M case "\x0d": // CTRL+M
this.xterm.write('\r\n'); this.resolveInput(this.input + this.writeLine('\n'));
this.handleReadComplete('\n'); this.input = '';
this.activeInput = false;
break; break;
case "\x7F": // BACKSPACE case "\x7F": // BACKSPACE
case "\x08": // CTRL+H 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) { handleCursorInsert(data) {
this.input += data; this.input += data;
this.xterm.write(data) this.xterm.write(data)
@ -176,9 +202,19 @@ class WasmTerminal {
this.activeInput = true this.activeInput = true
// Hack to allow stdout/stderr to finish before we figure out where input starts // Hack to allow stdout/stderr to finish before we figure out where input starts
setTimeout(() => {this.inputStartCursor = this.xterm.buffer.active.cursorX}, 1) 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) => { return new Promise((resolve, reject) => {
this.resolveInput = (value) => { this.resolveInput = (value) => {
this.input = ''
resolve(value) resolve(value)
} }
}) })
@ -188,9 +224,44 @@ class WasmTerminal {
this.xterm.clear(); this.xterm.clear();
} }
print(message) { print(charCode) {
const normInput = message.replace(/[\r\n]+/g, "\n").replace(/\n/g, "\r\n"); let array = [charCode];
this.xterm.write(normInput); 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')) terminal.open(document.getElementById('terminal'))
const stdio = { const stdio = {
stdout: (s) => { terminal.print(s) }, stdout: (charCode) => { terminal.print(charCode) },
stderr: (s) => { terminal.print(s) }, stderr: (charCode) => { terminal.print(charCode) },
stdin: async () => { stdin: async () => {
return await terminal.prompt() return await terminal.prompt()
} }

View file

@ -35,15 +35,11 @@ class StdinBuffer {
} }
} }
const stdoutBufSize = 128;
const stdoutBuf = new Int32Array()
let index = 0;
const stdout = (charCode) => { const stdout = (charCode) => {
if (charCode) { if (charCode) {
postMessage({ postMessage({
type: 'stdout', type: 'stdout',
stdout: String.fromCharCode(charCode), stdout: charCode,
}) })
} else { } else {
console.log(typeof charCode, charCode) console.log(typeof charCode, charCode)
@ -54,7 +50,7 @@ const stderr = (charCode) => {
if (charCode) { if (charCode) {
postMessage({ postMessage({
type: 'stderr', type: 'stderr',
stderr: String.fromCharCode(charCode), stderr: charCode,
}) })
} else { } else {
console.log(typeof charCode, charCode) console.log(typeof charCode, charCode)