Navigate Words in RichTextBox

A commonly asked question on the WPF forum is - how can I navigate RichTextBox's content word by word (see https://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=778441&SiteID=1). In today's post, we will see how this can be done using the TextPointer API.

What we need is a helper function which takes a TextPointer position and returns us the word start and word end position. It is a good idea to do a mini-spec for this function's behavior. What happens when the position passed is between words or at a word edge?

  • If the TextPointer is within a word or at start of a word boundary, the containing word will be returned.
  • If the TextPointer is between two words, the following (next) word will be returned.
  • If the TextPointer is at trailing word boundary, the following (next) word will be returned.

It is convenient to return the word start and end positions as a TextRange object. Also, our implementation of GetWordRange will return the range covering just the word, excluding any extra trailing whitespaces.

Disclaimer: Before, you dive into the code below, keep in mind that this version is intended for en-us locale only. This assumption greatly simplifies our word-break heuristic (simply use the whitespace character). In reality, any globalized application will need to handle many more sophisticated word-break cases for Korean, Thai, Middle-East and other languages. [May be in WPF v.next, we will implement such an API supporting globalization.]

Most of the code below is well-commented. Please post comments if you have questions!

using System;

using System.Windows;

using System.Windows.Documents;

namespace HyperlinkAutoDetect

{

    public static class WordBreaker

    {

        /// <summary>

        /// Returns a TextRange covering a word containing or following this TextPointer.

        /// </summary>

        /// <remarks>

        /// If this TextPointer is within a word or at start of word, the containing word range is returned.

        /// If this TextPointer is between two words, the following word range is returned.

        /// If this TextPointer is at trailing word boundary, the following word range is returned.

        /// </remarks>

        public static TextRange GetWordRange(TextPointer position)

        {

            TextRange wordRange = null;

            TextPointer wordStartPosition = null;

            TextPointer wordEndPosition = null;

            // Go forward first, to find word end position.

            wordEndPosition = GetPositionAtWordBoundary(position, /*wordBreakDirection*/LogicalDirection.Forward);

            if (wordEndPosition != null)

            {

                // Then travel backwards, to find word start position.

              wordStartPosition = GetPositionAtWordBoundary(wordEndPosition, /*wordBreakDirection*/LogicalDirection.Backward);

            }

            if (wordStartPosition != null && wordEndPosition != null)

            {

                wordRange = new TextRange(wordStartPosition, wordEndPosition);

            }

            return wordRange;

        }

        /// <summary>

        /// 1. When wordBreakDirection = Forward, returns a position at the end of the word,

        /// i.e. a position with a wordBreak character (space) following it.

        /// 2. When wordBreakDirection = Backward, returns a position at the start of the word,

        /// i.e. a position with a wordBreak character (space) preceeding it.

        /// 3. Returns null when there is no workbreak in the requested direction.

        /// </summary>

        private static TextPointer GetPositionAtWordBoundary(TextPointer position, LogicalDirection wordBreakDirection)

        {

            if (!position.IsAtInsertionPosition)

            {

                position = position.GetInsertionPosition(wordBreakDirection);

            }

            TextPointer navigator = position;

            while (navigator != null && !IsPositionNextToWordBreak(navigator, wordBreakDirection))

    {

                navigator = navigator.GetNextInsertionPosition(wordBreakDirection);

            }

            return navigator;

        }

        // Helper for GetPositionAtWordBoundary.

        // Returns true when passed TextPointer is next to a wordBreak in requested direction.

        private static bool IsPositionNextToWordBreak(TextPointer position, LogicalDirection wordBreakDirection)

        {

            bool isAtWordBoundary = false;

            // Skip over any formatting.

          if (position.GetPointerContext(wordBreakDirection) != TextPointerContext.Text)

            {

                position = position.GetInsertionPosition(wordBreakDirection);

            }

            if (position.GetPointerContext(wordBreakDirection) == TextPointerContext.Text)

            {

                LogicalDirection oppositeDirection = (wordBreakDirection == LogicalDirection.Forward) ?

                    LogicalDirection.Backward : LogicalDirection.Forward;

                char[] runBuffer = new char[1];

                char[] oppositeRunBuffer = new char[1];

                position.GetTextInRun(wordBreakDirection, runBuffer, /*startIndex*/0, /*count*/1);

                position.GetTextInRun(oppositeDirection, oppositeRunBuffer, /*startIndex*/0, /*count*/1);

                if (runBuffer[0] == ' ' && !(oppositeRunBuffer[0] == ' '))

                {

                    isAtWordBoundary = true;

                }

            }

            else

            {

                // If we're not adjacent to text then we always want to consider this position a "word break".

                // In practice, we're most likely next to an embedded object or a block boundary.

                isAtWordBoundary = true;

            }

            return isAtWordBoundary;

        }

    }

}