
A small change with a long reach
Our contribution to Avalonia's numeric-text processing was approved and merged into main on October 27, 2026. Pull request #22545 replaces repeated backward searches with one forward pass through the relevant text sequence.
The change addresses a specific bottleneck in bidirectional-text resolution. Long runs of digits could make the W2 phase revisit the same characters again and again. We kept the required behavior and changed how it finds the context it needs.
The story began with a document that looked almost too simple to be difficult: a very long paragraph of numbers.

The document stopped before the first line
During large-document checks for our Avalonia editor, we exercised the Source workflow with deliberately demanding input. A 900,000-digit paragraph stalled before the formatter produced its first line. This was a stress-test document, not a reported customer incident.
The visible symptom suggested a formatting problem. A captured managed stack gave us a more precise lead: execution was inside BidiAlgorithm.ProcessIsolatedRunSequence, reached during text shaping. The investigation moved from line wrapping to the work performed before a line could be laid out.
We reduced the problem to numeric bidirectional processing. In the original 60,000-number regression, the unchanged algorithm took 3.266 seconds in the documented local test run. That number belongs to that environment and that test, as recorded in the pull request. It is not an end-to-end editor benchmark.
Why would a row of ordinary digits need bidirectional processing at all?
Numbers still need directional context
Text engines must handle Latin letters, Arabic, Hebrew, punctuation and numbers together. The Unicode Bidirectional Algorithm, rule W2 uses the preceding strong directional type to decide how a European number is classified at this stage.
In this terminology, EN means European Number, AN means Arabic Number, AL means Arabic Letter, L means Left-to-Right, and R means Right-to-Left. These are algorithmic classes. Changing EN to AN does not replace the digit in the document or convert its numeric value.
W2 asks a local question: is the nearest preceding strong type AL? If it is, EN becomes AN. L or R stops that Arabic context from carrying farther. If no strong character has appeared, the sequence uses its start-of-sequence context, called sos.
Even simple-looking numeric input can pass through this machinery. The expensive part was repeatedly rediscovering the same answer.
How five digits turn into ten revisits
Take a leading sequence of five European-number digits with no strong character before them. For the first digit, there is no earlier character to inspect. The second looks past one earlier digit. The third looks past two. The fourth looks past three, and the fifth looks past four.
The outer loop visits each digit once. Inside it, the backward searches add this many earlier-character visits:
0 + 1 + 2 + 3 + 4 = 10
For n leading digits, the extra visits total n(n - 1) / 2. This triangular growth is why the W2 phase can do quadratic work, O(n²), on this input. Doubling a long numeric sequence approximately quadruples those repeated prefix visits.
Before, illustrative pseudocode: each European number starts a new search. This deliberately omits the other Bidi rules and Avalonia implementation details.
for each position i:
if type[i] is EN:
strong = sos
j = i - 1
while j >= 0:
if type[j] is L, R or AL:
strong = type[j]
break
j = j - 1
if strong is AL:
type[i] = ANRemember the answer as you move
The correction keeps one value: the most recent strong class. It starts at sos, changes when the pass encounters L, R or AL, and stays unchanged when it encounters a digit or another non-strong class.
Now each EN can consult that value directly. The forward pass already learned the answer on its way to the number. There is no reason to walk back through the prefix.
In the final merged implementation, this state is local to ProcessIsolatedRunSequence. The existing hasEN guard remains, and converting a number also sets hasAN so later processing has the correct information.
After, illustrative pseudocode: reset the remembered context for each isolating run sequence. W1 has already resolved nonspacing marks before this W2 pass.
strong = sos
for each position i:
current = type[i]
if current is L, R or AL:
strong = current
else if current is EN:
if strong is AL:
type[i] = ANFor a sequence of length n, this W2 scan performs O(n) work and uses one extra remembered class. The complete text engine still has shaping, wrapping, other Bidi rules and rendering to do. This correction establishes a linear pass for this phase, not a linear-time guarantee for the entire renderer.
A worked example: when the context changes
Consider these simplified classes in logical order: AL EN EN L EN. The initial AL becomes the remembered strong class, so W2 converts the next two EN classes to AN. Then L replaces the remembered context, and the final EN stays EN at this step.
The result is AL AN AN L EN. This is an intermediate class sequence, not the final visual order. Later rules still run. Keeping that distinction clear is essential when testing a text algorithm.
The review made the tests more useful
The first regression included a wall-clock limit. During review, Gillibald replaced the timer-based assertion with correctness checks and reported that the conformance tests passed. A time limit can fail on a busy machine and still let inefficient code pass on a fast one.
The merged regression tests go beyond checking a plain digit's resolved level. EN and AN can finish at the same level in simple cases, so a test could appear to pass even if W2 conversion were broken. The refined cases use European separators and terminators, whose later treatment under W4 and W5 makes that difference visible.
Other cases exercise changing strong context, a nonspacing mark after an Arabic letter, isolating boundaries, and reuse of the algorithm for another paragraph. Those boundaries matter: remembering context is useful only while that context is valid.
The tests establish correctness for these cases. The code structure establishes the removal of repeated W2 prefix scans. Neither is a universal stopwatch promise.
A practical route through text-performance problems
A stalled editor presents one symptom, but the time may be spent far from the operation the user thinks is slow. Our investigation points to a repeatable approach:
- Keep the workflow, then reduce the input. Start with what the user is doing. Reduce to a small framework-level reproduction without removing the pattern that triggers the cost.
- Capture where execution is actually spending time. A stack or profile can separate Bidi processing from wrapping, layout, application callbacks or painting.
- Count repeated visits. A short inner search can dominate a long input. Ask whether each item is rediscovering a fact the previous item already knew.
- Test meaning and boundaries. Cover mixed directions, inherited classes and state resets. Choose outputs that would actually change if the rule were wrong.
- Measure separately from correctness. Keep reproducible performance measurements with their environment and input, and keep ordinary regression assertions deterministic.
The fastest search is the one the previous character already answered.
What is available, and where to go next
Release status checked October 29, 2026. PR #22545 is merged upstream. The latest official stable Avalonia package checked was 12.1.4, released October 9, before this merge. The PR's automated package comment describes a prerelease CI build, not a stable release.
Our published editor currently uses Avalonia 12.1.4, so this upstream merge is not a claim that the correction has shipped in SpiceLogic's product. Consult Avalonia release notes and the editor's version information when choosing a package.
This contribution grew from the same practice behind our Avalonia keyboard-navigation contribution: investigate a real integration workflow, isolate a framework issue, and submit a focused correction with reviewable evidence.
If you are adding document authoring to an Avalonia application, explore our Avalonia HTML Editor Control. The integration guide and Unicode and right-to-left text documentation explain how to evaluate it with your own content.
The GitHub image is an actual dated capture. The other figures are editorial or conceptual illustrations, not screenshots or measured benchmarks.