Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TextRange GetPositionAtOffset not behaving as expected

I'm trying to implement a basic syntax highlight using WPF RichTextEditor. For this I want to display {} gropus in different colors.

Here's the code which is supposed to split the contents of a RichTextBox into different groups:

    List<Tag> SplitIntoParts(TextRange textRange, int level)
    {
        if (textRange==null||textRange.IsEmpty)
        {
            return new List<Tag>();
        }
        string text = textRange.Text;
        if (text.Length==0)
        {
            return new List<Tag>();
        }
        int startPos=-1, counter = 0;
        List<Tag> result=new List<Tag>();
        for (int i = 0; i < text.Length; i++)
        {
            if (text[i]=='{')
            {
                if (counter==0)
                {
                    startPos = i;
                }
                counter++;
            }
            if (text[i]=='}')
            {
                if (counter==1)
                {
                    Tag t = new Tag()
                                {
                                    StartPosition = textRange.Start.GetPositionAtOffset(startPos), 
                                    EndPosition = textRange.Start.GetPositionAtOffset(i+1), 
                                    Level = level,
                                    Word = text.Substring(startPos,i+1-startPos)
                                };
                    result.Add(t);
                    var tr=new TextRange(textRange.Start.GetPositionAtOffset(startPos + 1), textRange.Start.GetPositionAtOffset(i));
                    result.AddRange(SplitIntoParts(tr, level + 1));
                }
                counter--;
            }
        }
        if (counter>0)//some open branches still left
        {
            var i = text.Length;
            Tag t = new Tag()
            {
                StartPosition = textRange.Start.GetPositionAtOffset(startPos),
                EndPosition = textRange.End,
                Level = level,
                Word = text.Substring(startPos, i - startPos)
            };
            result.Add(t);
            result.AddRange(SplitIntoParts(new TextRange(textRange.Start.GetPositionAtOffset(startPos + 1), textRange.Start.GetPositionAtOffset(i - 1)), level + 1));
        }

        return result;
    }

In this code, I have found textRange.Start.GetPositionAtOffset(startPos + 1) to behave strangely:

let's say, the code has found following group:

{test|try}

and selected it with following code:

var t=new Tag()
                                {
                                    StartPosition = textRange.Start.GetPositionAtOffset(startPos), 
                                    EndPosition = textRange.Start.GetPositionAtOffset(i+1), 
                                    Level = level,
                                    Word = text.Substring(startPos,i+1-startPos)
                                };

(e.g. t.Word=='{test|try}')

When I try to do the same recursively through passing

var tr=new TextRange(textRange.Start.GetPositionAtOffset(startPos + 1), textRange.Start.GetPositionAtOffset(i));
result.AddRange(SplitIntoParts(tr, level + 1));

instead of "test|try", tr.Text =="{test"

Why am I getting this behavior, and how should I deal with it?

like image 330
Arsen Zahray Avatar asked Oct 26 '25 00:10

Arsen Zahray


1 Answers

GetPositionAtOffset doesn't count only the (visible) characters. Luckily, I got into the same problem recently, so I made a method that gets the TextPointer at the specified offset (the offset that only counts visible characters). First it may seem to be a bit complicated, but it really isn't :-).

As a parameter, it needs the inlines (from the rich text box, like RichTextBox.Document.Blocks.FirstBlock.Inlines, this respectively gets the inlines only of the first paragraph in the rtb, if any...); the second parameter is the offset itself.

It is recommended to give the third parameter, the TextPointer indicating the start of the content. If inlines are specified, the start position is determined from the first inline, but in there are no inlines, an exception is thrown, to avoid this, set the content start parameter to RichTextBox.Document.ContentStart. The method is as follows:

    /// <summary>
    /// Returns the position of the specified offset in the text specified by the inlines.
    /// </summary>
    /// <param name="inlines">The inlines which specifies the text.</param>
    /// <param name="offset">The offset within the text to get the position of.</param>
    /// <param name="contentStartPosition">The position where the content starts. If null, the position before the start of the first inline will be used. If null and there are no inlines, an exception is thrown.</param>
    /// <returns>A <see cref="TextPointer"/> indicating the position of the specified offset.</returns>
    public static TextPointer GetPositionAtOffset(this InlineCollection inlines, int offset, TextPointer contentStartPosition = null)
    {
        if (inlines == null)
            throw new ArgumentNullException(nameof(inlines));
        if (!inlines.Any() && contentStartPosition == null)//if no inlines, can't determine start of content
            throw new ArgumentException("A content start position has to be specified if the inlines collection is empty.", nameof(contentStartPosition));

        if (contentStartPosition == null)
            contentStartPosition = inlines.First().ContentStart.DocumentStart;//if no content start specified, gets it
        int offsetWithInlineBorders = 0;//collects the value of offset (with inline borders)
        foreach (var inline in inlines)
        {
            int inlineLength = (inline as Run)?.Text.Length ?? (inline is LineBreak ? 1 : 0);//gets the length of the inline (length of a Run is the lengts of its text, length of a LineBreak is 1, other types are ignored)

            if (inlineLength < offset)//if position specified by the offset is beyond this inline...
                offsetWithInlineBorders += inlineLength + 2;//...then the whole length is added with the two borders
            else if (inlineLength == offset)//if position specified by the offset is at the end of this inline...
                offsetWithInlineBorders += inlineLength + 1;//...then the whole length is added with only the opening border
            else //inlineLength > value, if the position specified by the offset is within this inline
            {
                offsetWithInlineBorders += offset + 1;//...then adds the remaining length (the offset itself), plus the opening border
                break;//the inlines beyond are not needed
            }
            offset -= inlineLength;//substracts the added inline length
        }

        return contentStartPosition.GetPositionAtOffset(
            Math.Min(Math.Max(offsetWithInlineBorders, 0), contentStartPosition.GetOffsetToPosition(contentStartPosition.DocumentEnd)));//if the value is not within the boundaries of the text, returns the start or the end of the text
    }

Good luck

like image 98
florien Avatar answered Oct 28 '25 14:10

florien



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!