diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 04:15:16 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 4b64d4b237b2bc5f8197e2232c84c0d1ed1a8fdb (patch) | |
| tree | 725cd69dd0ae6990eafd71ec25bb0e52b9f354e8 | |
| parent | 7c59482436656443b698e867c423b27dd365a96c (diff) | |
| download | pardes-4b64d4b237b2bc5f8197e2232c84c0d1ed1a8fdb.tar.gz pardes-4b64d4b237b2bc5f8197e2232c84c0d1ed1a8fdb.zip | |
A multi-line paste into Python's REPL runs whole, back at its prompt
The Enter went on Python's first output after the paste, before it had
taken in the whole paste, and one Enter leaves a multi-line input at
... anyway. Python's output now only restarts a 100 ms quiet wait (the
250 ms fallback stays), and a paste of more than one line gets the second
Enter a block gets. fs.py runs print(11)\nprint(22), the same with a
trailing newline, and a for block followed by a statement in a real
python3, each back at >>>.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | src/exec.zig | 27 | ||||
| -rw-r--r-- | src/pardes.zig | 14 | ||||
| -rw-r--r-- | test/fs.py | 12 |
3 files changed, 49 insertions, 4 deletions
diff --git a/src/exec.zig b/src/exec.zig index 6e6f528b..b8312a21 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -444,12 +444,28 @@ fn routeToRepl(p: *Pardes, from: usize, text: []const u8) Routed { } const repl_enter_wait_ns: u64 = 250 * std.time.ns_per_ms; +/// Python echoes a long paste over several reads: its Enter waits for the +/// echo to go quiet this long, not for the first of it. +const repl_quiet_ns: u64 = 100 * std.time.ns_per_ms; + +/// The host's clock now; core time (`now_ns`, `stepped_ns`) is as old as +/// the last frame, seconds in an idle session. +fn clockNow(p: *Pardes) u64 { + return if (p.host.vtable.now) |clock| clock(p.host.ctx) else p.now_ns; +} /// Sends the Enters a paste into pane `id`'s REPL is owed: when the REPL -/// has answered it (`answered`), or once the wait is up. +/// has answered it (`answered`), or once the wait is up. Python's answer +/// only restarts a short wait, so the Enter goes once its echo of the +/// whole paste is in (a multi-line paste read in part would leave it at +/// `...`). pub fn flushReplEnter(p: *Pardes, id: usize, answered: bool) void { const pane = p.panes[id] orelse return; if (pane.repl_enter == 0) return; + if (answered and (if (pane.repl) |r| r.lang == python_lang else false)) { + pane.repl_enter_due_ns = clockNow(p) + repl_quiet_ns; + return; + } if (!answered and p.now_ns < pane.repl_enter_due_ns) return; for (0..pane.repl_enter) |_| p.emitWrite(id, "\r"); pane.repl_enter = 0; @@ -492,7 +508,11 @@ pub fn sendToRepl(p: *Pardes, from: usize, to: usize, text: []const u8) void { // A Python block whose last line is indented, or a bare header, is not // over at one Enter, pasted into 3.13's REPL or typed into the old one: // the blank line a second Enter gives ends it. - const enters: u8 = if ((if (dst.repl) |r| r.lang == python_lang else false) and pythonBlockOpen(clean[0..n])) 2 else 1; + // So is a paste of more than one line: 3.13's REPL takes it as one + // input, which one Enter leaves at `...`. + const python = if (dst.repl) |r| r.lang == python_lang else false; + const lines = std.mem.count(u8, std.mem.trim(u8, clean[0..n], " \t\n"), "\n") + 1; + const enters: u8 = if (python and (pythonBlockOpen(clean[0..n]) or (pasted and lines > 1))) 2 else 1; if (pasted) { // An Enter read with the paste is part of it: 3.13's REPL takes it // as a newline in the pasted text, even a one-line one, and waits. @@ -503,8 +523,7 @@ pub fn sendToRepl(p: *Pardes, from: usize, to: usize, text: []const u8) void { // nobody watches), `stepped_ns` and `now_ns` are as old as the last // frame, and a wait counted from there would be over already, the // Enter going out with the paste. - const now = if (p.host.vtable.now) |clock| clock(p.host.ctx) else p.now_ns; - dst.repl_enter_due_ns = now + repl_enter_wait_ns; + dst.repl_enter_due_ns = clockNow(p) + repl_enter_wait_ns; } else for (0..enters) |_| p.emitWrite(to, "\r"); // Line by line, a blank line inside a Python block ends the block. if (!panes.Terminal.bracketedPaste(dst) and !dst.repl_warned and (if (dst.repl) |r| r.lang == python_lang else false) and diff --git a/src/pardes.zig b/src/pardes.zig index d6859f86..289dcee0 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -1652,7 +1652,21 @@ test "a REPL bound for a language takes the exec made on a body in it; tags, Exe try std.testing.expectEqualStrings("\x1b[200~print(1)\x1b[201~", Sent.said(1)); p.update(.{ .output = .{ .pane = 1, .bytes = "print(1)" } }); Sent.drain(p); + // Python's echo only restarts a short wait: the Enter goes once the + // echo of the whole paste has gone quiet. + try std.testing.expectEqualStrings("", Sent.said(1)); + p.now_ns = p.panes[1].?.repl_enter_due_ns; + p.update(.tick); + Sent.drain(p); try std.testing.expectEqualStrings("\r", Sent.said(1)); + // A paste of more than one line gets the blank line a block gets. + _ = exec.executeFrom(p, 0, "print(11)\nprint(22)", true); + Sent.drain(p); + try std.testing.expectEqualStrings("\x1b[200~print(11)\nprint(22)\x1b[201~", Sent.said(1)); + p.now_ns = p.panes[1].?.repl_enter_due_ns; + p.update(.tick); + Sent.drain(p); + try std.testing.expectEqualStrings("\r\r", Sent.said(1)); // Nothing answering, it goes when its wait is up. _ = exec.executeFrom(p, 0, "print(2)", true); p.update(.tick); // on the clock: not yet, however old core time is @@ -524,6 +524,18 @@ def python_repl_runs_one_line(binary): record = b'MX0 0 0 %d %s\n' % (len(line), line) client.rpc(118, struct.pack('<IQI', fid, 0, len(record)) + record) assert wait_for(f'\n{1000 + n}'), (n, body()) + # More than one line, with and without a newline at the end, + # and a block with a statement after it: each runs whole, + # and the REPL is back at its prompt, not at `...`. + for code, outputs in [(b'print(11)\nprint(22)', ['11', '22']), + (b'print(33)\nprint(44)\n', ['33', '44']), + (b'for i in range(2):\n print(55 + i)\nprint(77)\n', ['55', '56', '77'])]: + record = b'MX0 0 0 %d %s\n' % (len(code), code) + client.rpc(118, struct.pack('<IQI', fid, 0, len(record)) + record) + for out in outputs: + assert wait_for(f'\n{out}\n'), (code, out, body()) + time.sleep(.5) + assert body().rstrip().endswith('>>>'), (code, body()[-200:]) finally: client.close(fid) |
