diff --git a/src/Features/Core/Portable/AddImport/AbstractAddImportFeatureService.cs b/src/Features/Core/Portable/AddImport/AbstractAddImportFeatureService.cs index 7b25001f7c83ac77225fcd718131ee839a86d75b..8af27aaf8543d127b513bbd30a79f46084ebb113 100644 --- a/src/Features/Core/Portable/AddImport/AbstractAddImportFeatureService.cs +++ b/src/Features/Core/Portable/AddImport/AbstractAddImportFeatureService.cs @@ -8,10 +8,12 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Experiments; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.LanguageServices; using Microsoft.CodeAnalysis.Packaging; +using Microsoft.CodeAnalysis.Remote; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.SymbolSearch; using Microsoft.CodeAnalysis.Text; @@ -56,6 +58,30 @@ protected AbstractAddImportFeatureService() Document document, TextSpan span, string diagnosticId, ISymbolSearchService symbolSearchService, bool searchReferenceAssemblies, ImmutableArray packageSources, CancellationToken cancellationToken) + { + var session = await document.Project.Solution.TryCreateCodeAnalysisServiceSessionAsync( + AddImportOptions.OutOfProcessAllowed, WellKnownExperimentNames.RoslynFeatureOOP, + new RemoteSymbolSearchService(symbolSearchService, cancellationToken), cancellationToken).ConfigureAwait(false); + using (session) + { + if (session == null) + { + return await GetFixesInCurrentProcessAsync( + document, span, diagnosticId, symbolSearchService, + searchReferenceAssemblies, packageSources, cancellationToken).ConfigureAwait(false); + } + else + { + return await GetFixesInRemoteProcessAsync( + session, document, span, diagnosticId, + searchReferenceAssemblies, packageSources).ConfigureAwait(false); + } + } + } + + private async Task> GetFixesInCurrentProcessAsync( + Document document, TextSpan span, string diagnosticId, ISymbolSearchService symbolSearchService, + bool searchReferenceAssemblies, ImmutableArray packageSources, CancellationToken cancellationToken) { var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var node = root.FindToken(span.Start, findInsideTrivia: true) @@ -121,7 +147,7 @@ protected AbstractAddImportFeatureService() // things like the Interactive workspace as this will cause us to // create expensive bk-trees which we won't even be able to save for // future use. - if (!IsHostOrTestWorkspace(project)) + if (!IsHostOrTestOrRemoteWorkspace(project)) { return ImmutableArray.Empty; } @@ -130,10 +156,11 @@ protected AbstractAddImportFeatureService() return fuzzyReferences; } - private static bool IsHostOrTestWorkspace(Project project) + private static bool IsHostOrTestOrRemoteWorkspace(Project project) { return project.Solution.Workspace.Kind == WorkspaceKind.Host || - project.Solution.Workspace.Kind == WorkspaceKind.Test; + project.Solution.Workspace.Kind == WorkspaceKind.Test || + project.Solution.Workspace.Kind == WorkspaceKind.RemoteWorkspace; } private async Task> FindResultsAsync( @@ -152,7 +179,7 @@ private static bool IsHostOrTestWorkspace(Project project) // things like the Interactive workspace as we can't even add project // references to the interactive window. We could consider adding metadata // references with #r in the future. - if (IsHostOrTestWorkspace(project)) + if (IsHostOrTestOrRemoteWorkspace(project)) { // Now search unreferenced projects, and see if they have any source symbols that match // the search string. diff --git a/src/Features/Core/Portable/AddImport/AddImportFixData.cs b/src/Features/Core/Portable/AddImport/AddImportFixData.cs index 93835273e65e6f72c2d8b079a4c0215353a4279e..3bc4e1cd32823a04f6feb4a6e57cf2c5d2c997b6 100644 --- a/src/Features/Core/Portable/AddImport/AddImportFixData.cs +++ b/src/Features/Core/Portable/AddImport/AddImportFixData.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; using System.Collections.Immutable; +using System.Linq; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.Tags; using Microsoft.CodeAnalysis.Text; diff --git a/src/Features/Core/Portable/AddImport/Remote/AbstractAddImportFeatureService_Remote.cs b/src/Features/Core/Portable/AddImport/Remote/AbstractAddImportFeatureService_Remote.cs new file mode 100644 index 0000000000000000000000000000000000000000..32397899750521c4d1de416e76a835ab02036315 --- /dev/null +++ b/src/Features/Core/Portable/AddImport/Remote/AbstractAddImportFeatureService_Remote.cs @@ -0,0 +1,85 @@ +// 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.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Packaging; +using Microsoft.CodeAnalysis.Remote; +using Microsoft.CodeAnalysis.SymbolSearch; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.AddImport +{ + internal abstract partial class AbstractAddImportFeatureService + { + private async Task> GetFixesInRemoteProcessAsync( + RemoteHostClient.Session session, Document document, TextSpan span, string diagnosticId, + bool searchReferenceAssemblies, ImmutableArray packageSources) + { + var result = await session.InvokeAsync>( + nameof(IRemoteAddImportFeatureService.GetFixesAsync), + new object[] { document.Id, span, diagnosticId, searchReferenceAssemblies, packageSources }).ConfigureAwait(false); + + return result; + } + + /// + /// Used to supply the OOP server a callback that it can use to search for ReferenceAssemblies or + /// nuget packages. We can't necessarily do that search directly in the OOP server as our + /// 'SymbolSearchEngine' may actually be running in a *different* process (there is no guarantee + /// that all remote work happens in the same process). + /// + /// This does mean, currently, that when we call over to OOP to do a search, it will bounce + /// back to VS, which will then bounce back out to OOP to perform the Nuget/ReferenceAssembly + /// portion of the search. Ideally we could keep this all OOP. + /// + private class RemoteSymbolSearchService : IRemoteSymbolSearchUpdateEngine + { + private readonly ISymbolSearchService _symbolSearchService; + private readonly CancellationToken _cancellationToken; + + public RemoteSymbolSearchService( + ISymbolSearchService symbolSearchService, + CancellationToken cancellationToken) + { + _symbolSearchService = symbolSearchService; + _cancellationToken = cancellationToken; + } + + public Task UpdateContinuouslyAsync(string sourceName, string localSettingsDirectory) + { + // Remote side should never call this. + throw new NotImplementedException(); + } + + public async Task> FindPackagesWithTypeAsync( + string source, string name, int arity) + { + var result = await _symbolSearchService.FindPackagesWithTypeAsync( + source, name, arity, _cancellationToken).ConfigureAwait(false); + + return result; + } + + public async Task> FindPackagesWithAssemblyAsync( + string source, string name) + { + var result = await _symbolSearchService.FindPackagesWithAssemblyAsync( + source, name, _cancellationToken).ConfigureAwait(false); + + return result; + } + + public async Task> FindReferenceAssembliesWithTypeAsync( + string name, int arity) + { + var result = await _symbolSearchService.FindReferenceAssembliesWithTypeAsync( + name, arity, _cancellationToken).ConfigureAwait(false); + + return result; + } + } + } +} \ No newline at end of file diff --git a/src/Features/Core/Portable/AddImport/Remote/IRemoteAddImportFeatureService.cs b/src/Features/Core/Portable/AddImport/Remote/IRemoteAddImportFeatureService.cs new file mode 100644 index 0000000000000000000000000000000000000000..095f2af1018663ec812f5620735197a46bdb9fa1 --- /dev/null +++ b/src/Features/Core/Portable/AddImport/Remote/IRemoteAddImportFeatureService.cs @@ -0,0 +1,16 @@ +// 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.Collections.Immutable; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Packaging; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.AddImport +{ + internal interface IRemoteAddImportFeatureService + { + Task> GetFixesAsync( + DocumentId documentId, TextSpan span, string diagnosticId, + bool searchReferenceAssemblies, ImmutableArray packageSources); + } +} \ No newline at end of file diff --git a/src/Features/Core/Portable/Features.csproj b/src/Features/Core/Portable/Features.csproj index fa9d332ab21095b6ee05dddaca20bbadc79a38ec..9fe8ce6c52b12f7c7c472e9af24c0e4214eebf69 100644 --- a/src/Features/Core/Portable/Features.csproj +++ b/src/Features/Core/Portable/Features.csproj @@ -100,6 +100,7 @@ Shared\Utilities\DesktopShim.cs + @@ -108,6 +109,7 @@ + diff --git a/src/Workspaces/Core/Portable/Remote/RemoteHostClientExtensions.cs b/src/Workspaces/Core/Portable/Remote/RemoteHostClientExtensions.cs index 3969eba594d926ea4674117265e320293fd8d274..8170de202edbfab5200e4c9f112587e818aee4c7 100644 --- a/src/Workspaces/Core/Portable/Remote/RemoteHostClientExtensions.cs +++ b/src/Workspaces/Core/Portable/Remote/RemoteHostClientExtensions.cs @@ -57,17 +57,17 @@ public static bool IsOutOfProcessEnabled(this Workspace workspace, Option return false; } - // Treat experiments as always on in tests. - if (experimentName != null && workspace.Kind != WorkspaceKind.Test) - { - var experimentEnabled = workspace.Services.GetService(); - if (!experimentEnabled.IsExperimentEnabled(experimentName)) - { - return false; - } - } - - return true; + // Treat experiments as always on in tests. + if (experimentName != null && workspace.Kind != WorkspaceKind.Test) + { + var experimentEnabled = workspace.Services.GetService(); + if (!experimentEnabled.IsExperimentEnabled(experimentName)) + { + return false; + } + } + + return true; } public static async Task TryCreateCodeAnalysisServiceSessionAsync( @@ -86,7 +86,7 @@ public static bool IsOutOfProcessEnabled(this Workspace workspace, Option return null; } - return await client.TryCreateCodeAnalysisServiceSessionAsync(solution, cancellationToken).ConfigureAwait(false); + return await client.TryCreateCodeAnalysisServiceSessionAsync(solution, callbackTarget, cancellationToken).ConfigureAwait(false); } public static Task RunOnRemoteHostAsync( diff --git a/src/Workspaces/Remote/ServiceHub/ServiceHub.csproj b/src/Workspaces/Remote/ServiceHub/ServiceHub.csproj index 025bf020370a5ed54d71bb1f9c65aa6f3a3ebca4..b81929b434a88bc567db4a80ba75978bbfad39c6 100644 --- a/src/Workspaces/Remote/ServiceHub/ServiceHub.csproj +++ b/src/Workspaces/Remote/ServiceHub/ServiceHub.csproj @@ -74,6 +74,7 @@ + diff --git a/src/Workspaces/Remote/ServiceHub/Services/CodeAnalysisService_AddImport.cs b/src/Workspaces/Remote/ServiceHub/Services/CodeAnalysisService_AddImport.cs new file mode 100644 index 0000000000000000000000000000000000000000..98d0f7ee229f8890ed004f018c71f7e937dc8310 --- /dev/null +++ b/src/Workspaces/Remote/ServiceHub/Services/CodeAnalysisService_AddImport.cs @@ -0,0 +1,84 @@ +// 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.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.AddImport; +using Microsoft.CodeAnalysis.Packaging; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.SymbolSearch; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Remote +{ + internal partial class CodeAnalysisService : IRemoteAddImportFeatureService + { + public async Task> GetFixesAsync( + DocumentId documentId, TextSpan span, string diagnosticId, + bool searchReferenceAssemblies, ImmutableArray packageSources) + { + using (UserOperationBooster.Boost()) + { + var solution = await GetSolutionAsync().ConfigureAwait(false); + var document = solution.GetDocument(documentId); + + var service = document.GetLanguageService(); + + var symbolSearchService = new SymbolSearchService(this); + + var result = await service.GetFixesAsync( + document, span, diagnosticId, symbolSearchService, searchReferenceAssemblies, + packageSources, CancellationToken).ConfigureAwait(false); + + return result; + } + } + + /// + /// Provides an implementation of the ISymbolSearchService on the remote side so that + /// Add-Import can find results in nuget packages/reference assemblies. This works + /// by remoting *from* the OOP server back to the host, which can then forward this + /// appropriately to wherever the real ISymbolSearchService is running. This is necessary + /// because it's not guaranteed that the real ISymbolSearchService will be running in + /// the same process that is supplying the . + /// + /// Ideally we would not need to bounce back to the host for this. + /// + private class SymbolSearchService : ISymbolSearchService + { + private readonly CodeAnalysisService codeAnalysisService; + + public SymbolSearchService(CodeAnalysisService codeAnalysisService) + { + this.codeAnalysisService = codeAnalysisService; + } + + public async Task> FindPackagesWithTypeAsync( + string source, string name, int arity, CancellationToken cancellationToken) + { + var result = await codeAnalysisService.Rpc.InvokeAsync>( + nameof(FindPackagesWithTypeAsync), source, name, arity).ConfigureAwait(false); + + return result; + } + + public async Task> FindPackagesWithAssemblyAsync( + string source, string assemblyName, CancellationToken cancellationToken) + { + var result = await codeAnalysisService.Rpc.InvokeAsync>( + nameof(FindPackagesWithAssemblyAsync), source, assemblyName).ConfigureAwait(false); + + return result; + } + + public async Task> FindReferenceAssembliesWithTypeAsync( + string name, int arity, CancellationToken cancellationToken) + { + var result = await codeAnalysisService.Rpc.InvokeAsync>( + nameof(FindReferenceAssembliesWithTypeAsync), name, arity).ConfigureAwait(false); + + return result; + } + } + } +} \ No newline at end of file diff --git a/src/Workspaces/Remote/ServiceHub/Shared/RoslynJsonConverter.RoslynOnly.cs b/src/Workspaces/Remote/ServiceHub/Shared/RoslynJsonConverter.RoslynOnly.cs index d2a6a45aed585ba9633bd000eb05ce3e7e8f9556..82217223b0f3111f1364e2d60ebd1e46ec96c071 100644 --- a/src/Workspaces/Remote/ServiceHub/Shared/RoslynJsonConverter.RoslynOnly.cs +++ b/src/Workspaces/Remote/ServiceHub/Shared/RoslynJsonConverter.RoslynOnly.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Immutable; using System.Linq; +using Microsoft.CodeAnalysis.AddImport; +using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.DocumentHighlighting; using Microsoft.CodeAnalysis.Packaging; using Microsoft.CodeAnalysis.SymbolSearch; @@ -27,6 +29,8 @@ internal partial class AggregateJsonConverter : JsonConverter Add(builder, new PackageWithTypeResultJsonConverter()); Add(builder, new PackageWithAssemblyResultJsonConverter()); Add(builder, new ReferenceAssemblyWithTypeResultJsonConverter()); + + Add(builder, new AddImportFixDataJsonConverter()); } private class TodoCommentDescriptorJsonConverter : BaseJsonConverter @@ -286,5 +290,97 @@ protected override void WriteValue(JsonWriter writer, TaggedText source, JsonSer writer.WriteEndObject(); } } + + private class AddImportFixDataJsonConverter : BaseJsonConverter + { + protected override AddImportFixData ReadValue(JsonReader reader, JsonSerializer serializer) + { + Contract.ThrowIfFalse(reader.TokenType == JsonToken.StartObject); + + var kind = (AddImportFixKind)ReadProperty(reader); + var textChanges = ReadProperty>(serializer, reader); + var title = ReadProperty(reader); + var tags = ReadProperty>(serializer, reader); + var priority = (CodeActionPriority)ReadProperty(reader); + + var projectReferenceToAdd = ReadProperty(serializer, reader); + + var portableExecutableReferenceProjectId = ReadProperty(serializer, reader); + var portableExecutableReferenceFilePathToAdd = ReadProperty(reader); + + var assemblyReferenceAssemblyName = ReadProperty(reader); + var assemblyReferenceFullyQualifiedTypeName = ReadProperty(reader); + + var packageSource = ReadProperty(reader); + var packageName = ReadProperty(reader); + var packageVersionOpt = ReadProperty(reader); + + Contract.ThrowIfFalse(reader.Read()); + Contract.ThrowIfFalse(reader.TokenType == JsonToken.EndObject); + + switch (kind) + { + case AddImportFixKind.ProjectSymbol: + return AddImportFixData.CreateForProjectSymbol(textChanges, title, tags, priority, projectReferenceToAdd); + + case AddImportFixKind.MetadataSymbol: + return AddImportFixData.CreateForMetadataSymbol(textChanges, title, tags, priority, portableExecutableReferenceProjectId, portableExecutableReferenceFilePathToAdd); + + case AddImportFixKind.PackageSymbol: + return AddImportFixData.CreateForPackageSymbol(textChanges, packageSource, packageName, packageVersionOpt); + + case AddImportFixKind.ReferenceAssemblySymbol: + return AddImportFixData.CreateForReferenceAssemblySymbol(textChanges, title, assemblyReferenceAssemblyName, assemblyReferenceFullyQualifiedTypeName); + } + + throw ExceptionUtilities.Unreachable; + } + + protected override void WriteValue(JsonWriter writer, AddImportFixData source, JsonSerializer serializer) + { + writer.WriteStartObject(); + + writer.WritePropertyName(nameof(AddImportFixData.Kind)); + writer.WriteValue((int)source.Kind); + + writer.WritePropertyName(nameof(AddImportFixData.TextChanges)); + serializer.Serialize(writer, source.TextChanges); + + writer.WritePropertyName(nameof(AddImportFixData.Title)); + writer.WriteValue(source.Title); + + writer.WritePropertyName(nameof(AddImportFixData.Tags)); + serializer.Serialize(writer, source.Tags.NullToEmpty()); + + writer.WritePropertyName(nameof(AddImportFixData.Priority)); + writer.WriteValue((int)source.Priority); + + writer.WritePropertyName(nameof(AddImportFixData.ProjectReferenceToAdd)); + serializer.Serialize(writer, source.ProjectReferenceToAdd); + + writer.WritePropertyName(nameof(AddImportFixData.PortableExecutableReferenceProjectId)); + serializer.Serialize(writer, source.PortableExecutableReferenceProjectId); + + writer.WritePropertyName(nameof(AddImportFixData.PortableExecutableReferenceFilePathToAdd)); + writer.WriteValue(source.PortableExecutableReferenceFilePathToAdd); + + writer.WritePropertyName(nameof(AddImportFixData.AssemblyReferenceAssemblyName)); + writer.WriteValue(source.AssemblyReferenceAssemblyName); + + writer.WritePropertyName(nameof(AddImportFixData.AssemblyReferenceFullyQualifiedTypeName)); + writer.WriteValue(source.AssemblyReferenceFullyQualifiedTypeName); + + writer.WritePropertyName(nameof(AddImportFixData.PackageSource)); + writer.WriteValue(source.PackageSource); + + writer.WritePropertyName(nameof(AddImportFixData.PackageName)); + writer.WriteValue(source.PackageName); + + writer.WritePropertyName(nameof(AddImportFixData.PackageVersionOpt)); + writer.WriteValue(source.PackageVersionOpt); + + writer.WriteEndObject(); + } + } } } \ No newline at end of file diff --git a/src/Workspaces/Remote/ServiceHub/Shared/RoslynJsonConverter.SolutionIdConverters.cs b/src/Workspaces/Remote/ServiceHub/Shared/RoslynJsonConverter.SolutionIdConverters.cs index dcd2e0b21748506d5d1980660f8e930a5b2f2a2c..50b177de90dd176e6933fc2c49f05708260b9fe8 100644 --- a/src/Workspaces/Remote/ServiceHub/Shared/RoslynJsonConverter.SolutionIdConverters.cs +++ b/src/Workspaces/Remote/ServiceHub/Shared/RoslynJsonConverter.SolutionIdConverters.cs @@ -10,8 +10,13 @@ internal partial class AggregateJsonConverter : JsonConverter { private abstract class WorkspaceIdJsonConverter : BaseJsonConverter { - protected (Guid, string) ReadFromJsonObject(JsonReader reader) + protected (Guid, string)? ReadFromJsonObject(JsonReader reader) { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + Contract.ThrowIfFalse(reader.TokenType == JsonToken.StartObject); var (id, debugName) = ReadIdAndName(reader); @@ -51,8 +56,8 @@ private class SolutionIdJsonConverter : WorkspaceIdJsonConverter { protected override SolutionId ReadValue(JsonReader reader, JsonSerializer serializer) { - var (id, debugName) = ReadFromJsonObject(reader); - return SolutionId.CreateFromSerialized(id, debugName); + (Guid id, string debugName)? tuple = ReadFromJsonObject(reader); + return tuple == null ? null : SolutionId.CreateFromSerialized(tuple.Value.id, tuple.Value.debugName); } protected override void WriteValue(JsonWriter writer, SolutionId solutionId, JsonSerializer serializer) @@ -63,8 +68,8 @@ private class ProjectIdJsonConverter : WorkspaceIdJsonConverter { protected override ProjectId ReadValue(JsonReader reader, JsonSerializer serializer) { - var (id, debugName) = ReadFromJsonObject(reader); - return ProjectId.CreateFromSerialized(id, debugName); + (Guid id, string debugName)? tuple = ReadFromJsonObject(reader); + return tuple == null ? null : ProjectId.CreateFromSerialized(tuple.Value.id, tuple.Value.debugName); } protected override void WriteValue(JsonWriter writer, ProjectId projectId, JsonSerializer serializer) @@ -75,6 +80,11 @@ private class DocumentIdJsonConverter : WorkspaceIdJsonConverter { protected override DocumentId ReadValue(JsonReader reader, JsonSerializer serializer) { + if (reader.TokenType == JsonToken.Null) + { + return null; + } + Contract.ThrowIfFalse(reader.TokenType == JsonToken.StartObject); var projectId = ReadProperty(serializer, reader);