diff --git a/src/EditorFeatures/Test/Utilities/PatternMatcherTests.cs b/src/EditorFeatures/Test/Utilities/PatternMatcherTests.cs index 5b18ede0119a5..dddd34fd63cb5 100644 --- a/src/EditorFeatures/Test/Utilities/PatternMatcherTests.cs +++ b/src/EditorFeatures/Test/Utilities/PatternMatcherTests.cs @@ -414,6 +414,19 @@ public void TryMatchSingleWordPattern_CultureAwareSingleWordPreferCaseSensitiveE } } + [Fact] + public void TestCachingOfPriorResult() + { + using var matcher = PatternMatcher.CreatePatternMatcher("Goo", includeMatchedSpans: true, allowFuzzyMatching: true); + matcher.Matches("Go"); + + // Ensure that the above call ended up caching the result. + Assert.True(((PatternMatcher.SimplePatternMatcher)matcher).GetTestAccessor().LastCacheResultIs(areSimilar: true, candidateText: "Go")); + + matcher.Matches("DefNotAMatch"); + Assert.True(((PatternMatcher.SimplePatternMatcher)matcher).GetTestAccessor().LastCacheResultIs(areSimilar: false, candidateText: "DefNotAMatch")); + } + private static ImmutableArray PartListToSubstrings(string identifier, in TemporaryArray parts) { using var result = TemporaryArray.Empty; diff --git a/src/Workspaces/Core/Portable/FindSymbols/SearchQuery.cs b/src/Workspaces/Core/Portable/FindSymbols/SearchQuery.cs index cfb3ee9e37502..e1f2468370061 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SearchQuery.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SearchQuery.cs @@ -7,7 +7,7 @@ namespace Microsoft.CodeAnalysis.FindSymbols { - internal readonly struct SearchQuery : IDisposable + internal struct SearchQuery : IDisposable { /// The name being searched for. Is null in the case of custom predicate searching.. But /// can be used for faster index based searching when it is available. @@ -20,7 +20,10 @@ namespace Microsoft.CodeAnalysis.FindSymbols ///The predicate to fall back on if faster index searching is not possible. private readonly Func _predicate; - private readonly WordSimilarityChecker? _wordSimilarityChecker; + /// + /// Not readonly as this is mutable struct. + /// + private WordSimilarityChecker _wordSimilarityChecker; private SearchQuery(string name, SearchKind kind) { @@ -41,7 +44,7 @@ private SearchQuery(string name, SearchKind kind) // once and it can cache all the information it needs while it does the AreSimilar // check against all the possible candidates. _wordSimilarityChecker = new WordSimilarityChecker(name, substringsAreSimilar: false); - _predicate = _wordSimilarityChecker.Value.AreSimilar; + _predicate = _wordSimilarityChecker.AreSimilar; break; default: throw ExceptionUtilities.UnexpectedValue(kind); @@ -54,8 +57,8 @@ private SearchQuery(Func predicate) _predicate = predicate ?? throw new ArgumentNullException(nameof(predicate)); } - public void Dispose() - => _wordSimilarityChecker?.Dispose(); + public readonly void Dispose() + => _wordSimilarityChecker.Dispose(); public static SearchQuery Create(string name, SearchKind kind) => new(name, kind); @@ -69,7 +72,7 @@ public static SearchQuery CreateFuzzy(string name) public static SearchQuery CreateCustom(Func predicate) => new(predicate); - public Func GetPredicate() + public readonly Func GetPredicate() => _predicate; } } diff --git a/src/Workspaces/Core/Portable/PatternMatching/ContainerPatternMatcher.cs b/src/Workspaces/Core/Portable/PatternMatching/ContainerPatternMatcher.cs index eb12898fd5f53..1e928e708bdc1 100644 --- a/src/Workspaces/Core/Portable/PatternMatching/ContainerPatternMatcher.cs +++ b/src/Workspaces/Core/Portable/PatternMatching/ContainerPatternMatcher.cs @@ -80,7 +80,7 @@ private bool AddMatches(string container, ref TemporaryArray match i--, j--) { var containerName = containerParts[j]; - if (!MatchPatternSegment(containerName, _patternSegments[i], ref tempContainerMatches.AsRef(), fuzzyMatch)) + if (!MatchPatternSegment(containerName, ref _patternSegments[i], ref tempContainerMatches.AsRef(), fuzzyMatch)) { // This container didn't match the pattern piece. So there's no match at all. return false; diff --git a/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.TextChunk.cs b/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.TextChunk.cs index a6bf7a5bb3378..bce10547e3c9c 100644 --- a/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.TextChunk.cs +++ b/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.TextChunk.cs @@ -32,7 +32,10 @@ private struct TextChunk : IDisposable /// public TemporaryArray PatternHumps; - public readonly WordSimilarityChecker? SimilarityChecker; + /// + /// Not readonly as this value caches data within it, and so it needs to be able to mutate. + /// + public WordSimilarityChecker SimilarityChecker; public readonly bool IsLowercase; @@ -44,7 +47,7 @@ public TextChunk(string text, bool allowFuzzingMatching) this.SimilarityChecker = allowFuzzingMatching ? new WordSimilarityChecker(text, substringsAreSimilar: false) - : null; + : default; IsLowercase = !ContainsUpperCaseLetter(text); } @@ -52,7 +55,7 @@ public TextChunk(string text, bool allowFuzzingMatching) public void Dispose() { this.PatternHumps.Dispose(); - this.SimilarityChecker?.Dispose(); + this.SimilarityChecker.Dispose(); } } } diff --git a/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.cs b/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.cs index 787547afb4747..de471437d75f1 100644 --- a/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.cs +++ b/src/Workspaces/Core/Portable/PatternMatching/PatternMatcher.cs @@ -125,22 +125,22 @@ private static bool ContainsUpperCaseLetter(string pattern) private PatternMatch? MatchPatternChunk( string candidate, - in TextChunk patternChunk, + ref TextChunk patternChunk, bool punctuationStripped, bool fuzzyMatch) { return fuzzyMatch - ? FuzzyMatchPatternChunk(candidate, patternChunk, punctuationStripped) + ? FuzzyMatchPatternChunk(candidate, ref patternChunk, punctuationStripped) : NonFuzzyMatchPatternChunk(candidate, patternChunk, punctuationStripped); } private static PatternMatch? FuzzyMatchPatternChunk( string candidate, - in TextChunk patternChunk, + ref TextChunk patternChunk, bool punctuationStripped) { - Contract.ThrowIfNull(patternChunk.SimilarityChecker); - if (patternChunk.SimilarityChecker.Value.AreSimilar(candidate)) + Contract.ThrowIfTrue(patternChunk.SimilarityChecker.IsDefault); + if (patternChunk.SimilarityChecker.AreSimilar(candidate)) { return new PatternMatch( PatternMatchKind.Fuzzy, punctuationStripped, isCaseSensitive: false, matchedSpan: null); @@ -307,7 +307,7 @@ private static bool ContainsSpaceOrAsterisk(string text) /// If there's only one match, then the return value is that match. Otherwise it is null. private bool MatchPatternSegment( string candidate, - in PatternSegment segment, + ref PatternSegment segment, ref TemporaryArray matches, bool fuzzyMatch) { @@ -326,7 +326,7 @@ private bool MatchPatternSegment( if (!ContainsSpaceOrAsterisk(segment.TotalTextChunk.Text)) { var match = MatchPatternChunk( - candidate, segment.TotalTextChunk, punctuationStripped: false, fuzzyMatch: fuzzyMatch); + candidate, ref segment.TotalTextChunk, punctuationStripped: false, fuzzyMatch: fuzzyMatch); if (match != null) { matches.Add(match.Value); @@ -354,7 +354,7 @@ private bool MatchPatternSegment( if (subWordTextChunks.Length == 1) { var result = MatchPatternChunk( - candidate, subWordTextChunks[0], punctuationStripped: true, fuzzyMatch: fuzzyMatch); + candidate, ref subWordTextChunks[0], punctuationStripped: true, fuzzyMatch: fuzzyMatch); if (result == null) { return false; @@ -367,11 +367,11 @@ private bool MatchPatternSegment( { using var tempMatches = TemporaryArray.Empty; - foreach (var subWordTextChunk in subWordTextChunks) + for (int i = 0, n = subWordTextChunks.Length; i < n; i++) { // Try to match the candidate with this word var result = MatchPatternChunk( - candidate, subWordTextChunk, punctuationStripped: true, fuzzyMatch: fuzzyMatch); + candidate, ref subWordTextChunks[i], punctuationStripped: true, fuzzyMatch: fuzzyMatch); if (result == null) return false; diff --git a/src/Workspaces/Core/Portable/PatternMatching/SimplePatternMatcher.cs b/src/Workspaces/Core/Portable/PatternMatching/SimplePatternMatcher.cs index 1e1cb9fb9d8c0..75cc42f521aa5 100644 --- a/src/Workspaces/Core/Portable/PatternMatching/SimplePatternMatcher.cs +++ b/src/Workspaces/Core/Portable/PatternMatching/SimplePatternMatcher.cs @@ -4,6 +4,7 @@ #nullable disable +using System; using System.Globalization; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Collections; @@ -12,7 +13,7 @@ namespace Microsoft.CodeAnalysis.PatternMatching { internal partial class PatternMatcher { - private sealed partial class SimplePatternMatcher : PatternMatcher + internal sealed partial class SimplePatternMatcher : PatternMatcher { private PatternSegment _fullPatternSegment; @@ -48,8 +49,17 @@ public override bool AddMatches(string candidate, ref TemporaryArray new(this); + + public readonly struct TestAccessor(SimplePatternMatcher simplePatternMatcher) + { + public readonly bool LastCacheResultIs(bool areSimilar, string candidateText) + => simplePatternMatcher._fullPatternSegment.TotalTextChunk.SimilarityChecker.LastCacheResultIs(areSimilar, candidateText); } } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/WordSimilarityChecker.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/WordSimilarityChecker.cs index 9a75f7906af0e..2abaaf7871d65 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/WordSimilarityChecker.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/WordSimilarityChecker.cs @@ -32,6 +32,8 @@ private readonly struct CacheResult(string candidate, bool areSimilar, double si /// private readonly bool _substringsAreSimilar; + public readonly bool IsDefault => _source is null; + public WordSimilarityChecker(string text, bool substringsAreSimilar) { _source = text ?? throw new ArgumentNullException(nameof(text)); @@ -41,7 +43,12 @@ public WordSimilarityChecker(string text, bool substringsAreSimilar) } public readonly void Dispose() - => _editDistance.Dispose(); + { + if (this.IsDefault) + return; + + _editDistance.Dispose(); + } public static bool AreSimilar(string originalText, string candidateText) => AreSimilar(originalText, candidateText, substringsAreSimilar: false); @@ -156,5 +163,8 @@ private static double Penalty(string candidateText, string originalText) return 0; } + + public readonly bool LastCacheResultIs(bool areSimilar, string candidateText) + => _lastAreSimilarResult.AreSimilar == areSimilar && _lastAreSimilarResult.CandidateText == candidateText; } }