diff --git a/src/EditorFeatures/CSharp/FindReferences/CSharpFindReferencesService.cs b/src/EditorFeatures/CSharp/FindReferences/CSharpFindReferencesService.cs index 505927ffb2c624728768ef1ded5a591cc8b9daea..b11dbd5d7b83929bb1f160031470d24ec0863b5c 100644 --- a/src/EditorFeatures/CSharp/FindReferences/CSharpFindReferencesService.cs +++ b/src/EditorFeatures/CSharp/FindReferences/CSharpFindReferencesService.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Composition; +using Microsoft.CodeAnalysis.Editor.FindReferences; using Microsoft.CodeAnalysis.Editor.Host; -using Microsoft.CodeAnalysis.Editor.Implementation.FindReferences; using Microsoft.CodeAnalysis.Host.Mef; namespace Microsoft.CodeAnalysis.Editor.CSharp.FindReferences diff --git a/src/EditorFeatures/CSharp/GoToImplementation/CSharpGoToImplementationService.cs b/src/EditorFeatures/CSharp/GoToImplementation/CSharpGoToImplementationService.cs index a3a6b7e8805539793a22a18b80d53d832a40aba5..7714370401d72c4ca031efe67ffee5c0be39b616 100644 --- a/src/EditorFeatures/CSharp/GoToImplementation/CSharpGoToImplementationService.cs +++ b/src/EditorFeatures/CSharp/GoToImplementation/CSharpGoToImplementationService.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; using System.Composition; +using Microsoft.CodeAnalysis.Editor.GoToImplementation; using Microsoft.CodeAnalysis.Editor.Host; -using Microsoft.CodeAnalysis.Editor.Implementation.GoToImplementation; using Microsoft.CodeAnalysis.Host.Mef; namespace Microsoft.CodeAnalysis.Editor.CSharp.GoToImplementation @@ -14,9 +14,19 @@ internal sealed class CSharpGoToImplementationService : AbstractGoToImplementati { [ImportingConstructor] public CSharpGoToImplementationService( - [ImportMany]IEnumerable> presenters, - [ImportMany]IEnumerable> streamingPresenters) - : base(presenters, streamingPresenters) + [ImportMany]IEnumerable> presenters) + : base(presenters) + { + } + } + + [ExportLanguageService(typeof(IStreamingFindImplementationsService), LanguageNames.CSharp), Shared] + internal sealed class CSharpStreamingFindImplementationsService : AbstractGoToImplementationService + { + [ImportingConstructor] + public CSharpStreamingFindImplementationsService( + [ImportMany]IEnumerable> presenters) + : base(presenters) { } } diff --git a/src/EditorFeatures/Core/EditorFeatures.csproj b/src/EditorFeatures/Core/EditorFeatures.csproj index c42609167d4e3be345663f31ca2922c56d25c520..509d72f9a783fcb61c5615b90638a5841fe082d5 100644 --- a/src/EditorFeatures/Core/EditorFeatures.csproj +++ b/src/EditorFeatures/Core/EditorFeatures.csproj @@ -108,6 +108,7 @@ + @@ -256,9 +257,9 @@ - - - + + + @@ -279,7 +280,7 @@ - + @@ -376,9 +377,9 @@ - - - + + + diff --git a/src/EditorFeatures/Core/Implementation/FindReferences/AbstractFindReferencesService.ProgressAdapter.cs b/src/EditorFeatures/Core/FindReferences/AbstractFindReferencesService.ProgressAdapter.cs similarity index 98% rename from src/EditorFeatures/Core/Implementation/FindReferences/AbstractFindReferencesService.ProgressAdapter.cs rename to src/EditorFeatures/Core/FindReferences/AbstractFindReferencesService.ProgressAdapter.cs index 30ed6a39be40d0bcfecf1b0b8d74469514b8a126..2cbc9ad0eec4cdc44a035a3343dbf5de1f447c11 100644 --- a/src/EditorFeatures/Core/Implementation/FindReferences/AbstractFindReferencesService.ProgressAdapter.cs +++ b/src/EditorFeatures/Core/FindReferences/AbstractFindReferencesService.ProgressAdapter.cs @@ -9,7 +9,7 @@ using Microsoft.CodeAnalysis.Navigation; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.FindReferences +namespace Microsoft.CodeAnalysis.Editor.FindReferences { internal abstract partial class AbstractFindReferencesService { diff --git a/src/EditorFeatures/Core/Implementation/FindReferences/AbstractFindReferencesService.cs b/src/EditorFeatures/Core/FindReferences/AbstractFindReferencesService.cs similarity index 74% rename from src/EditorFeatures/Core/Implementation/FindReferences/AbstractFindReferencesService.cs rename to src/EditorFeatures/Core/FindReferences/AbstractFindReferencesService.cs index c522e305529da4e59794fc2d2cf4e290dc23e0e9..df55544e231468bb1ac2f22a551bc0f941aa7eca 100644 --- a/src/EditorFeatures/Core/Implementation/FindReferences/AbstractFindReferencesService.cs +++ b/src/EditorFeatures/Core/FindReferences/AbstractFindReferencesService.cs @@ -3,18 +3,17 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.FindUsages; using Microsoft.CodeAnalysis.Editor.Host; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; -using Microsoft.CodeAnalysis.Editor.SymbolMapping; -using Microsoft.CodeAnalysis.FindUsages; using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.FindUsages; using Microsoft.CodeAnalysis.Navigation; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.FindReferences +namespace Microsoft.CodeAnalysis.Editor.FindReferences { internal abstract partial class AbstractFindReferencesService : ForegroundThreadAffinitizedObject, IFindReferencesService, IStreamingFindReferencesService @@ -30,47 +29,13 @@ internal abstract partial class AbstractFindReferencesService : _navigableItemPresenters = navigableItemPresenters; } - /// - /// Common helper for both the synchronous and streaming versions of FAR. - /// It returns the symbol we want to search for and the solution we should - /// be searching. - /// - /// Note that the returned may absolutely *not* be - /// the same as document.Project.Solution. This is because - /// there may be symbol mapping involved (for example in Metadata-As-Source - /// scenarios). - /// - private async Task> GetRelevantSymbolAndProjectAtPositionAsync( - Document document, int position, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var symbol = await SymbolFinder.FindSymbolAtPositionAsync(document, position, cancellationToken: cancellationToken).ConfigureAwait(false); - if (symbol == null) - { - return null; - } - - // If this document is not in the primary workspace, we may want to search for results - // in a solution different from the one we started in. Use the starting workspace's - // ISymbolMappingService to get a context for searching in the proper solution. - var mappingService = document.Project.Solution.Workspace.Services.GetService(); - - var mapping = await mappingService.MapSymbolAsync(document, symbol, cancellationToken).ConfigureAwait(false); - if (mapping == null) - { - return null; - } - - return Tuple.Create(mapping.Symbol, mapping.Project); - } - private async Task, Solution>> FindReferencedSymbolsAsync( Document document, int position, IWaitContext waitContext) { var cancellationToken = waitContext.CancellationToken; - var symbolAndProject = await GetRelevantSymbolAndProjectAtPositionAsync(document, position, cancellationToken).ConfigureAwait(false); + var symbolAndProject = await FindUsagesHelpers.GetRelevantSymbolAndProjectAtPositionAsync( + document, position, cancellationToken).ConfigureAwait(false); if (symbolAndProject == null) { return null; @@ -98,7 +63,6 @@ public bool TryFindReferences(Document document, int position, IWaitContext wait { var cancellationToken = waitContext.CancellationToken; - // Otherwise, fall back to displaying SymbolFinder based references. var result = this.FindReferencedSymbolsAsync(document, position, waitContext).WaitAndGetResult(cancellationToken); return TryDisplayReferences(result); } @@ -164,7 +128,7 @@ private bool TryDisplayReferences(Tuple, Solution> cancellationToken.ThrowIfCancellationRequested(); // Find the symbol we want to search and the solution we want to search in. - var symbolAndProject = await GetRelevantSymbolAndProjectAtPositionAsync( + var symbolAndProject = await FindUsagesHelpers.GetRelevantSymbolAndProjectAtPositionAsync( document, position, cancellationToken).ConfigureAwait(false); if (symbolAndProject == null) { diff --git a/src/EditorFeatures/Core/Implementation/FindReferences/FindReferencesCommandHandler.cs b/src/EditorFeatures/Core/FindReferences/FindReferencesCommandHandler.cs similarity index 95% rename from src/EditorFeatures/Core/Implementation/FindReferences/FindReferencesCommandHandler.cs rename to src/EditorFeatures/Core/FindReferences/FindReferencesCommandHandler.cs index 2b60bbc5a354c47a2a31eebe6bc830951f4ec244..14a9ebbf395c3167724827c512b89fc5581b795b 100644 --- a/src/EditorFeatures/Core/Implementation/FindReferences/FindReferencesCommandHandler.cs +++ b/src/EditorFeatures/Core/FindReferences/FindReferencesCommandHandler.cs @@ -16,7 +16,7 @@ using Microsoft.VisualStudio.Text; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.Implementation.FindReferences +namespace Microsoft.CodeAnalysis.Editor.FindReferences { [ExportCommandHandler(PredefinedCommandHandlerNames.FindReferences, ContentTypeNames.RoslynContentType)] internal class FindReferencesCommandHandler : ICommandHandler @@ -83,7 +83,7 @@ private bool TryExecuteCommand(int caretPosition, Document document) var streamingEnabled = document.Project.Solution.Workspace.Options.GetOption(FeatureOnOffOptions.StreamingFindReferences, document.Project.Language); if (streamingEnabled && streamingService != null && streamingPresenter != null) { - StreamingFindReferences(document, streamingService, streamingPresenter, caretPosition); + StreamingFindReferences(document, caretPosition, streamingService, streamingPresenter); return true; } @@ -113,8 +113,9 @@ private IStreamingFindUsagesPresenter GetStreamingPresenter() } private async void StreamingFindReferences( - Document document, IStreamingFindReferencesService service, - IStreamingFindUsagesPresenter presenter, int caretPosition) + Document document, int caretPosition, + IStreamingFindReferencesService service, + IStreamingFindUsagesPresenter presenter) { try { diff --git a/src/EditorFeatures/Core/Implementation/FindReferences/IFindReferencesService.cs b/src/EditorFeatures/Core/FindReferences/IFindReferencesService.cs similarity index 100% rename from src/EditorFeatures/Core/Implementation/FindReferences/IFindReferencesService.cs rename to src/EditorFeatures/Core/FindReferences/IFindReferencesService.cs diff --git a/src/EditorFeatures/Core/FindUsages/FindUsagesContext.cs b/src/EditorFeatures/Core/FindUsages/FindUsagesContext.cs index 8762afb8a0060dfef1bbe40cbeddbc12eaf67242..e64900c8c165d4186540159577427a81766cefd3 100644 --- a/src/EditorFeatures/Core/FindUsages/FindUsagesContext.cs +++ b/src/EditorFeatures/Core/FindUsages/FindUsagesContext.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Threading; using System.Threading.Tasks; using Roslyn.Utilities; @@ -14,6 +15,10 @@ protected FindUsagesContext() { } + public virtual void ReportMessage(string message) + { + } + public virtual void SetSearchLabel(string displayName) { } diff --git a/src/EditorFeatures/Core/FindUsages/FindUsagesHelpers.cs b/src/EditorFeatures/Core/FindUsages/FindUsagesHelpers.cs new file mode 100644 index 0000000000000000000000000000000000000000..e5beea533e89b9201e61b1a1bfb3c19955ad1708 --- /dev/null +++ b/src/EditorFeatures/Core/FindUsages/FindUsagesHelpers.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.SymbolMapping; +using Microsoft.CodeAnalysis.FindSymbols; + +namespace Microsoft.CodeAnalysis.Editor.FindUsages +{ + internal static class FindUsagesHelpers + { + /// + /// Common helper for both the synchronous and streaming versions of FAR. + /// It returns the symbol we want to search for and the solution we should + /// be searching. + /// + /// Note that the returned may absolutely *not* be + /// the same as document.Project.Solution. This is because + /// there may be symbol mapping involved (for example in Metadata-As-Source + /// scenarios). + /// + public static async Task> GetRelevantSymbolAndProjectAtPositionAsync( + Document document, int position, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var symbol = await SymbolFinder.FindSymbolAtPositionAsync(document, position, cancellationToken: cancellationToken).ConfigureAwait(false); + if (symbol == null) + { + return null; + } + + // If this document is not in the primary workspace, we may want to search for results + // in a solution different from the one we started in. Use the starting workspace's + // ISymbolMappingService to get a context for searching in the proper solution. + var mappingService = document.Project.Solution.Workspace.Services.GetService(); + + var mapping = await mappingService.MapSymbolAsync(document, symbol, cancellationToken).ConfigureAwait(false); + if (mapping == null) + { + return null; + } + + return Tuple.Create(mapping.Symbol, mapping.Project); + } + } +} \ No newline at end of file diff --git a/src/EditorFeatures/Core/FindUsages/IFindUsagesContext.cs b/src/EditorFeatures/Core/FindUsages/IFindUsagesContext.cs index 79f415cd5ea60f0add2ca3a94286c5de89b1b602..35f34ab9c11724e3802d88992fe1c1f4f67db0a4 100644 --- a/src/EditorFeatures/Core/FindUsages/IFindUsagesContext.cs +++ b/src/EditorFeatures/Core/FindUsages/IFindUsagesContext.cs @@ -9,6 +9,14 @@ internal interface IFindUsagesContext { CancellationToken CancellationToken { get; } + /// + /// Report a message to be displayed to the user. + /// + void ReportMessage(string message); + + /// + /// Set the title of the window that results are displayed in. + /// void SetSearchLabel(string displayName); Task OnDefinitionFoundAsync(DefinitionItem definition); diff --git a/src/EditorFeatures/Core/GoToImplementation/AbstractGoToImplementationService.cs b/src/EditorFeatures/Core/GoToImplementation/AbstractGoToImplementationService.cs new file mode 100644 index 0000000000000000000000000000000000000000..f7bc7e50727217751a28a9c54e63316c3c4bde13 --- /dev/null +++ b/src/EditorFeatures/Core/GoToImplementation/AbstractGoToImplementationService.cs @@ -0,0 +1,219 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.FindUsages; +using Microsoft.CodeAnalysis.Editor.Host; +using Microsoft.CodeAnalysis.Editor.Implementation.GoToDefinition; +using Microsoft.CodeAnalysis.Editor.SymbolMapping; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.FindUsages; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.Navigation; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Editor.GoToImplementation +{ + internal abstract class AbstractGoToImplementationService : + IGoToImplementationService, IStreamingFindImplementationsService + { + private readonly IEnumerable> _navigableItemPresenters; + + public AbstractGoToImplementationService( + IEnumerable> navigableItemPresenters) + { + _navigableItemPresenters = navigableItemPresenters; + } + + public bool TryGoToImplementation(Document document, int position, CancellationToken cancellationToken, out string message) + { + var result = this.FindImplementationsAsync(document, position, cancellationToken).WaitAndGetResult(cancellationToken); + if (result == null) + { + message = EditorFeaturesResources.Cannot_navigate_to_the_symbol_under_the_caret; + return false; + } + + if (result.Value.message != null) + { + message = result.Value.message; + return false; + } + + return TryGoToImplementations( + result.Value.symbol, result.Value.project, + result.Value.implementations, cancellationToken, out message); + } + + public async Task<(ISymbol symbol, Project project, ImmutableArray implementations, string message)?> FindImplementationsAsync(Document document, int position, CancellationToken cancellationToken) + { + var symbolAndProject = await FindUsagesHelpers.GetRelevantSymbolAndProjectAtPositionAsync( + document, position, cancellationToken).ConfigureAwait(false); + if (symbolAndProject == null) + { + return null; + } + + return await FindImplementationsAsync( + symbolAndProject.Item1, symbolAndProject.Item2, cancellationToken).ConfigureAwait(false); + } + + private async Task<(ISymbol symbol, Project project, ImmutableArray implementations, string message)?> FindImplementationsAsync( + ISymbol symbol, Project project, CancellationToken cancellationToken) + { + var implementations = await FindImplementationsWorkerAsync( + symbol, project, cancellationToken).ConfigureAwait(false); + + var filteredSymbols = implementations.WhereAsArray( + s => !s.IsAbstract && s.Locations.Any(l => l.IsInSource)); + + return filteredSymbols.Length == 0 + ? (symbol, project, filteredSymbols, EditorFeaturesResources.The_symbol_has_no_implementations) + : (symbol, project, filteredSymbols, null); + } + + private async Task> FindImplementationsWorkerAsync( + ISymbol symbol, Project project, CancellationToken cancellationToken) + { + var solution = project.Solution; + if (symbol.IsInterfaceType() || symbol.IsImplementableMember()) + { + var implementations = await SymbolFinder.FindImplementationsAsync( + symbol, solution, cancellationToken: cancellationToken).ConfigureAwait(false); + + // It's important we use a HashSet here -- we may have cases in an inheritence hierarchy where more than one method + // in an overrides chain implements the same interface method, and we want to duplicate those. The easiest way to do it + // is to just use a HashSet. + var implementationsAndOverrides = new HashSet(); + + foreach (var implementation in implementations) + { + implementationsAndOverrides.Add(implementation); + + // FindImplementationsAsync will only return the base virtual/abstract method, not that method and the overrides + // of the method. We should also include those. + if (implementation.IsOverridable()) + { + var overrides = await SymbolFinder.FindOverridesAsync( + implementation, solution, cancellationToken: cancellationToken).ConfigureAwait(false); + implementationsAndOverrides.AddRange(overrides); + } + } + + return implementationsAndOverrides.ToImmutableArray(); + } + else if ((symbol as INamedTypeSymbol)?.TypeKind == TypeKind.Class) + { + var derivedClasses = await SymbolFinder.FindDerivedClassesAsync( + (INamedTypeSymbol)symbol, solution, cancellationToken: cancellationToken).ConfigureAwait(false); + var implementations = derivedClasses.Concat(symbol); + + return implementations.ToImmutableArray(); + } + else if (symbol.IsOverridable()) + { + var overrides = await SymbolFinder.FindOverridesAsync( + symbol, solution, cancellationToken: cancellationToken).ConfigureAwait(false); + var implementations = overrides.Concat(symbol); + + return implementations.ToImmutableArray(); + } + else + { + // This is something boring like a regular method or type, so we'll just go there directly + return ImmutableArray.Create(symbol); + //if (GoToDefinitionHelpers.TryGoToDefinition( + // symbol, project, + // _navigableItemPresenters, _streamingPresenters, cancellationToken)) + //{ + // message = null; + // return true; + //} + + //message = EditorFeaturesResources.Cannot_navigate_to_the_symbol_under_the_caret; + //return false; + } + } + + private bool TryGoToImplementations( + ISymbol symbol, Project project, ImmutableArray implementations, CancellationToken cancellationToken, out string message) + { + if (implementations.Length == 0) + { + message = EditorFeaturesResources.The_symbol_has_no_implementations; + return false; + } + else if (implementations.Length == 1) + { + GoToDefinitionHelpers.TryGoToDefinition( + implementations.Single(), project, _navigableItemPresenters, + SpecializedCollections.EmptyEnumerable>(), + cancellationToken); + message = null; + return true; + } + else + { + return TryPresentInNavigableItemsPresenter(symbol, project, implementations, out message); + } + } + + private bool TryPresentInNavigableItemsPresenter( + ISymbol symbol, Project project, ImmutableArray implementations, out string message) + { + // We have multiple symbols, so we'll build a list of all preferred locations for all the symbols + var navigableItems = implementations.SelectMany( + implementation => CreateItemsForImplementation(implementation, project.Solution)); + + var presenter = _navigableItemPresenters.First(); + + var taggedParts = NavigableItemFactory.GetSymbolDisplayTaggedParts(project, symbol); + + presenter.Value.DisplayResult(taggedParts.JoinText(), navigableItems); + message = null; + return true; + } + + private static IEnumerable CreateItemsForImplementation(ISymbol implementation, Solution solution) + { + var symbolDisplayService = solution.Workspace.Services.GetLanguageServices(implementation.Language).GetRequiredService(); + + return NavigableItemFactory.GetItemsFromPreferredSourceLocations( + solution, + implementation, + displayTaggedParts: symbolDisplayService.ToDisplayParts(implementation).ToTaggedText()); + } + + public async Task FindImplementationsAsync(Document document, int position, IFindUsagesContext context) + { + var tuple = await FindImplementationsAsync( + document, position, context.CancellationToken).ConfigureAwait(false); + if (tuple == null) + { + context.ReportMessage(EditorFeaturesResources.Cannot_navigate_to_the_symbol_under_the_caret); + return; + } + + var message = tuple.Value.message; + + if (message != null) + { + context.ReportMessage(message); + return; + } + + var project = tuple.Value.project; + + foreach (var implementation in tuple.Value.implementations) + { + var definitionItem = implementation.ToDefinitionItem(project.Solution); + await context.OnDefinitionFoundAsync(definitionItem).ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/src/EditorFeatures/Core/GoToImplementation/GoToImplementationCommandHandler.cs b/src/EditorFeatures/Core/GoToImplementation/GoToImplementationCommandHandler.cs new file mode 100644 index 0000000000000000000000000000000000000000..d6c01b60ea3cd9e3ff3e00153591e0337ec0092b --- /dev/null +++ b/src/EditorFeatures/Core/GoToImplementation/GoToImplementationCommandHandler.cs @@ -0,0 +1,230 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.Commands; +using Microsoft.CodeAnalysis.Editor.Host; +using Microsoft.CodeAnalysis.Editor.Shared.Extensions; +using Microsoft.CodeAnalysis.Editor.Shared.Options; +using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.FindUsages; +using Microsoft.CodeAnalysis.Notification; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Editor.GoToImplementation +{ + [ExportCommandHandler(PredefinedCommandHandlerNames.GoToImplementation, + ContentTypeNames.RoslynContentType)] + internal sealed class GoToImplementationCommandHandler : ICommandHandler + { + private readonly IWaitIndicator _waitIndicator; + private readonly IEnumerable> _streamingPresenters; + private readonly IAsynchronousOperationListener _asyncListener; + + + [ImportingConstructor] + public GoToImplementationCommandHandler( + IWaitIndicator waitIndicator, + [ImportMany] IEnumerable> streamingPresenters, + [ImportMany] IEnumerable> asyncListeners) + { + _waitIndicator = waitIndicator; + _streamingPresenters = streamingPresenters; + + _asyncListener = new AggregateAsynchronousOperationListener( + asyncListeners, FeatureAttribute.GoToImplementation); + } + + public CommandState GetCommandState(GoToImplementationCommandArgs args, Func nextHandler) + { + // Because this is expensive to compute, we just always say yes + return CommandState.Available; + } + + public void ExecuteCommand(GoToImplementationCommandArgs args, Action nextHandler) + { + var caret = args.TextView.GetCaretPoint(args.SubjectBuffer); + + if (caret.HasValue) + { + var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (document != null) + { + ExecuteCommand(document, caret.Value); + return; + } + } + + nextHandler(); + } + + private void ExecuteCommand(Document document, int caretPosition) + { + var streamingService = document.Project.LanguageServices.GetService(); + var synchronousService = document.Project.LanguageServices.GetService(); + + var streamingPresenter = GetStreamingPresenter(); + + // See if we're running on a host that can provide streaming results. + // We'll both need a FAR service that can stream results to us, and + // a presenter that can accept streamed results. + var streamingEnabled = document.Project.Solution.Workspace.Options.GetOption(FeatureOnOffOptions.StreamingGoToImplementation, document.Project.Language); + var canUseStreamingWindow = streamingEnabled && streamingService != null && streamingPresenter != null; + var canUseSynchronousWindow = synchronousService != null; + + if (canUseStreamingWindow || canUseSynchronousWindow) + { + // We have all the cheap stuff, so let's do expensive stuff now + string messageToShow = null; + _waitIndicator.Wait( + EditorFeaturesResources.Go_To_Implementation, + EditorFeaturesResources.Locating_implementations, + allowCancel: true, + action: context => + { + if (canUseStreamingWindow) + { + StreamingGoToImplementation( + document, caretPosition, + streamingService, streamingPresenter, + context.CancellationToken, out messageToShow); + } + else + { + synchronousService.TryGoToImplementation( + document, caretPosition, context.CancellationToken, out messageToShow); + } + }); + + if (messageToShow != null) + { + var notificationService = document.Project.Solution.Workspace.Services.GetService(); + notificationService.SendNotification(messageToShow, + title: EditorFeaturesResources.Go_To_Implementation, + severity: NotificationSeverity.Information); + } + } + } + + private void StreamingGoToImplementation( + Document document, int caretPosition, + IStreamingFindImplementationsService streamingService, + IStreamingFindUsagesPresenter streamingPresenter, + CancellationToken cancellationToken, + out string messageToShow) + { + var goToImplContext = new GoToImplementationContext(cancellationToken); + streamingService.FindImplementationsAsync(document, caretPosition, goToImplContext).Wait(cancellationToken); + + // If finding implementations reported a message, then just stop and show that + // message to the user. + messageToShow = goToImplContext.Message; + if (messageToShow != null) + { + return; + } + + // Ignore any definitions that we can't navigate to. + var definitions = goToImplContext.GetDefinitionItems() + .WhereAsArray(d => d.CanNavigateTo()); + + // See if there's a third party external item we can navigate to. If so, defer + // to that item and finish. + var externalItems = definitions.WhereAsArray(d => d.IsExternal); + foreach (var item in externalItems) + { + if (item.TryNavigateTo()) + { + return; + } + } + + var nonExternalItems = definitions.WhereAsArray(d => !d.IsExternal); + if (nonExternalItems.Length == 0) + { + return; + } + + if (nonExternalItems.Length == 1 && + nonExternalItems[0].SourceSpans.Length <= 1) + { + // There was only one location to navigate to. Just directly go to that location. + nonExternalItems[0].TryNavigateTo(); + return; + } + + // 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. + + var context = streamingPresenter.StartSearch(EditorFeaturesResources.Go_To_Implementation); + foreach (var definition in nonExternalItems) + { + context.OnDefinitionFoundAsync(definition).Wait(cancellationToken); + } + + // Note: we don't need to put this in a finally. The only time we might not hit + // this is if cancellation or another error gets thrown. In the former case, + // that means that a new search has started. We don't care about telling the + // context it has completed. In the latter case somethign wrong has happened + // and we don't want to run any more code code in this particular context. + context.OnCompletedAsync().Wait(cancellationToken); + } + + private IStreamingFindUsagesPresenter GetStreamingPresenter() + { + try + { + return _streamingPresenters.FirstOrDefault()?.Value; + } + catch + { + return null; + } + } + + private class GoToImplementationContext : FindUsagesContext + { + private readonly object _gate = new object(); + private readonly ImmutableArray.Builder _definitionItems = + ImmutableArray.CreateBuilder(); + + public override CancellationToken CancellationToken { get; } + + public GoToImplementationContext(CancellationToken cancellationToken) + { + CancellationToken = cancellationToken; + } + + public string Message { get; private set; } + + public override void ReportMessage(string message) + => Message = message; + + public ImmutableArray GetDefinitionItems() + { + lock (_gate) + { + return _definitionItems.ToImmutableArray(); + } + } + + public override Task OnDefinitionFoundAsync(DefinitionItem definition) + { + lock (_gate) + { + _definitionItems.Add(definition); + } + + return SpecializedTasks.EmptyTask; + } + } + } +} \ No newline at end of file diff --git a/src/EditorFeatures/Core/Implementation/GoToImplementation/IGoToImplementationService.cs b/src/EditorFeatures/Core/GoToImplementation/IGoToImplementationService.cs similarity index 75% rename from src/EditorFeatures/Core/Implementation/GoToImplementation/IGoToImplementationService.cs rename to src/EditorFeatures/Core/GoToImplementation/IGoToImplementationService.cs index f3e1669e9cbdb3f79ca682706675e7830689847e..9ce23b3dba6ccb1b292467d2e4e0d5bd0f1bc131 100644 --- a/src/EditorFeatures/Core/Implementation/GoToImplementation/IGoToImplementationService.cs +++ b/src/EditorFeatures/Core/GoToImplementation/IGoToImplementationService.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.FindUsages; using Microsoft.CodeAnalysis.Host; namespace Microsoft.CodeAnalysis.Editor @@ -14,4 +16,9 @@ internal interface IGoToImplementationService : ILanguageService /// True if navigating to the implementation of the symbol at the provided position succeeds. False, otherwise. bool TryGoToImplementation(Document document, int position, CancellationToken cancellationToken, out string message); } + + internal interface IStreamingFindImplementationsService : ILanguageService + { + Task FindImplementationsAsync(Document document, int position, IFindUsagesContext context); + } } diff --git a/src/EditorFeatures/Core/Implementation/GoToImplementation/AbstractGoToImplementationService.cs b/src/EditorFeatures/Core/Implementation/GoToImplementation/AbstractGoToImplementationService.cs deleted file mode 100644 index bf94cfbce6b8572f1d774d1e7b56e1cd3d335197..0000000000000000000000000000000000000000 --- a/src/EditorFeatures/Core/Implementation/GoToImplementation/AbstractGoToImplementationService.cs +++ /dev/null @@ -1,207 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using Microsoft.CodeAnalysis.Editor.Host; -using Microsoft.CodeAnalysis.Editor.SymbolMapping; -using Microsoft.CodeAnalysis.FindSymbols; -using Microsoft.CodeAnalysis.FindUsages; -using Microsoft.CodeAnalysis.LanguageServices; -using Microsoft.CodeAnalysis.Navigation; -using Microsoft.CodeAnalysis.Shared.Extensions; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.Editor.Implementation.GoToImplementation -{ - internal abstract class AbstractGoToImplementationService : IGoToImplementationService - { - private readonly IEnumerable> _navigableItemPresenters; - private readonly IEnumerable> _streamingPresenters; - - public AbstractGoToImplementationService( - IEnumerable> navigableItemPresenters, - IEnumerable> streamingPresenters) - { - _navigableItemPresenters = navigableItemPresenters; - _streamingPresenters = streamingPresenters; - } - - public bool TryGoToImplementation(Document document, int position, CancellationToken cancellationToken, out string message) - { - var symbol = SymbolFinder.FindSymbolAtPositionAsync(document, position, cancellationToken).WaitAndGetResult(cancellationToken); - if (symbol != null) - { - // Map the symbol if necessary back to the originating workspace if we're invoking from something - // like metadata as source - var mappingService = document.Project.Solution.Workspace.Services.GetRequiredService(); - var mapping = mappingService.MapSymbolAsync(document, symbol, cancellationToken).WaitAndGetResult(cancellationToken); - - if (mapping != null) - { - return TryGoToImplementationOnMappedSymbol(mapping, cancellationToken, out message); - } - } - - message = EditorFeaturesResources.Cannot_navigate_to_the_symbol_under_the_caret; - return false; - } - - private bool TryGoToImplementationOnMappedSymbol(SymbolMappingResult mapping, CancellationToken cancellationToken, out string message) - { - if (mapping.Symbol.IsInterfaceType() || mapping.Symbol.IsImplementableMember()) - { - var implementations = - SymbolFinder.FindImplementationsAsync(mapping.Symbol, mapping.Solution, cancellationToken: cancellationToken) - .WaitAndGetResult(cancellationToken); - - // It's important we use a HashSet here -- we may have cases in an inheritence hierarchy where more than one method - // in an overrides chain implements the same interface method, and we want to duplicate those. The easiest way to do it - // is to just use a HashSet. - var implementationsAndOverrides = new HashSet(); - - foreach (var implementation in implementations) - { - implementationsAndOverrides.Add(implementation); - - // FindImplementationsAsync will only return the base virtual/abstract method, not that method and the overrides - // of the method. We should also include those. - if (implementation.IsOverridable()) - { - implementationsAndOverrides.AddRange( - SymbolFinder.FindOverridesAsync(implementation, mapping.Solution, cancellationToken: cancellationToken).WaitAndGetResult(cancellationToken)); - } - } - - return TryGoToImplementations(implementationsAndOverrides, mapping, cancellationToken, out message); - } - else if ((mapping.Symbol as INamedTypeSymbol)?.TypeKind == TypeKind.Class) - { - var implementations = - SymbolFinder.FindDerivedClassesAsync((INamedTypeSymbol)mapping.Symbol, mapping.Solution, cancellationToken: cancellationToken) - .WaitAndGetResult(cancellationToken) - .Concat(mapping.Symbol); - - return TryGoToImplementations(implementations, mapping, cancellationToken, out message); - } - else if (mapping.Symbol.IsOverridable()) - { - var implementations = - SymbolFinder.FindOverridesAsync(mapping.Symbol, mapping.Solution, cancellationToken: cancellationToken) - .WaitAndGetResult(cancellationToken) - .Concat(mapping.Symbol); - - return TryGoToImplementations(implementations, mapping, cancellationToken, out message); - } - else - { - // This is something boring like a regular method or type, so we'll just go there directly - if (GoToDefinition.GoToDefinitionHelpers.TryGoToDefinition( - mapping.Symbol, mapping.Project, - _navigableItemPresenters, _streamingPresenters, cancellationToken)) - { - message = null; - return true; - } - - message = EditorFeaturesResources.Cannot_navigate_to_the_symbol_under_the_caret; - return false; - } - } - - private bool TryGoToImplementations(IEnumerable candidateImplementations, SymbolMappingResult mapping, CancellationToken cancellationToken, out string message) - { - var implementations = candidateImplementations - .Where(s => !s.IsAbstract && s.Locations.Any(l => l.IsInSource)) - .ToList(); - - if (implementations.Count == 0) - { - message = EditorFeaturesResources.The_symbol_has_no_implementations; - return false; - } - else if (implementations.Count == 1) - { - GoToDefinition.GoToDefinitionHelpers.TryGoToDefinition( - implementations.Single(), mapping.Project, - _navigableItemPresenters, _streamingPresenters, cancellationToken); - message = null; - return true; - } - else - { - return TryPresentInFindUsagesPresenter(mapping, implementations, cancellationToken, out message) || - TryPresentInNavigableItemsPresenter(mapping, implementations, out message); - } - } - - private bool TryPresentInFindUsagesPresenter( - SymbolMappingResult mapping, List implementations, CancellationToken cancellationToken, out string message) - { - message = null; - - var presenter = GetFindUsagesPresenter(); - if (presenter == null) - { - return false; - } - - var definitionItems = implementations.Select(s => - s.ToDefinitionItem(mapping.Solution)).ToImmutableArrayOrEmpty(); - - var context = presenter.StartSearch(EditorFeaturesResources.Go_To_Implementation); - try - { - foreach (var item in definitionItems) - { - context.OnDefinitionFoundAsync(item).Wait(cancellationToken); - } - } - finally - { - context.OnCompletedAsync().Wait(cancellationToken); - } - - return true; - } - - private IStreamingFindUsagesPresenter GetFindUsagesPresenter() - { - try - { - return _streamingPresenters.FirstOrDefault()?.Value; - } - catch - { - return null; - } - } - - private bool TryPresentInNavigableItemsPresenter( - SymbolMappingResult mapping, List implementations, out string message) - { - // We have multiple symbols, so we'll build a list of all preferred locations for all the symbols - var navigableItems = implementations.SelectMany( - implementation => CreateItemsForImplementation(implementation, mapping.Solution)); - - var presenter = _navigableItemPresenters.First(); - - var taggedParts = NavigableItemFactory.GetSymbolDisplayTaggedParts(mapping.Project, mapping.Symbol); - - presenter.Value.DisplayResult(taggedParts.JoinText(), navigableItems); - message = null; - return true; - } - - private static IEnumerable CreateItemsForImplementation(ISymbol implementation, Solution solution) - { - var symbolDisplayService = solution.Workspace.Services.GetLanguageServices(implementation.Language).GetRequiredService(); - - return NavigableItemFactory.GetItemsFromPreferredSourceLocations( - solution, - implementation, - displayTaggedParts: symbolDisplayService.ToDisplayParts(implementation).ToTaggedText()); - } - } -} \ No newline at end of file diff --git a/src/EditorFeatures/Core/Implementation/GoToImplementation/GoToImplementationCommandHandler.cs b/src/EditorFeatures/Core/Implementation/GoToImplementation/GoToImplementationCommandHandler.cs deleted file mode 100644 index cb90c5479943160c137e67cf36854c6ed74fd046..0000000000000000000000000000000000000000 --- a/src/EditorFeatures/Core/Implementation/GoToImplementation/GoToImplementationCommandHandler.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.ComponentModel.Composition; -using Microsoft.CodeAnalysis.Editor.Commands; -using Microsoft.CodeAnalysis.Editor.Host; -using Microsoft.CodeAnalysis.Editor.Shared.Extensions; -using Microsoft.CodeAnalysis.Notification; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.CodeAnalysis.Editor.Implementation.GoToImplementation -{ - [ExportCommandHandler(PredefinedCommandHandlerNames.GoToImplementation, - ContentTypeNames.RoslynContentType)] - internal sealed class GoToImplementationCommandHandler : ICommandHandler - { - private readonly IWaitIndicator _waitIndicator; - - [ImportingConstructor] - public GoToImplementationCommandHandler( - IWaitIndicator waitIndicator) - { - _waitIndicator = waitIndicator; - } - - public CommandState GetCommandState(GoToImplementationCommandArgs args, Func nextHandler) - { - // Because this is expensive to compute, we just always say yes - return CommandState.Available; - } - - public void ExecuteCommand(GoToImplementationCommandArgs args, Action nextHandler) - { - var caret = args.TextView.GetCaretPoint(args.SubjectBuffer); - - if (caret.HasValue) - { - var document = args.SubjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (document != null) - { - var service = document.Project.LanguageServices.GetService(); - - if (service != null) - { - // We have all the cheap stuff, so let's do expensive stuff now - string messageToShow = null; - bool succeeded = false; - _waitIndicator.Wait( - EditorFeaturesResources.Go_To_Implementation, - EditorFeaturesResources.Locating_implementations, - allowCancel: true, - action: context => succeeded = service.TryGoToImplementation(document, caret.Value, context.CancellationToken, out messageToShow)); - - if (messageToShow != null) - { - var notificationService = document.Project.Solution.Workspace.Services.GetService(); - notificationService.SendNotification(messageToShow, - title: EditorFeaturesResources.Go_To_Implementation, - severity: NotificationSeverity.Information); - } - } - - return; - } - } - - nextHandler(); - } - } -} diff --git a/src/EditorFeatures/Core/Shared/Options/FeatureOnOffOptions.cs b/src/EditorFeatures/Core/Shared/Options/FeatureOnOffOptions.cs index 65f134c0b46f188383c56a5aafef0b3895a7eba6..72cb18a4a0c0fd218e4b961fc52055ca58981560 100644 --- a/src/EditorFeatures/Core/Shared/Options/FeatureOnOffOptions.cs +++ b/src/EditorFeatures/Core/Shared/Options/FeatureOnOffOptions.cs @@ -87,9 +87,15 @@ internal static class FeatureOnOffOptions /// implemented this feature yet. /// [ExportOption] - public static readonly PerLanguageOption RefactoringVerification = new PerLanguageOption(nameof(FeatureOnOffOptions), nameof(RefactoringVerification), defaultValue: false); + public static readonly PerLanguageOption RefactoringVerification = new PerLanguageOption( + nameof(FeatureOnOffOptions), nameof(RefactoringVerification), defaultValue: false); [ExportOption] - public static readonly PerLanguageOption StreamingFindReferences = new PerLanguageOption(nameof(FeatureOnOffOptions), nameof(StreamingFindReferences), defaultValue: true); + public static readonly PerLanguageOption StreamingFindReferences = new PerLanguageOption( + nameof(FeatureOnOffOptions), nameof(StreamingFindReferences), defaultValue: true); + + [ExportOption] + public static readonly PerLanguageOption StreamingGoToImplementation = new PerLanguageOption( + nameof(FeatureOnOffOptions), nameof(StreamingGoToImplementation), defaultValue: true); } -} +} \ No newline at end of file diff --git a/src/Features/Core/Portable/Shared/TestHooks/FeatureAttribute_Names.cs b/src/Features/Core/Portable/Shared/TestHooks/FeatureAttribute_Names.cs index 6d87b91cb7e3112ed0e628f3b7b7bcd59c1042e3..1b4ecebf36ce774a8159a7300ea410ec72abc663 100644 --- a/src/Features/Core/Portable/Shared/TestHooks/FeatureAttribute_Names.cs +++ b/src/Features/Core/Portable/Shared/TestHooks/FeatureAttribute_Names.cs @@ -9,15 +9,20 @@ internal partial class FeatureAttribute public const string BraceHighlighting = nameof(BraceHighlighting); public const string CallHierarchy = nameof(CallHierarchy); public const string Classification = nameof(Classification); + public const string CodeModel = nameof(CodeModel); public const string CompletionSet = nameof(CompletionSet); public const string DesignerAttribute = nameof(DesignerAttribute); - public const string ErrorSquiggles = nameof(ErrorSquiggles); + public const string DiagnosticService = nameof(DiagnosticService); public const string ErrorList = nameof(ErrorList); - public const string FindReferences = nameof(FindReferences); - public const string TodoCommentList = nameof(TodoCommentList); + public const string ErrorSquiggles = nameof(ErrorSquiggles); public const string EventHookup = nameof(EventHookup); + public const string FindReferences = nameof(FindReferences); + public const string GlobalOperation = nameof(GlobalOperation); + public const string GoToImplementation = nameof(GoToImplementation); public const string GraphProvider = nameof(GraphProvider); + public const string InfoBar = nameof(InfoBar); public const string KeywordHighlighting = nameof(KeywordHighlighting); + public const string LightBulb = nameof(LightBulb); public const string LineSeparators = nameof(LineSeparators); public const string NavigateTo = nameof(NavigateTo); public const string NavigationBar = nameof(NavigationBar); @@ -26,15 +31,11 @@ internal partial class FeatureAttribute public const string ReferenceHighlighting = nameof(ReferenceHighlighting); public const string Rename = nameof(Rename); public const string RenameTracking = nameof(RenameTracking); + public const string RuleSetEditor = nameof(RuleSetEditor); public const string SignatureHelp = nameof(SignatureHelp); public const string Snippets = nameof(Snippets); public const string SolutionCrawler = nameof(SolutionCrawler); + public const string TodoCommentList = nameof(TodoCommentList); public const string Workspace = nameof(Workspace); - public const string LightBulb = nameof(LightBulb); - public const string CodeModel = nameof(CodeModel); - public const string GlobalOperation = nameof(GlobalOperation); - public const string DiagnosticService = nameof(DiagnosticService); - public const string RuleSetEditor = nameof(RuleSetEditor); - public const string InfoBar = nameof(InfoBar); } }