From d85947340bfad6e6dcb4f02cfd2699f20447076a Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 13 Jun 2024 09:42:16 -0700 Subject: [PATCH 01/17] Initial decoders --- .../AbstractDefinitionLocationService.cs | 73 +++- .../Core/CompilerExtensions.projitems | 2 + .../Core/Utilities/Base64Utilities.cs | 346 ++++++++++++++++++ .../Utilities/InterceptslocationUtilities.cs | 97 +++++ 4 files changed, 506 insertions(+), 12 deletions(-) create mode 100644 src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/Base64Utilities.cs create mode 100644 src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/InterceptslocationUtilities.cs diff --git a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs index cffeec9377a2c..3a736ca367189 100644 --- a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs +++ b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editor.Host; @@ -11,6 +12,7 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Navigation; @@ -119,24 +121,71 @@ protected AbstractDefinitionLocationService( // instead navigate to the actual interface member. // // In the future we can expand this with other mappings if appropriate. - var interfaceImpls = symbol.ExplicitOrImplicitInterfaceImplementations(); - if (interfaceImpls.Length == 0) - return null; + return await TryGetExplicitInterfaceLocationAsync().ConfigureAwait(false) ?? + await TryGetInterceptedLocationAsync().ConfigureAwait(false); + + async ValueTask TryGetExplicitInterfaceLocationAsync() + { + var interfaceImpls = symbol.ExplicitOrImplicitInterfaceImplementations(); + if (interfaceImpls.Length == 0) + return null; + + var title = string.Format(EditorFeaturesResources._0_implemented_members, + FindUsagesHelpers.GetDisplayName(symbol)); + + using var _ = ArrayBuilder.GetInstance(out var builder); + foreach (var impl in interfaceImpls) + { + builder.AddRange(await GoToDefinitionFeatureHelpers.GetDefinitionsAsync( + impl, solution, thirdPartyNavigationAllowed: false, cancellationToken).ConfigureAwait(false)); + } - var title = string.Format(EditorFeaturesResources._0_implemented_members, - FindUsagesHelpers.GetDisplayName(symbol)); + var definitions = builder.ToImmutable(); - using var _ = ArrayBuilder.GetInstance(out var builder); - foreach (var impl in interfaceImpls) + return await _streamingPresenter.GetStreamingLocationAsync( + _threadingContext, solution.Workspace, title, definitions, cancellationToken).ConfigureAwait(false); + } + + async ValueTask TryGetInterceptedLocationAsync() { - builder.AddRange(await GoToDefinitionFeatureHelpers.GetDefinitionsAsync( - impl, solution, thirdPartyNavigationAllowed: false, cancellationToken).ConfigureAwait(false)); + if (symbol is not IMethodSymbol method) + return null; + + // Find attributes of the form: [InterceptsLocationAttribute(version: 1, data: "...")]; + + var attributes = method.GetAttributes(); + var interceptsLocationAttributes = attributes.WhereAsArray(IsInterceptsLocationAttribute); + if (interceptsLocationAttributes.Length == 0) + return null; + + // Single location, just navigate directly to that. + if (interceptsLocationAttributes is [var interceptsLocationAttribute]) + { + if (!TryDecodeInterceptsLocationData(interceptsLocationAttribute, out var location)) + return null; + } } + } - var definitions = builder.ToImmutable(); + private static bool IsInterceptsLocationAttribute(AttributeData attribute) + { + return attribute.AttributeClass?.Name == "InterceptsLocationAttribute" && + attribute.ConstructorArguments is [{ Value: 1 }, { Value: string }]; + } + + private static bool TryDecodeInterceptsLocationData(AttributeData attribute) + { + Contract.ThrowIfFalse(IsInterceptsLocationAttribute(attribute)); + var data = (string)attribute.ConstructorArguments[1].Value!; - return await _streamingPresenter.GetStreamingLocationAsync( - _threadingContext, solution.Workspace, title, definitions, cancellationToken).ConfigureAwait(false); + try + { + + } + finally + { + + } } private static async Task IsThirdPartyNavigationAllowedAsync( diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems index 9c48a83311efc..26c8ebb0eb1d7 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems @@ -490,6 +490,7 @@ + @@ -503,6 +504,7 @@ + diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/Base64Utilities.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/Base64Utilities.cs new file mode 100644 index 0000000000000..373343f4c6a8d --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/Base64Utilities.cs @@ -0,0 +1,346 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// To suppress warnings about the code styles the runtime team uses in the functions from them. +// + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Microsoft.CodeAnalysis.Shared.Utilities; + +// From https://github.com/dotnet/runtime/blob/6927fea7b4bca1dc2cea7a0afba0373c1303cedc/src/libraries/System.Private.CoreLib/src/System/Convert.cs#L2659 + +internal static class Base64Utilities +{ + // Pre-computing this table using a custom string(s_characters) and GenerateDecodingMapAndVerify (found in tests) + private static ReadOnlySpan DecodingMap => + [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, // 62 is placed at index 43 (for +), 63 at index 47 (for /) + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, // 52-61 are placed at index 48-57 (for 0-9), 64 at index 61 (for =) + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, // 0-25 are placed at index 65-90 (for A-Z) + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, // 26-51 are placed at index 97-122 (for a-z) + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Bytes over 122 ('z') are invalid and cannot be decoded + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // Hence, padding the map with 255, which indicates invalid input + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + ]; + + private const byte EncodingPad = (byte)'='; // '=', for padding + + private static bool IsSpace(this char c) => c == ' ' || c == '\t' || c == '\r' || c == '\n'; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteThreeLowOrderBytes(ref byte destination, int value) + { + destination = (byte)(value >> 16); + Unsafe.Add(ref destination, 1) = (byte)(value >> 8); + Unsafe.Add(ref destination, 2) = (byte)value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Decode(ref char encodedChars, ref sbyte decodingMap) + { + int i0 = encodedChars; + int i1 = Unsafe.Add(ref encodedChars, 1); + int i2 = Unsafe.Add(ref encodedChars, 2); + int i3 = Unsafe.Add(ref encodedChars, 3); + + if (((i0 | i1 | i2 | i3) & 0xffffff00) != 0) + return -1; // One or more chars falls outside the 00..ff range. This cannot be a valid Base64 character. + + i0 = Unsafe.Add(ref decodingMap, i0); + i1 = Unsafe.Add(ref decodingMap, i1); + i2 = Unsafe.Add(ref decodingMap, i2); + i3 = Unsafe.Add(ref decodingMap, i3); + + i0 <<= 18; + i1 <<= 12; + i2 <<= 6; + + i0 |= i3; + i1 |= i2; + + i0 |= i1; + return i0; + } + + public static bool TryFromBase64Chars(ReadOnlySpan chars, Span bytes, out int bytesWritten) + { + // This is actually local to one of the nested blocks but is being declared at the top as we don't want multiple stackallocs + // for each iteraton of the loop. + Span tempBuffer = stackalloc char[4]; // Note: The tempBuffer size could be made larger than 4 but the size must be a multiple of 4. + + bytesWritten = 0; + + while (chars.Length != 0) + { + // Attempt to decode a segment that doesn't contain whitespace. + bool complete = TryDecodeFromUtf16(chars, bytes, out int consumedInThisIteration, out int bytesWrittenInThisIteration); + bytesWritten += bytesWrittenInThisIteration; + if (complete) + return true; + + chars = chars.Slice(consumedInThisIteration); + bytes = bytes.Slice(bytesWrittenInThisIteration); + + Debug.Assert(chars.Length != 0); // If TryDecodeFromUtf16() consumed the entire buffer, it could not have returned false. + if (chars[0].IsSpace()) + { + // If we got here, the very first character not consumed was a whitespace. We can skip past any consecutive whitespace, then continue decoding. + + int indexOfFirstNonSpace = 1; + while (true) + { + if (indexOfFirstNonSpace == chars.Length) + break; + if (!chars[indexOfFirstNonSpace].IsSpace()) + break; + indexOfFirstNonSpace++; + } + + chars = chars.Slice(indexOfFirstNonSpace); + + if ((bytesWrittenInThisIteration % 3) != 0 && chars.Length != 0) + { + // If we got here, the last successfully decoded block encountered an end-marker, yet we have trailing non-whitespace characters. + // That is not allowed. + bytesWritten = default; + return false; + } + + // We now loop again to decode the next run of non-space characters. + } + else + { + Debug.Assert(chars.Length != 0 && !chars[0].IsSpace()); + + // If we got here, it is possible that there is whitespace that occurred in the middle of a 4-byte chunk. That is, we still have + // up to three Base64 characters that were left undecoded by the fast-path helper because they didn't form a complete 4-byte chunk. + // This is hopefully the rare case (multiline-formatted base64 message with a non-space character width that's not a multiple of 4.) + // We'll filter out whitespace and copy the remaining characters into a temporary buffer. + CopyToTempBufferWithoutWhiteSpace(chars, tempBuffer, out int consumedFromChars, out int charsWritten); + if ((charsWritten & 0x3) != 0) + { + // Even after stripping out whitespace, the number of characters is not divisible by 4. This cannot be a legal Base64 string. + bytesWritten = default; + return false; + } + + tempBuffer = tempBuffer.Slice(0, charsWritten); + if (!TryDecodeFromUtf16(tempBuffer, bytes, out int consumedFromTempBuffer, out int bytesWrittenFromTempBuffer)) + { + bytesWritten = default; + return false; + } + bytesWritten += bytesWrittenFromTempBuffer; + chars = chars.Slice(consumedFromChars); + bytes = bytes.Slice(bytesWrittenFromTempBuffer); + + if ((bytesWrittenFromTempBuffer % 3) != 0) + { + // If we got here, this decode contained one or more padding characters ('='). We can accept trailing whitespace after this + // but nothing else. + for (int i = 0; i < chars.Length; i++) + { + if (!chars[i].IsSpace()) + { + bytesWritten = default; + return false; + } + } + return true; + } + + // We now loop again to decode the next run of non-space characters. + } + } + + return true; + } + + private static void CopyToTempBufferWithoutWhiteSpace(ReadOnlySpan chars, Span tempBuffer, out int consumed, out int charsWritten) + { + Debug.Assert(tempBuffer.Length != 0); // We only bound-check after writing a character to the tempBuffer. + + charsWritten = 0; + for (int i = 0; i < chars.Length; i++) + { + char c = chars[i]; + if (!c.IsSpace()) + { + tempBuffer[charsWritten++] = c; + if (charsWritten == tempBuffer.Length) + { + consumed = i + 1; + return; + } + } + } + consumed = chars.Length; + } + + private static bool TryDecodeFromUtf16(ReadOnlySpan utf16, Span bytes, out int consumed, out int written) + { + ref char srcChars = ref MemoryMarshal.GetReference(utf16); + ref byte destBytes = ref MemoryMarshal.GetReference(bytes); + + int srcLength = utf16.Length & ~0x3; // only decode input up to the closest multiple of 4. + int destLength = bytes.Length; + + int sourceIndex = 0; + int destIndex = 0; + + if (utf16.Length == 0) + goto DoneExit; + + ref sbyte decodingMap = ref MemoryMarshal.GetReference(DecodingMap); + + // Last bytes could have padding characters, so process them separately and treat them as valid. + const int skipLastChunk = 4; + + int maxSrcLength; + if (destLength >= (srcLength >> 2) * 3) + { + maxSrcLength = srcLength - skipLastChunk; + } + else + { + // This should never overflow since destLength here is less than int.MaxValue / 4 * 3 (i.e. 1610612733) + // Therefore, (destLength / 3) * 4 will always be less than 2147483641 + maxSrcLength = (destLength / 3) * 4; + } + + while (sourceIndex < maxSrcLength) + { + int result = Decode(ref Unsafe.Add(ref srcChars, sourceIndex), ref decodingMap); + if (result < 0) + goto InvalidExit; + WriteThreeLowOrderBytes(ref Unsafe.Add(ref destBytes, destIndex), result); + destIndex += 3; + sourceIndex += 4; + } + + if (maxSrcLength != srcLength - skipLastChunk) + goto InvalidExit; + + // If input is less than 4 bytes, srcLength == sourceIndex == 0 + // If input is not a multiple of 4, sourceIndex == srcLength != 0 + if (sourceIndex == srcLength) + { + goto InvalidExit; + } + + int i0 = Unsafe.Add(ref srcChars, srcLength - 4); + int i1 = Unsafe.Add(ref srcChars, srcLength - 3); + int i2 = Unsafe.Add(ref srcChars, srcLength - 2); + int i3 = Unsafe.Add(ref srcChars, srcLength - 1); + if (((i0 | i1 | i2 | i3) & 0xffffff00) != 0) + goto InvalidExit; + + i0 = Unsafe.Add(ref decodingMap, i0); + i1 = Unsafe.Add(ref decodingMap, i1); + + i0 <<= 18; + i1 <<= 12; + + i0 |= i1; + + if (i3 != EncodingPad) + { + i2 = Unsafe.Add(ref decodingMap, i2); + i3 = Unsafe.Add(ref decodingMap, i3); + + i2 <<= 6; + + i0 |= i3; + i0 |= i2; + + if (i0 < 0) + goto InvalidExit; + if (destIndex > destLength - 3) + goto InvalidExit; + WriteThreeLowOrderBytes(ref Unsafe.Add(ref destBytes, destIndex), i0); + destIndex += 3; + } + else if (i2 != EncodingPad) + { + i2 = Unsafe.Add(ref decodingMap, i2); + + i2 <<= 6; + + i0 |= i2; + + if (i0 < 0) + goto InvalidExit; + if (destIndex > destLength - 2) + goto InvalidExit; + Unsafe.Add(ref destBytes, destIndex) = (byte)(i0 >> 16); + Unsafe.Add(ref destBytes, destIndex + 1) = (byte)(i0 >> 8); + destIndex += 2; + } + else + { + if (i0 < 0) + goto InvalidExit; + if (destIndex > destLength - 1) + goto InvalidExit; + Unsafe.Add(ref destBytes, destIndex) = (byte)(i0 >> 16); + destIndex++; + } + + sourceIndex += 4; + + if (srcLength != utf16.Length) + goto InvalidExit; + +DoneExit: + consumed = sourceIndex; + written = destIndex; + return true; + +InvalidExit: + consumed = sourceIndex; + written = destIndex; + Debug.Assert((consumed % 4) == 0); + return false; + } + + public static bool TryGetDecodedLength(string encodedString, out int decodedLength) + { + decodedLength = -1; + + // Base64 encoded strings may end with 0, 1, or 2 padding (=) characters + var padding = GetPadding(encodedString); + if (padding > 2) + return false; + + decodedLength = (encodedString.Length * 3) / 4 - padding; + return true; + } + + private static int GetPadding(string attributeData) + { + var padding = 0; + var index = attributeData.Length - 1; + + while (index >= 0 && attributeData[index] == '=') + { + padding++; + index--; + } + + return padding; + } +} diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/InterceptslocationUtilities.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/InterceptslocationUtilities.cs new file mode 100644 index 0000000000000..1b750fa844882 --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/InterceptslocationUtilities.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// To suppress warnings about the code styles the runtime team uses in the functions from them. +// + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.Shared.Collections; + +namespace Microsoft.CodeAnalysis.Shared.Utilities; + +internal static class InterceptsLocationUtilities +{ + + public record struct InterceptsLocationData(ImmutableArray ContentHash, int Position); + + public static ImmutableArray GetInterceptsLocationData(ImmutableArray attributes) + { + using var result = TemporaryArray.Empty; + + foreach (var attribute in attributes) + { + if (TryGetInterceptsLocationData(attribute, out var data)) + result.Add(data); + } + + return result.ToImmutableAndClear(); + } + + public static bool TryGetInterceptsLocationData(AttributeData attribute, out InterceptsLocationData result) + { + if (attribute is + { + AttributeClass.Name: "InterceptsLocationAttribute", + ConstructorArguments: [{ Value: int version }, { Value: string attributeData }] + }) + { + if (version == 1) + return TryGetInterceptsLocationDataVersion1(attributeData, out result); + + // Add more supported versions here in the future if the compiler adds any. + } + + result = default; + return false; + } + + private static bool TryGetInterceptsLocationDataVersion1(string attributeData, out InterceptsLocationData result) + { + result = default; + + if (!Base64Utilities.TryGetDecodedLength(attributeData, out var decodedLength)) + return false; + + // V1 format: + // - 16 bytes of target file content hash (xxHash128) + // - int32 position (little endian) + // - utf-8 display filename + const int HashIndex = 0; + const int HashSize = 16; + const int PositionIndex = HashIndex + HashSize; + const int PositionSize = sizeof(int); + const int DisplayNameIndex = PositionIndex + PositionSize; + const int MinLength = DisplayNameIndex; + if (decodedLength < MinLength) + return false; + + var rentedArray = decodedLength < 1024 + ? null + : ArrayPool.Shared.Rent(decodedLength); + + try + { + var bytes = rentedArray is null + ? stackalloc byte[decodedLength] + : rentedArray.AsSpan(0, decodedLength); + + if (!Base64Utilities.TryFromBase64Chars(attributeData.AsSpan(), bytes, out _)) + return false; + + var contentHash = bytes[HashIndex..HashSize].ToImmutableArray(); + var position = BinaryPrimitives.ReadInt32LittleEndian(bytes.Slice(PositionIndex)); + + result = new(contentHash, position); + return true; + } + finally + { + if (rentedArray is not null) + ArrayPool.Shared.Return(rentedArray); + } + } +} From a4b610074b8da1a77d499f3b9998dae02f33768e Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 13 Jun 2024 10:07:16 -0700 Subject: [PATCH 02/17] in progress --- ...nitionLocationService.ByteArrayComparer.cs | 27 +++ .../AbstractDefinitionLocationService.cs | 168 +++++++++++------- .../Compiler/Core/ObjectPools/Extensions.cs | 12 +- 3 files changed, 137 insertions(+), 70 deletions(-) create mode 100644 src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.ByteArrayComparer.cs diff --git a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.ByteArrayComparer.cs b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.ByteArrayComparer.cs new file mode 100644 index 0000000000000..26d08c2789afa --- /dev/null +++ b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.ByteArrayComparer.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Navigation; + +internal abstract partial class AbstractDefinitionLocationService +{ + private sealed class ByteArrayComparer : IEqualityComparer> + { + public static readonly ByteArrayComparer Instance = new(); + + private ByteArrayComparer() { } + + public bool Equals(ImmutableArray x, ImmutableArray y) + => x.SequenceEqual(y); + + public int GetHashCode(ImmutableArray obj) + => Hash.CombineValues(obj); + } +} diff --git a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs index 3a736ca367189..dbb919ba092bf 100644 --- a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs +++ b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs @@ -3,6 +3,10 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editor.Host; @@ -12,22 +16,20 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Utilities; +using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Navigation; -internal abstract class AbstractDefinitionLocationService : IDefinitionLocationService +internal abstract partial class AbstractDefinitionLocationService( + IThreadingContext threadingContext, + IStreamingFindUsagesPresenter streamingPresenter) : IDefinitionLocationService { - private readonly IThreadingContext _threadingContext; - private readonly IStreamingFindUsagesPresenter _streamingPresenter; + private readonly IThreadingContext _threadingContext = threadingContext; + private readonly IStreamingFindUsagesPresenter _streamingPresenter = streamingPresenter; - protected AbstractDefinitionLocationService( - IThreadingContext threadingContext, - IStreamingFindUsagesPresenter streamingPresenter) - { - _threadingContext = threadingContext; - _streamingPresenter = streamingPresenter; - } + private static readonly ConditionalWeakTable, DocumentId>>> s_projectToLazyContentHashMap = new(); private static Task GetNavigableLocationAsync( Document document, int position, CancellationToken cancellationToken) @@ -42,6 +44,8 @@ protected AbstractDefinitionLocationService( public async Task GetDefinitionLocationAsync(Document document, int position, CancellationToken cancellationToken) { + var symbolService = document.GetRequiredLanguageService(); + // We want to compute this as quickly as possible so that the symbol be squiggled and navigated to. We // don't want to wait on expensive operations like computing source-generators or skeletons if we can avoid // it. So first try with a frozen document, then fallback to a normal document. This mirrors how go-to-def @@ -49,47 +53,53 @@ protected AbstractDefinitionLocationService( return await GetDefinitionLocationWorkerAsync(document.WithFrozenPartialSemantics(cancellationToken)).ConfigureAwait(false) ?? await GetDefinitionLocationWorkerAsync(document).ConfigureAwait(false); - async Task GetDefinitionLocationWorkerAsync(Document document) + async ValueTask GetDefinitionLocationWorkerAsync(Document document) + { + return await GetControlFlowTargetLocationAsync(document).ConfigureAwait(false) ?? + await GetSymbolLocationAsync(document).ConfigureAwait(false); + } + + async ValueTask GetControlFlowTargetLocationAsync(Document document) { - var symbolService = document.GetRequiredLanguageService(); var (controlFlowTarget, controlFlowSpan) = await symbolService.GetTargetIfControlFlowAsync( document, position, cancellationToken).ConfigureAwait(false); - if (controlFlowTarget != null) - { - var location = await GetNavigableLocationAsync( - document, controlFlowTarget.Value, cancellationToken).ConfigureAwait(false); - return location is null ? null : new DefinitionLocation(location, new DocumentSpan(document, controlFlowSpan)); - } - else - { - // Try to compute the referenced symbol and attempt to go to definition for the symbol. - var (symbol, project, span) = await symbolService.GetSymbolProjectAndBoundSpanAsync( - document, position, cancellationToken).ConfigureAwait(false); - if (symbol is null) - return null; - - // if the symbol only has a single source location, and we're already on it, - // try to see if there's a better symbol we could navigate to. - var remappedLocation = await GetAlternativeLocationIfAlreadyOnDefinitionAsync( - project, position, symbol, originalDocument: document, cancellationToken).ConfigureAwait(false); - if (remappedLocation != null) - return new DefinitionLocation(remappedLocation, new DocumentSpan(document, span)); - - var isThirdPartyNavigationAllowed = await IsThirdPartyNavigationAllowedAsync( - symbol, position, document, cancellationToken).ConfigureAwait(false); - - var location = await GoToDefinitionHelpers.GetDefinitionLocationAsync( - symbol, - project.Solution, - _threadingContext, - _streamingPresenter, - thirdPartyNavigationAllowed: isThirdPartyNavigationAllowed, - cancellationToken: cancellationToken).ConfigureAwait(false); - if (location is null) - return null; - - return new DefinitionLocation(location, new DocumentSpan(document, span)); - } + if (controlFlowTarget == null) + return null; + + var location = await GetNavigableLocationAsync( + document, controlFlowTarget.Value, cancellationToken).ConfigureAwait(false); + return location is null ? null : new DefinitionLocation(location, new DocumentSpan(document, controlFlowSpan)); + } + + async ValueTask GetSymbolLocationAsync(Document document) + { + // Try to compute the referenced symbol and attempt to go to definition for the symbol. + var (symbol, project, span) = await symbolService.GetSymbolProjectAndBoundSpanAsync( + document, position, cancellationToken).ConfigureAwait(false); + if (symbol is null) + return null; + + // if the symbol only has a single source location, and we're already on it, + // try to see if there's a better symbol we could navigate to. + var remappedLocation = await GetAlternativeLocationIfAlreadyOnDefinitionAsync( + project, position, symbol, originalDocument: document, cancellationToken).ConfigureAwait(false); + if (remappedLocation != null) + return new DefinitionLocation(remappedLocation, new DocumentSpan(document, span)); + + var isThirdPartyNavigationAllowed = await IsThirdPartyNavigationAllowedAsync( + symbol, position, document, cancellationToken).ConfigureAwait(false); + + var location = await GoToDefinitionHelpers.GetDefinitionLocationAsync( + symbol, + project.Solution, + _threadingContext, + _streamingPresenter, + thirdPartyNavigationAllowed: isThirdPartyNavigationAllowed, + cancellationToken: cancellationToken).ConfigureAwait(false); + if (location is null) + return null; + + return new DefinitionLocation(location, new DocumentSpan(document, span)); } } @@ -154,38 +164,60 @@ protected AbstractDefinitionLocationService( // Find attributes of the form: [InterceptsLocationAttribute(version: 1, data: "...")]; var attributes = method.GetAttributes(); - var interceptsLocationAttributes = attributes.WhereAsArray(IsInterceptsLocationAttribute); - if (interceptsLocationAttributes.Length == 0) + var interceptsLocationDatas = InterceptsLocationUtilities.GetInterceptsLocationData(attributes); + if (interceptsLocationDatas.Length == 0) return null; - // Single location, just navigate directly to that. - if (interceptsLocationAttributes is [var interceptsLocationAttribute]) + var lazyContentHashToDocumentMap = s_projectToLazyContentHashMap.GetValue( + project.State, + static projectState => AsyncLazy.Create( + static (projectState, cancellationToken) => ComputeContentHashToDocumentMapAsync(projectState, cancellationToken), + projectState)); + + var contentHashToDocumentMap = await lazyContentHashToDocumentMap.GetValueAsync(cancellationToken).ConfigureAwait(false); + + using var _ = ArrayBuilder.GetInstance(out var documentSpans); + + foreach (var interceptsLocationData in interceptsLocationDatas) { - if (!TryDecodeInterceptsLocationData(interceptsLocationAttribute, out var location)) - return null; + if (contentHashToDocumentMap.TryGetValue(interceptsLocationData.ContentHash, out var documentId)) + { + var document = solution.GetDocument(documentId); + if (document != null) + documentSpans.Add(new DocumentSpan(document, new TextSpan(interceptsLocationData.Position, 0))); + } } - } - } - private static bool IsInterceptsLocationAttribute(AttributeData attribute) - { - return attribute.AttributeClass?.Name == "InterceptsLocationAttribute" && - attribute.ConstructorArguments is [{ Value: 1 }, { Value: string }]; + documentSpans.RemoveDuplicates(); + + if (documentSpans.Count == 0) + { + return null; + } + else if (documentSpans is [var documentSpan]) + { + // Just one document span this mapped to. Navigate directly do that. + return await documentSpan.GetNavigableLocationAsync(cancellationToken).ConfigureAwait(false); + } + else + { + // Multiple document spans this mapped to. Show them all. + } + } } - private static bool TryDecodeInterceptsLocationData(AttributeData attribute) + private static async Task, DocumentId>> ComputeContentHashToDocumentMapAsync(ProjectState projectState, CancellationToken cancellationToken) { - Contract.ThrowIfFalse(IsInterceptsLocationAttribute(attribute)); - var data = (string)attribute.ConstructorArguments[1].Value!; + var result = new Dictionary, DocumentId>(ByteArrayComparer.Instance); - try + foreach (var (documentId, documentState) in projectState.DocumentStates.States) { - + var text = await documentState.GetTextAsync(cancellationToken).ConfigureAwait(false); + var checksum = text.GetContentHash(); + result[checksum] = documentId; } - finally - { - } + return result; } private static async Task IsThirdPartyNavigationAllowedAsync( diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/ObjectPools/Extensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/ObjectPools/Extensions.cs index 87d7a10fb553b..136483231f399 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/ObjectPools/Extensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/ObjectPools/Extensions.cs @@ -64,10 +64,18 @@ public static PooledObject> GetPooledObject(this Obj return pooledObject; } - public static PooledObject> GetPooledObject(this ObjectPool> pool, out HashSet list) + public static PooledObject> GetPooledObject(this ObjectPool> pool, out HashSet set) { var pooledObject = PooledObject>.Create(pool); - list = pooledObject.Object; + set = pooledObject.Object; + return pooledObject; + } + + public static PooledObject> GetPooledObject(this ObjectPool> pool, out Dictionary dictionary) + where TKey : notnull + { + var pooledObject = PooledObject>.Create(pool); + dictionary = pooledObject.Object; return pooledObject; } From 0bab31753d5e541b0b9699c88ad99667ced946f7 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 13 Jun 2024 10:18:47 -0700 Subject: [PATCH 03/17] Complete --- .../Core/EditorFeaturesResources.resx | 3 ++ .../AbstractDefinitionLocationService.cs | 44 ++++++++++++++++++- .../Core/xlf/EditorFeaturesResources.cs.xlf | 5 +++ .../Core/xlf/EditorFeaturesResources.de.xlf | 5 +++ .../Core/xlf/EditorFeaturesResources.es.xlf | 5 +++ .../Core/xlf/EditorFeaturesResources.fr.xlf | 5 +++ .../Core/xlf/EditorFeaturesResources.it.xlf | 5 +++ .../Core/xlf/EditorFeaturesResources.ja.xlf | 5 +++ .../Core/xlf/EditorFeaturesResources.ko.xlf | 5 +++ .../Core/xlf/EditorFeaturesResources.pl.xlf | 5 +++ .../xlf/EditorFeaturesResources.pt-BR.xlf | 5 +++ .../Core/xlf/EditorFeaturesResources.ru.xlf | 5 +++ .../Core/xlf/EditorFeaturesResources.tr.xlf | 5 +++ .../xlf/EditorFeaturesResources.zh-Hans.xlf | 5 +++ .../xlf/EditorFeaturesResources.zh-Hant.xlf | 5 +++ .../FindUsages/DefinitionItemFactory.cs | 2 +- 16 files changed, 111 insertions(+), 3 deletions(-) diff --git a/src/EditorFeatures/Core/EditorFeaturesResources.resx b/src/EditorFeatures/Core/EditorFeaturesResources.resx index 9fee174d54195..a5f72e0271e5a 100644 --- a/src/EditorFeatures/Core/EditorFeaturesResources.resx +++ b/src/EditorFeatures/Core/EditorFeaturesResources.resx @@ -613,6 +613,9 @@ Do you want to proceed? '{0}' declarations + + '{0}' intercepted locations + An inline rename session is active for identifier '{0}'. Invoke inline rename again to access additional options. You may continue to edit the identifier being renamed at any time. For screenreaders. {0} is the identifier being renamed. diff --git a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs index dbb919ba092bf..8e26985fa66bd 100644 --- a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs +++ b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs @@ -9,6 +9,7 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.Editor.Host; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.FindUsages; @@ -194,14 +195,53 @@ internal abstract partial class AbstractDefinitionLocationService( { return null; } - else if (documentSpans is [var documentSpan]) + else if (documentSpans.Count == 1) { // Just one document span this mapped to. Navigate directly do that. - return await documentSpan.GetNavigableLocationAsync(cancellationToken).ConfigureAwait(false); + return await documentSpans[0].GetNavigableLocationAsync(cancellationToken).ConfigureAwait(false); } else { + var title = string.Format(EditorFeaturesResources._0_intercepted_locations, + FindUsagesHelpers.GetDisplayName(method)); + + var definitionItem = method.ToNonClassifiedDefinitionItem(solution, includeHiddenLocations: true); + + var referenceItems = new List(); + var classificationOptions = ClassificationOptions.Default with { ClassifyObsoleteSymbols = false }; + foreach (var documentSpan in documentSpans) + { + var classifiedSpans = await ClassifiedSpansAndHighlightSpanFactory.ClassifyAsync( + documentSpan, classifiedSpans: null, classificationOptions, cancellationToken).ConfigureAwait(false); + + referenceItems.Add(new SourceReferenceItem( + definitionItem, documentSpan, classifiedSpans, SymbolUsageInfo.None, additionalProperties: [])); + } + // Multiple document spans this mapped to. Show them all. + return new NavigableLocation(async (options, cancellationToken) => + { + // Can only navigate or present items on UI thread. + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + + // We have multiple definitions, or we have definitions with multiple locations. Present this to the + // user so they can decide where they want to go to. + // + // We ignore the cancellation token returned by StartSearch as we're in a context where + // we've computed all the results and we're synchronously populating the UI with it. + var (context, _) = _streamingPresenter.StartSearch(title, new StreamingFindUsagesPresenterOptions(SupportsReferences: true)); + try + { + await context.OnDefinitionFoundAsync(definitionItem, cancellationToken).ConfigureAwait(false); + await context.OnReferencesFoundAsync(referenceItems.AsAsyncEnumerable(), cancellationToken).ConfigureAwait(false); + } + finally + { + await context.OnCompletedAsync(cancellationToken).ConfigureAwait(false); + } + + return true; + }); } } } diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf index fd8b9483a1338..e837524aefb29 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf @@ -642,6 +642,11 @@ Implementované členy {0} + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) Počet nevyřešených konfliktů: {0} diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.de.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.de.xlf index 2243c67170acd..1c1dca0a8ab5c 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.de.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.de.xlf @@ -642,6 +642,11 @@ {0} implementierte Member + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) {0} nicht lösbare(r) Konflikt(e) diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.es.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.es.xlf index 2cc1faf434f46..a479c396a06d9 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.es.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.es.xlf @@ -642,6 +642,11 @@ Miembros implementados: "{0}" + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) {0} conflicto(s) sin solución diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.fr.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.fr.xlf index f799bb0145e0f..99fdddd9a88a9 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.fr.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.fr.xlf @@ -642,6 +642,11 @@ '{0}' membres implémentés + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) {0} conflit(s) ne pouvant pas être résolu(s) diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.it.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.it.xlf index 2a18a501c2814..c2cc1431e3aff 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.it.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.it.xlf @@ -642,6 +642,11 @@ Membri implementati di '{0}' + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) {0} conflitti non risolti diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ja.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ja.xlf index fcfee30eeda27..a43834152d6cc 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ja.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ja.xlf @@ -642,6 +642,11 @@ '{0}' が実装されたメンバー + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) 未解決の競合 {0} diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ko.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ko.xlf index 6c8eaceb982f6..5762222896565 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ko.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ko.xlf @@ -642,6 +642,11 @@ 구현된 멤버 '{0}'개 + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) 해결할 수 없는 충돌 {0}개 diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pl.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pl.xlf index 13f60bbdf554b..2feef605642a1 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pl.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pl.xlf @@ -642,6 +642,11 @@ Zaimplementowane składowe: „{0}” + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) Liczba nierozwiązanych konfliktów: {0} diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pt-BR.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pt-BR.xlf index 54d40bd631201..594a2296c71d9 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pt-BR.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.pt-BR.xlf @@ -642,6 +642,11 @@ '{0}' membros implementados + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) {0} conflito(s) não solucionável(is) diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ru.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ru.xlf index 0a8b441320d98..18be327488fc5 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ru.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.ru.xlf @@ -642,6 +642,11 @@ Реализованные члены "{0}" + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) {0} неразрешимые конфликты diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.tr.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.tr.xlf index 3cdad91baa090..6c27471e2cbd7 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.tr.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.tr.xlf @@ -642,6 +642,11 @@ '{0}' uygulanan üye + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) {0} çözümlenemeyen çakışma diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hans.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hans.xlf index e58d77cdc186e..bee93e7b6b75d 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hans.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hans.xlf @@ -642,6 +642,11 @@ “{0}”个实现的成员 + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) {0} 个无法解决的冲突 diff --git a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hant.xlf b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hant.xlf index 5c6301e581345..a8141bae1ff94 100644 --- a/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hant.xlf +++ b/src/EditorFeatures/Core/xlf/EditorFeaturesResources.zh-Hant.xlf @@ -642,6 +642,11 @@ 已實作 '{0}' 的成員 + + '{0}' intercepted locations + '{0}' intercepted locations + + {0} unresolvable conflict(s) 有 {0} 個未解決的衝突 diff --git a/src/Features/Core/Portable/FindUsages/DefinitionItemFactory.cs b/src/Features/Core/Portable/FindUsages/DefinitionItemFactory.cs index dbe6714f0003f..6e0b6620bb61a 100644 --- a/src/Features/Core/Portable/FindUsages/DefinitionItemFactory.cs +++ b/src/Features/Core/Portable/FindUsages/DefinitionItemFactory.cs @@ -299,13 +299,13 @@ private static ImmutableDictionary GetProperties(ISymbol definit var document = referenceLocation.Document; var sourceSpan = location.SourceSpan; + var documentSpan = new DocumentSpan(document, sourceSpan); var options = await optionsProvider.GetOptionsAsync(document.Project.Services, cancellationToken).ConfigureAwait(false); // We don't want to classify obsolete symbols as it is very expensive, and it's not necessary for find all // references to strike out code in the window displaying results. options = options with { ClassifyObsoleteSymbols = false }; - var documentSpan = new DocumentSpan(document, sourceSpan); var classifiedSpans = await ClassifiedSpansAndHighlightSpanFactory.ClassifyAsync( documentSpan, classifiedSpans: null, options, cancellationToken).ConfigureAwait(false); From 92ba5e824b843bde04d29ff54be965b2b6c9dd4b Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 13 Jun 2024 11:08:24 -0700 Subject: [PATCH 04/17] Test work --- .../CSharpGoToDefinitionTests.vb | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/EditorFeatures/Test2/GoToDefinition/CSharpGoToDefinitionTests.vb b/src/EditorFeatures/Test2/GoToDefinition/CSharpGoToDefinitionTests.vb index 0c890ed6a17b2..11747096bfa86 100644 --- a/src/EditorFeatures/Test2/GoToDefinition/CSharpGoToDefinitionTests.vb +++ b/src/EditorFeatures/Test2/GoToDefinition/CSharpGoToDefinitionTests.vb @@ -5,7 +5,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.GoToDefinition - Public Class CSharpGoToDefinitionTests + Public NotInheritable Class CSharpGoToDefinitionTests Inherits GoToDefinitionTestsBase #Region "P2P Tests" @@ -3725,5 +3725,25 @@ class Program Await TestAsync(workspace) End Function + + + Public Async Function TestInterceptors_AttributeMissingVersion() As Task + Dim workspace = + + + +partial class Program +{ + public void Method(int argument) + { + Goo(0); + } +} + + + + + Await TestAsync(workspace) + End Function End Class End Namespace From 035727c6ebe3e7b259a6bb5b198fc8052cbc095b Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 13 Jun 2024 12:52:27 -0700 Subject: [PATCH 05/17] Revert --- src/Features/Core/Portable/FindUsages/DefinitionItemFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Features/Core/Portable/FindUsages/DefinitionItemFactory.cs b/src/Features/Core/Portable/FindUsages/DefinitionItemFactory.cs index 6e0b6620bb61a..dbe6714f0003f 100644 --- a/src/Features/Core/Portable/FindUsages/DefinitionItemFactory.cs +++ b/src/Features/Core/Portable/FindUsages/DefinitionItemFactory.cs @@ -299,13 +299,13 @@ private static ImmutableDictionary GetProperties(ISymbol definit var document = referenceLocation.Document; var sourceSpan = location.SourceSpan; - var documentSpan = new DocumentSpan(document, sourceSpan); var options = await optionsProvider.GetOptionsAsync(document.Project.Services, cancellationToken).ConfigureAwait(false); // We don't want to classify obsolete symbols as it is very expensive, and it's not necessary for find all // references to strike out code in the window displaying results. options = options with { ClassifyObsoleteSymbols = false }; + var documentSpan = new DocumentSpan(document, sourceSpan); var classifiedSpans = await ClassifiedSpansAndHighlightSpanFactory.ClassifyAsync( documentSpan, classifiedSpans: null, options, cancellationToken).ConfigureAwait(false); From 714450aa18a70a4f5e8b5c661e16107fee2b2a6e Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 13 Jun 2024 14:21:50 -0700 Subject: [PATCH 06/17] Tests --- .../AbstractDefinitionLocationService.cs | 6 +- .../CSharpGoToDefinitionTests.vb | 237 +++++++++++++++++- .../GoToDefinition/GoToDefinitionTestsBase.vb | 28 ++- 3 files changed, 264 insertions(+), 7 deletions(-) diff --git a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs index 8e26985fa66bd..06c4905d10c93 100644 --- a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs +++ b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs @@ -185,7 +185,11 @@ internal abstract partial class AbstractDefinitionLocationService( { var document = solution.GetDocument(documentId); if (document != null) - documentSpans.Add(new DocumentSpan(document, new TextSpan(interceptsLocationData.Position, 0))); + { + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var token = root.FindToken(interceptsLocationData.Position); + documentSpans.Add(new DocumentSpan(document, token.Span)); + } } } diff --git a/src/EditorFeatures/Test2/GoToDefinition/CSharpGoToDefinitionTests.vb b/src/EditorFeatures/Test2/GoToDefinition/CSharpGoToDefinitionTests.vb index 11747096bfa86..d42d41abba7ad 100644 --- a/src/EditorFeatures/Test2/GoToDefinition/CSharpGoToDefinitionTests.vb +++ b/src/EditorFeatures/Test2/GoToDefinition/CSharpGoToDefinitionTests.vb @@ -2,6 +2,10 @@ ' The .NET Foundation licenses this file to you under the MIT license. ' See the LICENSE file in the project root for more information. +Imports Microsoft.CodeAnalysis.CSharp +Imports Microsoft.CodeAnalysis.CSharp.Syntax +Imports Microsoft.CodeAnalysis.Editor.UnitTests.Utilities.GoToHelpers + Namespace Microsoft.CodeAnalysis.Editor.UnitTests.GoToDefinition @@ -3731,19 +3735,248 @@ class Program Dim workspace = - -partial class Program + +partial partial class Program { public void Method(int argument) { Goo(0); } } + + <%= s_interceptsLocationCode %> + + +using System.Runtime.CompilerServices; + +partial partial class Program +{ + [InterceptsLocationAttribute("")] + public void $$[|Method|](int argument) + { + } +} Await TestAsync(workspace) End Function + + + Public Async Function TestInterceptors_UnsupportedVersion() As Task + Dim workspace = + + + +partial partial class Program +{ + public void Method(int argument) + { + Goo(0); + } +} + <%= s_interceptsLocationCode %> + + +using System.Runtime.CompilerServices; + +partial partial class Program +{ + [InterceptsLocationAttribute(-1, "")] + public void $$[|Method|](int argument) + { + } +} + + + + + Await TestAsync(workspace) + End Function + + + Public Async Function TestInterceptors_EmptyData() As Task + Dim workspace = + + + +partial partial class Program +{ + public void Method(int argument) + { + Goo(0); + } +} + <%= s_interceptsLocationCode %> + + +using System.Runtime.CompilerServices; + +partial partial class Program +{ + [InterceptsLocationAttribute(1, "")] + public void $$[|Method|](int argument) + { + } +} + + + + + Await TestAsync(workspace) + End Function + + + Public Async Function TestInterceptors_BogusData() As Task + Dim workspace = + + + +partial partial class Program +{ + public void Method(int argument) + { + Goo(0); + } +} + <%= s_interceptsLocationCode %> + + +using System.Runtime.CompilerServices; + +partial partial class Program +{ + [InterceptsLocationAttribute(1, "*")] + public void $$[|Method|](int argument) + { + } +} + + + + + Await TestAsync(workspace) + End Function + + + Public Async Function TestInterceptors_JustPadding() As Task + Dim workspace = + + + +partial partial class Program +{ + public void Method(int argument) + { + Goo(0); + } +} + <%= s_interceptsLocationCode %> + + +using System.Runtime.CompilerServices; + +partial partial class Program +{ + [InterceptsLocationAttribute(1, "=")] + public void $$[|Method|](int argument) + { + } +} + + + + + Await TestAsync(workspace) + End Function + +#Disable Warning RSEXPERIMENTAL002 ' Type is for evaluation purposes only and is subject to change or removal in future updates. + + Private Const s_interceptsLocationCode = " +namespace System.Runtime.CompilerServices +{ + [System.AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] + public sealed class InterceptsLocationAttribute : System.Attribute + { + public InterceptsLocationAttribute(int version, string data) { } + } +}" + + Private Async Function TestInterceptor(code As String, getInvocations As Func(Of SyntaxNode, IEnumerable(Of InvocationExpressionSyntax))) As Task + Dim firstFileContents = code & s_interceptsLocationCode + + Dim primordialWorkspace = + + + <%= firstFileContents %> + + + + Using testWorkspace = EditorTestWorkspace.Create(primordialWorkspace, composition:=GoToTestHelpers.Composition) + Dim solution = testWorkspace.CurrentSolution + Dim project = solution.Projects.Single() + Dim document = project.Documents.Single() + + Dim root = Await document.GetSyntaxRootAsync() + Dim invocations = getInvocations(root) + + Dim semanticModel = Await document.GetSemanticModelAsync() + Dim attributeText = "" + + For Each invocation In invocations + Dim location = semanticModel.GetInterceptableLocation(invocation) + attributeText += location.GetInterceptsLocationAttributeSyntax() & vbCrLf + Next + + Dim finalWorkspace = + + + <%= firstFileContents %> + +public partial class Program +{ + <%= attributeText %>public void $$Method() + { + } +} + + + + + Await TestAsync(finalWorkspace) + End Using + End Function + + + Public Async Function TestInterceptors_SingleCaller() As Task + Await TestInterceptor(" +public partial class Program +{ + public void Method(int argument) + { + [|Goo|](0); + } +}", Function(root) root.DescendantNodes().OfType(Of InvocationExpressionSyntax)) + End Function + + + Public Async Function TestInterceptors_SingleInterceptorForMultipleLocations() As Task + Await TestInterceptor(" +public partial class Program +{ + public void Method1() + { + {|PresenterLocation:Goo|}(0); + } + + public void Method2() + { + this.{|PresenterLocation:Goo|}(1); + } +}", Function(root) root.DescendantNodes().OfType(Of InvocationExpressionSyntax)) + End Function + +#Enable Warning RSEXPERIMENTAL002 ' Type is for evaluation purposes only and is subject to change or removal in future updates. End Class End Namespace diff --git a/src/EditorFeatures/Test2/GoToDefinition/GoToDefinitionTestsBase.vb b/src/EditorFeatures/Test2/GoToDefinition/GoToDefinitionTestsBase.vb index 3fdd31f5428d8..07e52249e7fd7 100644 --- a/src/EditorFeatures/Test2/GoToDefinition/GoToDefinitionTestsBase.vb +++ b/src/EditorFeatures/Test2/GoToDefinition/GoToDefinitionTestsBase.vb @@ -62,13 +62,33 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.GoToDefinition expectedLocations.Sort() + + Dim expectedPresenterLocations = workspace.Documents. + Where(Function(d) d.AnnotatedSpans.ContainsKey("PresenterLocation")). + Select(Function(d) (d.Id, spans:=d.AnnotatedSpans("PresenterLocation"))) + Dim context = presenter.Context If expectedResult Then If expectedLocations.Count = 0 Then - ' if there is not expected locations, it means symbol navigation is used - Assert.True(mockSymbolNavigationService._triedNavigationToSymbol, "a navigation took place") - Assert.Null(mockDocumentNavigationService._documentId) - Assert.False(presenterCalled) + If expectedPresenterLocations.Any() Then + ' multiple results shown in the streaming presenter. + Assert.True(presenterCalled) + + Dim presenterReferences = context.GetReferences() + + Assert.Equal(presenterReferences.Length, expectedPresenterLocations.Sum(Function(t) t.spans.Length)) + + For Each tuple In expectedPresenterLocations + For Each sourceSpan In tuple.spans + Assert.True(presenterReferences.Any(Function(r) r.SourceSpan.Document.Id = tuple.Id AndAlso r.SourceSpan.SourceSpan = sourceSpan)) + Next + Next + Else + ' if there is not expected locations, it means symbol navigation is used + Assert.True(mockSymbolNavigationService._triedNavigationToSymbol, "a navigation took place") + Assert.Null(mockDocumentNavigationService._documentId) + Assert.False(presenterCalled) + End If Else Assert.False(mockSymbolNavigationService._triedNavigationToSymbol) From c48fd7dd2030667417b5542d77940a9355082180 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 13 Jun 2024 14:23:58 -0700 Subject: [PATCH 07/17] Remove unused usings --- .../Core/Navigation/AbstractDefinitionLocationService.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs index 06c4905d10c93..fe8573890d054 100644 --- a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs +++ b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -18,7 +17,6 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; -using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Navigation; From 9e9659216a88bf5c4e73af9dc469acf9e89f9ff0 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 13 Jun 2024 14:24:35 -0700 Subject: [PATCH 08/17] Whitespace --- .../Compiler/Core/Utilities/InterceptslocationUtilities.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/InterceptslocationUtilities.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/InterceptslocationUtilities.cs index 1b750fa844882..de25afdec717d 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/InterceptslocationUtilities.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/InterceptslocationUtilities.cs @@ -15,7 +15,6 @@ namespace Microsoft.CodeAnalysis.Shared.Utilities; internal static class InterceptsLocationUtilities { - public record struct InterceptsLocationData(ImmutableArray ContentHash, int Position); public static ImmutableArray GetInterceptsLocationData(ImmutableArray attributes) From c450e56edf29aebadc519d2d43197029a38aa5c1 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 13 Jun 2024 14:30:28 -0700 Subject: [PATCH 09/17] Docs --- .../Compiler/Core/Utilities/InterceptslocationUtilities.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/InterceptslocationUtilities.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/InterceptslocationUtilities.cs index de25afdec717d..d6db0f1597849 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/InterceptslocationUtilities.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/InterceptslocationUtilities.cs @@ -10,11 +10,17 @@ using System.Buffers.Binary; using System.Collections.Immutable; using Microsoft.CodeAnalysis.Shared.Collections; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Shared.Utilities; internal static class InterceptsLocationUtilities { + /// Content hash of the original document the containing the invocation to be intercepted. + /// (See ) + /// The position in the file of the invocation that was intercepted. This is the absolute + /// start of the name token being invoked (e.g. this.$$Goo(x, y, z)) (see ). public record struct InterceptsLocationData(ImmutableArray ContentHash, int Position); public static ImmutableArray GetInterceptsLocationData(ImmutableArray attributes) From 1b6cd09b3e90c9257a90e26b1df4d5f9c679cdfc Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 13 Jun 2024 17:24:52 -0700 Subject: [PATCH 10/17] Update src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems --- .../Compiler/Core/CompilerExtensions.projitems | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems index 26c8ebb0eb1d7..8fde93497b1d0 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems @@ -490,7 +490,7 @@ - + From 8b6a6c77740174808ae5ecf5879b8cfa78a1036a Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 13 Jun 2024 17:25:16 -0700 Subject: [PATCH 11/17] Update src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems --- .../Compiler/Core/CompilerExtensions.projitems | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems index 8fde93497b1d0..79c13beae1c95 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems @@ -504,7 +504,7 @@ - + From 72163f6632335085ddfe3c8d27085f7a6cd7b7d2 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 13 Jun 2024 17:26:28 -0700 Subject: [PATCH 12/17] comment --- .../Core/Navigation/AbstractDefinitionLocationService.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs index fe8573890d054..0a0b390cfb446 100644 --- a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs +++ b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs @@ -125,11 +125,8 @@ internal abstract partial class AbstractDefinitionLocationService( if (definitionDocument != originalDocument) return null; - // Ok, we were already on the definition. Look for better symbols we could show results - // for instead. For now, just see if we're on an interface member impl. If so, we can - // instead navigate to the actual interface member. - // - // In the future we can expand this with other mappings if appropriate. + // Ok, we were already on the definition. Look for better symbols we could show results for instead. This can be + // expanded with other mappings in the future if appropriate. return await TryGetExplicitInterfaceLocationAsync().ConfigureAwait(false) ?? await TryGetInterceptedLocationAsync().ConfigureAwait(false); From 08a7899b0f049d9e8513690c22b6c324303f3c7d Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 13 Jun 2024 17:27:18 -0700 Subject: [PATCH 13/17] add capacity --- .../Core/Navigation/AbstractDefinitionLocationService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs index 0a0b390cfb446..f91d7c506db72 100644 --- a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs +++ b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs @@ -206,7 +206,7 @@ internal abstract partial class AbstractDefinitionLocationService( var definitionItem = method.ToNonClassifiedDefinitionItem(solution, includeHiddenLocations: true); - var referenceItems = new List(); + var referenceItems = new List(capacity: documentSpans.Count); var classificationOptions = ClassificationOptions.Default with { ClassifyObsoleteSymbols = false }; foreach (var documentSpan in documentSpans) { From e3afcacbe2ad579febe6fe386a4a789bb5a23ae3 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 13 Jun 2024 22:15:46 -0700 Subject: [PATCH 14/17] Move to doc/project state --- ...nitionLocationService.ByteArrayComparer.cs | 27 ------------------- .../AbstractDefinitionLocationService.cs | 7 +++-- .../Workspace/Solution/DocumentState.cs | 14 ++++++++++ .../Workspace/Solution/ProjectState.cs | 23 ++++++++++++++++ .../Solution/ProjectState_Checksum.cs | 1 + .../Core/Utilities/ImmutableArrayComparer.cs | 24 +++++++++++++++++ 6 files changed, 65 insertions(+), 31 deletions(-) delete mode 100644 src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.ByteArrayComparer.cs create mode 100644 src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ImmutableArrayComparer.cs diff --git a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.ByteArrayComparer.cs b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.ByteArrayComparer.cs deleted file mode 100644 index 26d08c2789afa..0000000000000 --- a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.ByteArrayComparer.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.Navigation; - -internal abstract partial class AbstractDefinitionLocationService -{ - private sealed class ByteArrayComparer : IEqualityComparer> - { - public static readonly ByteArrayComparer Instance = new(); - - private ByteArrayComparer() { } - - public bool Equals(ImmutableArray x, ImmutableArray y) - => x.SequenceEqual(y); - - public int GetHashCode(ImmutableArray obj) - => Hash.CombineValues(obj); - } -} diff --git a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs index f91d7c506db72..779a5b19dc0dd 100644 --- a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs +++ b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs @@ -247,13 +247,12 @@ internal abstract partial class AbstractDefinitionLocationService( private static async Task, DocumentId>> ComputeContentHashToDocumentMapAsync(ProjectState projectState, CancellationToken cancellationToken) { - var result = new Dictionary, DocumentId>(ByteArrayComparer.Instance); + var result = new Dictionary, DocumentId>(ImmutableArrayComparer.Instance); foreach (var (documentId, documentState) in projectState.DocumentStates.States) { - var text = await documentState.GetTextAsync(cancellationToken).ConfigureAwait(false); - var checksum = text.GetContentHash(); - result[checksum] = documentId; + var contentHash = await documentState.GetContentHashAsync(cancellationToken).ConfigureAwait(false); + result[contentHash] = documentId; } return result; diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs index 8e7daaa323d16..5eaacf839d0b6 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; @@ -33,6 +34,8 @@ internal partial class DocumentState : TextDocumentState // null if the document doesn't support syntax trees: private readonly ITreeAndVersionSource? _treeSource; + private ImmutableArray _contentHash; + protected DocumentState( LanguageServices languageServices, IDocumentServiceProvider? documentServiceProvider, @@ -97,6 +100,17 @@ public SourceCodeKind SourceCodeKind public bool IsGenerated => Attributes.IsGenerated; + public async ValueTask> GetContentHashAsync(CancellationToken cancellationToken) + { + if (_contentHash.IsDefault) + { + var text = await this.GetTextAsync(cancellationToken).ConfigureAwait(false); + ImmutableInterlocked.InterlockedCompareExchange(ref _contentHash, text.GetContentHash(), default); + } + + return _contentHash; + } + protected static ITreeAndVersionSource CreateLazyFullyParsedTree( ITextAndVersionSource newTextSource, LoadTextOptions loadTextOptions, diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs index c45b2002fbb71..76cdc43030a29 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs @@ -52,6 +52,8 @@ internal partial class ProjectState // Checksums for this solution state private readonly AsyncLazy _lazyChecksums; + private readonly AsyncLazy, DocumentId>> _lazyContentHashToDocumentId; + /// /// Analyzer config options to be used for specific trees. /// @@ -83,6 +85,7 @@ private ProjectState( _projectInfo = ClearAllDocumentsFromProjectInfo(projectInfo); _lazyChecksums = AsyncLazy.Create(static (self, cancellationToken) => self.ComputeChecksumsAsync(cancellationToken), arg: this); + _lazyContentHashToDocumentId = AsyncLazy.Create(static (self, cancellationToken) => self.ComputeContentHashToDocumentIdAsync(cancellationToken), arg: this); } public ProjectState(LanguageServices languageServices, ProjectInfo projectInfo) @@ -123,6 +126,20 @@ public ProjectState(LanguageServices languageServices, ProjectInfo projectInfo) _projectInfo = ClearAllDocumentsFromProjectInfo(projectInfoFixed); _lazyChecksums = AsyncLazy.Create(static (self, cancellationToken) => self.ComputeChecksumsAsync(cancellationToken), arg: this); + _lazyContentHashToDocumentId = AsyncLazy.Create(static (self, cancellationToken) => self.ComputeContentHashToDocumentIdAsync(cancellationToken), arg: this); + } + + private async Task, DocumentId>> ComputeContentHashToDocumentIdAsync(CancellationToken cancellationToken) + { + var result = new Dictionary, DocumentId>(ImmutableArrayComparer.Instance); + foreach (var (documentId, documentState) in this.DocumentStates.States) + { + var text = await documentState.GetTextAsync(cancellationToken).ConfigureAwait(false); + var contentHash = text.GetContentHash(); + result[contentHash] = documentId; + } + + return result; } private static ProjectInfo ClearAllDocumentsFromProjectInfo(ProjectInfo projectInfo) @@ -133,6 +150,12 @@ private static ProjectInfo ClearAllDocumentsFromProjectInfo(ProjectInfo projectI .WithAnalyzerConfigDocuments([]); } + public async ValueTask GetDocumentAsync(ImmutableArray contentHash, CancellationToken cancellationToken) + { + var map = await _lazyContentHashToDocumentId.GetValueAsync(cancellationToken).ConfigureAwait(false); + return map.TryGetValue(contentHash, out var documentId) ? DocumentStates.GetState(documentId) : null; + } + private ProjectInfo FixProjectInfo(ProjectInfo projectInfo) { if (projectInfo.CompilationOptions == null) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState_Checksum.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState_Checksum.cs index cad59046c71f9..297e0732254ec 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState_Checksum.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState_Checksum.cs @@ -5,6 +5,7 @@ #nullable disable using System; +using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.ErrorReporting; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ImmutableArrayComparer.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ImmutableArrayComparer.cs new file mode 100644 index 0000000000000..6ec8efb021439 --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/ImmutableArrayComparer.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Roslyn.Utilities; + +namespace Roslyn.Utilities; + +internal sealed class ImmutableArrayComparer : IEqualityComparer> +{ + public static readonly ImmutableArrayComparer Instance = new(); + + private ImmutableArrayComparer() { } + + public bool Equals(ImmutableArray x, ImmutableArray y) + => x.SequenceEqual(y); + + public int GetHashCode(ImmutableArray obj) + => Hash.CombineValues(obj); +} From 56198383b21850de617bbdc20d41d09bd8fa46f5 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 13 Jun 2024 22:21:31 -0700 Subject: [PATCH 15/17] Update location --- .../AbstractDefinitionLocationService.cs | 37 ++++--------------- .../Portable/Workspace/Solution/Project.cs | 6 +++ .../Workspace/Solution/ProjectState.cs | 4 +- .../Core/CompilerExtensions.projitems | 1 + 4 files changed, 17 insertions(+), 31 deletions(-) diff --git a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs index 779a5b19dc0dd..fa4993c960c63 100644 --- a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs +++ b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs @@ -28,8 +28,6 @@ internal abstract partial class AbstractDefinitionLocationService( private readonly IThreadingContext _threadingContext = threadingContext; private readonly IStreamingFindUsagesPresenter _streamingPresenter = streamingPresenter; - private static readonly ConditionalWeakTable, DocumentId>>> s_projectToLazyContentHashMap = new(); - private static Task GetNavigableLocationAsync( Document document, int position, CancellationToken cancellationToken) { @@ -164,25 +162,19 @@ internal abstract partial class AbstractDefinitionLocationService( if (interceptsLocationDatas.Length == 0) return null; - var lazyContentHashToDocumentMap = s_projectToLazyContentHashMap.GetValue( - project.State, - static projectState => AsyncLazy.Create( - static (projectState, cancellationToken) => ComputeContentHashToDocumentMapAsync(projectState, cancellationToken), - projectState)); - - var contentHashToDocumentMap = await lazyContentHashToDocumentMap.GetValueAsync(cancellationToken).ConfigureAwait(false); - using var _ = ArrayBuilder.GetInstance(out var documentSpans); - foreach (var interceptsLocationData in interceptsLocationDatas) + foreach (var (contentHash, position) in interceptsLocationDatas) { - if (contentHashToDocumentMap.TryGetValue(interceptsLocationData.ContentHash, out var documentId)) + var document = await project.GetDocumentAsync(contentHash, cancellationToken).ConfigureAwait(false); + + if (document != null) { - var document = solution.GetDocument(documentId); - if (document != null) + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + if (position >= 0 && position < root.FullSpan.Length) { - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var token = root.FindToken(interceptsLocationData.Position); + var token = root.FindToken(position); documentSpans.Add(new DocumentSpan(document, token.Span)); } } @@ -245,19 +237,6 @@ internal abstract partial class AbstractDefinitionLocationService( } } - private static async Task, DocumentId>> ComputeContentHashToDocumentMapAsync(ProjectState projectState, CancellationToken cancellationToken) - { - var result = new Dictionary, DocumentId>(ImmutableArrayComparer.Instance); - - foreach (var (documentId, documentState) in projectState.DocumentStates.States) - { - var contentHash = await documentState.GetContentHashAsync(cancellationToken).ConfigureAwait(false); - result[contentHash] = documentId; - } - - return result; - } - private static async Task IsThirdPartyNavigationAllowedAsync( ISymbol symbolToNavigateTo, int caretPosition, Document document, CancellationToken cancellationToken) { diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs index 7cbe5fd3bbb63..d5775d1b4f502 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs @@ -807,4 +807,10 @@ private string GetDebuggerDisplay() internal SkippedHostAnalyzersInfo GetSkippedAnalyzersInfo(DiagnosticAnalyzerInfoCache infoCache) => Solution.SolutionState.Analyzers.GetSkippedAnalyzersInfo(this, infoCache); + + internal async ValueTask GetDocumentAsync(ImmutableArray contentHash, CancellationToken cancellationToken) + { + var documentId = await _projectState.GetDocumentIdAsync(contentHash, cancellationToken).ConfigureAwait(false); + return documentId is null ? null : GetDocument(documentId); + } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs index 76cdc43030a29..fa92679fa2d5c 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs @@ -150,10 +150,10 @@ private static ProjectInfo ClearAllDocumentsFromProjectInfo(ProjectInfo projectI .WithAnalyzerConfigDocuments([]); } - public async ValueTask GetDocumentAsync(ImmutableArray contentHash, CancellationToken cancellationToken) + public async ValueTask GetDocumentIdAsync(ImmutableArray contentHash, CancellationToken cancellationToken) { var map = await _lazyContentHashToDocumentId.GetValueAsync(cancellationToken).ConfigureAwait(false); - return map.TryGetValue(contentHash, out var documentId) ? DocumentStates.GetState(documentId) : null; + return map.TryGetValue(contentHash, out var documentId) ? documentId : null; } private ProjectInfo FixProjectInfo(ProjectInfo projectInfo) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems index 79c13beae1c95..b13db5d68ad71 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems @@ -485,6 +485,7 @@ + From c18fb1bb179c403527da469c44193fd4fb501a1d Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 13 Jun 2024 22:23:13 -0700 Subject: [PATCH 16/17] usings --- .../Core/Navigation/AbstractDefinitionLocationService.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs index fa4993c960c63..268dfefc74608 100644 --- a/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs +++ b/src/EditorFeatures/Core/Navigation/AbstractDefinitionLocationService.cs @@ -2,10 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections.Generic; -using System.Collections.Immutable; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Classification; @@ -17,7 +14,6 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Navigation; From 3936d56f518a7af87e3de0d59c7859ef28b5e48b Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Thu, 13 Jun 2024 22:27:41 -0700 Subject: [PATCH 17/17] fix test --- .../Test2/GoToDefinition/GoToDefinitionTestsBase.vb | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/EditorFeatures/Test2/GoToDefinition/GoToDefinitionTestsBase.vb b/src/EditorFeatures/Test2/GoToDefinition/GoToDefinitionTestsBase.vb index 07e52249e7fd7..eb5a4c2d975d9 100644 --- a/src/EditorFeatures/Test2/GoToDefinition/GoToDefinitionTestsBase.vb +++ b/src/EditorFeatures/Test2/GoToDefinition/GoToDefinitionTestsBase.vb @@ -6,7 +6,6 @@ Imports System.Threading Imports Microsoft.CodeAnalysis.Editor.CSharp.Navigation Imports Microsoft.CodeAnalysis.Editor.Shared.Utilities Imports Microsoft.CodeAnalysis.Editor.UnitTests.Utilities.GoToHelpers -Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces Imports Microsoft.CodeAnalysis.Editor.VisualBasic.Navigation Imports Microsoft.CodeAnalysis.Navigation Imports Microsoft.VisualStudio.Text @@ -62,7 +61,6 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.GoToDefinition expectedLocations.Sort() - Dim expectedPresenterLocations = workspace.Documents. Where(Function(d) d.AnnotatedSpans.ContainsKey("PresenterLocation")). Select(Function(d) (d.Id, spans:=d.AnnotatedSpans("PresenterLocation"))) @@ -133,7 +131,6 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.GoToDefinition Assert.Null(mockDocumentNavigationService._documentId) Assert.False(presenterCalled) End If - End Using End Function End Class