diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/Microsoft.Toolkit.Uwp.UI.Controls.csproj b/Microsoft.Toolkit.Uwp.UI.Controls/Microsoft.Toolkit.Uwp.UI.Controls.csproj
index bf18c15c898..c32e10404d7 100644
--- a/Microsoft.Toolkit.Uwp.UI.Controls/Microsoft.Toolkit.Uwp.UI.Controls.csproj
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/Microsoft.Toolkit.Uwp.UI.Controls.csproj
@@ -59,10 +59,21 @@
+
+
+
+
+
+
+ Designer
+ MSBuild:Compile
+
+
+
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/PlainTextCommandBarFlyout.cs b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/PlainTextCommandBarFlyout.cs
new file mode 100644
index 00000000000..7adbc988c2e
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/PlainTextCommandBarFlyout.cs
@@ -0,0 +1,17 @@
+using Windows.UI.Xaml.Controls;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ internal class PlainTextCommandBarFlyout : TextCommandBarFlyout
+ {
+ public PlainTextCommandBarFlyout()
+ {
+ Opening += (sender, o) =>
+ {
+ PrimaryCommands.Clear();
+
+ // TODO: Limit Pasting to plain-text only
+ };
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/RichSuggestBox.Events.cs b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/RichSuggestBox.Events.cs
new file mode 100644
index 00000000000..73f6229aa30
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/RichSuggestBox.Events.cs
@@ -0,0 +1,15 @@
+using Windows.Foundation;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ public partial class RichSuggestBox
+ {
+ public event TypedEventHandler SuggestionsRequested;
+
+ public event TypedEventHandler SuggestionChosen;
+
+ public event TypedEventHandler TextChanged;
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/RichSuggestBox.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/RichSuggestBox.Properties.cs
new file mode 100644
index 00000000000..b5083430df5
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/RichSuggestBox.Properties.cs
@@ -0,0 +1,137 @@
+using System.Collections.ObjectModel;
+using Windows.UI.Text;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Media;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ public partial class RichSuggestBox
+ {
+ public static readonly DependencyProperty PlaceholderTextProperty =
+ DependencyProperty.Register(
+ nameof(PlaceholderText),
+ typeof(string),
+ typeof(RichSuggestBox),
+ new PropertyMetadata(string.Empty));
+
+ public static readonly DependencyProperty RichEditBoxStyleProperty =
+ DependencyProperty.Register(
+ nameof(RichEditBoxStyle),
+ typeof(Style),
+ typeof(RichSuggestBox),
+ new PropertyMetadata(null));
+
+ public static readonly DependencyProperty HeaderProperty =
+ DependencyProperty.Register(
+ nameof(Header),
+ typeof(object),
+ typeof(RichSuggestBox),
+ new PropertyMetadata(null));
+
+ public static readonly DependencyProperty DescriptionProperty =
+ DependencyProperty.Register(
+ nameof(Description),
+ typeof(object),
+ typeof(RichSuggestBox),
+ new PropertyMetadata(null));
+
+ public static readonly DependencyProperty TextWrappingProperty =
+ DependencyProperty.Register(
+ nameof(TextWrapping),
+ typeof(TextWrapping),
+ typeof(RichSuggestBox),
+ new PropertyMetadata(TextWrapping.NoWrap));
+
+ public static readonly DependencyProperty ClipboardCopyFormatProperty =
+ DependencyProperty.Register(
+ nameof(ClipboardCopyFormat),
+ typeof(RichEditClipboardFormat),
+ typeof(RichSuggestBox),
+ new PropertyMetadata(RichEditClipboardFormat.PlainText));
+
+ public static readonly DependencyProperty SuggestionBackgroundProperty =
+ DependencyProperty.Register(
+ nameof(SuggestionBackground),
+ typeof(SolidColorBrush),
+ typeof(RichSuggestBox),
+ new PropertyMetadata(null));
+
+ public static readonly DependencyProperty SuggestionForegroundProperty =
+ DependencyProperty.Register(
+ nameof(SuggestionForeground),
+ typeof(SolidColorBrush),
+ typeof(RichSuggestBox),
+ new PropertyMetadata(null));
+
+ public static readonly DependencyProperty PrefixesProperty =
+ DependencyProperty.Register(
+ nameof(Prefixes),
+ typeof(string),
+ typeof(RichSuggestBox),
+ new PropertyMetadata("@", OnPrefixesChanged));
+
+ public string PlaceholderText
+ {
+ get => (string)GetValue(PlaceholderTextProperty);
+ set => SetValue(PlaceholderTextProperty, value);
+ }
+
+ public Style RichEditBoxStyle
+ {
+ get => (Style)GetValue(RichEditBoxStyleProperty);
+ set => SetValue(RichEditBoxStyleProperty, value);
+ }
+
+ public object Header
+ {
+ get => GetValue(HeaderProperty);
+ set => SetValue(HeaderProperty, value);
+ }
+
+ public object Description
+ {
+ get => GetValue(DescriptionProperty);
+ set => SetValue(DescriptionProperty, value);
+ }
+
+ public TextWrapping TextWrapping
+ {
+ get => (TextWrapping)GetValue(TextWrappingProperty);
+ set => SetValue(TextWrappingProperty, value);
+ }
+
+ public RichEditClipboardFormat ClipboardCopyFormat
+ {
+ get => (RichEditClipboardFormat)GetValue(ClipboardCopyFormatProperty);
+ set => SetValue(ClipboardCopyFormatProperty, value);
+ }
+
+ public SolidColorBrush SuggestionBackground
+ {
+ get => (SolidColorBrush)GetValue(SuggestionBackgroundProperty);
+ set => SetValue(SuggestionBackgroundProperty, value);
+ }
+
+ public SolidColorBrush SuggestionForeground
+ {
+ get => (SolidColorBrush)GetValue(SuggestionForegroundProperty);
+ set => SetValue(SuggestionForegroundProperty, value);
+ }
+
+ public string Prefixes
+ {
+ get => (string)GetValue(PrefixesProperty);
+ set => SetValue(PrefixesProperty, value);
+ }
+
+ ///
+ /// Gets object used for lock
+ ///
+ protected object LockObj { get; }
+
+ public RichEditTextDocument TextDocument => _richEditBox?.TextDocument;
+
+ public ReadOnlyObservableCollection Tokens { get; }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/RichSuggestBox.cs b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/RichSuggestBox.cs
new file mode 100644
index 00000000000..bd00ece615f
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/RichSuggestBox.cs
@@ -0,0 +1,554 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Toolkit.Uwp.Deferred;
+using Windows.System;
+using Windows.UI.Text;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Controls.Primitives;
+using Windows.UI.Xaml.Input;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ [TemplatePart(Name = PartRichEditBox, Type = typeof(RichEditBox))]
+ [TemplatePart(Name = PartSuggestionsPopup, Type = typeof(Popup))]
+ [TemplatePart(Name = PartSuggestionsList, Type = typeof(ListViewBase))]
+ public partial class RichSuggestBox : ItemsControl
+ {
+ private const string PartRichEditBox = "RichEditBox";
+ private const string PartSuggestionsPopup = "SuggestionsPopup";
+ private const string PartSuggestionsList = "SuggestionsList";
+
+ private Popup _suggestionPopup;
+ private RichEditBox _richEditBox;
+ private ListViewBase _suggestionsList;
+
+ private int _suggestionChoice;
+ private string _currentQuery;
+ private string _currentPrefix;
+ private bool _ignoreChange;
+ private ITextRange _currentRange;
+ private CancellationTokenSource _suggestionRequestedTokenSource;
+ private readonly Dictionary _tokens;
+ private readonly ObservableCollection _visibleTokens;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public RichSuggestBox()
+ {
+ _tokens = new Dictionary();
+ _visibleTokens = new ObservableCollection();
+ Tokens = new ReadOnlyObservableCollection(_visibleTokens);
+ LockObj = new object();
+
+ DefaultStyleKey = typeof(RichSuggestBox);
+
+ RegisterPropertyChangedCallback(ItemsSourceProperty, ItemsSource_PropertyChanged);
+ }
+
+ public void ClearUndoRedoSuggestionHistory()
+ {
+ TextDocument.ClearUndoRedoHistory();
+ if (_tokens.Count == 0)
+ {
+ return;
+ }
+
+ var keysToDelete = _tokens.Where(pair => !pair.Value.Active).Select(pair => pair.Key).ToArray();
+ foreach (var key in keysToDelete)
+ {
+ _tokens.Remove(key);
+ }
+ }
+
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ _suggestionPopup = (Popup)GetTemplateChild(PartSuggestionsPopup);
+ _richEditBox = (RichEditBox)GetTemplateChild(PartRichEditBox);
+ _suggestionsList = (ListViewBase)GetTemplateChild(PartSuggestionsList);
+
+ _richEditBox.SizeChanged += RichEditBox_SizeChanged;
+ _richEditBox.TextChanging += RichEditBox_TextChanging;
+ _richEditBox.TextChanged += RichEditBox_TextChanged;
+ _richEditBox.SelectionChanging += RichEditBox_SelectionChanging;
+ _richEditBox.SelectionChanged += RichEditBox_SelectionChanged;
+ _richEditBox.AddHandler(PointerPressedEvent, new PointerEventHandler(RichEditBoxPointerEventHandler), true);
+ AddKeyboardAccelerators();
+ _suggestionsList.ItemClick += SuggestionsList_ItemClick;
+
+ _suggestionsList.GotFocus += (sender, args) => _richEditBox.Focus(FocusState.Programmatic);
+ LostFocus += (sender, args) => ShowSuggestionsPopup(false);
+ }
+
+ private static void OnPrefixesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var view = (RichSuggestBox)d;
+
+ var newValue = (string)e.NewValue;
+ var prefixes = EnforcePrefixesRequirements(newValue);
+
+ if (newValue != prefixes)
+ {
+ view.SetValue(PrefixesProperty, prefixes);
+ }
+ }
+
+ private static string EnforcePrefixesRequirements(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ return "@";
+ }
+
+ var possibles = string.Concat(value.Where(char.IsPunctuation));
+ return string.IsNullOrEmpty(possibles) ? "@" : possibles;
+ }
+
+ private void RichEditBox_SelectionChanging(RichEditBox sender, RichEditBoxSelectionChangingEventArgs args)
+ {
+ TextDocument.BeginUndoGroup();
+
+ var selection = TextDocument.Selection;
+ if (selection.Type != SelectionType.InsertionPoint)
+ {
+ return;
+ }
+
+ var range = selection.GetClone();
+ range.Expand(TextRangeUnit.Link);
+ if (!_tokens.ContainsKey(range.Link))
+ {
+ return;
+ }
+
+ if (range.StartPosition < selection.StartPosition && selection.EndPosition < range.EndPosition)
+ {
+ // Prevent user from manually editing the link
+ selection.SetRange(range.StartPosition, range.EndPosition);
+ }
+ else if (selection.StartPosition == range.StartPosition)
+ {
+ // Reset formatting if selection is sandwiched between 2 adjacent links
+ // or if the link is at the beginning of the document
+ range.MoveStart(TextRangeUnit.Link, -1);
+ if (selection.StartPosition != range.StartPosition || selection.StartPosition == 0)
+ {
+ ApplyDefaultFormatToRange(selection);
+ }
+ }
+ }
+
+ private async void RichEditBox_SelectionChanged(object sender, RoutedEventArgs e)
+ {
+ await RequestForSuggestionsAsync();
+ }
+
+ private void RichEditBoxPointerEventHandler(object sender, PointerRoutedEventArgs e)
+ {
+ ShowSuggestionsPopup(false);
+ }
+
+ private void AddKeyboardAccelerators()
+ {
+ var enterKeyAccelerator = new KeyboardAccelerator { Key = VirtualKey.Enter };
+ var downKeyAccelerator = new KeyboardAccelerator { Key = VirtualKey.Down };
+ var upKeyAccelerator = new KeyboardAccelerator { Key = VirtualKey.Up };
+ var escapeKeyAccelerator = new KeyboardAccelerator { Key = VirtualKey.Escape };
+
+ enterKeyAccelerator.Invoked += RichEditBoxKeyboardAccelerator_Invoked;
+ downKeyAccelerator.Invoked += RichEditBoxKeyboardAccelerator_Invoked;
+ upKeyAccelerator.Invoked += RichEditBoxKeyboardAccelerator_Invoked;
+ escapeKeyAccelerator.Invoked += RichEditBoxKeyboardAccelerator_Invoked;
+
+ _richEditBox.KeyboardAccelerators.Add(enterKeyAccelerator);
+ _richEditBox.KeyboardAccelerators.Add(downKeyAccelerator);
+ _richEditBox.KeyboardAccelerators.Add(upKeyAccelerator);
+ _richEditBox.KeyboardAccelerators.Add(escapeKeyAccelerator);
+ }
+
+ private void RichEditBoxKeyboardAccelerator_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
+ {
+ var itemsList = _suggestionsList.Items;
+ if (!_suggestionPopup.IsOpen || itemsList == null || itemsList.Count == 0)
+ {
+ return;
+ }
+
+ var key = args.KeyboardAccelerator.Key;
+ switch (key)
+ {
+ case VirtualKey.Up when itemsList.Count == 1:
+ case VirtualKey.Down when itemsList.Count == 1:
+ {
+ _suggestionsList.SelectedItem = itemsList[0];
+ break;
+ }
+ case VirtualKey.Up:
+ {
+ _suggestionChoice = _suggestionChoice <= 0 ? itemsList.Count : _suggestionChoice - 1;
+ _suggestionsList.SelectedItem = _suggestionChoice == 0 ? null : itemsList[_suggestionChoice - 1];
+ args.Handled = true;
+ break;
+ }
+ case VirtualKey.Down:
+ {
+ _suggestionChoice = _suggestionChoice >= itemsList.Count ? 0 : _suggestionChoice + 1;
+ _suggestionsList.SelectedItem = _suggestionChoice == 0 ? null : itemsList[_suggestionChoice - 1];
+ args.Handled = true;
+ break;
+ }
+ case VirtualKey.Enter when _suggestionsList.SelectedItem != null:
+ {
+ ShowSuggestionsPopup(false);
+ _ = OnSuggestionSelectedAsync(_suggestionsList.SelectedItem);
+ args.Handled = true;
+ break;
+ }
+ case VirtualKey.Escape:
+ {
+ ShowSuggestionsPopup(false);
+ args.Handled = true;
+ break;
+ }
+ }
+ }
+
+ private async void SuggestionsList_ItemClick(object sender, ItemClickEventArgs e)
+ {
+ var selectedItem = e.ClickedItem;
+ await OnSuggestionSelectedAsync(selectedItem);
+ }
+
+ private void RichEditBox_TextChanging(RichEditBox sender, RichEditBoxTextChangingEventArgs args)
+ {
+ if (_ignoreChange)
+ {
+ return;
+ }
+
+ _ignoreChange = true;
+ ValidateTokensInDocument();
+ TextDocument.EndUndoGroup();
+ TextDocument.BeginUndoGroup();
+ _ignoreChange = false;
+ }
+
+ private void RichEditBox_TextChanged(object sender, RoutedEventArgs e)
+ {
+ TextChanged?.Invoke((RichEditBox)sender, e);
+ UpdateVisibleTokenList();
+ }
+
+ private void RichEditBox_SizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ _suggestionPopup.VerticalOffset = e.NewSize.Height;
+ _suggestionsList.MaxWidth = e.NewSize.Width;
+ }
+
+ private void ItemsSource_PropertyChanged(DependencyObject sender, DependencyProperty dp)
+ {
+ _suggestionChoice = 0;
+ ShowSuggestionsPopup(ItemsSource is IEnumerable);
+ }
+
+ private async Task RequestForSuggestionsAsync()
+ {
+ _suggestionRequestedTokenSource?.Cancel();
+
+ if (TryExtractQueryFromSelection(out var prefix, out var query, out var range) &&
+ SuggestionsRequested != null)
+ {
+ _suggestionRequestedTokenSource = new CancellationTokenSource();
+ _currentPrefix = prefix;
+ _currentQuery = query;
+ _currentRange = range;
+
+ var cancellationToken = _suggestionRequestedTokenSource.Token;
+ var eventArgs = new SuggestionsRequestedEventArgs { Query = query, Prefix = prefix };
+ try
+ {
+ await SuggestionsRequested.InvokeAsync(this, eventArgs, cancellationToken);
+ }
+ catch (TaskCanceledException)
+ {
+ eventArgs.Cancel = true;
+ }
+ }
+ else
+ {
+ ShowSuggestionsPopup(false);
+ }
+ }
+
+ private async Task OnSuggestionSelectedAsync(object selectedItem)
+ {
+ var range = _currentRange;
+ var id = Guid.NewGuid();
+ var prefix = _currentPrefix;
+ var query = _currentQuery;
+
+ // range has length of 0 at the end of the commit.
+ // Checking length == 0 to avoid committing twice.
+ if (SuggestionChosen == null || prefix == null || query == null || range == null ||
+ range.Length == 0)
+ {
+ return;
+ }
+
+ var eventArgs = new SuggestionChosenEventArgs
+ {
+ Id = id,
+ Prefix = prefix,
+ Query = query,
+ SelectedItem = selectedItem,
+ Format = CreateSuggestionTokenFormat()
+ };
+ var textBefore = range.Text;
+ await SuggestionChosen.InvokeAsync(this, eventArgs);
+ var text = eventArgs.Text;
+
+ // Since this operation is async, the document may have changed at this point.
+ // Double check if the range still has the expected query.
+ if (string.IsNullOrEmpty(text) || textBefore != range.Text ||
+ !TryExtractQueryFromRange(range, out var testPrefix, out var testQuery) ||
+ testPrefix != prefix || testQuery != query)
+ {
+ return;
+ }
+
+ lock (LockObj)
+ {
+ var displayText = prefix + text;
+ var tokenRange = CommitSuggestionIntoDocument(range, displayText, id, eventArgs.Format);
+
+ var token = new SuggestionInfo(id, displayText, this) { Active = true, Item = selectedItem };
+ token.UpdateTextRange(tokenRange);
+ _tokens.Add(tokenRange.Link, token);
+ }
+ }
+
+ private ITextRange CommitSuggestionIntoDocument(ITextRange range, string displayText, Guid id, SuggestionTokenFormat format)
+ {
+ _ignoreChange = true;
+ TextDocument.BeginUndoGroup();
+
+ range.SetText(TextSetOptions.Unhide, displayText);
+ range.Link = $"\"{id}\"";
+
+ range.CharacterFormat.BackgroundColor = format.Background;
+ range.CharacterFormat.ForegroundColor = format.Foreground;
+ range.CharacterFormat.Bold = format.Bold;
+ range.CharacterFormat.Italic = format.Italic;
+ range.CharacterFormat.Underline = format.Underline;
+
+ var returnRange = TextDocument.GetRange(range.StartPosition, range.EndPosition);
+
+ range.Collapse(false);
+ range.SetText(TextSetOptions.Unhide, " ");
+ range.Collapse(false);
+ TextDocument.Selection.SetRange(range.EndPosition, range.EndPosition);
+
+ TextDocument.EndUndoGroup();
+ _ignoreChange = false;
+ return returnRange;
+ }
+
+ private void ValidateTokensInDocument()
+ {
+ foreach (var (_, token) in _tokens)
+ {
+ token.Active = false;
+ }
+
+ var range = TextDocument.GetRange(0, 0);
+ range.SetIndex(TextRangeUnit.Character, -1, false);
+
+ // Handle link at the very end of the document where GetIndex fails to detect
+ range.Expand(TextRangeUnit.Link);
+ ValidateTokenFromRange(range);
+
+ var nextIndex = range.GetIndex(TextRangeUnit.Link);
+ while (nextIndex != 0 && nextIndex != 1)
+ {
+ range.Move(TextRangeUnit.Link, -1);
+
+ var validateRange = range.GetClone();
+ validateRange.Expand(TextRangeUnit.Link);
+
+ // Adjacent links have the same index. Manually check each link with Collapse and Expand.
+ var previousText = validateRange.Text;
+ var hasAdjacentToken = true;
+ while (hasAdjacentToken)
+ {
+ ValidateTokenFromRange(validateRange);
+ validateRange.Collapse(false);
+ validateRange.Expand(TextRangeUnit.Link);
+
+ hasAdjacentToken = validateRange.Text != previousText;
+ previousText = validateRange.Text;
+ }
+
+ nextIndex = range.GetIndex(TextRangeUnit.Link);
+ }
+ }
+
+ private bool ValidateTokenFromRange(ITextRange range)
+ {
+ if (range.Length == 0 || string.IsNullOrEmpty(range.Link) ||
+ !_tokens.TryGetValue(range.Link, out var token))
+ {
+ // Handle case where range.Link is empty but it still recognized and rendered as a link
+ if (range.CharacterFormat.LinkType == LinkType.FriendlyLinkName)
+ {
+ range.Link = string.Empty;
+ }
+ return false;
+ }
+
+ if (token.ToString() != range.Text)
+ {
+ //range.Link = string.Empty;
+ range.CharacterFormat = TextDocument.GetDefaultCharacterFormat();
+ token.Active = false;
+ return false;
+ }
+
+ token.UpdateTextRange(range);
+ token.Active = true;
+ return true;
+ }
+
+ private void ShowSuggestionsPopup(bool show)
+ {
+ _suggestionPopup.IsOpen = show;
+ if (!show)
+ {
+ _suggestionChoice = 0;
+ }
+ }
+
+ private bool TryExtractQueryFromSelection(out string prefix, out string query, out ITextRange range)
+ {
+ prefix = string.Empty;
+ query = string.Empty;
+ range = null;
+ if (TextDocument.Selection.Type != SelectionType.InsertionPoint)
+ {
+ return false;
+ }
+
+ // Check if selection is on existing link (suggestion)
+ var expandCount = TextDocument.Selection.GetClone().Expand(TextRangeUnit.Link);
+ if (expandCount != 0)
+ {
+ return false;
+ }
+
+ var selection = TextDocument.Selection.GetClone();
+ selection.MoveStart(TextRangeUnit.Word, -1);
+ if (selection.Length == 0)
+ {
+ return false;
+ }
+
+ range = selection;
+ if (TryExtractQueryFromRange(selection, out prefix, out query))
+ {
+ return true;
+ }
+
+ selection.MoveStart(TextRangeUnit.Word, -1);
+ if (TryExtractQueryFromRange(selection, out prefix, out query))
+ {
+ return true;
+ }
+
+ range = null;
+ return false;
+ }
+
+ private bool TryExtractQueryFromRange(ITextRange range, out string prefix, out string query)
+ {
+ prefix = string.Empty;
+ query = string.Empty;
+ range.GetText(TextGetOptions.NoHidden, out var possibleQuery);
+ if (possibleQuery.Length > 0 && Prefixes.Contains(possibleQuery[0]) &&
+ !possibleQuery.Any(char.IsWhiteSpace) && string.IsNullOrEmpty(range.Link))
+ {
+ if (possibleQuery.Length == 1)
+ {
+ prefix = possibleQuery;
+ return true;
+ }
+
+ prefix = possibleQuery[0].ToString();
+ query = possibleQuery.Substring(1);
+ return true;
+ }
+
+ return false;
+ }
+
+ private SuggestionTokenFormat CreateSuggestionTokenFormat()
+ {
+ var defaultFormat = TextDocument.GetDefaultCharacterFormat();
+ if (SuggestionBackground != null)
+ {
+ defaultFormat.BackgroundColor = SuggestionBackground.Color;
+ }
+
+ if (SuggestionForeground != null)
+ {
+ defaultFormat.ForegroundColor = SuggestionForeground.Color;
+ }
+
+ return new SuggestionTokenFormat
+ {
+ Foreground = defaultFormat.ForegroundColor,
+ Background = defaultFormat.BackgroundColor,
+ Italic = defaultFormat.Italic,
+ Bold = defaultFormat.Bold,
+ Underline = defaultFormat.Underline
+ };
+ }
+
+ private void ApplyDefaultFormatToRange(ITextRange range)
+ {
+ var defaultFormat = TextDocument.GetDefaultCharacterFormat();
+ range.CharacterFormat.BackgroundColor = defaultFormat.BackgroundColor;
+ range.CharacterFormat.ForegroundColor = defaultFormat.ForegroundColor;
+ range.CharacterFormat.Bold = defaultFormat.Bold;
+ range.CharacterFormat.Italic = defaultFormat.Italic;
+ range.CharacterFormat.Underline = defaultFormat.Underline;
+ }
+
+ private void UpdateVisibleTokenList()
+ {
+ lock (LockObj)
+ {
+ var toBeRemoved = _visibleTokens.Where(x => !x.Active).ToArray();
+
+ foreach (var elem in toBeRemoved)
+ {
+ _visibleTokens.Remove(elem);
+ }
+
+ var toBeAdded = _tokens.Where(pair => pair.Value.Active && !_visibleTokens.Contains(pair.Value))
+ .Select(pair => pair.Value).ToArray();
+
+ foreach (var elem in toBeAdded)
+ {
+ _visibleTokens.Add(elem);
+ }
+ }
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/RichSuggestBox.xaml b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/RichSuggestBox.xaml
new file mode 100644
index 00000000000..5f3cb590dd6
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/RichSuggestBox.xaml
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/SuggestionChosenEventArgs.cs b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/SuggestionChosenEventArgs.cs
new file mode 100644
index 00000000000..6ba680e6a3b
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/SuggestionChosenEventArgs.cs
@@ -0,0 +1,20 @@
+using System;
+using Microsoft.Toolkit.Uwp.Deferred;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ public class SuggestionChosenEventArgs : DeferredEventArgs
+ {
+ public string Query { get; internal set; }
+
+ public string Prefix { get; internal set; }
+
+ public string Text { get; set; }
+
+ public object SelectedItem { get; internal set; }
+
+ public Guid Id { get; internal set; }
+
+ public SuggestionTokenFormat Format { get; internal set; }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/SuggestionInfo.cs b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/SuggestionInfo.cs
new file mode 100644
index 00000000000..9893cde2ae5
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/SuggestionInfo.cs
@@ -0,0 +1,59 @@
+using System;
+using System.ComponentModel;
+using Windows.UI.Text;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ public class SuggestionInfo : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public RichSuggestBox Owner { get; }
+
+ public Guid Id { get; }
+
+ public string DisplayText { get; }
+
+ public object Item { get; set; }
+
+ public int RangeStart { get; private set; }
+
+ public int RangeEnd { get; private set; }
+
+ public int Position => _range?.GetIndex(TextRangeUnit.Character) - 1 ?? 0;
+
+ internal bool Active { get; set; }
+
+ private ITextRange _range;
+
+ public SuggestionInfo(Guid id, string displayText, RichSuggestBox owner)
+ {
+ Id = id;
+ DisplayText = displayText;
+ Owner = owner;
+ }
+
+ internal void UpdateTextRange(ITextRange range)
+ {
+ if (_range == null)
+ {
+ _range = range;
+ RangeStart = _range.StartPosition;
+ RangeEnd = _range.EndPosition;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty));
+ }
+ else if (RangeStart != range.StartPosition || RangeEnd != range.EndPosition)
+ {
+ _range.SetRange(range.StartPosition, range.EndPosition);
+ RangeStart = _range.StartPosition;
+ RangeEnd = _range.EndPosition;
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(string.Empty));
+ }
+ }
+
+ public override string ToString()
+ {
+ return $"HYPERLINK \"{Id}\"{DisplayText}";
+ }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/SuggestionTokenFormat.cs b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/SuggestionTokenFormat.cs
new file mode 100644
index 00000000000..61d7ad16720
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/SuggestionTokenFormat.cs
@@ -0,0 +1,18 @@
+using Windows.UI;
+using Windows.UI.Text;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ public class SuggestionTokenFormat
+ {
+ public Color Foreground { get; set; }
+
+ public Color Background { get; set; }
+
+ public FormatEffect Italic { get; set; }
+
+ public FormatEffect Bold { get; set; }
+
+ public UnderlineType Underline { get; set; }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/SuggestionsRequestedEventArgs.cs b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/SuggestionsRequestedEventArgs.cs
new file mode 100644
index 00000000000..def23b9ec13
--- /dev/null
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/RichSuggestBox/SuggestionsRequestedEventArgs.cs
@@ -0,0 +1,11 @@
+using Microsoft.Toolkit.Uwp.Deferred;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ public class SuggestionsRequestedEventArgs : DeferredCancelEventArgs
+ {
+ public string Prefix { get; set; }
+
+ public string Query { get; set; }
+ }
+}
diff --git a/Microsoft.Toolkit.Uwp.UI.Controls/Themes/Generic.xaml b/Microsoft.Toolkit.Uwp.UI.Controls/Themes/Generic.xaml
index 6422f8690d2..55634288f15 100644
--- a/Microsoft.Toolkit.Uwp.UI.Controls/Themes/Generic.xaml
+++ b/Microsoft.Toolkit.Uwp.UI.Controls/Themes/Generic.xaml
@@ -26,6 +26,7 @@
+