diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/MsSpellCheckLib/SpellChecker/SpellChecker.cs b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/MsSpellCheckLib/SpellChecker/SpellChecker.cs index eb3a37b5a16..98d597617d8 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/MsSpellCheckLib/SpellChecker/SpellChecker.cs +++ b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/MsSpellCheckLib/SpellChecker/SpellChecker.cs @@ -28,16 +28,16 @@ namespace MsSpellCheckLib /// a resilient (to out-of-proc COM server failures) interface to callers. /// /// - /// The ISpellCheckerFactory and IUserDictionareisRegistrar methods are implemented using the following pattern. - /// For a method Foo(), we see the following entries: - /// + /// The ISpellCheckerFactory and IUserDictionareisRegistrar methods are implemented using the following pattern. + /// For a method Foo(), we see the following entries: + /// /// 1. The most basic implementation of the method. /// private FooImpl(); - /// + /// /// 2. Some resilience added to the basic implementation. This calls into FooImpl repeatedly. /// private FooImplWithRetries(bool shouldSuppressCOMExceptions); - /// - /// 3. Finally, the version that is exposed to callers. + /// + /// 3. Finally, the version that is exposed to callers. /// public Foo(bool shouldSuppressCOMExceptions = true); /// internal partial class SpellChecker : IDisposable @@ -71,7 +71,7 @@ private bool Init(bool shouldSuppressCOMExceptions = true) #region GetLanguageTage /// - /// We really don't need to call into COM to get this + /// We really don't need to call into COM to get this /// value since we cache it. /// public string GetLanguageTag() @@ -88,23 +88,23 @@ public List SuggestImpl(string word) { IEnumString suggestions = _speller.Value.Suggest(word); - return - suggestions != null ? - suggestions.ToList(shouldSuppressCOMExceptions:false, shouldReleaseCOMObject:true) : - null; + return + suggestions != null ? + suggestions.ToList(shouldSuppressCOMExceptions:false, shouldReleaseCOMObject:true) : + null; } - + public List SuggestImplWithRetries(string word, bool shouldSuppressCOMExceptions = true) { List result = null; bool callSucceeded = RetryHelper.TryExecuteFunction( - func: () => { return SuggestImpl(word); }, + func: () => { return SuggestImpl(word); }, result: out result, preamble: () => Init(shouldSuppressCOMExceptions), ignoredExceptions: SuppressedExceptions[shouldSuppressCOMExceptions]); - return callSucceeded ? result : null; + return callSucceeded ? result : null; } public List Suggest(string word, bool shouldSuppressCOMExceptions = true) @@ -114,7 +114,7 @@ public List Suggest(string word, bool shouldSuppressCOMExceptions = true #endregion // Suggest - #region Add + #region Add private void AddImpl(string word) { @@ -123,8 +123,8 @@ private void AddImpl(string word) private void AddImplWithRetries(string word, bool shouldSuppressCOMExceptions = true) { - // AddImpl and Init are SecuritySafeCritical, so it is okay to - // create an anon. lambdas that calls into them, and pass + // AddImpl and Init are SecuritySafeCritical, so it is okay to + // create an anon. lambdas that calls into them, and pass // those lambdas below. RetryHelper.TryCallAction( action: () => AddImpl(word), @@ -141,7 +141,7 @@ public void Add(string word, bool shouldSuppressCOMExceptions = true) #endregion // Add - #region Ignore + #region Ignore private void IgnoreImpl(string word) { @@ -150,8 +150,8 @@ private void IgnoreImpl(string word) public void IgnoreImplWithRetries(string word, bool shouldSuppressCOMExceptions = true) { - // IgnoreImpl and Init are SecuritySafeCritical, so it is okay to - // create anon. lambdas that calls into them, and pass + // IgnoreImpl and Init are SecuritySafeCritical, so it is okay to + // create anon. lambdas that calls into them, and pass // those lambdas below. RetryHelper.TryCallAction( action: () => IgnoreImpl(word), @@ -189,7 +189,7 @@ public void AutoCorrect(string from, string to, bool suppressCOMExceptions = tru #endregion - #region GetOptionValue + #region GetOptionValue private byte GetOptionValueImpl(string optionId) { @@ -214,16 +214,16 @@ public byte GetOptionValue(string optionId, bool suppressCOMExceptions = true) return GetOptionValueImplWithRetries(optionId, suppressCOMExceptions); } - #endregion // GetOptionValue + #endregion // GetOptionValue #region GetOptionIds private List GetOptionIdsImpl() { IEnumString optionIds = _speller.Value.OptionIds; - return (optionIds != null) ? optionIds.ToList(false, true) : null; + return (optionIds != null) ? optionIds.ToList(false, true) : null; } - + private List GetOptionIdsImplWithRetries(bool suppressCOMExceptions) { List optionIds = null; @@ -234,7 +234,7 @@ private List GetOptionIdsImplWithRetries(bool suppressCOMExceptions) preamble: () => Init(suppressCOMExceptions), ignoredExceptions: SuppressedExceptions[suppressCOMExceptions]); - return callSucceeded ? optionIds : null; + return callSucceeded ? optionIds : null; } public List GetOptionIds(bool suppressCOMExceptions = true) @@ -261,7 +261,7 @@ private string GetIdImplWithRetries(bool suppressCOMExceptions) preamble: () => Init(suppressCOMExceptions), ignoredExceptions: SuppressedExceptions[suppressCOMExceptions]); - return callSucceeded ? id : null; + return callSucceeded ? id : null; } string GetId(bool suppressCOMExceptions = true) @@ -288,7 +288,7 @@ private string GetLocalizedNameImplWithRetries(bool suppressCOMExceptions) preamble: () => Init(suppressCOMExceptions), ignoredExceptions: SuppressedExceptions[suppressCOMExceptions]); - return callSucceeded ? localizedName : null; + return callSucceeded ? localizedName : null; } public string GetLocalizedName(bool suppressCOMExceptions = true) @@ -303,7 +303,7 @@ public string GetLocalizedName(bool suppressCOMExceptions = true) private OptionDescription GetOptionDescriptionImpl(string optionId) { IOptionDescription iod = _speller.Value.GetOptionDescription(optionId); - return (iod != null) ? OptionDescription.Create(iod, false, true) : null; + return (iod != null) ? OptionDescription.Create(iod, false, true) : null; } private OptionDescription GetOptionDescriptionImplWithRetries(string optionId, bool suppressCOMExceptions) @@ -316,7 +316,7 @@ private OptionDescription GetOptionDescriptionImplWithRetries(string optionId, b preamble: () => Init(suppressCOMExceptions), ignoredExceptions: SuppressedExceptions[suppressCOMExceptions]); - return callSucceeded ? optionDescription : null; + return callSucceeded ? optionDescription : null; } public OptionDescription GetOptionDescription(string optionId, bool suppressCOMExceptions = true) @@ -326,12 +326,12 @@ public OptionDescription GetOptionDescription(string optionId, bool suppressCOME #endregion // GetOptionDescription - #region Check + #region Check private List CheckImpl(string text) { IEnumSpellingError errors = _speller.Value.Check(text); - return (errors != null) ? errors.ToList(this, text, false, true) : null; + return (errors != null) ? errors.ToList(this, text, false, true) : null; } private List CheckImplWithRetries(string text, bool suppressCOMExceptions) @@ -344,7 +344,7 @@ private List CheckImplWithRetries(string text, bool suppressCOMEx preamble: () => Init(suppressCOMExceptions), ignoredExceptions: SuppressedExceptions[suppressCOMExceptions]); - return callSucceeded ? errors : null; + return callSucceeded ? errors : null; } public List Check(string text, bool suppressCOMExceptions = true) @@ -359,7 +359,7 @@ public List Check(string text, bool suppressCOMExceptions = true) public List ComprehensiveCheckImpl(string text) { IEnumSpellingError errors = _speller.Value.ComprehensiveCheck(text); - return (errors != null) ? errors.ToList(this, text, false, true) : null; + return (errors != null) ? errors.ToList(this, text, false, true) : null; } public List ComprehensiveCheckImplWithRetries(string text, bool shouldSuppressCOMExceptions = true) @@ -372,7 +372,7 @@ public List ComprehensiveCheckImplWithRetries(string text, bool s preamble: () => Init(shouldSuppressCOMExceptions), ignoredExceptions: SuppressedExceptions[shouldSuppressCOMExceptions]); - return callSucceeded ? errors : null; + return callSucceeded ? errors : null; } public List ComprehensiveCheck(string text, bool shouldSuppressCOMExceptions = true) @@ -382,6 +382,108 @@ public List ComprehensiveCheck(string text, bool shouldSuppressCO #endregion // ComprehensiveCheck + #region HasErrors + + // This returns true if the given text has any spelling errors. + // It is a shortcut for + // ComprehensiveCheck(text)?.Count != 0 + // that avoids the (expensive) creation of the managed list of errors and + // their suggested corrections. + public bool HasErrorsImpl(string text) + { + IEnumSpellingError errors = _speller.Value.ComprehensiveCheck(text); + return (errors != null) ? errors.HasErrors(false, true) : false; + } + + public bool HasErrorsImplWithRetries(string text, bool shouldSuppressCOMExceptions = true) + { + bool hasErrors = false; + bool callSucceeded = + RetryHelper.TryExecuteFunction( + func: () => HasErrorsImpl(text), + result: out hasErrors, + preamble: () => Init(shouldSuppressCOMExceptions), + ignoredExceptions: SuppressedExceptions[shouldSuppressCOMExceptions]); + + return callSucceeded ? hasErrors : false; + } + + public bool HasErrors(string text, bool shouldSuppressCOMExceptions = true) + { + if (_disposed || String.IsNullOrWhiteSpace(text)) + return false; + + // In practice, this method is called many times on the same few + // words in the vicinity of the insertion caret. The calls to the + // native spell-checker can be expensive (more so for misspelled + // that have many nearby corrections), enough to cause lags in + // response time. To mitigate this, we keep a cache of the most + // recent queries and answer from the cache when possible, avoiding + // the expensive native calls about 80% of the time. + + // The _hasErrorsCache member can be set to null by another thread + // when the native spell-checker changes. To avoid NREs, use a local + // reference here. If the cache is nulled out while we're in + // this method, the worst that happens is that a new entry we add + // to the old cache won't be visible to the next query, causing one + // extra "avoidable" native query. It's not worth the effort and + // synchronization overhead to "solve" this very infrequent case. + List hasErrorsCache = _hasErrorsCache; + + // search the MRU cache for the text + int cacheSize = (hasErrorsCache != null) ? hasErrorsCache.Count : 0; + int index; + for (index = 0; index < cacheSize; ++index) + { + if (text == hasErrorsCache[index].Text) + break; + } + + HasErrorsResult result; + if (index < cacheSize) + { + // if found, use the cached result + result = hasErrorsCache[index]; + } + else + { + // otherwise, get the result from the native spell checker + result = new HasErrorsResult(text, HasErrorsImplWithRetries(text, shouldSuppressCOMExceptions)); + + // add it to the cache, initializing as needed + if (hasErrorsCache == null) + { + hasErrorsCache = new List(HasErrorsCacheCapacity); + _hasErrorsCache = hasErrorsCache; + } + + if (cacheSize < HasErrorsCacheCapacity) + { + // add an entry at index cacheSize. It will get overwritten + // in the first iteration of the move-to-front loop, + // but we have to add something so that the reference + // to cache[index] doesn't hit an out-of-range exception. + hasErrorsCache.Add(result); + } + else + { + index = HasErrorsCacheCapacity - 1; + } + } + + // move the entry to the front of the cache (to preserve MRU), + // and return the result + for (; index > 0; --index) + { + hasErrorsCache[index] = hasErrorsCache[index-1]; + } + hasErrorsCache[0] = result; + + return result.HasErrors; + } + + #endregion HasErrors + #region Add/Remove SpellCheckerChanged support private uint? add_SpellCheckerChangedImpl(ISpellCheckerChangedEventHandler handler) @@ -399,7 +501,7 @@ public List ComprehensiveCheck(string text, bool shouldSuppressCO preamble: () => Init(suppressCOMExceptions), ignoredExceptions: SuppressedExceptions[suppressCOMExceptions]); - return callSucceeded ? eventCookie : null; + return callSucceeded ? eventCookie : null; } private uint? add_SpellCheckerChanged(ISpellCheckerChangedEventHandler handler, bool suppressCOMExceptions = true) @@ -422,21 +524,23 @@ private void remove_SpellCheckerChangedImplWithRetries(uint eventCookie, bool su private void remove_SpellCheckerChanged(uint eventCookie, bool suppressCOMExceptions = true) { - if (_disposed) return; + if (_disposed) return; remove_SpellCheckerChangedImplWithRetries(eventCookie, suppressCOMExceptions); } /// - /// This is called when the ISpellChecker instnace stored in .Value - /// changes (likely due to a COM failure and reinitialization). When this happens, + /// This is called when the ISpellChecker instance stored in .Value + /// changes (likely due to a COM failure and reinitialization). When this happens, /// we will re-register with add_SpellCheckerChanged if appropriate and update - /// the eventCookie. Thsi will in-turn permit users of the SpellChecker type - /// to listen to SpellChecker.Changed event when the underlying ISpellChecker - /// instance indicates a change. + /// the eventCookie. Thsi will in-turn permit users of the SpellChecker type + /// to listen to SpellChecker.Changed event when the underlying ISpellChecker + /// instance indicates a change. /// private void SpellerInstanceChanged(object sender, PropertyChangedEventArgs args) { - // Re-register callbacks with ISpellChecker + _hasErrorsCache = null; // cached HasErrors results are no longer valid + + // Re-register callbacks with ISpellChecker if (_changed != null) { lock (_changed) @@ -450,18 +554,20 @@ private void SpellerInstanceChanged(object sender, PropertyChangedEventArgs args } /// - /// Called when ISpellChecker instnace calls into _spellCheckerChangedEventHandler.Invoke - /// to indicate a change. Invoke in turn calls OnChanged. + /// Called when ISpellChecker instance calls into _spellCheckerChangedEventHandler.Invoke + /// to indicate a change. Invoke in turn calls OnChanged. /// internal virtual void OnChanged(SpellCheckerChangedEventArgs e) { + _hasErrorsCache = null; // cached HasErrors results are no longer valid + _changed?.Invoke(this, e); } #region Events /// - /// Event used to receive notifications when the underlying ISpellChecker + /// Event used to receive notifications when the underlying ISpellChecker /// instance indicates a change. /// public event EventHandler Changed @@ -547,13 +653,23 @@ public void Dispose() private string _languageTag; // Change notification related fields - SpellCheckerChangedEventHandler _spellCheckerChangedEventHandler; + SpellCheckerChangedEventHandler _spellCheckerChangedEventHandler; private uint? _eventCookie = null; - private event EventHandler _changed; + private event EventHandler _changed; + + // caching HasErrors results + private class HasErrorsResult : Tuple + { + public HasErrorsResult(string text, bool hasErrors) : base(text, hasErrors) {} + public string Text { get { return Item1; } } + public bool HasErrors { get { return Item2; } } + } + private List _hasErrorsCache; + const int HasErrorsCacheCapacity = 10; // cache the most recent 10 results private bool _disposed = false; - #endregion // Fields + #endregion // Fields } } } diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/MsSpellCheckLib/Utils/Extensions.cs b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/MsSpellCheckLib/Utils/Extensions.cs index d643b1f153b..2751836b12c 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/MsSpellCheckLib/Utils/Extensions.cs +++ b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/MsSpellCheckLib/Utils/Extensions.cs @@ -30,8 +30,8 @@ internal static class Extensions /// Extracts a list of strings from an RCW.IEnumString instance. /// internal static List ToList( - this IEnumString enumString, - bool shouldSuppressCOMExceptions = true, + this IEnumString enumString, + bool shouldSuppressCOMExceptions = true, bool shouldReleaseCOMObject = true) { var result = new List(); @@ -77,10 +77,10 @@ internal static List ToList( /// Extracts a list of SpellingError's from an RCW.IEnumSpellingError instance. /// internal static List ToList( - this IEnumSpellingError spellingErrors, - SpellChecker spellChecker, - string text, - bool shouldSuppressCOMExceptions = true, + this IEnumSpellingError spellingErrors, + SpellChecker spellChecker, + string text, + bool shouldSuppressCOMExceptions = true, bool shouldReleaseCOMObject = true) { if (spellingErrors == null) @@ -99,7 +99,7 @@ internal static List ToList( if (iSpellingError == null) { // no more ISpellingError objects left in the enum - break; + break; } var error = new SpellingError(iSpellingError, spellChecker, text, shouldSuppressCOMExceptions, true); @@ -108,8 +108,8 @@ internal static List ToList( } catch (COMException) when (shouldSuppressCOMExceptions) { - // do nothing here - // the exception filter does it all. + // do nothing here + // the exception filter does it all. } finally { @@ -125,7 +125,7 @@ internal static List ToList( /// /// Determines whether a collection of SpellingError instances /// has any actual errors, or whether they represent a 'clean' - /// result. + /// result. /// internal static bool IsClean(this List errors) { @@ -134,7 +134,7 @@ internal static bool IsClean(this List errors) throw new ArgumentNullException(nameof(errors)); } - bool isClean = true; + bool isClean = true; foreach (var error in errors) { if (error.CorrectiveAction != CorrectiveAction.None) @@ -146,5 +146,56 @@ internal static bool IsClean(this List errors) return isClean; } + + /// + /// Determines whether an RCW.IEnumSpellingError instance has any errors, + /// without asking for expensive details. + /// + internal static bool HasErrors( + this IEnumSpellingError spellingErrors, + bool shouldSuppressCOMExceptions = true, + bool shouldReleaseCOMObject = true) + { + if (spellingErrors == null) + { + throw new ArgumentNullException(nameof(spellingErrors)); + } + + bool result = false; + + try + { + while (!result) + { + ISpellingError iSpellingError = spellingErrors.Next(); + + if (iSpellingError == null) + { + // no more ISpellingError objects left in the enum + break; + } + + if ((CorrectiveAction)iSpellingError.CorrectiveAction != CorrectiveAction.None) + { + result = true; + } + Marshal.ReleaseComObject(iSpellingError); + } + } + catch (COMException) when (shouldSuppressCOMExceptions) + { + // do nothing here + // the exception filter does it all. + } + finally + { + if (shouldReleaseCOMObject) + { + Marshal.ReleaseComObject(spellingErrors); + } + } + + return result; + } } } diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/WinRTSpellerInteropExtensions.cs b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/WinRTSpellerInteropExtensions.cs index 37ea34c0d86..75f4eab8339 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/WinRTSpellerInteropExtensions.cs +++ b/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/WinRTSpellerInteropExtensions.cs @@ -15,7 +15,7 @@ internal static class WinRTSpellerInteropExtensions { /// /// Tokenizes using , and then identifies fixes-up - /// the tokens to account for any missed text "in-between" those tokens. + /// the tokens to account for any missed text "in-between" those tokens. /// /// Word-breaker instance /// The text being tokenized @@ -23,35 +23,35 @@ internal static class WinRTSpellerInteropExtensions /// Calling instance /// /// - /// Windows.Data.Text.WordsSegmenter tends to drop punctuation characters like period ('.') + /// Windows.Data.Text.WordsSegmenter tends to drop punctuation characters like period ('.') /// when tokenizing text. Though this behavior is compatible with a vast majority of text-processing - /// scenarios (like word-counting), it is not ideal for spell-checking. - /// - /// In this method, the following augmented heuristic is applied to update the token-list generated by - /// . - /// + /// scenarios (like word-counting), it is not ideal for spell-checking. + /// + /// In this method, the following augmented heuristic is applied to update the token-list generated by + /// . + /// /// - Identify if any text 'missingFragment' has been dropped by the - /// - If the token immediately preceding 'missingFragment', previousToken, has a spelling error, then attempt to + /// - If the token immediately preceding 'missingFragment', previousToken, has a spelling error, then attempt to /// create new candiate tokens in the following order: - /// + /// /// previousToken + missingFragment[0..0] /// previousToken + missingFragment[0..1] /// previousToken + missingFragment[0..2] /// ... /// ... /// previousToken + missingFragment[0..LEN-1], where LEN = LEN(missingFragment) - /// - /// - Select the first candidate token that is free of spelling errors, and replace 'previousToken' with it. + /// + /// - Select the first candidate token that is free of spelling errors, and replace 'previousToken' with it. /// - For performance reasons, we choose a constant MAXLEN = 4 such that when LEN > MAXLEN, only MAXLEN - /// tokens are considered. - /// - MAXLEN = 4 is a somewhat arbitrary choice, though it seems more than sufficient to address common - /// problems this heuristic is intended to help with. - /// + /// tokens are considered. + /// - MAXLEN = 4 is a somewhat arbitrary choice, though it seems more than sufficient to address common + /// problems this heuristic is intended to help with. + /// /// - Typical word-breaking problems that have been observed empirically involve only one missed character, /// for which MAXLEN=1 would be sufficient. MAXLEN=4 is chosen as a sufficiently-large tradeoff between - /// correctness and performance. - /// - /// - Also see https://github.com/dotnet/wpf/pull/2753#issuecomment-602120768 for a discussion related to this. + /// correctness and performance. + /// + /// - Also see https://github.com/dotnet/wpf/pull/2753#issuecomment-602120768 for a discussion related to this. /// public static IReadOnlyList ComprehensiveGetTokens( this WordsSegmenter segmenter, @@ -79,7 +79,7 @@ public static IReadOnlyList ComprehensiveGetTokens( { // There is a "gap" between the last recorded token and the current token. // Identify the missing token and add it as a "supplementary word segment" - but only if the token - // turns out to be a substantial one (i.e., if the string is non-blank/non-empty). + // turns out to be a substantial one (i.e., if the string is non-blank/non-empty). var missingFragment = new SpellerSegment( text, @@ -99,7 +99,6 @@ public static IReadOnlyList ComprehensiveGetTokens( } } - allTokens.Add( new SpellerSegment( text, @@ -112,7 +111,8 @@ public static IReadOnlyList ComprehensiveGetTokens( } if (tokens.Count > 0 && - spellChecker?.ComprehensiveCheck(tokens[tokens.Count - 1].Text)?.Count != 0 && + spellChecker != null && + spellChecker.HasErrors(tokens[tokens.Count - 1].Text) && predictedNextTokenStartPosition < text.Length) { // There is a token possibly missing at the end of the string @@ -139,8 +139,8 @@ public static IReadOnlyList ComprehensiveGetTokens( } /// - /// Checks through combinations of + substrings() and - /// returns the first spellcheck-clean result. + /// Checks through combinations of + substrings() and + /// returns the first spellcheck-clean result. /// /// Spell-checker /// Overall document text within which the text-ranges are computed @@ -149,33 +149,41 @@ public static IReadOnlyList ComprehensiveGetTokens( /// /// /// See note about MAXLEN in - /// which explains the rationale behind the value of the constant AlternateFormsMaximumCount. + /// which explains the rationale behind the value of the constant AlternateFormsMaximumCount. /// - private static WinRTSpellerInterop.TextRange? GetSpellCheckCleanSubstitutionToken( - SpellChecker spellChecker, + private static WinRTSpellerInterop.TextRange? GetSpellCheckCleanSubstitutionToken( + SpellChecker spellChecker, string documentText, SpellerSegment lastToken, SpellerSegment missingFragment) { const int AlternateFormsMaximumCount = 4; - if (string.IsNullOrWhiteSpace(missingFragment?.Text) || - string.IsNullOrWhiteSpace(lastToken?.Text) || - string.IsNullOrWhiteSpace(documentText)) + string lastTokenText = lastToken?.Text; + string missingFragmentText = missingFragment?.Text.TrimEnd('\0'); + + if (string.IsNullOrWhiteSpace(missingFragmentText) || + string.IsNullOrWhiteSpace(lastTokenText) || + string.IsNullOrWhiteSpace(documentText) || + spellChecker == null || + !spellChecker.HasErrors(lastTokenText)) { return null; } - int altFormsCount = Math.Min(missingFragment.TextRange.Length, AlternateFormsMaximumCount); - var spellingErrors = spellChecker?.ComprehensiveCheck(lastToken.Text); - if (spellingErrors?.Count != 0) + string previousAltForm = lastTokenText; + int altFormsCount = Math.Min(missingFragmentText.Length, AlternateFormsMaximumCount); + + // One of the substring-permutations of the missingFragment - when concatenated with 'lastToken' - could be a viable + // replacement for 'lastToken' + for (int i = 1; i <= altFormsCount; i++) { - // One of the substring-permutations of the missingFragment - when concatenated with 'lastToken' - could be a viable - // replacement for 'lastToken' - for (int i = 1; i <= altFormsCount; i++) + var altForm = documentText.Substring(lastToken.TextRange.Start, lastTokenText.Length + i).TrimEnd('\0').TrimEnd(); + + if (altForm.Length > previousAltForm.Length) { - var altForm = documentText.Substring(lastToken.TextRange.Start, lastToken.TextRange.Length + i).TrimEnd(); - if (spellChecker?.ComprehensiveCheck(altForm)?.Count == 0) + previousAltForm = altForm; + if (!spellChecker.HasErrors(altForm)) { // Use this altForm in place lastToken return new WinRTSpellerInterop.TextRange( @@ -183,6 +191,15 @@ public static IReadOnlyList ComprehensiveGetTokens( altForm.Length); } } + else + { + // trimming yielded an altForm we've already checked, don't check again. + // We could stop checking now if we knew that an altForm with + // embedded trimmable characters could never be a correctly spelled + // token. That's probably true, but the spell-checking docs don't + // guarantee it. So for safety, leaving the next line commented out. + // return null; + } } return null;