Skip to content

Commit a379bdc

Browse files
committed
auto-indentation in _pyrepl
1 parent e9875ec commit a379bdc

File tree

2 files changed

+142
-22
lines changed

2 files changed

+142
-22
lines changed

Lib/_pyrepl/readline.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ class ReadlineAlikeReader(historical_reader.HistoricalReader, CompletingReader):
9999
# Instance fields
100100
config: ReadlineConfig
101101
more_lines: MoreLinesCallable | None = None
102+
last_used_indentation: str | None = None
102103

103104
def __post_init__(self) -> None:
104105
super().__post_init__()
@@ -157,6 +158,11 @@ def get_trimmed_history(self, maxlength: int) -> list[str]:
157158
cut = 0
158159
return self.history[cut:]
159160

161+
def update_last_used_indentation(self) -> None:
162+
indentation = _get_first_indentation(self.buffer)
163+
if indentation is not None:
164+
self.last_used_indentation = indentation
165+
160166
# --- simplified support for reading multiline Python statements ---
161167

162168
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
211217
return prevlinestart, indent
212218

213219

220+
def _get_first_indentation(buffer: list[str]) -> str | None:
221+
indented_line_start = None
222+
for i in range(len(buffer)):
223+
if (i < len(buffer) - 1
224+
and buffer[i] == "\n"
225+
and buffer[i + 1] in " \t"
226+
):
227+
indented_line_start = i + 1
228+
elif indented_line_start is not None and buffer[i] not in " \t\n":
229+
return ''.join(buffer[indented_line_start : i])
230+
return None
231+
232+
233+
def _is_last_char_colon(buffer: list[str]) -> bool:
234+
i = len(buffer)
235+
while i > 0:
236+
i -= 1
237+
if buffer[i] not in " \t\n": # ignore whitespaces
238+
return buffer[i] == ":"
239+
return False
240+
241+
214242
class maybe_accept(commands.Command):
215243
def do(self) -> None:
216244
r: ReadlineAlikeReader
@@ -227,9 +255,18 @@ def do(self) -> None:
227255
# auto-indent the next line like the previous line
228256
prevlinestart, indent = _get_previous_line_indent(r.buffer, r.pos)
229257
r.insert("\n")
230-
if not self.reader.paste_mode and indent:
231-
for i in range(prevlinestart, prevlinestart + indent):
232-
r.insert(r.buffer[i])
258+
if not self.reader.paste_mode:
259+
if indent:
260+
for i in range(prevlinestart, prevlinestart + indent):
261+
r.insert(r.buffer[i])
262+
r.update_last_used_indentation()
263+
if _is_last_char_colon(r.buffer):
264+
if r.last_used_indentation is not None:
265+
indentation = r.last_used_indentation
266+
else:
267+
# default
268+
indentation = " " * 4
269+
r.insert(indentation)
233270
elif not self.reader.paste_mode:
234271
self.finish = True
235272
else:

Lib/test/test_pyrepl/test_pyrepl.py

Lines changed: 102 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,26 @@
55
from unittest import TestCase
66
from unittest.mock import patch
77

8-
from .support import FakeConsole, handle_all_events, handle_events_narrow_console
9-
from .support import more_lines, multiline_input, code_to_events
8+
from .support import (
9+
FakeConsole,
10+
handle_all_events,
11+
handle_events_narrow_console,
12+
more_lines,
13+
multiline_input,
14+
code_to_events,
15+
)
1016
from _pyrepl.console import Event
1117
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
1218
from _pyrepl.readline import multiline_input as readline_multiline_input
1319

1420

