mirror of https://github.com/python/cpython
[3.13] gh-111201: auto-indentation in _pyrepl (GH-119348) (#119427)
(cherry picked from commit cd516cd1f5
)
Co-authored-by: Arnon Yaari <wiggin15@yahoo.com>
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
This commit is contained in:
parent
81440c5ba1
commit
9435124d4a
|
@ -99,6 +99,7 @@ class ReadlineAlikeReader(historical_reader.HistoricalReader, CompletingReader):
|
||||||
# Instance fields
|
# Instance fields
|
||||||
config: ReadlineConfig
|
config: ReadlineConfig
|
||||||
more_lines: MoreLinesCallable | None = None
|
more_lines: MoreLinesCallable | None = None
|
||||||
|
last_used_indentation: str | None = None
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
super().__post_init__()
|
super().__post_init__()
|
||||||
|
@ -157,6 +158,11 @@ class ReadlineAlikeReader(historical_reader.HistoricalReader, CompletingReader):
|
||||||
cut = 0
|
cut = 0
|
||||||
return self.history[cut:]
|
return self.history[cut:]
|
||||||
|
|
||||||
|
def update_last_used_indentation(self) -> None:
|
||||||
|
indentation = _get_first_indentation(self.buffer)
|
||||||
|
if indentation is not None:
|
||||||
|
self.last_used_indentation = indentation
|
||||||
|
|
||||||
# --- simplified support for reading multiline Python statements ---
|
# --- simplified support for reading multiline Python statements ---
|
||||||
|
|
||||||
def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]:
|
def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]:
|
||||||
|
@ -211,6 +217,28 @@ def _get_previous_line_indent(buffer: list[str], pos: int) -> tuple[int, int | N
|
||||||
return prevlinestart, indent
|
return prevlinestart, indent
|
||||||
|
|
||||||
|
|
||||||
|
def _get_first_indentation(buffer: list[str]) -> str | None:
|
||||||
|
indented_line_start = None
|
||||||
|
for i in range(len(buffer)):
|
||||||
|
if (i < len(buffer) - 1
|
||||||
|
and buffer[i] == "\n"
|
||||||
|
and buffer[i + 1] in " \t"
|
||||||
|
):
|
||||||
|
indented_line_start = i + 1
|
||||||
|
elif indented_line_start is not None and buffer[i] not in " \t\n":
|
||||||
|
return ''.join(buffer[indented_line_start : i])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_last_char_colon(buffer: list[str]) -> bool:
|
||||||
|
i = len(buffer)
|
||||||
|
while i > 0:
|
||||||
|
i -= 1
|
||||||
|
if buffer[i] not in " \t\n": # ignore whitespaces
|
||||||
|
return buffer[i] == ":"
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class maybe_accept(commands.Command):
|
class maybe_accept(commands.Command):
|
||||||
def do(self) -> None:
|
def do(self) -> None:
|
||||||
r: ReadlineAlikeReader
|
r: ReadlineAlikeReader
|
||||||
|
@ -227,9 +255,18 @@ class maybe_accept(commands.Command):
|
||||||
# auto-indent the next line like the previous line
|
# auto-indent the next line like the previous line
|
||||||
prevlinestart, indent = _get_previous_line_indent(r.buffer, r.pos)
|
prevlinestart, indent = _get_previous_line_indent(r.buffer, r.pos)
|
||||||
r.insert("\n")
|
r.insert("\n")
|
||||||
if not self.reader.paste_mode and indent:
|
if not self.reader.paste_mode:
|
||||||
for i in range(prevlinestart, prevlinestart + indent):
|
if indent:
|
||||||
r.insert(r.buffer[i])
|
for i in range(prevlinestart, prevlinestart + indent):
|
||||||
|
r.insert(r.buffer[i])
|
||||||
|
r.update_last_used_indentation()
|
||||||
|
if _is_last_char_colon(r.buffer):
|
||||||
|
if r.last_used_indentation is not None:
|
||||||
|
indentation = r.last_used_indentation
|
||||||
|
else:
|
||||||
|
# default
|
||||||
|
indentation = " " * 4
|
||||||
|
r.insert(indentation)
|
||||||
elif not self.reader.paste_mode:
|
elif not self.reader.paste_mode:
|
||||||
self.finish = True
|
self.finish = True
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -5,19 +5,31 @@ import rlcompleter
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from .support import FakeConsole, handle_all_events, handle_events_narrow_console
|
from .support import (
|
||||||
from .support import more_lines, multiline_input, code_to_events
|
FakeConsole,
|
||||||
|
handle_all_events,
|
||||||
|
handle_events_narrow_console,
|
||||||
|
more_lines,
|
||||||
|
multiline_input,
|
||||||
|
code_to_events,
|
||||||
|
)
|
||||||
from _pyrepl.console import Event
|
from _pyrepl.console import Event
|
||||||
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
|
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
|
||||||
from _pyrepl.readline import multiline_input as readline_multiline_input
|
from _pyrepl.readline import multiline_input as readline_multiline_input
|
||||||
|
|
||||||
|
|
||||||
class TestCursorPosition(TestCase):
|
class TestCursorPosition(TestCase):
|
||||||
|
def prepare_reader(self, events):
|
||||||
|
console = FakeConsole(events)
|
||||||
|
config = ReadlineConfig(readline_completer=None)
|
||||||
|
reader = ReadlineAlikeReader(console=console, config=config)
|
||||||
|
return reader
|
||||||
|
|
||||||
def test_up_arrow_simple(self):
|
def test_up_arrow_simple(self):
|
||||||
# fmt: off
|
# fmt: off
|
||||||
code = (
|
code = (
|
||||||
'def f():\n'
|
"def f():\n"
|
||||||
' ...\n'
|
" ...\n"
|
||||||
)
|
)
|
||||||
# fmt: on
|
# fmt: on
|
||||||
events = itertools.chain(
|
events = itertools.chain(
|
||||||
|
@ -34,8 +46,8 @@ class TestCursorPosition(TestCase):
|
||||||
def test_down_arrow_end_of_input(self):
|
def test_down_arrow_end_of_input(self):
|
||||||
# fmt: off
|
# fmt: off
|
||||||
code = (
|
code = (
|
||||||
'def f():\n'
|
"def f():\n"
|
||||||
' ...\n'
|
" ...\n"
|
||||||
)
|
)
|
||||||
# fmt: on
|
# fmt: on
|
||||||
events = itertools.chain(
|
events = itertools.chain(
|
||||||
|
@ -300,6 +312,79 @@ class TestCursorPosition(TestCase):
|
||||||
self.assertEqual(reader.pos, 10)
|
self.assertEqual(reader.pos, 10)
|
||||||
self.assertEqual(reader.cxy, (1, 1))
|
self.assertEqual(reader.cxy, (1, 1))
|
||||||
|
|
||||||
|
def test_auto_indent_default(self):
|
||||||
|
# fmt: off
|
||||||
|
input_code = (
|
||||||
|
"def f():\n"
|
||||||
|
"pass\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
output_code = (
|
||||||
|
"def f():\n"
|
||||||
|
" pass\n"
|
||||||
|
" "
|
||||||
|
)
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
def test_auto_indent_continuation(self):
|
||||||
|
# auto indenting according to previous user indentation
|
||||||
|
# fmt: off
|
||||||
|
events = itertools.chain(
|
||||||
|
code_to_events("def f():\n"),
|
||||||
|
# add backspace to delete default auto-indent
|
||||||
|
[
|
||||||
|
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
|
||||||
|
],
|
||||||
|
code_to_events(
|
||||||
|
" pass\n"
|
||||||
|
"pass\n\n"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
output_code = (
|
||||||
|
"def f():\n"
|
||||||
|
" pass\n"
|
||||||
|
" pass\n"
|
||||||
|
" "
|
||||||
|
)
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
reader = self.prepare_reader(events)
|
||||||
|
output = multiline_input(reader)
|
||||||
|
self.assertEqual(output, output_code)
|
||||||
|
|
||||||
|
def test_auto_indent_prev_block(self):
|
||||||
|
# auto indenting according to indentation in different block
|
||||||
|
# fmt: off
|
||||||
|
events = itertools.chain(
|
||||||
|
code_to_events("def f():\n"),
|
||||||
|
# add backspace to delete default auto-indent
|
||||||
|
[
|
||||||
|
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
|
||||||
|
],
|
||||||
|
code_to_events(
|
||||||
|
" pass\n"
|
||||||
|
"pass\n\n"
|
||||||
|
),
|
||||||
|
code_to_events(
|
||||||
|
"def g():\n"
|
||||||
|
"pass\n\n"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
output_code = (
|
||||||
|
"def g():\n"
|
||||||
|
" pass\n"
|
||||||
|
" "
|
||||||
|
)
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
reader = self.prepare_reader(events)
|
||||||
|
output1 = multiline_input(reader)
|
||||||
|
output2 = multiline_input(reader)
|
||||||
|
self.assertEqual(output2, output_code)
|
||||||
|
|
||||||
|
|
||||||
class TestPyReplOutput(TestCase):
|
class TestPyReplOutput(TestCase):
|
||||||
def prepare_reader(self, events):
|
def prepare_reader(self, events):
|
||||||
|
@ -316,14 +401,12 @@ class TestPyReplOutput(TestCase):
|
||||||
|
|
||||||
def test_multiline_edit(self):
|
def test_multiline_edit(self):
|
||||||
events = itertools.chain(
|
events = itertools.chain(
|
||||||
code_to_events("def f():\n ...\n\n"),
|
code_to_events("def f():\n...\n\n"),
|
||||||
[
|
[
|
||||||
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
||||||
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
||||||
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
|
||||||
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
|
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
|
||||||
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
|
|
||||||
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
|
|
||||||
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
|
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
|
||||||
Event(evt="key", data="g", raw=bytearray(b"g")),
|
Event(evt="key", data="g", raw=bytearray(b"g")),
|
||||||
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
|
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
|
||||||
|
@ -334,9 +417,9 @@ class TestPyReplOutput(TestCase):
|
||||||
reader = self.prepare_reader(events)
|
reader = self.prepare_reader(events)
|
||||||
|
|
||||||
output = multiline_input(reader)
|
output = multiline_input(reader)
|
||||||
self.assertEqual(output, "def f():\n ...\n ")
|
self.assertEqual(output, "def f():\n ...\n ")
|
||||||
output = multiline_input(reader)
|
output = multiline_input(reader)
|
||||||
self.assertEqual(output, "def g():\n ...\n ")
|
self.assertEqual(output, "def g():\n ...\n ")
|
||||||
|
|
||||||
def test_history_navigation_with_up_arrow(self):
|
def test_history_navigation_with_up_arrow(self):
|
||||||
events = itertools.chain(
|
events = itertools.chain(
|
||||||
|
@ -485,6 +568,7 @@ class TestPyReplCompleter(TestCase):
|
||||||
@property
|
@property
|
||||||
def test_func(self):
|
def test_func(self):
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
warnings.warn("warnings\n")
|
warnings.warn("warnings\n")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -508,12 +592,12 @@ class TestPasteEvent(TestCase):
|
||||||
def test_paste(self):
|
def test_paste(self):
|
||||||
# fmt: off
|
# fmt: off
|
||||||
code = (
|
code = (
|
||||||
'def a():\n'
|
"def a():\n"
|
||||||
' for x in range(10):\n'
|
" for x in range(10):\n"
|
||||||
' if x%2:\n'
|
" if x%2:\n"
|
||||||
' print(x)\n'
|
" print(x)\n"
|
||||||
' else:\n'
|
" else:\n"
|
||||||
' pass\n'
|
" pass\n"
|
||||||
)
|
)
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
@ -534,10 +618,10 @@ class TestPasteEvent(TestCase):
|
||||||
def test_paste_mid_newlines(self):
|
def test_paste_mid_newlines(self):
|
||||||
# fmt: off
|
# fmt: off
|
||||||
code = (
|
code = (
|
||||||
'def f():\n'
|
"def f():\n"
|
||||||
' x = y\n'
|
" x = y\n"
|
||||||
' \n'
|
" \n"
|
||||||
' y = z\n'
|
" y = z\n"
|
||||||
)
|
)
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
@ -558,16 +642,16 @@ class TestPasteEvent(TestCase):
|
||||||
def test_paste_mid_newlines_not_in_paste_mode(self):
|
def test_paste_mid_newlines_not_in_paste_mode(self):
|
||||||
# fmt: off
|
# fmt: off
|
||||||
code = (
|
code = (
|
||||||
'def f():\n'
|
"def f():\n"
|
||||||
' x = y\n'
|
"x = y\n"
|
||||||
' \n'
|
"\n"
|
||||||
' y = z\n\n'
|
"y = z\n\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
expected = (
|
expected = (
|
||||||
'def f():\n'
|
"def f():\n"
|
||||||
' x = y\n'
|
" x = y\n"
|
||||||
' '
|
" "
|
||||||
)
|
)
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
@ -579,20 +663,20 @@ class TestPasteEvent(TestCase):
|
||||||
def test_paste_not_in_paste_mode(self):
|
def test_paste_not_in_paste_mode(self):
|
||||||
# fmt: off
|
# fmt: off
|
||||||
input_code = (
|
input_code = (
|
||||||
'def a():\n'
|
"def a():\n"
|
||||||
' for x in range(10):\n'
|
"for x in range(10):\n"
|
||||||
' if x%2:\n'
|
"if x%2:\n"
|
||||||
' print(x)\n'
|
"print(x)\n"
|
||||||
' else:\n'
|
"else:\n"
|
||||||
' pass\n\n'
|
"pass\n\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
output_code = (
|
output_code = (
|
||||||
'def a():\n'
|
"def a():\n"
|
||||||
' for x in range(10):\n'
|
" for x in range(10):\n"
|
||||||
' if x%2:\n'
|
" if x%2:\n"
|
||||||
' print(x)\n'
|
" print(x)\n"
|
||||||
' else:'
|
" else:"
|
||||||
)
|
)
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
@ -605,25 +689,25 @@ class TestPasteEvent(TestCase):
|
||||||
"""Test that bracketed paste using \x1b[200~ and \x1b[201~ works."""
|
"""Test that bracketed paste using \x1b[200~ and \x1b[201~ works."""
|
||||||
# fmt: off
|
# fmt: off
|
||||||
input_code = (
|
input_code = (
|
||||||
'def a():\n'
|
"def a():\n"
|
||||||
' for x in range(10):\n'
|
" for x in range(10):\n"
|
||||||
'\n'
|
"\n"
|
||||||
' if x%2:\n'
|
" if x%2:\n"
|
||||||
' print(x)\n'
|
" print(x)\n"
|
||||||
'\n'
|
"\n"
|
||||||
' else:\n'
|
" else:\n"
|
||||||
' pass\n'
|
" pass\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
output_code = (
|
output_code = (
|
||||||
'def a():\n'
|
"def a():\n"
|
||||||
' for x in range(10):\n'
|
" for x in range(10):\n"
|
||||||
'\n'
|
"\n"
|
||||||
' if x%2:\n'
|
" if x%2:\n"
|
||||||
' print(x)\n'
|
" print(x)\n"
|
||||||
'\n'
|
"\n"
|
||||||
' else:\n'
|
" else:\n"
|
||||||
' pass\n'
|
" pass\n"
|
||||||
)
|
)
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue