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 {
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()
}

View File

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