1521
class TestCursorPosition(TestCase):
22+
def prepare_reader(self, events):
23+
console = FakeConsole(events)
24+
config = ReadlineConfig(readline_completer=None)
25+
reader = ReadlineAlikeReader(console=console, config=config)
26+
return reader
27+
1628
def test_up_arrow_simple(self):
1729
# fmt: off
1830
code = (
@@ -300,6 +312,79 @@ def test_cursor_position_after_wrap_and_move_up(self):
300312
self.assertEqual(reader.pos, 10)
301313
self.assertEqual(reader.cxy, (1, 1))
302314

315+
def test_auto_indent_default(self):
316+
# fmt: off
317+
input_code = (
318+
'def f():\n'
319+
'pass\n\n'
320+
)
321+
322+
output_code = (
323+
'def f():\n'
324+
' pass\n'
325+
' '
326+
)
327+
# fmt: on
328+
329+
def test_auto_indent_continuation(self):
330+
# auto indenting according to previous user indentation
331+
# fmt: off
332+
events = itertools.chain(
333+
code_to_events("def f():\n"),
334+
# add backspace to delete default auto-indent
335+
[
336+
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
337+
],
338+
code_to_events(
339+
" pass\n"
340+
"pass\n\n"
341+
),
342+
)
343+
344+
output_code = (
345+
'def f():\n'
346+
' pass\n'
347+
' pass\n'
348+
' '
349+
)
350+
# fmt: on
351+
352+
reader = self.prepare_reader(events)
353+
output = multiline_input(reader)
354+
self.assertEqual(output, output_code)
355+
356+
def test_auto_indent_prev_block(self):
357+
# auto indenting according to indentation in different block
358+
# fmt: off
359+
events = itertools.chain(
360+
code_to_events("def f():\n"),
361+
# add backspace to delete default auto-indent
362+
[
363+
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
364+
],
365+
code_to_events(
366+
" pass\n"
367+
"pass\n\n"
368+
),
369+
code_to_events(
370+
'def g():\n'
371+
'pass\n\n'
372+
),
373+
)
374+
375+
376+
output_code = (
377+
'def g():\n'
378+
' pass\n'
379+
' '
380+
)
381+
# fmt: on
382+
383+
reader = self.prepare_reader(events)
384+
output1 = multiline_input(reader)
385+
output2 = multiline_input(reader)
386+
self.assertEqual(output2, output_code)
387+
303388

304389
class TestPyReplOutput(TestCase):
305390
def prepare_reader(self, events):
@@ -316,14 +401,12 @@ def test_basic(self):
316401

317402
def test_multiline_edit(self):
318403
events = itertools.chain(
319-
code_to_events("def f():\n ...\n\n"),
404+
code_to_events("def f():\n...\n\n"),
320405
[
321406
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
322407
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
323408
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
324409
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
325-
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
326-
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
327410
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
328411
Event(evt="key", data="g", raw=bytearray(b"g")),
329412
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
@@ -334,9 +417,9 @@ def test_multiline_edit(self):
334417
reader = self.prepare_reader(events)
335418

336419
output = multiline_input(reader)
337-
self.assertEqual(output, "def f():\n ...\n ")
420+
self.assertEqual(output, "def f():\n ...\n ")
338421
output = multiline_input(reader)
339-
self.assertEqual(output, "def g():\n ...\n ")
422+
self.assertEqual(output, "def g():\n ...\n ")
340423

341424
def test_history_navigation_with_up_arrow(self):
342425
events = itertools.chain(
@@ -559,14 +642,14 @@ def test_paste_mid_newlines_not_in_paste_mode(self):
559642
# fmt: off
560643
code = (
561644
'def f():\n'
562-
' x = y\n'
563-
' \n'
564-
' y = z\n\n'
645+
'x = y\n'
646+
'\n'
647+
'y = z\n\n'
565648
)
566649

567650
expected = (
568651
'def f():\n'
569-
' x = y\n'
652+
' x = y\n'
570653
' '
571654
)
572655
# fmt: on
@@ -580,19 +663,19 @@ def test_paste_not_in_paste_mode(self):
580663
# fmt: off
581664
input_code = (
582665
'def a():\n'
583-
' for x in range(10):\n'
584-
' if x%2:\n'
585-
' print(x)\n'
586-
' else:\n'
587-
' pass\n\n'
666+
'for x in range(10):\n'
667+
'if x%2:\n'
668+
'print(x)\n'
669+
'else:\n'
670+
'pass\n\n'
588671
)
589672

590673
output_code = (
591674
'def a():\n'
592-
' for x in range(10):\n'
593-
' if x%2:\n'
675+
' for x in range(10):\n'
676+
' if x%2:\n'
594677
' print(x)\n'
595-
' else:'
678+
' else:'
596679
)
597680
# fmt: on
598681

0 commit comments

Comments
 (0)