← pod_r5_v1h

rich_3180

failed WRONG_FIX wrong_fix · 16 tool calls · 257 s · Textualize/rich

Task input

Fix double-width characters disappearing when wrapping

## Type of changes

- [x] Bug fix
- [ ] New feature
- [ ] Documentation / docstrings
- [ ] Tests
- [ ] Other

## Checklist

- [x] I've run the latest [black](https://github.com/psf/black) with default args on new code.
- [x] I've updated CHANGELOG.md and CONTRIBUTORS.md where appropriate.
- [x] I've added tests for new code.
- [x] I accept that @willmcgugan may be pedantic in the code review.

## Description

Update wrapping logic to fix issues with CJK charcters disappearing when the "fold" location sat *within* a double-width character. Ensure we retain browser logic of: 


> if there is no space on the current line, move to a new line, and if theres not enough space on the entire new line, fold the text over multiple lines at appropriate locations.

Adds some additional tests and docstrings, documentation etc.

Fixes #3176 

The wrapping process is overall still quite simple and doesn't match the browser in many cases. For example, wrapping does not consider punctuation (lines can begin with punctuation), and whitespace is handled differently (but practically speaking it seems sensible).

Tool calls (17)

#ToolArgumentsResult
1run_command
{
 "command": "grep -rn \"wrap\" rich | grep \"width\" | head -20"
}
{"status": "ok", "stdout": "rich/text.py:1233: offsets = divide_line(str(line), width, fold=wrap_overflow == \"fold\")\nrich/text.py:1239: console, width, justi
full
{"status": "ok", "stdout": "rich/text.py:1233:                offsets = divide_line(str(line), width, fold=wrap_overflow == \"fold\")\nrich/text.py:1239:                    console, width, justify=wrap_justify, overflow=wrap_overflow\nrich/text.py:1242:                line.truncate(width, overflow=wrap_overflow)\nrich/table.py:554:                [(column.width is None and not column.no_wrap) for column in columns],\nrich/table.py:585:        cls, widths: List[int], wrapable: List[bool], max_width: int\nrich/table.py:602:                    width for width, allow_wrap in zip(widths, wrapable) if allow_wrap\nrich/table.py:605:                    width if allow_wrap and width != max_column else 0\nrich/table.py:606:                    for width, allow_wrap in zip(widths, wrapable)\nrich/table.py:610:                    (1 if (width == max_column and allow_wrap) else 0)\nrich/table.py:611:                    for width, allow_wrap in zip(widths, wrapable)\n", "stderr": "", "exit_code": 0}
2run_command
{
 "command": "grep -rn \"divide_line\" rich | head -20"
}
{"status": "ok", "stdout": "rich/_wrap.py:20:def divide_line(text: str, width: int, fold: bool = True) -> List[int]:\nrich/text.py:20:from ._wrap import divide_
full
{"status": "ok", "stdout": "rich/_wrap.py:20:def divide_line(text: str, width: int, fold: bool = True) -> List[int]:\nrich/text.py:20:from ._wrap import divide_line\nrich/text.py:1233:                offsets = divide_line(str(line), width, fold=wrap_overflow == \"fold\")\n", "stderr": "", "exit_code": 0}
3run_command
{
 "command": "sed -n '1,100p' rich/_wrap.py"
}
{"status": "ok", "stdout": "import re\nfrom typing import Iterable, List, Tuple\n\nfrom ._loop import loop_last\nfrom .cells import cell_len, chop_cells\n\nre_w
full
{"status": "ok", "stdout": "import re\nfrom typing import Iterable, List, Tuple\n\nfrom ._loop import loop_last\nfrom .cells import cell_len, chop_cells\n\nre_word = re.compile(r\"\\s*\\S+\\s*\")\n\n\ndef words(text: str) -> Iterable[Tuple[int, int, str]]:\n    position = 0\n    word_match = re_word.match(text, position)\n    while word_match is not None:\n        start, end = word_match.span()\n        word = word_match.group(0)\n        yield start, end, word\n        word_match = re_word.match(text, end)\n\n\ndef divide_line(text: str, width: int, fold: bool = True) -> List[int]:\n    divides: List[int] = []\n    append = divides.append\n    line_position = 0\n    _cell_len = cell_len\n    for start, _end, word in words(text):\n        word_length = _cell_len(word.rstrip())\n        if line_position + word_length > width:\n            if word_length > width:\n                if fold:\n                    chopped_words = chop_cells(word, max_size=width, position=0)\n                    for last, line in loop_last(chopped_words):\n                        if start:\n                            append(start)\n\n                        if last:\n                            line_position = _cell_len(line)\n                        else:\n                            start += len(line)\n                else:\n                    if start:\n                        append(start)\n                    line_position = _cell_len(word)\n            elif line_position and start:\n                append(start)\n                line_position = _cell_len(word)\n        else:\n            line_position += _cell_len(word)\n    return divides\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    from .console import Console\n\n    console = Console(width=10)\n    console.print(\"12345 abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWXYZ 12345\")\n    print(chop_cells(\"abcdefghijklmnopqrstuvwxyz\", 10, position=2))\n", "stderr": "", "exit_code": 0}
4run_command
{
 "command": "cat rich/_wrap.py"
}
{"status": "ok", "stdout": "import re\nfrom typing import Iterable, List, Tuple\n\nfrom ._loop import loop_last\nfrom .cells import cell_len, chop_cells\n\nre_w
full
{"status": "ok", "stdout": "import re\nfrom typing import Iterable, List, Tuple\n\nfrom ._loop import loop_last\nfrom .cells import cell_len, chop_cells\n\nre_word = re.compile(r\"\\s*\\S+\\s*\")\n\n\ndef words(text: str) -> Iterable[Tuple[int, int, str]]:\n    position = 0\n    word_match = re_word.match(text, position)\n    while word_match is not None:\n        start, end = word_match.span()\n        word = word_match.group(0)\n        yield start, end, word\n        word_match = re_word.match(text, end)\n\n\ndef divide_line(text: str, width: int, fold: bool = True) -> List[int]:\n    divides: List[int] = []\n    append = divides.append\n    line_position = 0\n    _cell_len = cell_len\n    for start, _end, word in words(text):\n        word_length = _cell_len(word.rstrip())\n        if line_position + word_length > width:\n            if word_length > width:\n                if fold:\n                    chopped_words = chop_cells(word, max_size=width, position=0)\n                    for last, line in loop_last(chopped_words):\n                        if start:\n                            append(start)\n\n                        if last:\n                            line_position = _cell_len(line)\n                        else:\n                            start += len(line)\n                else:\n                    if start:\n                        append(start)\n                    line_position = _cell_len(word)\n            elif line_position and start:\n                append(start)\n                line_position = _cell_len(word)\n        else:\n            line_position += _cell_len(word)\n    return divides\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    from .console import Console\n\n    console = Console(width=10)\n    console.print(\"12345 abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWXYZ 12345\")\n    print(chop_cells(\"abcdefghijklmnopqrstuvwxyz\", 10, position=2))\n", "stderr": "", "exit_code": 0}
5run_command
{
 "command": "cat rich/cells.py"
}
{"status": "ok", "stdout": "import re\nfrom functools import lru_cache\nfrom typing import Callable, List\n\nfrom ._cell_widths import CELL_WIDTHS\n\n# Regex to
full
{"status": "ok", "stdout": "import re\nfrom functools import lru_cache\nfrom typing import Callable, List\n\nfrom ._cell_widths import CELL_WIDTHS\n\n# Regex to match sequence of the most common character ranges\n_is_single_cell_widths = re.compile(\"^[\\u0020-\\u006f\\u00a0\\u02ff\\u0370-\\u0482]*$\").match\n\n\n@lru_cache(4096)\ndef cached_cell_len(text: str) -> int:\n    \"\"\"Get the number of cells required to display text.\n\n    This method always caches, which may use up a lot of memory. It is recommended to use\n    `cell_len` over this method.\n\n    Args:\n        text (str): Text to display.\n\n    Returns:\n        int: Get the number of cells required to display text.\n    \"\"\"\n    _get_size = get_character_cell_size\n    total_size = sum(_get_size(character) for character in text)\n    return total_size\n\n\ndef cell_len(text: str, _cell_len: Callable[[str], int] = cached_cell_len) -> int:\n    \"\"\"Get the number of cells required to display text.\n\n    Args:\n        text (str): Text to display.\n\n    Returns:\n        int: Get the number of cells required to display text.\n    \"\"\"\n    if len(text) < 512:\n        return _cell_len(text)\n    _get_size = get_character_cell_size\n    total_size = sum(_get_size(character) for character in text)\n    return total_size\n\n\n@lru_cache(maxsize=4096)\ndef get_character_cell_size(character: str) -> int:\n    \"\"\"Get the cell size of a character.\n\n    Args:\n        character (str): A single character.\n\n    Returns:\n        int: Number of cells (0, 1 or 2) occupied by that character.\n    \"\"\"\n    return _get_codepoint_cell_size(ord(character))\n\n\n@lru_cache(maxsize=4096)\ndef _get_codepoint_cell_size(codepoint: int) -> int:\n    \"\"\"Get the cell size of a character.\n\n    Args:\n        codepoint (int): Codepoint of a character.\n\n    Returns:\n        int: Number of cells (0, 1 or 2) occupied by that character.\n    \"\"\"\n\n    _table = CELL_WIDTHS\n    lower_bound = 0\n    uppe
6run_command
{
 "command": "python3 - <<'PY'\nfrom rich.cells import cell_len, chop_cells\nfrom rich._wrap import divide_line\n\ndef test_cjk_wrap():\n    # CJK characters are double-width\n    # \"こんにちは\" (Konnichiwa) is 5 characters, each width 2. Total width 10.\n    text = \"こんにちは\"\n    width = 3\n    # fold=True\n    divides = divide_line(text, width, fold=True)\n    print(f\"Text: {text}, Width: {width}, Divides: {divides}\")\n    # If width is 3, the first character (width 2) fits.\n    # The second character (width 2) makes it 4, which > 3.\n    # So it should be chopped.\n    # \"こ\" (2) fits. \"ん\" (2) does not fit in 3.\n    # chop_cells(\"こんにちは\", 3, 0) should be called for \"こんにちは\" (since it's one word)\n    # chop_cells returns lines in reverse order.\n    # \"は\" (2) -> line 1, total 2\n    # \"ち\" (2) -> line 2, total 2\n    # \"に\" (2) -> line 3, total 2\n    # \"ん\" (2) -> line 4, total 2\n    # \"こ\" (2) -> line 5, total 2\n    # Wait, chop_cells reverses the text first.\n    # characters = [('こ', 2), ('ん', 2), ('に', 2), ('ち', 2), ('は', 2)]\n    # reversed = [('は', 2), ('ち', 2), ('に', 2), ('ん', 2), ('こ', 2)]\n    # total_size = 0\n    # char='は', size=2: total=2, append('は')\n    # char='ち', size=2: total=2+2=4 > 3: lines=[['は'], ['ち']], total=2\n    # char='に', size=2: total=2+2=4 > 3: lines=[['は'], ['ち'], ['に']], total=2\n    # char='ん', size=2: total=2+2=4 > 3: lines=[['は'], ['ち'], ['に'], ['ん']], total=2\n    # char='こ', size=2: total=2+2=4 > 3: lines=[['は'], ['ち'], ['に'], ['ん'], ['こ']], total=2\n    # returns [\"は\", \"ち\", \"に\", \"ん\", \"こ\"]\n    \n    # Now let's see what divide_line does:\n    # word = \"こんにちは\"\n    # word_length = 10\n    # 0 + 10 > 3\n    # word_length (10) > 3\n    # fold=True:\n    # chopped_words = chop_cells(\"こんにちは\", 3, 0) -> [\"は\", \"ち\", \"に\", \"ん\", \"こ\"]\n    # for last, line in loop_last(chopped_words):\n    # loop_last yields (False, \"は\"), (False, \"ち\"), (False, \"に\"), (False, \"ん\"), (True, \"こ\")\n    # 1. last=False, line=\"は\": start=0 (no append), start += len(\"は\") = 1\n    # 2. last=False, line=\"ち\": start=0 (no append), start += len(\"ち\") = 2\n    # 3. last=False, line=\"に\": start=0 (no append), start += len(\"に\") = 3\n    # 4. last=False, line=\"ん\": start=0 (no append), start += len(\"ん\") = 4\n    # 5. last=True, line=\"こ\": start=0 (no append), line_position = cell_len(\"こ\") = 2\n    # Result: divides = []\n    # But if start was > 0, it would append start.\n    \n    # Let's try with some prefix\n    text2 = \"abcこんにちは\" # 3 + 10 = 13 cells\n    width2 = 5\n    # word = \"abcこんにちは\" (one word)\n    # word_length = 13\n    # 0 + 13 > 5\n    # word_length (13) > 5\n    # fold=True:\n    # chop_cells(\"abcこんにちは\", 5, 0)\n    # chars = [('a',1),('b',1),('c',1),('こ',2),('ん',2),('に',2),('ち',2),('は',2)]\n    # reversed = [('は',2),('ち',2),('に',2),('ん',2),('こ',2),('c',1),('b',1),('a',1)]\n    # line 1: 'は', 'ち' (total 4)\n    # line 2: 'に', 'ん' (total 4)\n    # line 3: 'こ', 'c', 'b', 'a' (total 2+1+1+1 = 5)\n    # returns [\"はち\", \"にん\", \"こcba\"] (roughly)\n    \n    # Wait, chop_cells is weird. It reverses the characters and then joins them.\n    # For \"abcこんにちは\", it would return something like [\"はち\", \"にん\", \"こcba\"] reversed?\n    # No, it returns [ \"\".join(line) for line in lines ] where lines were built by iterating reversed characters.\n    # Let's actually run it.\nPY\npython3 -c \"from rich.cells import chop_cells; print(chop_cells('abcこんにちは', 5, 0))\"\n"
}
{"status": "ok", "stdout": "['\u306f\u3061', '\u306b\u3093', '\u3053cba']\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "['\u306f\u3061', '\u306b\u3093', '\u3053cba']\n", "stderr": "", "exit_code": 0}
7run_command
{
 "command": "python3 - <<'PY'\nfrom rich._wrap import divide_line\ntext = \"abcこんにちは\"\nwidth = 5\nprint(f\"Text: {text}, Width: {width}, Divides: {divide_line(text, width, fold=True)}\")\n\ntext2 = \"こんにちは\"\nwidth2 = 3\nprint(f\"Text: {text2}, Width: {width2}, Divides: {divide_line(text2, width2, fold=True)}\")\nPY\n"
}
{"status": "ok", "stdout": "Text: abc\u3053\u3093\u306b\u3061\u306f, Width: 5, Divides: [2, 4]\nText: \u3053\u3093\u306b\u3061\u306f, Width: 3, Divides: [1, 2,
full
{"status": "ok", "stdout": "Text: abc\u3053\u3093\u306b\u3061\u306f, Width: 5, Divides: [2, 4]\nText: \u3053\u3093\u306b\u3061\u306f, Width: 3, Divides: [1, 2, 3, 4]\n", "stderr": "", "exit_code": 0}
8run_command
{
 "command": "python3 - <<'PY'\nfrom rich.cells import chop_cells\nfrom rich._wrap import divide_line\nfrom rich.cells import cell_len\n\ndef test_cjk_disappearance():\n    # If we have a word that is exactly 4 cells wide, and width is 3.\n    # \"こんにちは\" is 10 cells wide.\n    # Width 3.\n    # divide_line(\"こんにちは\", 3, fold=True) returns [1, 2, 3, 4]\n    # The offsets are indices in the string.\n    # String: こ(0) ん(1) に(2) ち(3) は(4)\n    # Offsets: [1, 2, 3, 4]\n    # This means lines are:\n    # text[0:1] = \"こ\" (2 cells)\n    # text[1:2] = \"ん\" (2 cells)\n    # text[2:3] = \"に\" (2 cells)\n    # text[3:4] = \"ち\" (2 cells)\n    # text[4:] = \"は\" (2 cells)\n    # Each line is 2 cells, which is <= 3. This is correct.\n    \n    # What if the fold location is WITHIN a double-width character?\n    # Let's look at chop_cells:\n    # if total_size + size > max_size:\n    #     lines.append([character])\n    #     append = lines[-1].append\n    #     total_size = size\n    #\n    # For \"こんにちは\" (all width 2), width 3:\n    # reversed chars: ('は',2), ('ち',2), ('に',2), ('ん',2), ('こ',2)\n    # 1. char='は', size=2: total=2, append('は')\n    # 2. char='ち', size=2: total=2+2=4 > 3: lines=[['は'], ['ち']], total=2\n    # 3. char='に', size=2: total=2+2=4 > 3: lines=[['は'], ['ち'], ['に']], total=2\n    # ...\n    # Result: [\"は\", \"ち\", \"に\", \"ん\", \"こ\"]\n    # These are the chopped words.\n    #\n    # In divide_line:\n    # for last, line in loop_last(chopped_words):\n    #     if start:\n    #         append(start)\n    #     if last:\n    #         line_position = _cell_len(line)\n    #     else:\n    #         start += len(line)\n    #\n    # If text = \"こんにちは\", width = 3:\n    # start = 0, word = \"こんにちは\"\n    # chopped_words = [\"は\", \"ち\", \"に\", \"ん\", \"こ\"]\n    # 1. last=False, line=\"は\": start=0, start += 1 = 1\n    # 2. last=False, line=\"ち\": start=1, append(1), start += 1 = 2\n    # 3. last=False, line=\"に\": start=2, append(2), start += 1 = 3\n    # 4. last=False, line=\"ん\": start=3, append(3), start += 1 = 4\n    # 5. last=True, line=\"こ\": start=4, append(4), line_position = cell_len(\"こ\") = 2\n    # Result: [1, 2, 3, 4]\n    \n    # Now let's try a case where a character might disappear.\n    # If the word is \" a\" (width 2), and width is 1.\n    # words(\" a\") -> (0, 2, \" a\")\n    # word_length = cell_len(\" a\".rstrip()) = cell_len(\" a\".rstrip()) = 1?\n    # No, \" a\".rstrip() is \" a\" if it's not trailing.\n    # Let's try \"a \"\n    # words(\"a \") -> (0, 2, \"a \")\n    # word_length = cell_len(\"a \".rstrip()) = cell_len(\"a\") = 1.\n    # line_position = 0. 0 + 1 <= 1. line_position = 1.\n    # Result: []\n    \n    # The issue is \"double-width characters disappearing when wrapping\".\n    # This likely happens when `start` is updated by `len(line)`, but `line`\n    # doesn't match the actual character boundary because of how `chop_cells`\n    # or `divide_line` is implemented.\n    \n    # Wait, if `chop_cells` returns characters in reverse order of the string,\n    # but then we use `len(line)` to increment `start`...\n    # \"こんにちは\"\n    # characters: [('こ', 2), ('ん', 2), ('に', 2), ('ち', 2), ('は', 2)]\n    # reversed: [('は', 2), ('ち', 2), ('に', 2), ('ん', 2), ('こ', 2)]\n    # lines: [['は'], ['ち'], ['に'], ['ん'], ['こ']]\n    # chopped_words: [\"は\", \"ち\", \"に\", \"ん\", \"こ\"]\n    #\n    # In divide_line:\n    # 1. line=\"は\" (index 4), len=1. start = 0 + 1 = 1. (Wrong! \"は\" is at index 4)\n    # 2. line=\"ち\" (index 3), len=1. start = 1. append(1). start = 1 + 1 = 2.\n    # 3. line=\"に\" (index 2), len=1. start = 2. append(2). start = 2 + 1 = 3.\n    # 4. line=\"ん\" (index 1), len=1. start = 3. append(3). start = 3 + 1 = 4.\n    # 5. line=\"こ\" (index 0), len=1. start = 4. append(4). line_position = 2.\n    # Result: [1, 2, 3, 4].\n    #\n    # For \"こんにちは\", this happens to work because all characters have length 1.\n    # But if we had different lengths?\n    # \"aこんにちは\" (a=1, othe
{"status": "ok", "stdout": "['cba\u306f', '\u3061\u306b', '\u3093\u3053']\n", "stderr": " File \"<stdin>\", line 272\n # This is definitely wrong. `chopped_word
full
{"status": "ok", "stdout": "['cba\u306f', '\u3061\u306b', '\u3093\u3053']\n", "stderr": "  File \"<stdin>\", line 272\n    # This is definitely wrong. `chopped_words` should be reversed before iterating.\nIndentationError: expected an indented block after function definition on line 5\n", "exit_code": 0}
9run_command
{
 "command": "python3 - <<'PY'\nfrom rich._wrap import divide_line\ntext = \"こんにちはabc\"\nwidth = 5\nprint(f\"Divides: {divide_line(text, width, fold=True)}\")\nPY\n"
}
{"status": "ok", "stdout": "Divides: [4, 6]\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Divides: [4, 6]\n", "stderr": "", "exit_code": 0}
10run_command
{
 "command": "python3 - <<'PY'\nfrom rich.cells import cell_len\ntext = \"こんにちはabc\"\ndivides = [4, 6]\nlines = []\nstart = 0\nfor d in divides:\n    lines.append(text[start:d])\n    start = d\nlines.append(text[start:])\nprint(f\"Lines: {lines}\")\nprint(f\"Widths: {[cell_len(l) for l in lines]}\")\nPY\n"
}
{"status": "ok", "stdout": "Lines: ['\u3053\u3093\u306b\u3061', '\u306fa', 'bc']\nWidths: [8, 3, 2]\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Lines: ['\u3053\u3093\u306b\u3061', '\u306fa', 'bc']\nWidths: [8, 3, 2]\n", "stderr": "", "exit_code": 0}
11run_command
{
 "command": "cd /workspace && python3 - <<'PY'\nfrom pathlib import Path\np = Path(\"rich/_wrap.py\"); s = p.read_text()\nold = \"\"\"                    chopped_words = chop_cells(word, max_size=width, position=0)\n                    for last, line in loop_last(chopped_words):\"\"\"\nnew = \"\"\"                    chopped_words = chop_cells(word, max_size=width, position=0)[::-1]\n                    for last, line in loop_last(chopped_words):\"\"\"\nassert s.count(old) == 1, (\"old text found this many times:\", s.count(old))\nt = s.replace(old, new); compile(t, str(p), \"exec\"); p.write_text(t)\nPY\n"
}
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "", "stderr": "", "exit_code": 0}
12run_command
{
 "command": "python3 - <<'PY'\nfrom rich._wrap import divide_line\ntext = \"こんにちはabc\"\nwidth = 5\nprint(f\"Divides: {divide_line(text, width, fold=True)}\")\nPY\n"
}
{"status": "ok", "stdout": "Divides: [2, 4]\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Divides: [2, 4]\n", "stderr": "", "exit_code": 0}
13run_command
{
 "command": "python3 - <<'PY'\nfrom rich.cells import cell_len\ntext = \"こんにちはabc\"\ndivides = [2, 4]\nlines = []\nstart = 0\nfor d in divides:\n    lines.append(text[start:d])\n    start = d\nlines.append(text[start:])\nprint(f\"Lines: {lines}\")\nprint(f\"Widths: {[cell_len(l) for l in lines]}\")\nPY\n"
}
{"status": "ok", "stdout": "Lines: ['\u3053\u3093', '\u306b\u3061', '\u306fabc']\nWidths: [4, 4, 5]\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Lines: ['\u3053\u3093', '\u306b\u3061', '\u306fabc']\nWidths: [4, 4, 5]\n", "stderr": "", "exit_code": 0}
14run_command
{
 "command": "python3 - <<'PY'\nfrom rich._wrap import divide_line\ntext = \"abcこんにちは\"\nwidth = 5\nprint(f\"Divides: {divide_line(text, width, fold=True)}\")\nPY\n"
}
{"status": "ok", "stdout": "Divides: [4, 6]\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Divides: [4, 6]\n", "stderr": "", "exit_code": 0}
15run_command
{
 "command": "python3 - <<'PY'\nfrom rich.cells import cell_len\ntext = \"abcこんにちは\"\ndivides = [4, 6]\nlines = []\nstart = 0\nfor d in divides:\n    lines.append(text[start:d])\n    start = d\nlines.append(text[start:])\nprint(f\"Lines: {lines}\")\nprint(f\"Widths: {[cell_len(l) for l in lines]}\")\nPY\n"
}
{"status": "ok", "stdout": "Lines: ['abc\u3053', '\u3093\u306b', '\u3061\u306f']\nWidths: [5, 4, 4]\n", "stderr": "", "exit_code": 0}
full
{"status": "ok", "stdout": "Lines: ['abc\u3053', '\u3093\u306b', '\u3061\u306f']\nWidths: [5, 4, 4]\n", "stderr": "", "exit_code": 0}
16run_command
{
 "command": "git diff"
}
{"status": "ok", "stdout": "diff --git a/rich/_wrap.py b/rich/_wrap.py\nindex c45f193f..528b18cc 100644\n--- a/rich/_wrap.py\n+++ b/rich/_wrap.py\n@@ -27,7 +27,
full
{"status": "ok", "stdout": "diff --git a/rich/_wrap.py b/rich/_wrap.py\nindex c45f193f..528b18cc 100644\n--- a/rich/_wrap.py\n+++ b/rich/_wrap.py\n@@ -27,7 +27,7 @@ def divide_line(text: str, width: int, fold: bool = True) -> List[int]:\n         if line_position + word_length > width:\n             if word_length > width:\n                 if fold:\n-                    chopped_words = chop_cells(word, max_size=width, position=0)\n+                    chopped_words = chop_cells(word, max_size=width, position=0)[::-1]\n                     for last, line in loop_last(chopped_words):\n                         if start:\n                             append(start)\n", "stderr": "", "exit_code": 0}
17submit_patch
{}
{"status": "ok", "patch_size": 630, "files_changed": 1}
full
{"status": "ok", "patch_size": 630, "files_changed": 1}

Patch

diff --git a/rich/_wrap.py b/rich/_wrap.py
index c45f193f..528b18cc 100644
--- a/rich/_wrap.py
+++ b/rich/_wrap.py
@@ -27,7 +27,7 @@ def divide_line(text: str, width: int, fold: bool = True) -> List[int]:
         if line_position + word_length > width:
             if word_length > width:
                 if fold:
-                    chopped_words = chop_cells(word, max_size=width, position=0)
+                    chopped_words = chop_cells(word, max_size=width, position=0)[::-1]
                     for last, line in loop_last(chopped_words):
                         if start:
                             append(start)

Test output

show
....F
=================================== FAILURES ===================================
_______________________________ test_chop_cells ________________________________

    def test_chop_cells():
        """Simple example of splitting cells into lines of width 3."""
        text = "abcdefghijk"
>       assert chop_cells(text, 3) == ["abc", "def", "ghi", "jk"]
E       AssertionError: assert ['kji', 'hgf', 'edc', 'ba'] == ['abc', 'def', 'ghi', 'jk']
E         
E         At index 0 diff: 'kji' != 'abc'
E         Use -v to get more diff

tests/test_cells.py:49: AssertionError
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 4 passed in 0.21s