fix(util): split_into_lines merges a word split across 2+ colored spans#6844
fix(util): split_into_lines merges a word split across 2+ colored spans#6844chuenchen309 wants to merge 1 commit into
Conversation
ESC_TEXT_REGEX's greedy posttext group makes pretext structurally empty
for every match after the first, so the boundary-detection logic
("pretext empty => treat as space before") was only actually correct
for the first match. A second colored span mid-word (e.g. two
highlighted typo fixes within one word) got mistaken for the start of
a new word, inserting a spurious space. Track the real boundary state
across iterations instead.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #6844 +/- ##
==========================================
+ Coverage 75.56% 75.61% +0.04%
==========================================
Files 163 163
Lines 21285 21289 +4
Branches 3359 3360 +1
==========================================
+ Hits 16085 16097 +12
+ Misses 4411 4403 -8
Partials 789 789
🚀 New features to boost your workflow:
|
|
What sort of negative consequences does this bug have? |
|
Display-only — no data is touched — but it makes the import prompt show a string that isn't the candidate.
from beets.util.diff import colordiff
from beets.util.color import uncolorize
from beets.util.layout import split_into_lines
left, right = colordiff("Extraordinary", "Extrabordinory")
# right == 'Extra\x1b[1;32mb\x1b[39;49;00mordin\x1b[1;32mo\x1b[39;49;00mry'
[uncolorize(l) for l in split_into_lines(right, 100, 100)]
# master: ['Extrabordin ory'] <- spurious space inside the word
# fix: ['Extrabordinory']So at the point where you're deciding whether to accept a match, the candidate renders as It also mis-wraps: at width 12 master breaks after Only reachable when the wrapping path runs ( |
Bug
split_into_lines()inbeets/util/layout.pyreconstructs "words" fromESC_TEXT_REGEX.finditer()matches to correctly wrap colored diff text at whitespace. Each match haspretext/esc/text/reset/posttextgroups;posttextis[^ESC]*(greedy), so it always consumes all plain text up to the next escape sequence. That meanspretextis structurally always empty for every match after the first — the regex engine's next match simply starts right where the previous one'sposttextleft off.The code treats an empty
pretextas proof there's a word boundary ("space before") right there:This is only actually true for the first match (where empty pretext really does mean "start of string"). For every later match, it's a coincidence of the regex structure, not evidence of a real space — so a word containing two or more separately-colored spans (e.g. two highlighted typo-fix characters within one word) gets a spurious space inserted at the second span.
split_into_linesis reachable fromget_layout_lines/get_column_layout, used bybeets/ui/commands/import_/display.pyto wrap diff output duringbeet importwhen a line exceeds terminal width. The colored text comes fromcolordiff()(beets/util/diff.py), which produces exactly this multi-span-per-word coloring whenever a correction touches two non-adjacent parts of the same word.Fix
Track the real boundary state explicitly across
finditeriterations, instead of inferring it purely from the current match'spretextbeing empty:(
posttextempty meansresetis followed directly by the next escape with nothing in between, so the boundary question falls back to whether the current match's owntextended in a space.)Testing
Added
test_split_into_lines_two_spans_in_one_wordtotest/util/test_layout.py. Verified red onmaster(git stashthe source fix — the word gets split with a spurious space), green after. Fulltest/util/suite (191 passed, 8 skipped — no PIL/ImageMagick/platform-specific) passes with no regressions, including the existing multi-span test cases intest_layout.py(which cover the every-match-has-a-real-space case, e.g."test " * 3— confirmed my fix'stext[-1]==" "fallback keeps those correctly separated).ruff check/ruff format --check/mypyclean.Added a changelog entry per
CONTRIBUTING.rst's reminder.Duplicate-work check
No open PR touches
layout.py.This PR was drafted with AI assistance (Claude); I independently reproduced the bug against the real, unmodified module and verified the fix. I reviewed the change and take responsibility for it.