https://github.com/python/cpython/commit/b54ae4cdfce4b84b22889b5c5b9ed0ab9bcb3702
commit: b54ae4cdfce4b84b22889b5c5b9ed0ab9bcb3702
branch: main
author: Serhiy Storchaka <[email protected]>
committer: terryjreedy <[email protected]>
date: 2026-09-23T15:33:10-04:00
summary:

gh-103089: Colorize only the beginning of very long lines in IDLE (#157647)

Adding a tag to a Tk text line takes time proportional to the number
of tags already in the line, so a line with hundreds of thousands of
tokens took hours to colorize.  Now only the first 2000 characters of
a line are colorized, tag positions are relative to the line start,
and tags are added from the end to the start.

files:
A Misc/NEWS.d/next/IDLE/2026-09-17-02-00-00.gh-issue-103089.longline.rst
M Lib/idlelib/colorizer.py
M Lib/idlelib/idle_test/test_colorizer.py

diff --git a/Lib/idlelib/colorizer.py b/Lib/idlelib/colorizer.py
index 5f18a22d2f0507..dbe0723cfdbf96 100644
--- a/Lib/idlelib/colorizer.py
+++ b/Lib/idlelib/colorizer.py
@@ -8,6 +8,11 @@
 
 DEBUG = False
 
+# Adding a tag to a line takes time proportional to the number of tags
+# already in the line, so only the beginning of a line is colorized;
+# the rest is usually not visible anyway (gh-103089).
+MAX_COLORIZED_LINE = 2000
+
 
 def any(name, alternates):
     "Return a named group pattern matching list of alternates."
@@ -350,16 +355,39 @@ def _add_tags_in_section(self, chars, head):
         `chars` is a string with the text to parse and to which
         highlighting is to be applied.
 
-            `head` is the index in the text widget where the text is found.
+        `head` is the index in the text widget where the text is found.
         """
-        for m in self.prog.finditer(chars):
+        # Positions are relative to the start of the current line, so that
+        # Tk does not resolve them through the previous lines.
+        line = int(head.split('.')[0])
+        line_start = 0  # Offset of the current line in chars.
+        tags = []
+        pos = 0
+        while True:
+            m = self.prog.search(chars, pos)
+            if m is None:
+                break
             for name, matched_text in matched_named_groups(m):
                 a, b = m.span(name)
-                self._add_tag(a, b, head, name)
+                tags.append((a - line_start, b - line_start, head, name))
                 if matched_text in ("def", "class"):
                     if m1 := self.idprog.match(chars, b):
                         a, b = m1.span(1)
-                        self._add_tag(a, b, head, "DEFINITION")
+                        tags.append((a - line_start, b - line_start,
+                                     head, "DEFINITION"))
+            pos = m.end()
+            if '\n' in m[0]:
+                line += m[0].count('\n')
+                line_start = m.start() + m[0].rindex('\n') + 1
+                head = f"{line}.0"
+            elif pos - line_start >= MAX_COLORIZED_LINE:
+                # The rest of a long line is not colorized.
+                pos = chars.find('\n', pos)
+                if pos < 0:
+                    break
+        # Adding a tag is faster if there are no tags after it.
+        for args in reversed(tags):
+            self._add_tag(*args)
 
     def removecolors(self):
         "Remove all colorizing tags."
diff --git a/Lib/idlelib/idle_test/test_colorizer.py 
b/Lib/idlelib/idle_test/test_colorizer.py
index 5bb4f5b3ff36e6..16a8b10864ba2e 100644
--- a/Lib/idlelib/idle_test/test_colorizer.py
+++ b/Lib/idlelib/idle_test/test_colorizer.py
@@ -573,6 +573,18 @@ def test_long_multiline_string(self):
             e"""
             ''')
         self._assert_highlighting(source, {'STRING': [('1.0', '5.4')]})
+        source = '"""a\nb""" + str\n'
+        self._assert_highlighting(source, {'STRING': [('1.0', '2.4')],
+                                           'BUILTIN': [('2.7', '2.10')]})
+
+    def test_long_line(self):
+        # gh-103089: only the first MAX_COLORIZED_LINE characters of a line
+        # are colorized.
+        n = colorizer.MAX_COLORIZED_LINE
+        source = f"pass\n{'x' * (n - 3)}'a', 'b'\n'c'\n"
+        self._assert_highlighting(source, {'KEYWORD': [('1.0', '1.4')],
+                                           'STRING': [(f'2.{n-3}', f'2.{n}'),
+                                                      ('3.0', '3.3')]})
 
     @run_in_tk_mainloop(delay=50)
     def test_incremental_editing(self):
diff --git 
a/Misc/NEWS.d/next/IDLE/2026-09-17-02-00-00.gh-issue-103089.longline.rst 
b/Misc/NEWS.d/next/IDLE/2026-09-17-02-00-00.gh-issue-103089.longline.rst
new file mode 100644
index 00000000000000..0a7037eab869ff
--- /dev/null
+++ b/Misc/NEWS.d/next/IDLE/2026-09-17-02-00-00.gh-issue-103089.longline.rst
@@ -0,0 +1,2 @@
+IDLE no longer hangs when opening a file with very long lines.
+Only the first 2,000 characters of a line are colorized.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to