Bug
Location
src/AvaloniaEdit.TextMate/GenericLineTransformer.cs → SetTextStyle:
|
length = (line.EndOffset - startIndex) - line.Offset - startIndex; |
Description
When a requested style span extends past the end of a DocumentLine, the length-clamping branch subtracts startIndex twice, producing a negative length* and an inverted endOffset < startOffset. This causes ChangeLinePart — which performs its own strict ArgumentOutOfRangeException validation — to throw on every call where the span overruns the line.
Affected Code (current main)
if ((line.Offset + startIndex + length) > line.EndOffset)
{
// BUG: startIndex is subtracted twice
length = (line.EndOffset - startIndex) - line.Offset - startIndex;
}
Root Cause
startIndex is a line-relative index (0 = first character of this line), while line.Offset and line.EndOffset are document-absolute offsets.
Expanding the current formula reveals the double subtraction:
(line.EndOffset - startIndex) - line.Offset - startIndex = line.EndOffset - line.Offset - 2 * startIndex ← startIndex counted twice
The correct remaining length from startIndex to end of line is simply:
line.EndOffset - (line.Offset + startIndex)
EndOffset Semantics (confirmed from DocumentLine.cs):
|
/// <summary> |
|
/// Gets the end offset of the line in the document's text (the offset before the line delimiter). |
|
/// Runtime: O(log n) |
|
/// </summary> |
|
/// <exception cref="InvalidOperationException">The line was deleted.</exception> |
|
/// <remarks>EndOffset = <see cref="Offset"/> + <see cref="Length"/>.</remarks> |
|
public int EndOffset => Offset + Length; |
Using EndOffset as the clamp boundary is intentionally correct — it stops styling before the \r, \n, or \r\n terminator.
Concrete Example
| Property |
Value |
line.Offset |
100 |
line.EndOffset |
120 |
startIndex |
15 |
Requested length |
10 (span 115–125, past end) |
- Correct clamped length:
120 − (100 + 15) = 5
- Current formula:
(120 − 15) − 100 − 15 = −10 ❌
With length = -10:
startOffset = 115, endOffset = 105 → inverted span
ChangeLinePart immediately throws ArgumentOutOfRangeException because it validates endOffset >= startOffset
Why Silent Clamping Is The Wrong Fix
ChangeLinePart already enforces its own contract with explicit throws (confirmed in DocumentColorizingTransformer.cs):
|
if (startOffset < _currentDocumentLineStartOffset || startOffset > _currentDocumentLineEndOffset) |
|
throw new ArgumentOutOfRangeException(nameof(startOffset), startOffset, "Value must be between " + _currentDocumentLineStartOffset + " and " + _currentDocumentLineEndOffset); |
|
if (endOffset < startOffset || endOffset > _currentDocumentLineEndOffset) |
|
throw new ArgumentOutOfRangeException(nameof(endOffset), endOffset, "Value must be between " + startOffset + " and " + _currentDocumentLineEndOffset); |
Proposed Fix
Replace the clamping branch with upfront parameter validation using the modern BCL throw helpers (.NET 6–8):
public void SetTextStyle(
DocumentLine line,
int startIndex,
int length,
IBrush foreground,
IBrush background,
FontStyle fontStyle,
FontWeight fontWeigth,
bool isUnderline)
{
ArgumentNullException.ThrowIfNull(line);
ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, line.Length);
ArgumentOutOfRangeException.ThrowIfNegative(length);
if (startIndex + length > line.Length)
throw new ArgumentOutOfRangeException(nameof(length), length,
$"startIndex ({startIndex}) + length ({length}) exceeds line length ({line.Length}).");
if (length == 0)
return; // zero-length span — no-op by design, do not style anything (per Discussion #586: https://github.com/AvaloniaUI/AvaloniaEdit/discussions/586
int startOffset = line.Offset + startIndex;
int endOffset = line.Offset + startIndex + length;
if (startOffset > CurrentContext.Document.TextLength ||
endOffset > CurrentContext.Document.TextLength)
return;
ChangeLinePart(
startOffset,
endOffset,
visualLine => ChangeVisualLine(visualLine, foreground, background, fontStyle, fontWeigth, isUnderline));
}
💬 Discussion #586 — Intended behavior of SetTextStyle when length == 0 or startIndex < 0
Steps to Reproduce
- Subclass
GenericLineTransformer and override TransformLine.
- Call
SetTextStyle with startIndex and length whose sum exceeds line.Length (i.e., the span would extend past line.EndOffset).
- An
ArgumentOutOfRangeException is thrown from within ChangeLinePart with a confusing message about document-absolute offsets.
Expected Behavior
Callers that pass an out-of-range startIndex or length receive an ArgumentOutOfRangeException immediately at SetTextStyle with a clear
message identifying the offending parameter.
Actual Behavior
The clamping formula corrupts length to a negative value, ChangeLinePart throws ArgumentOutOfRangeException with a message about internal document offsets that the caller never dealt with directly.
Bug
Location
src/AvaloniaEdit.TextMate/GenericLineTransformer.cs→SetTextStyle:AvaloniaEdit/src/AvaloniaEdit.TextMate/GenericLineTransformer.cs
Line 49 in e88d73a
Description
When a requested style span extends past the end of a
DocumentLine, the length-clamping branch subtractsstartIndextwice, producing a negativelength* and an invertedendOffset < startOffset. This causesChangeLinePart— which performs its own strictArgumentOutOfRangeExceptionvalidation — to throw on every call where the span overruns the line.Affected Code (current
main)Root Cause
startIndexis a line-relative index (0 = first character of this line), whileline.Offsetandline.EndOffsetare document-absolute offsets.Expanding the current formula reveals the double subtraction:
(line.EndOffset - startIndex) - line.Offset - startIndex = line.EndOffset - line.Offset - 2 * startIndex← startIndex counted twiceThe correct remaining length from
startIndexto end of line is simply:line.EndOffset - (line.Offset + startIndex)EndOffsetSemantics (confirmed fromDocumentLine.cs):AvaloniaEdit/src/AvaloniaEdit/Document/DocumentLine.cs
Lines 138 to 144 in e88d73a
Using
EndOffsetas the clamp boundary is intentionally correct — it stops styling before the\r,\n, or\r\nterminator.Concrete Example
line.Offsetline.EndOffsetstartIndexlength120 − (100 + 15)= 5(120 − 15) − 100 − 15= −10 ❌With
length = -10:startOffset = 115,endOffset = 105→ inverted spanChangeLinePartimmediately throwsArgumentOutOfRangeExceptionbecause it validatesendOffset >= startOffsetWhy Silent Clamping Is The Wrong Fix
ChangeLinePartalready enforces its own contract with explicit throws (confirmed inDocumentColorizingTransformer.cs):AvaloniaEdit/src/AvaloniaEdit/Rendering/DocumentColorizingTransformer.cs
Lines 84 to 87 in e88d73a
Proposed Fix
Replace the clamping branch with upfront parameter validation using the modern BCL throw helpers (.NET 6–8):
Steps to Reproduce
GenericLineTransformerand overrideTransformLine.SetTextStylewithstartIndexandlengthwhose sum exceedsline.Length(i.e., the span would extend pastline.EndOffset).ArgumentOutOfRangeExceptionis thrown from withinChangeLinePartwith a confusing message about document-absolute offsets.Expected Behavior
Callers that pass an out-of-range
startIndexorlengthreceive anArgumentOutOfRangeExceptionimmediately atSetTextStylewith a clearmessage identifying the offending parameter.
Actual Behavior
The clamping formula corrupts
lengthto a negative value,ChangeLinePartthrowsArgumentOutOfRangeExceptionwith a message about internal document offsets that the caller never dealt with directly.