Skip to content

gh-136251: Improvements to WASM demo REPL #136252

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Jul 21, 2025
Merged
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
af4aa0d
EMSCRIPTEN BUILD: export HEAPU32 from emscripten build so that python…
adqm Jul 2, 2025
394815e
WASM REPL: fix loading xterm.js (broken in 2f1cee8477)
adqm Jul 2, 2025
aab9ccc
WASM REPL: basic navigation in REPL (arrow keys, home/end, etc)
adqm Jul 2, 2025
bd52abe
WASM REPL: reset the python worker after a program exits so that we c…
adqm Jul 2, 2025
77cc952
WASM REPL: handle a few more special keys (tab, ctrl+c)
adqm Jul 2, 2025
4cfa3f1
WASM REPL: use ace editor instead of a plain textbox for code snippet
adqm Jul 2, 2025
b688663
WASM REPL: some organization, refactoring, fixes for history
adqm Jul 2, 2025
43a47dc
WASM REPL: put demo at index.html and change text of the 'Run Code' b…
adqm Jul 3, 2025
6d5f698
WASM REPL: update README.md to account for demo location moving
adqm Jul 3, 2025
16cfea2
WASM REPL: add HEAPU32 to exports in configure.ac
adqm Jul 3, 2025
faf2ba4
WASM REPL: fix for ACE setup in Chromium
adqm Jul 3, 2025
70d4a17
WASM REPL: run prettier on index.html
adqm Jul 3, 2025
ce5ebca
WASM REPL: lower-case 'code' on 'Run code' button
adqm Jul 3, 2025
66cf49d
WASM REPL: remove trailing slashes from void tags in index.html
adqm Jul 5, 2025
8fe92d6
add short news blurb
adqm Jul 5, 2025
25fd7fc
change text of news blurb
adqm Jul 16, 2025
9a26d70
WASM REPL: better implementation of ctrl+c behavior
adqm Jul 16, 2025
c477e30
WASM REPL: show error message when SharedArrayBuffer is not available
adqm Jul 16, 2025
4dcc6fb
WASM REPL: maintain history during browsing session
adqm Jul 16, 2025
e06a7b2
WASM REPL: make the magic ctrl+c string vary with each run
adqm Jul 17, 2025
3e76313
Merge branch 'main' into wasm_repl
hoodmane Jul 21, 2025
26ddd07
Fix emscripten in browser
hoodmane Jul 21, 2025
7d33525
Merge branch 'main' into wasm_repl
hoodmane Jul 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
WASM REPL: run prettier on index.html
  • Loading branch information
adqm committed Jul 3, 2025
commit 70d4a171caaaa2fdb5b028059e514dedd4048651
140 changes: 77 additions & 63 deletions Tools/wasm/emscripten/web_example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
margin: 0 auto;
}
#editor {
padding:5px;
padding: 5px;
border: 1px solid black;
width: 100%;
height: 300px;
Expand Down Expand Up @@ -139,7 +139,7 @@

class WasmTerminal {
constructor() {
this.reset()
this.reset();

this.xterm = new Terminal({
scrollback: 10000,
Expand All @@ -158,7 +158,7 @@
this.xterm.onData(this.handleTermData);
}

reset(){
reset() {
this.inputBuffer = new BufferQueue();
this.input = "";
this.resolveInput = null;
Expand Down Expand Up @@ -203,29 +203,29 @@
if (!(ord === 0x1b || ord == 0x7f || ord < 32)) {
this.inputBuffer.addData(data);
}
// TODO: Handle more escape sequences?
// TODO: Handle more escape sequences?
} else if (ord === 0x1b) {
// Handle special characters
switch(data.slice(1)){
case '[A': // up
switch (data.slice(1)) {
case "[A": // up
this.historyBack();
break;
case '[B': // down
case "[B": // down
this.historyForward();
break;
case '[C': // right
case "[C": // right
this.cursorRight();
break;
case '[D': // left
case "[D": // left
this.cursorLeft();
break;
case '[H': // home key
case "[H": // home key
this.cursorHome(true);
break;
case '[F': // end key
case "[F": // end key
this.cursorEnd(true);
break;
case '[3~': // delete key
case "[3~": // delete key
this.deleteAtCursor();
break;
default:
Expand All @@ -248,10 +248,12 @@
break;
case "\x03": // CTRL+C
this.input = "";
this.xterm.write("\n")
this.xterm.write('\x1b[' + (this.cursorPosition + 4) + 'D');
this.xterm.write("\n");
this.xterm.write(
"\x1b[" + (this.cursorPosition + 4) + "D",
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Native console also writes out KeyboardInterrupt when you press CTRL+C, why not do that here too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can definitely add that. I had thought about it initially but felt like it was awkward for me just to print that when Python wasn't actually ever seeing the CTRL+C. But I think you're right that it would be good to mimic what people see at the normal REPL. I can make that change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CTRL+C handling is actually not really working correctly in my current implementation, unfortunately. Its behavior matches that of the regular REPL if we're dealing with single-line inputs only, but in multiline method, it doesn't match (my implementation results in the code being run instead of ignored). I'd like to actually send a SIGINT in the case of receiving a CTRL+C, but I haven't yet figured out how to do that.

If I'm not able to find a way to make that actually work the same way the regular REPL does, the question is whether we just accept this limitation and leave it as-is, or remove the CTRL+C case entirely.

this.cursorPosition = 0;
this.resolveInput("" + '\n');
this.resolveInput("" + "\n");
break;
case "\x09": // TAB
this.handleTab();
Expand All @@ -274,8 +276,8 @@
}
};

clearLine(){
this.xterm.write('\x1b[K')
clearLine() {
this.xterm.write("\x1b[K");
}

writeLine(line) {
Expand All @@ -286,12 +288,15 @@

handleCursorInsert(data) {
const trailing = this.input.slice(this.cursorPosition);
this.input = this.input.slice(0, this.cursorPosition) + data + trailing;
this.input =
this.input.slice(0, this.cursorPosition) +
data +
trailing;
this.cursorPosition += data.length;
this.xterm.write(data);
if (trailing.length !== 0){
if (trailing.length !== 0) {
this.xterm.write(trailing);
this.xterm.write('\x1b[' + trailing.length + 'D');
this.xterm.write("\x1b[" + trailing.length + "D");
}
this.updateHistory();
}
Expand All @@ -303,12 +308,12 @@
const suffix = this.input.slice(this.cursorPosition);
const count = 4 - (this.cursorPosition % 4);
const toAdd = " ".repeat(count);
this.input = prefix + toAdd + suffix
this.input = prefix + toAdd + suffix;
this.cursorHome(false);
this.clearLine();
this.xterm.write(this.input);
if (suffix){
this.xterm.write('\x1b[' + suffix.length + 'D');
if (suffix) {
this.xterm.write("\x1b[" + suffix.length + "D");
}
this.cursorPosition += count;
this.updateHistory(false);
Expand All @@ -323,77 +328,85 @@
return;
}
const trailing = this.input.slice(this.cursorPosition);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this change do?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of the backspace handler (though there's similar code for adding a character, and for the delete key). Now that we have the ability to use the arrow keys to move around within a line, we need something like this to make sure that if we backspace/delete/insert in the middle of a line we can still see all of the characters that come after the one we just deleted.

this.input = this.input.slice(0, this.cursorPosition - 1) + trailing;
this.input =
this.input.slice(0, this.cursorPosition - 1) + trailing;
this.cursorLeft();
this.clearLine();
if (trailing.length !== 0){
if (trailing.length !== 0) {
this.xterm.write(trailing);
this.xterm.write('\x1b[' + trailing.length + 'D');
this.xterm.write("\x1b[" + trailing.length + "D");
}
this.updateHistory();
}

deleteAtCursor(){
if (this.cursorPosition < this.input.length){
const trailing = this.input.slice(this.cursorPosition + 1);
this.input = this.input.slice(0, this.cursorPosition) + trailing;
deleteAtCursor() {
if (this.cursorPosition < this.input.length) {
const trailing = this.input.slice(
this.cursorPosition + 1,
);
this.input =
this.input.slice(0, this.cursorPosition) + trailing;
this.clearLine();
if (trailing.length !== 0){
if (trailing.length !== 0) {
this.xterm.write(trailing);
this.xterm.write('\x1b[' + trailing.length + 'D');
this.xterm.write("\x1b[" + trailing.length + "D");
}
this.updateHistory();
}
}

cursorRight(){
if (this.cursorPosition < this.input.length){
this.cursorPosition += 1;
this.xterm.write('\x1b[C');
}
cursorRight() {
if (this.cursorPosition < this.input.length) {
this.cursorPosition += 1;
this.xterm.write("\x1b[C");
}
}

cursorLeft(){
if (this.cursorPosition > 0){
cursorLeft() {
if (this.cursorPosition > 0) {
this.cursorPosition -= 1;
this.xterm.write('\x1b[D');
this.xterm.write("\x1b[D");
}
}

cursorHome(updatePosition) {
if (this.cursorPosition > 0){
this.xterm.write('\x1b[' + this.cursorPosition + 'D');
if (this.cursorPosition > 0) {
this.xterm.write("\x1b[" + this.cursorPosition + "D");
if (updatePosition) {
this.cursorPosition = 0;
}
}
}

cursorEnd() {
if (this.cursorPosition < this.input.length){
this.xterm.write('\x1b[' + (this.input.length - this.cursorPosition) + 'C');
if (this.cursorPosition < this.input.length) {
this.xterm.write(
"\x1b[" +
(this.input.length - this.cursorPosition) +
"C",
);
this.cursorPosition = this.input.length;
}
}

updateHistory(){
if (this.historyIndex !== -1){
updateHistory() {
if (this.historyIndex !== -1) {
this.historyBuffer[this.historyIndex] = this.input;
}else{
} else {
this.beforeHistoryNav = this.input;
}
}

historyBack(){
if (this.history.length === 0){
historyBack() {
if (this.history.length === 0) {
return;
}else if (this.historyIndex === -1){
} else if (this.historyIndex === -1) {
// we're not currently navigating the history; store
// the current command and then look at the end of our
// history buffer
this.beforeHistoryNav = this.input;
this.historyIndex = this.history.length - 1;
}else if (this.historyIndex > 0){
} else if (this.historyIndex > 0) {
this.historyIndex -= 1;
}
this.input = this.historyBuffer[this.historyIndex];
Expand All @@ -403,14 +416,14 @@
this.cursorPosition = this.input.length;
}

historyForward(){
if (this.history.length === 0 || this.historyIndex === -1){
historyForward() {
if (this.history.length === 0 || this.historyIndex === -1) {
// we're not currently navigating the history; NOP.
return;
}else if (this.historyIndex < this.history.length - 1){
} else if (this.historyIndex < this.history.length - 1) {
this.historyIndex += 1;
this.input = this.historyBuffer[this.historyIndex];
}else if (this.historyIndex == this.history.length - 1){
} else if (this.historyIndex == this.history.length - 1) {
// we're coming back from the last history value; reset
// the input to whatever it was when we started going
// through the history
Expand Down Expand Up @@ -449,9 +462,10 @@
}
return new Promise((resolve, reject) => {
this.resolveInput = (value) => {
if (value.replace(/\s/g, '').length != 0){
if (this.historyIndex !== -1){
this.historyBuffer[this.historyIndex] = this.history[this.historyIndex];
if (value.replace(/\s/g, "").length != 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally it would be nice to save the history when refreshing. Maybe add a "TODO"? (Not to imply that anyone will ever do it.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's definitely easy to change things so that the history persists if you do multiple iterations of "Start REPL" followed by "Stop". I actually kind of went out of my way to clear the history between REPL sessions.

Saving history across reloads also probably wouldn't be too terribly hard if we wanted that; we could shove the history into sessionStorage so that it would persist through a regular refresh but get cleared on closing the tab or (I believe) on a hard reload of the page. Happy to give that a shot if you think it's worthwhile.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think saving the history is nice... particularly combined with reverse search! But it's up to you if you want to bother.

if (this.historyIndex !== -1) {
this.historyBuffer[this.historyIndex] =
this.history[this.historyIndex];
}
this.history.push(value.slice(0, -1));
this.historyBuffer.push(value.slice(0, -1));
Expand Down Expand Up @@ -594,11 +608,11 @@
finishedCallback,
);
};
var editor;
document.addEventListener('DOMContentLoaded', () => {
editor = ace.edit("editor");
editor.session.setMode("ace/mode/python");
});
var editor;
document.addEventListener("DOMContentLoaded", () => {
editor = ace.edit("editor");
editor.session.setMode("ace/mode/python");
});
</script>
</head>
<body>
Expand Down
Loading