Skip to content

Commit 5090e05

Browse files
committed
auto-indentation in _pyrepl
1 parent 10b1bd9 commit 5090e05

File tree

2 files changed

+137
-21
lines changed

2 files changed

+137
-21
lines changed

Lib/_pyrepl/readline.py

+40-3
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class ReadlineAlikeReader(historical_reader.HistoricalReader, CompletingReader):
9898
# Instance fields
9999
config: ReadlineConfig
100100
more_lines: MoreLinesCallable | None = None
101+
last_used_indentation: str | None = None
101102

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

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

161167
def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]:
@@ -210,6 +216,28 @@ def _get_previous_line_indent(buffer: list[str], pos: int) -> tuple[int, int | N
210216
return prevlinestart, indent
211217

212218

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

Lib/test/test_pyrepl/test_pyrepl.py

+97-18
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44
import unittest
55
from unittest import TestCase
66

7-
from .support import FakeConsole, handle_all_events, handle_events_narrow_console, multiline_input, code_to_events
7+
from .support import (
8+
FakeConsole,
9+
handle_all_events,
10+
handle_events_narrow_console,
11+
multiline_input,
12+
code_to_events,
13+
)
814
from _pyrepl.console import Event
915
from _pyrepl.readline import ReadlineAlikeReader, ReadlineConfig
1016

@@ -313,14 +319,12 @@ def test_basic(self):
313319

314320
def test_multiline_edit(self):
315321
events = itertools.chain(
316-
code_to_events("def f():\n ...\n\n"),
322+
code_to_events("def f():\n...\n\n"),
317323
[
318324
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
319325
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
320326
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
321327
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
322-
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
323-
Event(evt="key", data="right", raw=bytearray(b"\x1bOC")),
324328
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
325329
Event(evt="key", data="g", raw=bytearray(b"g")),
326330
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
@@ -331,9 +335,9 @@ def test_multiline_edit(self):
331335
reader = self.prepare_reader(events)
332336

333337
output = multiline_input(reader)
334-
self.assertEqual(output, "def f():\n ...\n ")
338+
self.assertEqual(output, "def f():\n ...\n ")
335339
output = multiline_input(reader)
336-
self.assertEqual(output, "def g():\n ...\n ")
340+
self.assertEqual(output, "def g():\n ...\n ")
337341

338342
def test_history_navigation_with_up_arrow(self):
339343
events = itertools.chain(
@@ -537,14 +541,14 @@ def test_paste_mid_newlines_not_in_paste_mode(self):
537541
# fmt: off
538542
code = (
539543
'def f():\n'
540-
' x = y\n'
541-
' \n'
542-
' y = z\n\n'
544+
'x = y\n'
545+
'\n'
546+
'y = z\n\n'
543547
)
544548

545549
expected = (
546550
'def f():\n'
547-
' x = y\n'
551+
' x = y\n'
548552
' '
549553
)
550554
# fmt: on
@@ -558,19 +562,19 @@ def test_paste_not_in_paste_mode(self):
558562
# fmt: off
559563
input_code = (
560564
'def a():\n'
561-
' for x in range(10):\n'
562-
' if x%2:\n'
563-
' print(x)\n'
564-
' else:\n'
565-
' pass\n\n'
565+
'for x in range(10):\n'
566+
'if x%2:\n'
567+
'print(x)\n'
568+
'else:\n'
569+
'pass\n\n'
566570
)
567571

568572
output_code = (
569573
'def a():\n'
570-
' for x in range(10):\n'
571-
' if x%2:\n'
574+
' for x in range(10):\n'
575+
' if x%2:\n'
572576
' print(x)\n'
573-
' else:'
577+
' else:'
574578
)
575579
# fmt: on
576580

@@ -634,6 +638,81 @@ def test_bracketed_paste_single_line(self):
634638
output = multiline_input(reader)
635639
self.assertEqual(output, input_code)
636640

641+
def test_auto_indent_default(self):
642+
# fmt: off
643+
input_code = (
644+
'def f():\n'
645+
'pass\n\n'
646+
)
647+
648+
output_code = (
649+
'def f():\n'
650+
' pass\n'
651+
' '
652+
)
653+
# fmt: on
654+
655+
def test_auto_indent_continuation(self):
656+
# auto indenting according to previous user indentation
657+
# fmt: off
658+
events = itertools.chain(
659+
code_to_events("def f():\n"),
660+
# add backspace to delete default auto-indent
661+
[
662+
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
663+
],
664+
code_to_events(
665+
" pass\n"
666+
"pass\n\n"
667+
),
668+
)
669+
670+
output_code = (
671+
'def f():\n'
672+
' pass\n'
673+
' pass\n'
674+
' '
675+
)
676+
677+
# fmt: on
678+
679+
reader = self.prepare_reader(events)
680+
output = multiline_input(reader)
681+
self.assertEqual(output, output_code)
682+
683+
def test_auto_indent_prev_block(self):
684+
# auto indenting according to indentation in different block
685+
# fmt: off
686+
events = itertools.chain(
687+
code_to_events("def f():\n"),
688+
# add backspace to delete default auto-indent
689+
[
690+
Event(evt="key", data="backspace", raw=bytearray(b"\x7f")),
691+
],
692+
code_to_events(
693+
" pass\n"
694+
"pass\n\n"
695+
),
696+
code_to_events(
697+
'def g():\n'
698+
'pass\n\n'
699+
),
700+
)
701+
702+
703+
output_code = (
704+
'def g():\n'
705+
' pass\n'
706+
' '
707+
)
708+
709+
# fmt: on
710+
711+
reader = self.prepare_reader(events)
712+
output1 = multiline_input(reader)
713+
output2 = multiline_input(reader)
714+
self.assertEqual(output2, output_code)
715+
637716

638717
if __name__ == "__main__":
639718
unittest.main()

0 commit comments

Comments
 (0)