From 377808b0d93b3aa10eef525728424cd3d6577e17 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Mon, 4 May 2026 14:28:42 +0800 Subject: [PATCH 1/4] 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/4] 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/4] 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/4] 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`.