From 6e80edb1f3f7b89c77f6b9f26cb6fba252d0592d Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 8 May 2025 23:24:48 +0800 Subject: [PATCH 1/4] Refactor `_should_auto_indent()` --- Lib/_pyrepl/readline.py | 44 +++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index 560a9db192169e..46354c315c6eea 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -254,19 +254,37 @@ def _should_auto_indent(buffer: list[str], pos: int) -> bool: # check if last character before "pos" is a colon, ignoring # whitespaces and comments. last_char = None - while pos > 0: - pos -= 1 - if last_char is None: - if buffer[pos] not in " \t\n#": # ignore whitespaces and comments - last_char = buffer[pos] - else: - # even if we found a non-whitespace character before - # original pos, we keep going back until newline is reached - # to make sure we ignore comments - if buffer[pos] == "\n": - break - if buffer[pos] == "#": - last_char = None + # A stack to keep track of string delimiters (quotes). Push a quote when + # entering a string, and pop it when the string ends. When the stack is + # empty, we're not inside a string. If encounter a '#' while not inside a + # string, it's a comment start; otherwise, it's just a '#' character within + # a string. + in_string: list[str] = [] + in_comment = False + i = -1 + while i < pos - 1: + i += 1 + char = buffer[i] + + # update last_char + if char == "#": + if in_string: + last_char = char # '#' inside a string is just a character + else: + in_comment = True + elif char == "\n": + # newline ends a comment + in_comment = False + elif char not in " \t" and not in_comment and not in_string: + # update last_char with non-whitespace chars outside comments and strings + last_char = char + + # update stack + if char in "\"'" and (i == 0 or buffer[i - 1] != "\\"): + if in_string and in_string[-1] == char: + in_string.pop() + else: + in_string.append(char) return last_char == ":" From e4ef49de7a0a3d0ed271e672d3b9f07b429a9006 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Thu, 8 May 2025 23:25:32 +0800 Subject: [PATCH 2/4] Add tests --- Lib/test/test_pyrepl/test_pyrepl.py | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index fc8114891d12dd..5a5cb9f67184f2 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -605,6 +605,43 @@ def test_auto_indent_ignore_comments(self): output = multiline_input(reader) self.assertEqual(output, output_code) + def test_auto_indent_noncomment_hash(self): + # fmt: off + events = code_to_events( + "if ' ' == '#':\n" + "pass\n\n" + ) + + output_code = ( + "if ' ' == '#':\n" + " pass\n" + " " + ) + # fmt: on + + reader = self.prepare_reader(events) + output = multiline_input(reader) + self.assertEqual(output, output_code) + + def test_auto_indent_multiline_string(self): + # fmt: off + events = code_to_events( + "s = '''\n" + "Note:\n" + "'''\n\n" + ) + + output_code = ( + "s = '''\n" + "Note:\n" + "'''" + ) + # fmt: on + + reader = self.prepare_reader(events) + output = multiline_input(reader) + self.assertEqual(output, output_code) + class TestPyReplOutput(ScreenEqualMixin, TestCase): def prepare_reader(self, events): From 2113f88222152a305ea67a1324f1c484907f9899 Mon Sep 17 00:00:00 2001 From: Tan Long Date: Sat, 10 May 2025 04:55:03 +0800 Subject: [PATCH 3/4] Don't indent if cursor line is already indented --- Lib/_pyrepl/readline.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/Lib/_pyrepl/readline.py b/Lib/_pyrepl/readline.py index 46354c315c6eea..2e943a33f0d0ce 100644 --- a/Lib/_pyrepl/readline.py +++ b/Lib/_pyrepl/readline.py @@ -261,6 +261,11 @@ def _should_auto_indent(buffer: list[str], pos: int) -> bool: # a string. in_string: list[str] = [] in_comment = False + char_line_indent_start = None + char_line_indent = 0 + lastchar_line_indent = 0 + cursor_line_indent = 0 + i = -1 while i < pos - 1: i += 1 @@ -275,9 +280,18 @@ def _should_auto_indent(buffer: list[str], pos: int) -> bool: elif char == "\n": # newline ends a comment in_comment = False - elif char not in " \t" and not in_comment and not in_string: - # update last_char with non-whitespace chars outside comments and strings - last_char = char + if i < pos - 1 and buffer[i + 1] in " \t": + char_line_indent_start = i + 1 + else: + char_line_indent_start = None # clear last line's line_indent_start + char_line_indent = 0 + elif char not in " \t": + if char_line_indent_start is not None: + char_line_indent = i - char_line_indent_start + if not in_comment and not in_string: + # update last_char with non-whitespace chars outside comments and strings + last_char = char + lastchar_line_indent = char_line_indent # update stack if char in "\"'" and (i == 0 or buffer[i - 1] != "\\"): @@ -285,7 +299,8 @@ def _should_auto_indent(buffer: list[str], pos: int) -> bool: in_string.pop() else: in_string.append(char) - return last_char == ":" + cursor_line_indent = char_line_indent + return last_char == ":" and cursor_line_indent <= lastchar_line_indent class maybe_accept(commands.Command): From dee83020f76a1684629a48cb2e88229c542f742f Mon Sep 17 00:00:00 2001 From: Tan Long Date: Fri, 9 May 2025 22:12:48 +0800 Subject: [PATCH 4/4] Add test --- Lib/test/test_pyrepl/test_pyrepl.py | 42 ++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_pyrepl/test_pyrepl.py b/Lib/test/test_pyrepl/test_pyrepl.py index 5a5cb9f67184f2..ec5a30945f95e5 100644 --- a/Lib/test/test_pyrepl/test_pyrepl.py +++ b/Lib/test/test_pyrepl/test_pyrepl.py @@ -572,6 +572,46 @@ def test_auto_indent_with_comment(self): output = multiline_input(reader) self.assertEqual(output, output_code) + # fmt: off + events = code_to_events( + "def f():\n" + "# foo\n" + "pass\n\n" + ) + + output_code = ( + "def f():\n" + " # foo\n" + " pass\n" + " " + ) + # fmt: on + + reader = self.prepare_reader(events) + output = multiline_input(reader) + self.assertEqual(output, output_code) + + # fmt: off + events = itertools.chain( + code_to_events("def f():\n"), + [ + Event(evt="key", data="backspace", raw=b"\x08"), + ], + code_to_events("# foo\npass\n\n") + ) + + output_code = ( + "def f():\n" + "# foo\n" + " pass\n" + " " + ) + # fmt: on + + reader = self.prepare_reader(events) + output = multiline_input(reader) + self.assertEqual(output, output_code) + def test_auto_indent_with_multicomment(self): # fmt: off events = code_to_events( @@ -605,7 +645,7 @@ def test_auto_indent_ignore_comments(self): output = multiline_input(reader) self.assertEqual(output, output_code) - def test_auto_indent_noncomment_hash(self): + def test_auto_indent_hashtag(self): # fmt: off events = code_to_events( "if ' ' == '#':\n"