diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 03:35:43 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | d067c77813f9ce9fcc64b33e6b0284510ac02185 (patch) | |
| tree | eb530db7320603af748aa7535c93090a36b78134 | |
| parent | 3e941015ebd2da88a296479f407e9bbf06287234 (diff) | |
| download | pardes-d067c77813f9ce9fcc64b33e6b0284510ac02185.tar.gz pardes-d067c77813f9ce9fcc64b33e6b0284510ac02185.zip | |
A REPL paste's Enter waits on the clock, not on stale core time
sendToRepl timed the Enter's fallback from stepped_ns, which in an idle
detached session is as old as the last frame: the wait was over before it
began and the \r went out with the paste, which 3.13's REPL then reads as
part of it (4 of 5 sends lost their Enter). The wait now starts from the
host's clock and is checked against now_ns; the REPL's echo of the paste
still sends it sooner, and the wake is scheduled for the deadline. fs.py
sends five times after two idle seconds each in a real pty.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | src/exec.zig | 9 | ||||
| -rw-r--r-- | src/pardes.zig | 12 | ||||
| -rw-r--r-- | test/fs.py | 8 |
3 files changed, 23 insertions, 6 deletions
diff --git a/src/exec.zig b/src/exec.zig index 484b85d7..963c8763 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -450,7 +450,7 @@ const repl_enter_wait_ns: u64 = 250 * std.time.ns_per_ms; 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 p.stepped_ns < pane.repl_enter_due_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; } @@ -499,7 +499,12 @@ pub fn sendToRepl(p: *Pardes, from: usize, to: usize, text: []const u8) void { // It goes once the REPL has answered the paste (its next output), // or after a moment if nothing comes. dst.repl_enter = enters; - dst.repl_enter_due_ns = p.stepped_ns + repl_enter_wait_ns; + // From the clock now, not core time: idle (a detached session + // 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; } 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 dab27160..9297c829 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -1655,10 +1655,13 @@ test "a REPL bound for a language takes the exec made on a body in it; tags, Exe try std.testing.expectEqualStrings("\r", Sent.said(1)); // Nothing answering, it goes when its wait is up. _ = exec.executeFrom(p, 0, "print(2)", true); - p.stepped_ns += 300 * std.time.ns_per_ms; + p.update(.tick); // on the clock: not yet, however old core time is + Sent.drain(p); + try std.testing.expect(std.mem.endsWith(u8, Sent.said(1), "\x1b[200~print(2)\x1b[201~")); + p.now_ns = p.panes[1].?.repl_enter_due_ns; // the clock reaches its wait p.update(.tick); Sent.drain(p); - try std.testing.expect(std.mem.endsWith(u8, Sent.said(1), "\x1b[200~print(2)\x1b[201~\r")); + try std.testing.expectEqualStrings("\r", Sent.said(1)); p.update(.{ .output = .{ .pane = 1, .bytes = "\x1b[?2004l" } }); Sent.len[1] = 0; // A block left open by its indented last line gets the blank line that @@ -6571,9 +6574,10 @@ pub const Pardes = struct { const wait = Messages.messageWake(p, pane) orelse continue; frames = if (frames) |soonest| @min(soonest, wait) else wait; }; - // A paste's Enter waiting on its REPL goes at the end of its wait. + // A paste's Enter waiting on its REPL goes at the end of its wait, + // a time on the clock: the frames from `stepped_ns` that reach it. for (p.panes) |slot| if (slot) |pane| if (pane.repl_enter > 0) { - const wait = @max(1, (pane.repl_enter_due_ns -| p.stepped_ns) / animation.frame_ns); + const wait = @max(1, (pane.repl_enter_due_ns -| p.stepped_ns + animation.frame_ns - 1) / animation.frame_ns); frames = if (frames) |soonest| @min(soonest, wait) else wait; }; return if (frames) |n| p.stepped_ns + n * animation.frame_ns else null; @@ -515,6 +515,14 @@ def python_repl_runs_one_line(binary): record = b'MX0 0 0 10 print(6*7)\n' client.rpc(118, struct.pack('<IQI', fid, 0, len(record)) + record) assert wait_for('\n42'), body() + # After the session has sat idle, the Enter still waits for + # the paste: core time is as old as the last frame then. + for n in range(5): + time.sleep(2) + line = f'print({1000 + n})'.encode() + 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()) finally: client.close(fid) |
