← oracle_full

rich_4006

resolved RESOLVED UNSUBMITTED PASS · None tool calls · 0 s · Textualize/rich

Task input

fix for infinite loop in split_graphemes

Fix for https://github.com/Textualize/rich/issues/3958

Tool calls (0)

#ToolArgumentsResult
No trace captured.

Patch

--- a/rich/cells.py
+++ b/rich/cells.py
@@ -161,14 +161,19 @@ def _cell_len(text: str, unicode_version: str) -> int:
 def split_graphemes(
     text: str, unicode_version: str = "auto"
 ) -> "tuple[list[CellSpan], int]":
-    """Divide text into spans that define a single grapheme.
+    """Divide text into spans that define a single grapheme, and additionally return the cell length of the whole string.
+
+    The returned spans will cover every index in the string, with no gaps. It is possible for some graphemes to have a cell length of zero.
+    This can occur for nonsense strings like two zero width joiners, or for control codes that don't contribute to the grapheme size.
 
     Args:
         text: String to split.
         unicode_version: Unicode version, `"auto"` to auto detect, `"latest"` for the latest unicode version.
 
     Returns:
-        List of spans.
+        A tuple of a list of *spans* and the cell length of the entire string. A span is a list of tuples
+            of three values consisting of (<START>, <END>, <CELL LENGTH>), where START and END are string indices,
+            and CELL LENGTH is the cell length of the single grapheme.
     """
 
     cell_table = load_cell_table(unicode_version)
@@ -181,32 +186,48 @@ def split_graphemes(
     SPECIAL = {"\u200d", "\ufe0f"}
     while index < codepoint_count:
         if (character := text[index]) in SPECIAL:
+            if not spans:
+                # ZWJ or variation selector at the beginning of the string doesn't really make sense.
+                # But handle it, we must.
+                spans.append((index, index := index + 1, 0))
+                continue
             if character == "\u200d":
                 # zero width joiner
-                index += 2
-                if spans:
-                    start, _end, cell_length = spans[-1]
-                    spans[-1] = (start, index, cell_length)
-            elif last_measured_character:
+                # The condition handles the case where a ZWJ is at the end of the string, and has nothing to join
+                index += 2 if index < (codepoint_count - 1) else 1
+                start, _end, cell_length = spans[-1]
+                spans[-1] = (start, index, cell_length)
+            else:
                 # variation selector 16
                 index += 1
-                if spans:
+                if last_measured_character:
                     start, _end, cell_length = spans[-1]
                     if last_measured_character in cell_table.narrow_to_wide:
                         last_measured_character = None
                         cell_length += 1
                         total_width += 1
                     spans[-1] = (start, index, cell_length)
+                else:
+                    # No previous character to change the size of.
+                    # Shouldn't occur in practice.
+                    # But handle it, we must.
+                    start, _end, cell_length = spans[-1]
+                    spans[-1] = (start, index, cell_length)
             continue
 
         if character_width := get_character_cell_size(character, unicode_version):
             last_measured_character = character
             spans.append((index, index := index + 1, character_width))
             total_width += character_width
-        elif spans:
-            # zero width characters are associated with the previous character
-            start, _end, cell_length = spans[-1]
-            spans[-1] = (start, index := index + 1, cell_length)
+        else:
+            # Character has zero width
+            if spans:
+                # zero width characters are associated with the previous character
+                start, _end, cell_length = spans[-1]
+                spans[-1] = (start, index := index + 1, cell_length)
+            else:
+                # A zero width character with no prior spans
+                spans.append((index, index := index + 1, 0))
 
     return (spans, total_width)
 

Test output

show
...................................................................      [100%]
67 passed in 0.04s