Repository: Textualize/rich
Pull request #3506 (2024-09-28 19:19:50)

fix split cells
Fixed `Segment._split_cells` issue discovered in https://github.com/Textualize/textual/issues/4996


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 59e8a187..c40bc948 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Rich will display tracebacks with finely grained error locations on python 3.11+ https://github.com/Textualize/rich/pull/3486
 
+
+### Fixed
+
+- Fixed issue with Segment._split_cells https://github.com/Textualize/rich/pull/3506
+
 ## [13.8.1] - 2024-09-10
 
 ### Fixed
diff --git a/rich/segment.py b/rich/segment.py
index 603d5097..86c8c9a9 100644
--- a/rich/segment.py
+++ b/rich/segment.py
@@ -109,16 +109,29 @@ class Segment(NamedTuple):
     @classmethod
     @lru_cache(1024 * 16)
     def _split_cells(cls, segment: "Segment", cut: int) -> Tuple["Segment", "Segment"]:
+        """Split a segment in to two at a given cell position.
+
+        Note that splitting a double-width character, may result in that character turning
+        into two spaces.
+
+        Args:
+            segment (Segment): A segment to split.
+            cut (int): A cell position to cut on.
+
+        Returns:
+            A tuple of two segments.
+        """
         text, style, control = segment
         _Segment = Segment
-
         cell_length = segment.cell_length
         if cut >= cell_length:
             return segment, _Segment("", style, control)
 
         cell_size = get_character_cell_size
 
-        pos = int((cut / cell_length) * (len(text) - 1))
+        pos = int((cut / cell_length) * (len(text))) - 1
+        if pos < 0:
+            pos = 0
 
         before = text[:pos]
         cell_pos = cell_len(before)


Test patch:
diff --git a/tests/test_segment.py b/tests/test_segment.py
index 3db60b90..4dac53c8 100644
--- a/tests/test_segment.py
+++ b/tests/test_segment.py
@@ -2,6 +2,7 @@ from io import StringIO
 
 import pytest
 
+from rich.cells import cell_len
 from rich.segment import ControlType, Segment, SegmentLines, Segments
 from rich.style import Style
 
@@ -284,6 +285,34 @@ def test_split_cells_emoji(text, split, result):
     assert Segment(text).split_cells(split) == result
 
 
+def test_split_cells_mixed() -> None:
+    """Check that split cells splits on cell positions."""
+    # Caused https://github.com/Textualize/textual/issues/4996 in Textual
+    test = Segment("早乙女リリエル (CV: 徳井青）")
+    for position in range(1, test.cell_length):
+        left, right = Segment.split_cells(test, position)
+        assert cell_len(left.text) == position
+        assert cell_len(right.text) == test.cell_length - position
+
+
+def test_split_cells_doubles() -> None:
+    """Check that split cells splits on cell positions with all double width characters."""
+    test = Segment("早" * 20)
+    for position in range(1, test.cell_length):
+        left, right = Segment.split_cells(test, position)
+        assert cell_len(left.text) == position
+        assert cell_len(right.text) == test.cell_length - position
+
+
+def test_split_cells_single() -> None:
+    """Check that split cells splits on cell positions with all single width characters."""
+    test = Segment("A" * 20)
+    for position in range(1, test.cell_length):
+        left, right = Segment.split_cells(test, position)
+        assert cell_len(left.text) == position
+        assert cell_len(right.text) == test.cell_length - position
+
+
 def test_segment_lines_renderable():
     lines = [[Segment("hello"), Segment(" "), Segment("world")], [Segment("foo")]]
     segment_lines = SegmentLines(lines)