Skip to content

Commit dae0375

Browse files
authored
gh-111201: Improve pyrepl auto indentation (#119606)
- auto-indent when editing multi-line block - ignore comments
1 parent 94e9585 commit dae0375

File tree

3 files changed

+101
-11
lines changed

3 files changed

+101
-11
lines changed

Lib/_pyrepl/readline.py

+19-8
Original file line numberDiff line numberDiff line change
@@ -237,13 +237,24 @@ def _get_first_indentation(buffer: list[str]) -> str | None:
237237
return None
238238

239239

240-
def _is_last_char_colon(buffer: list[str]) -> bool:
241-
i = len(buffer)
242-
while i > 0:
243-
i -= 1
244-
if buffer[i] not in " \t\n": # ignore whitespaces
245-
return buffer[i] == ":"
246-
return False
240+
def _should_auto_indent(buffer: list[str], pos: int) -> bool:
241+
# check if last character before "pos" is a colon, ignoring
242+
# whitespaces and comments.
243+
last_char = None
244+
while pos > 0:
245+
pos -= 1
246+
if last_char is None:
247+
if buffer[pos] not in " \t\n": # ignore whitespaces
248+
last_char = buffer[pos]
249+
else:
250+
# even if we found a non-whitespace character before
251+
# original pos, we keep going back until newline is reached
252+
# to make sure we ignore comments
253+
if buffer[pos] == "\n":
254+
break
255+
if buffer[pos] == "#":
256+
last_char = None
257+
return last_char == ":"
247258

248259

249260
class maybe_accept(commands.Command):
@@ -280,7 +291,7 @@ def _newline_before_pos():
280291
for i in range(prevlinestart, prevlinestart + indent):
281292
r.insert(r.buffer[i])
282293
r.update_last_used_indentation()
283-
if _is_last_char_colon(r.buffer):
294+
if _should_auto_indent(r.buffer, r.pos):
284295
if r.last_used_indentation is not None:
285296
indentation = r.last_used_indentation
286297
else:

Lib/test/test_pyrepl/test_pyrepl.py

+80-1
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,14 @@ def test_cursor_position_after_wrap_and_move_up(self):
312312
self.assertEqual(reader.pos, 10)
313313
self.assertEqual(reader.cxy, (1, 1))
314314

315+
316+
class TestPyReplAutoindent(TestCase):
317+
def prepare_reader(self, events):
318+
console = FakeConsole(events)
319+
config = ReadlineConfig(readline_completer=None)
320+
reader = ReadlineAlikeReader(console=console, config=config)
321+
return reader
322+
315323
def test_auto_indent_default(self):
316324
# fmt: off
317325
input_code = (
@@ -372,7 +380,6 @@ def test_auto_indent_prev_block(self):
372380
),
373381
)
374382

375-
376383
output_code = (
377384
"def g():\n"
378385
" pass\n"
@@ -385,6 +392,78 @@ def test_auto_indent_prev_block(self):
385392
output2 = multiline_input(reader)
386393
self.assertEqual(output2, output_code)
387394

395+
def test_auto_indent_multiline(self):
396+
# fmt: off
397+
events = itertools.chain(
398+
code_to_events(
399+
"def f():\n"
400+
"pass"
401+
),
402+
[
403+
# go to the end of the first line
404+
Event(evt="key", data="up", raw=bytearray(b"\x1bOA")),
405+
Event(evt="key", data="\x05", raw=bytearray(b"\x1bO5")),
406+
# new line should be autoindented
407+
Event(evt="key", data="\n", raw=bytearray(b"\n")),
408+
],
409+
code_to_events(
410+
"pass"
411+
),
412+
[
413+
# go to end of last line
414+
Event(evt="key", data="down", raw=bytearray(b"\x1bOB")),
415+
Event(evt="key", data="\x05", raw=bytearray(b"\x1bO5")),
416+
# double newline to terminate the block
417+
Event(evt="key", data="\n", raw=bytearray(b"\n")),
418+
Event(evt="key", data="\n", raw=bytearray(b"\n")),
419+
],
420+
)
421+
422+
output_code = (
423+
"def f():\n"
424+
" pass\n"
425+
" pass\n"
426+
" "
427+
)
428+
# fmt: on
429+
430+
reader = self.prepare_reader(events)
431+
output = multiline_input(reader)
432+
self.assertEqual(output, output_code)
433+
434+
def test_auto_indent_with_comment(self):
435+
# fmt: off
436+
events = code_to_events(
437+
"def f(): # foo\n"
438+
"pass\n\n"
439+
)
440+
441+
output_code = (
442+
"def f(): # foo\n"
443+
" pass\n"
444+
" "
445+
)
446+
# fmt: on
447+
448+
reader = self.prepare_reader(events)
449+
output = multiline_input(reader)
450+
self.assertEqual(output, output_code)
451+
452+
def test_auto_indent_ignore_comments(self):
453+
# fmt: off
454+
events = code_to_events(
455+
"pass #:\n"
456+
)
457+
458+
output_code = (
459+
"pass #:"
460+
)
461+
# fmt: on
462+
463+
reader = self.prepare_reader(events)
464+
output = multiline_input(reader)
465+
self.assertEqual(output, output_code)
466+
388467

389468
class TestPyReplOutput(TestCase):
390469
def prepare_reader(self, events):

Lib/test/test_pyrepl/test_reader.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,8 @@ def test_newline_within_block_trailing_whitespace(self):
168168

169169
expected = (
170170
"def foo():\n"
171-
"\n"
172-
"\n"
171+
" \n"
172+
" \n"
173173
" a = 1\n"
174174
" \n"
175175
" " # HistoricalReader will trim trailing whitespace

0 commit comments

Comments
 (0)