After testing meziantou's solution and getting misplaced textrange's with my test data, I came up with this by traversing the text range on a per character basis. I scoured the internet and meziantou's sample is the ONLY thing out there I came across that even considers multiple runs (which should be pretty common you would think right?) but it does not work with new lines or uielements. My solution below DOES, see comments for more details and where to handle inline elements:
public static List<TextRange> FindStringRangesFromPosition(TextPointer position, string matchStr, bool isCaseSensitive = false) {
var matchRangeList = new List<TextRange>();
while (position != null) {
var hlr = FindStringRangeFromPosition(position, matchStr, isCaseSensitive);
if (hlr == null) {
break;
} else {
matchRangeList.Add(hlr);
position = hlr.End;
}
}
return matchRangeList;
}
public static TextRange FindStringRangeFromPosition(TextPointer position, string matchStr, bool isCaseSensitive = false) {
int curIdx = 0;
TextPointer startPointer = null;
StringComparison stringComparison = isCaseSensitive ? StringComparison.CurrentCulture : StringComparison.CurrentCultureIgnoreCase;
while (position != null) {
if (position.GetPointerContext(LogicalDirection.Forward) != TextPointerContext.Text) {
if(position.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.EmbeddedElement) {
var inlineUIelement = position.Parent;
//handle inlineUIelement.Child contents here...
}
position = position.GetNextContextPosition(LogicalDirection.Forward);
continue;
}
var runStr = position.GetTextInRun(LogicalDirection.Forward);
if (string.IsNullOrEmpty(runStr)) {
position = position.GetNextContextPosition(LogicalDirection.Forward);
continue;
}
//only concerned with current character of match string
int runIdx = runStr.IndexOf(matchStr[curIdx].ToString(), stringComparison);
if (runIdx == -1) {
//if no match found reset search
curIdx = 0;
if (startPointer == null) {
position = position.GetNextContextPosition(LogicalDirection.Forward);
} else {
//when no match somewhere after first character reset search to the position AFTER beginning of last partial match
position = startPointer.GetPositionAtOffset(1, LogicalDirection.Forward);
startPointer = null;
}
continue;
}
if (curIdx == 0) {
//beginning of range found at runIdx
startPointer = position.GetPositionAtOffset(runIdx, LogicalDirection.Forward);
}
if (curIdx == matchStr.Length - 1) {
//each character has been matched
var endPointer = position.GetPositionAtOffset(runIdx, LogicalDirection.Forward);
//for edge cases of repeating characters these loops ensure start is not early and last character isn't lost
if (isCaseSensitive) {
while (endPointer != null && !new TextRange(startPointer, endPointer).Text.Contains(matchStr)) {
endPointer = endPointer.GetPositionAtOffset(1, LogicalDirection.Forward);
}
} else {
while (endPointer != null && !new TextRange(startPointer, endPointer).Text.ToLower().Contains(matchStr.ToLower())) {
endPointer = endPointer.GetPositionAtOffset(1, LogicalDirection.Forward);
}
}
if (endPointer == null) {
return null;
}
while (startPointer != null && new TextRange(startPointer, endPointer).Text.Length > matchStr.Length) {
startPointer = startPointer.GetPositionAtOffset(1, LogicalDirection.Forward);
}
if (startPointer == null) {
return null;
}
return new TextRange(startPointer, endPointer);
} else {
//prepare loop for next match character
curIdx++;
//iterate position one offset AFTER match offset
position = position.GetPositionAtOffset(runIdx + 1, LogicalDirection.Forward);
}
}
return null;
}
Usage:
List<TextRange> matchRangeList = FindStringRangesFromPosition(rtb.Document.ContentStart,"expert",false);