From 8469def61324feb5e278f88bebad36af7a0d997e Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 23:38:31 -0300 Subject: A send to Python's REPL runs once, the Enter after the paste, not in it A one-line send (print(1)) to Python 3.14 stopped at ... and needed another Enter: its REPL takes a bracketed paste, and an Enter read in the same read as the paste is part of the paste, one line or not (checked in a real pty). Into a REPL that asked for bracketed paste, the Enters (two for an open block) now wait for its next output, its answer to the paste, or 250 ms with nothing. fs-test runs python3 in a real pty and checks a one-line exec prints at once; the docs' pty/data recipe says to send the Enter apart. Co-Authored-By: Claude Opus 5.5 --- src/exec.zig | 25 ++++++++++++++++++++++--- src/panes.zig | 4 ++++ src/pardes.zig | 29 ++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/exec.zig b/src/exec.zig index 56e6a2a9..2f279d8e 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -443,6 +443,18 @@ fn routeToRepl(p: *Pardes, from: usize, text: []const u8) Routed { return .asked; } +const repl_enter_wait_ns: u64 = 250 * std.time.ns_per_ms; + +/// 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. +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; + for (0..pane.repl_enter) |_| p.emitWrite(id, "\r"); + pane.repl_enter = 0; +} + fn pythonBlockOpen(text: []const u8) bool { const trimmed = std.mem.trimEnd(u8, text, " \t\n"); const start = if (std.mem.lastIndexOfScalar(u8, trimmed, '\n')) |nl| nl + 1 else 0; @@ -475,13 +487,20 @@ pub fn sendToRepl(p: *Pardes, from: usize, to: usize, text: []const u8) void { var said: [128]u8 = undefined; return p.setMessage(from, std.fmt.bufPrint(&said, "Repl: {d} bytes is more than can be sent at once; send less", .{n}) catch "Repl: too long to send"); } + const pasted = panes.Terminal.bracketedPaste(dst); edit.typeToTty(p, to, dst, clean[0..n]); - p.emitWrite(to, "\r"); // 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. - if ((if (dst.repl) |r| r.lang == python_lang else false) and pythonBlockOpen(clean[0..n])) - p.emitWrite(to, "\r"); + const enters: u8 = if ((if (dst.repl) |r| r.lang == python_lang else false) and pythonBlockOpen(clean[0..n])) 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. + // 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; + } 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 std.mem.indexOfScalar(u8, text, '\n') != null) diff --git a/src/panes.zig b/src/panes.zig index c33fd577..7aea5898 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -135,6 +135,10 @@ pub const Pane = struct { repl: ?struct { lang: u8, letter: u8 } = null, /// It has been told that its program takes no bracketed paste. repl_warned: bool = false, + /// Enters owed to a REPL after a bracketed paste (exec.sendToRepl): sent + /// with its next output, the paste taken, or at `repl_enter_due_ns`. + repl_enter: u8 = 0, + repl_enter_due_ns: u64 = 0, /// A question with its answers in it (`repl_choice`). question: [96]u8 = undefined, question_len: u8 = 0, diff --git a/src/pardes.zig b/src/pardes.zig index c2bde804..ea03b3eb 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -1638,6 +1638,24 @@ test "a REPL bound for a language takes the exec made on a body in it; tags, Exe _ = exec.executeFrom(p, 0, "a = 1\r\nb = \x1b[201~2", true); Sent.drain(p); try std.testing.expectEqualStrings("a = 1\rb = [201~2\r", Sent.said(1)); + // Into a REPL that takes a bracketed paste (3.13's), the Enter waits + // for the REPL to answer the paste: read with it, it would be part of it. + p.update(.{ .output = .{ .pane = 1, .bytes = "\x1b[?2004h" } }); + Sent.drain(p); + _ = exec.executeFrom(p, 0, "print(1)", true); + Sent.drain(p); + try std.testing.expectEqualStrings("\x1b[200~print(1)\x1b[201~", Sent.said(1)); + p.update(.{ .output = .{ .pane = 1, .bytes = "print(1)" } }); + Sent.drain(p); + 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); + Sent.drain(p); + try std.testing.expect(std.mem.endsWith(u8, Sent.said(1), "\x1b[200~print(2)\x1b[201~\r")); + 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 // ends it: a second Enter. _ = exec.executeFrom(p, 0, "def f():\n return 1", true); @@ -5333,7 +5351,10 @@ pub const Pardes = struct { } p.clip_pending = null; }, - .tick => Messages.advanceMessages(p), + .tick => { + Messages.advanceMessages(p); + for (0..p.panes.len) |id| exec.flushReplEnter(p, id, false); + }, else => {}, } if (p.fs.listeners != 0) p.fs.origin = switch (ev) { @@ -5377,6 +5398,7 @@ pub const Pardes = struct { ctlfs.events.notePtyOutput(p, o.pane, o.bytes); panes.Terminal.feedOutput(p, pane, o.bytes); ctlfs.pty.noteMarks(p, o.pane, pane); + exec.flushReplEnter(p, o.pane, true); }, .exited => |e| if (p.panes[e.pane]) |pane| { if (pane.command == null) @@ -6474,6 +6496,11 @@ 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. + 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); + frames = if (frames) |soonest| @min(soonest, wait) else wait; + }; return if (frames) |n| p.stepped_ns + n * animation.frame_ns else null; } -- cgit v1.3