Skip to content

SetTextStyle in GenericLineTransformer — startIndex subtracted twice in length-clamping branch, producing negative length and inverted offsets #587

@udlose

Description

@udlose

Bug

Location

src/AvaloniaEdit.TextMate/GenericLineTransformer.csSetTextStyle:

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 #586Intended behavior of SetTextStyle when length == 0 or startIndex < 0

Steps to Reproduce

  1. Subclass GenericLineTransformer and override TransformLine.
  2. Call SetTextStyle with startIndex and length whose sum exceeds line.Length (i.e., the span would extend past line.EndOffset).
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions