public static int GetNextCaretPosition(ITextSource textSource, int offset, LogicalDirection direction,
CaretPositioningMode mode)
{
if (textSource == null)
throw new ArgumentNullException("textSource");
switch (mode)
{
case CaretPositioningMode.Normal:
case CaretPositioningMode.EveryCodepoint:
case CaretPositioningMode.WordBorder:
case CaretPositioningMode.WordBorderOrSymbol:
case CaretPositioningMode.WordStart:
case CaretPositioningMode.WordStartOrSymbol:
break; // OK
default:
throw new ArgumentException("Unsupported CaretPositioningMode: " + mode, "mode");
}
if (direction != LogicalDirection.Backward
&& direction != LogicalDirection.Forward)
{
throw new ArgumentException("Invalid LogicalDirection: " + direction, "direction");
}
var textLength = textSource.TextLength;
if (textLength <= 0)
{
// empty document? has a normal caret position at 0, though no word borders
if (IsNormal(mode))
{
if (offset > 0 && direction == LogicalDirection.Backward) return 0;
if (offset < 0 && direction == LogicalDirection.Forward) return 0;
}
return -1;
}
while (true)
{
var nextPos = direction == LogicalDirection.Backward ? offset - 1 : offset + 1;
// return -1 if there is no further caret position in the text source
// we also need this to handle offset values outside the valid range
if (nextPos < 0 || nextPos > textLength)
return -1;
// check if we've run against the textSource borders.
// a 'textSource' usually isn't the whole document, but a single VisualLineElement.
if (nextPos == 0)
{
// at the document start, there's only a word border
// if the first character is not whitespace
if (IsNormal(mode) || !char.IsWhiteSpace(textSource.GetCharAt(0)))
return nextPos;
}
else if (nextPos == textLength)
{
// at the document end, there's never a word start
if (mode != CaretPositioningMode.WordStart && mode != CaretPositioningMode.WordStartOrSymbol)
{
// at the document end, there's only a word border
// if the last character is not whitespace
if (IsNormal(mode) || !char.IsWhiteSpace(textSource.GetCharAt(textLength - 1)))
return nextPos;
}
}
else
{
var charBefore = textSource.GetCharAt(nextPos - 1);
var charAfter = textSource.GetCharAt(nextPos);
// Don't stop in the middle of a surrogate pair
if (!char.IsSurrogatePair(charBefore, charAfter))
{
var classBefore = GetCharacterClass(charBefore);
var classAfter = GetCharacterClass(charAfter);
// get correct class for characters outside BMP:
if (char.IsLowSurrogate(charBefore) && nextPos >= 2)
{
classBefore = GetCharacterClass(textSource.GetCharAt(nextPos - 2), charBefore);
}
if (char.IsHighSurrogate(charAfter) && nextPos + 1 < textLength)
{
classAfter = GetCharacterClass(charAfter, textSource.GetCharAt(nextPos + 1));
}
if (StopBetweenCharacters(mode, classBefore, classAfter))
{
return nextPos;
}
}
}
// we'll have to continue searching...
offset = nextPos;
}
}