Skip to content

Commit a8e333d

Browse files
treyhunnertiran
andauthored
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 <[email protected]>
1 parent 5f2c91a commit a8e333d

File tree

3 files changed

+99
-25
lines changed

3 files changed

+99
-25
lines changed

Tools/wasm/.editorconfig

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
root = false # This extends the root .editorconfig
2+
3+
[*.{html,js}]
4+
trim_trailing_whitespace = true
5+
insert_final_newline = true
6+
indent_style = space
7+
indent_size = 4

Tools/wasm/python.html

+90-19
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
class WasmTerminal {
101101

102102
constructor() {
103+
this.inputBuffer = new BufferQueue();
103104
this.input = ''
104105
this.resolveInput = null
105106
this.activeInput = false
@@ -123,28 +124,47 @@
123124
this.xterm.open(container);
124125
}
125126

126-
handleReadComplete(lastChar) {
127-
this.resolveInput(this.input + lastChar)
128-
this.activeInput = false
129-
}
130-
131127
handleTermData = (data) => {
132-
if (!this.activeInput) {
133-
return
134-
}
135128
const ord = data.charCodeAt(0);
136-
let ofs;
129+
data = data.replace(/\r(?!\n)/g, "\n") // Convert lone CRs to LF
137130

131+
// Handle pasted data
132+
if (data.length > 1 && data.includes("\n")) {
133+
let alreadyWrittenChars = 0;
134+
// If line already had data on it, merge pasted data with it
135+
if (this.input != '') {
136+
this.inputBuffer.addData(this.input);
137+
alreadyWrittenChars = this.input.length;
138+
this.input = '';
139+
}
140+
this.inputBuffer.addData(data);
141+
// If input is active, write the first line
142+
if (this.activeInput) {
143+
let line = this.inputBuffer.nextLine();
144+
this.writeLine(line.slice(alreadyWrittenChars));
145+
this.resolveInput(line);
146+
this.activeInput = false;
147+
}
148+
// When input isn't active, add to line buffer
149+
} else if (!this.activeInput) {
150+
// Skip non-printable characters
151+
if (!(ord === 0x1b || ord == 0x7f || ord < 32)) {
152+
this.inputBuffer.addData(data);
153+
}
138154
// TODO: Handle ANSI escape sequences
139-
if (ord === 0x1b) {
155+
} else if (ord === 0x1b) {
140156
// Handle special characters
141157
} else if (ord < 32 || ord === 0x7f) {
142158
switch (data) {
143-
case "\r": // ENTER
159+
case "\x0c": // CTRL+L
160+
this.clear();
161+
break;
162+
case "\n": // ENTER
144163
case "\x0a": // CTRL+J
145164
case "\x0d": // CTRL+M
146-
this.xterm.write('\r\n');
147-
this.handleReadComplete('\n');
165+
this.resolveInput(this.input + this.writeLine('\n'));
166+
this.input = '';
167+
this.activeInput = false;
148168
break;
149169
case "\x7F": // BACKSPACE
150170
case "\x08": // CTRL+H
@@ -157,6 +177,12 @@
157177
}
158178
}
159179

180+
writeLine(line) {
181+
this.xterm.write(line.slice(0, -1))
182+
this.xterm.write('\r\n');
183+
return line;
184+
}
185+
160186
handleCursorInsert(data) {
161187
this.input += data;
162188
this.xterm.write(data)
@@ -176,9 +202,19 @@
176202
this.activeInput = true
177203
// Hack to allow stdout/stderr to finish before we figure out where input starts
178204
setTimeout(() => {this.inputStartCursor = this.xterm.buffer.active.cursorX}, 1)
205+
// If line buffer has a line ready, send it immediately
206+
if (this.inputBuffer.hasLineReady()) {
207+
return new Promise((resolve, reject) => {
208+
resolve(this.writeLine(this.inputBuffer.nextLine()));
209+
this.activeInput = false;
210+
})
211+
// If line buffer has an incomplete line, use it for the active line
212+
} else if (this.inputBuffer.lastLineIsIncomplete()) {
213+
// Hack to ensure cursor input start doesn't end up after user input
214+
setTimeout(() => {this.handleCursorInsert(this.inputBuffer.nextLine())}, 1);
215+
}
179216
return new Promise((resolve, reject) => {
180217
this.resolveInput = (value) => {
181-
this.input = ''
182218
resolve(value)
183219
}
184220
})
@@ -188,9 +224,44 @@
188224
this.xterm.clear();
189225
}
190226

191-
print(message) {
192-
const normInput = message.replace(/[\r\n]+/g, "\n").replace(/\n/g, "\r\n");
193-
this.xterm.write(normInput);
227+
print(charCode) {
228+
let array = [charCode];
229+
if (charCode == 10) {
230+
array = [13, 10]; // Replace \n with \r\n
231+
}
232+
this.xterm.write(new Uint8Array(array));
233+
}
234+
}
235+
236+
class BufferQueue {
237+
constructor(xterm) {
238+
this.buffer = []
239+
}
240+
241+
isEmpty() {
242+
return this.buffer.length == 0
243+
}
244+
245+
lastLineIsIncomplete() {
246+
return !this.isEmpty() && !this.buffer[this.buffer.length-1].endsWith("\n")
247+
}
248+
249+
hasLineReady() {
250+
return !this.isEmpty() && this.buffer[0].endsWith("\n")
251+
}
252+
253+
addData(data) {
254+
let lines = data.match(/.*(\n|$)/g)
255+
if (this.lastLineIsIncomplete()) {
256+
this.buffer[this.buffer.length-1] += lines.shift()
257+
}
258+
for (let line of lines) {
259+
this.buffer.push(line)
260+
}
261+
}
262+
263+
nextLine() {
264+
return this.buffer.shift()
194265
}
195266
}
196267

@@ -202,8 +273,8 @@
202273
terminal.open(document.getElementById('terminal'))
203274

204275
const stdio = {
205-
stdout: (s) => { terminal.print(s) },
206-
stderr: (s) => { terminal.print(s) },
276+
stdout: (charCode) => { terminal.print(charCode) },
277+
stderr: (charCode) => { terminal.print(charCode) },
207278
stdin: async () => {
208279
return await terminal.prompt()
209280
}

Tools/wasm/python.worker.js

+2-6
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,11 @@ class StdinBuffer {
3535
}
3636
}
3737

38-
const stdoutBufSize = 128;
39-
const stdoutBuf = new Int32Array()
40-
let index = 0;
41-
4238
const stdout = (charCode) => {
4339
if (charCode) {
4440
postMessage({
4541
type: 'stdout',
46-
stdout: String.fromCharCode(charCode),
42+
stdout: charCode,
4743
})
4844
} else {
4945
console.log(typeof charCode, charCode)
@@ -54,7 +50,7 @@ const stderr = (charCode) => {
5450
if (charCode) {
5551
postMessage({
5652
type: 'stderr',
57-
stderr: String.fromCharCode(charCode),
53+
stderr: charCode,
5854
})
5955
} else {
6056
console.log(typeof charCode, charCode)

0 commit comments

Comments
 (0)