From 377808b0d93b3aa10eef525728424cd3d6577e17 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 4 May 2026 14:28:42 +0800 Subject: [PATCH 1/6] Support 'editting in external editor' --- Lib/_pyrepl/commands.py | 45 +++++++++++++++++++++++++++++++++++++++++ Lib/_pyrepl/reader.py | 1 + 2 files changed, 46 insertions(+) diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index e79fbfa6bb0b38..b2517451b42978 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -24,6 +24,10 @@ import time from typing import TYPE_CHECKING +lazy import subprocess +lazy import tempfile +lazy from pathlib import Path + # Categories of actions: # killing # yanking @@ -519,3 +523,44 @@ def do(self) -> None: s=time.time() - start, ) self.reader.insert(data.replace(done, "")) + + +class edit_in_editor(EditCommand): + def do(self) -> None: + r = self.reader + + editor = os.environ.get("VISUAL") or os.environ.get("EDITOR") + if not editor: + editor = "vi" if os.name != "nt" else "notepad" + + with tempfile.NamedTemporaryFile(mode="w+", suffix=".py", delete=False, encoding="utf-8") as f: + tmp_path = Path(f.name) + f.write("".join(r.buffer)) + f.flush() + + try: + with r.suspend(): + cmd = editor.split() + [str(tmp_path)] + try: + subprocess.call(cmd) + except FileNotFoundError: + r.error(f"Editor not found: {editor}") + return + except Exception as e: + r.error(f"Failed to run editor: {e}") + return + + try: + new_text = tmp_path.read_text(encoding="utf-8").rstrip("\n") + r.buffer.clear() + r.buffer.extend(new_text) + r.pos = len(r.buffer) + r.invalidate_full() + r.console.repaint() + except Exception as e: + r.error(f"Failed to read edited file: {e}") + finally: + try: + tmp_path.unlink(missing_ok=True) + except Exception: + pass diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index b8e1e425b0bb35..a34fdd83bb414f 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -104,6 +104,7 @@ def make_default_commands() -> dict[CommandName, CommandClass]: (r"\C-u", "unix-line-discard"), (r"\C-w", "unix-word-rubout"), (r"\C-x\C-u", "upcase-region"), + (r"\C-x\C-e", "edit-in-editor"), (r"\C-y", "yank"), *(() if sys.platform == "win32" else ((r"\C-z", "suspend"), )), (r"\M-b", "backward-word"), From 9c9e1fd7170c1c36f4298ca016e3af292e98f2eb Mon Sep 17 00:00:00 2001 From: Tan Long Date: Tue, 5 May 2026 09:26:00 +0800 Subject: [PATCH 2/6] renaming 'edit_in_editor' to 'open_input_in_editor' (the same name as in IPython) --- Lib/_pyrepl/commands.py | 2 +- Lib/_pyrepl/reader.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index b2517451b42978..1baf32679fa919 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -525,7 +525,7 @@ def do(self) -> None: self.reader.insert(data.replace(done, "")) -class edit_in_editor(EditCommand): +class open_input_in_editor(EditCommand): def do(self) -> None: r = self.reader diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index a34fdd83bb414f..7392c746dfad96 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -104,7 +104,7 @@ def make_default_commands() -> dict[CommandName, CommandClass]: (r"\C-u", "unix-line-discard"), (r"\C-w", "unix-word-rubout"), (r"\C-x\C-u", "upcase-region"), - (r"\C-x\C-e", "edit-in-editor"), + (r"\C-x\C-e", "open-input-in-editor"), (r"\C-y", "yank"), *(() if sys.platform == "win32" else ((r"\C-z", "suspend"), )), (r"\M-b", "backward-word"), From 707c8b31441bafcac92266cf7bf57c8eeba1bdd9 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Tue, 5 May 2026 09:58:53 +0800 Subject: [PATCH 3/6] formatting --- Lib/_pyrepl/commands.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index 1baf32679fa919..6083dfe924ffd6 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -533,7 +533,9 @@ def do(self) -> None: if not editor: editor = "vi" if os.name != "nt" else "notepad" - with tempfile.NamedTemporaryFile(mode="w+", suffix=".py", delete=False, encoding="utf-8") as f: + with tempfile.NamedTemporaryFile( + mode="w+", suffix=".py", delete=False, encoding="utf-8" + ) as f: tmp_path = Path(f.name) f.write("".join(r.buffer)) f.flush() From cc3a3a8d99a9174dff13110c4dd760671df794be Mon Sep 17 00:00:00 2001 From: Tan Long Date: Tue, 5 May 2026 10:12:19 +0800 Subject: [PATCH 4/6] blurb --- .../next/Library/2026-05-05-10-12-13.gh-issue-149392.vsURNh.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-05-05-10-12-13.gh-issue-149392.vsURNh.rst diff --git a/Misc/NEWS.d/next/Library/2026-05-05-10-12-13.gh-issue-149392.vsURNh.rst b/Misc/NEWS.d/next/Library/2026-05-05-10-12-13.gh-issue-149392.vsURNh.rst new file mode 100644 index 00000000000000..518e486d8f2d3e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-05-10-12-13.gh-issue-149392.vsURNh.rst @@ -0,0 +1 @@ +Add "Open Input in Editor" support to :mod:`!_pyrepl`. From e3be9b70909ec9e15ab80f60598fd4da92c17a3d Mon Sep 17 00:00:00 2001 From: Tan Long Date: Wed, 6 May 2026 21:19:40 +0800 Subject: [PATCH 5/6] use list unpacking instead of concatenation --- Lib/_pyrepl/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index 6083dfe924ffd6..16efebee4f17e5 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -542,7 +542,7 @@ def do(self) -> None: try: with r.suspend(): - cmd = editor.split() + [str(tmp_path)] + cmd = [*editor.split(), str(tmp_path)] try: subprocess.call(cmd) except FileNotFoundError: From ed9437233cb6448434e98d6a63b4826cd1178306 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sun, 10 May 2026 17:51:28 +0800 Subject: [PATCH 6/6] remove 'r.console.repaint()' Problem: Suppose type 'im' in PyREPL and then enter editor, completing the statement 'import os'. After saving and exiting, there will be '>>> im>>> import os' instead of '>>> import os'. With a 'r.console.repaint()', the problem is gone, but only on Linux. On Windows, the 'replaint()' is not implemented in windows_console.py and the '>>> im>>> import os' problem persists. I suppose 'replaint()' is not the way to go. Remove 'r.console.repaint()' and go to find a solution workable on all platforms, not only Linux. --- Lib/_pyrepl/commands.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index 16efebee4f17e5..83743b350394d9 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -558,7 +558,6 @@ def do(self) -> None: r.buffer.extend(new_text) r.pos = len(r.buffer) r.invalidate_full() - r.console.repaint() except Exception as e: r.error(f"Failed to read edited file: {e}") finally: