diff --git a/Roslyn.sln b/Roslyn.sln index 704cad8e79ba5..0d508cfec2548 100644 --- a/Roslyn.sln +++ b/Roslyn.sln @@ -577,11 +577,11 @@ Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.CodeAnalysis.Cont EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.Contracts.Package", "src\Dependencies\Contracts\Microsoft.CodeAnalysis.Contracts.Package.csproj", "{A8D5CFFA-7F9E-C35B-4F19-D63F6EC1D5CA}" EndProject -Project("{9a19103f-16f7-4668-be54-9a1e7a4f7556}") = "Microsoft.CodeAnalysis.ExternalAccess.Razor", "src\Tools\ExternalAccess\Razor\EditorFeatures\Microsoft.CodeAnalysis.ExternalAccess.Razor.csproj", "{068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.ExternalAccess.Razor", "src\Tools\ExternalAccess\Razor\EditorFeatures\Microsoft.CodeAnalysis.ExternalAccess.Razor.csproj", "{068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}" EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.CodeAnalysis.ExternalAccess.Razor.Shared", "src\Tools\ExternalAccess\Razor\Shared\Microsoft.CodeAnalysis.ExternalAccess.Razor.Shared.shproj", "{4853A78A-4EC4-4D86-9F02-D0DDEAE85520}" EndProject -Project("{9a19103f-16f7-4668-be54-9a1e7a4f7556}") = "Microsoft.CodeAnalysis.ExternalAccess.Razor.Features", "src\Tools\ExternalAccess\Razor\Features\Microsoft.CodeAnalysis.ExternalAccess.Razor.Features.csproj", "{D5A8E20C-E8D2-4A57-906A-263994D8731D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.ExternalAccess.Razor.Features", "src\Tools\ExternalAccess\Razor\Features\Microsoft.CodeAnalysis.ExternalAccess.Razor.Features.csproj", "{D5A8E20C-E8D2-4A57-906A-263994D8731D}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.LanguageServices.ExternalAccess.Copilot", "src\VisualStudio\ExternalAccess\Copilot\Microsoft.VisualStudio.LanguageServices.ExternalAccess.Copilot.csproj", "{9EB058F3-10C9-8F3F-AD9E-72CB362A0928}" EndProject @@ -729,6 +729,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenerateDocumentationAndCon EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestReferenceAssembly", "src\RoslynAnalyzers\TestReferenceAssembly\TestReferenceAssembly.csproj", "{31EB654C-B562-73B4-2456-78FA875515D2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.ExternalAccess.Extensions", "src\Tools\ExternalAccess\Extensions\Microsoft.CodeAnalysis.ExternalAccess.Extensions.csproj", "{6C816C16-D563-884A-D65B-5E68C6FB6659}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1567,6 +1569,14 @@ Global {A8D5CFFA-7F9E-C35B-4F19-D63F6EC1D5CA}.Debug|Any CPU.Build.0 = Debug|Any CPU {A8D5CFFA-7F9E-C35B-4F19-D63F6EC1D5CA}.Release|Any CPU.ActiveCfg = Release|Any CPU {A8D5CFFA-7F9E-C35B-4F19-D63F6EC1D5CA}.Release|Any CPU.Build.0 = Release|Any CPU + {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}.Release|Any CPU.Build.0 = Release|Any CPU + {D5A8E20C-E8D2-4A57-906A-263994D8731D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5A8E20C-E8D2-4A57-906A-263994D8731D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5A8E20C-E8D2-4A57-906A-263994D8731D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5A8E20C-E8D2-4A57-906A-263994D8731D}.Release|Any CPU.Build.0 = Release|Any CPU {9EB058F3-10C9-8F3F-AD9E-72CB362A0928}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9EB058F3-10C9-8F3F-AD9E-72CB362A0928}.Debug|Any CPU.Build.0 = Debug|Any CPU {9EB058F3-10C9-8F3F-AD9E-72CB362A0928}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1787,14 +1797,10 @@ Global {31EB654C-B562-73B4-2456-78FA875515D2}.Debug|Any CPU.Build.0 = Debug|Any CPU {31EB654C-B562-73B4-2456-78FA875515D2}.Release|Any CPU.ActiveCfg = Release|Any CPU {31EB654C-B562-73B4-2456-78FA875515D2}.Release|Any CPU.Build.0 = Release|Any CPU - {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3}.Release|Any CPU.Build.0 = Release|Any CPU - {D5A8E20C-E8D2-4A57-906A-263994D8731D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D5A8E20C-E8D2-4A57-906A-263994D8731D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D5A8E20C-E8D2-4A57-906A-263994D8731D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D5A8E20C-E8D2-4A57-906A-263994D8731D}.Release|Any CPU.Build.0 = Release|Any CPU + {6C816C16-D563-884A-D65B-5E68C6FB6659}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C816C16-D563-884A-D65B-5E68C6FB6659}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C816C16-D563-884A-D65B-5E68C6FB6659}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C816C16-D563-884A-D65B-5E68C6FB6659}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2061,6 +2067,9 @@ Global {2559DAF9-784E-4C29-E0E1-70204B1FD56E} = {C2D1346B-9665-4150-B644-075CF1636BAA} {BD974609-C68B-4BE6-9682-EB132462B50D} = {C2D1346B-9665-4150-B644-075CF1636BAA} {A8D5CFFA-7F9E-C35B-4F19-D63F6EC1D5CA} = {C2D1346B-9665-4150-B644-075CF1636BAA} + {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3} = {8977A560-45C2-4EC2-A849-97335B382C74} + {4853A78A-4EC4-4D86-9F02-D0DDEAE85520} = {8977A560-45C2-4EC2-A849-97335B382C74} + {D5A8E20C-E8D2-4A57-906A-263994D8731D} = {8977A560-45C2-4EC2-A849-97335B382C74} {9EB058F3-10C9-8F3F-AD9E-72CB362A0928} = {5880FECB-91F1-4AB8-8726-75EAFA8A918E} {8087BDE4-6707-05A5-5F84-DFE6628E8EC8} = {6FE0875A-E178-4766-BCC6-87D37F273102} {B095320B-6854-EBEC-98DB-26E06D97183A} = {6FE0875A-E178-4766-BCC6-87D37F273102} @@ -2133,9 +2142,7 @@ Global {A1EBC0BC-D378-B9B7-7A57-EF7DF11E3ED7} = {482C1FC7-4FD6-4381-8078-73BEBFAF4349} {29080628-23A6-1DCB-F15E-93F1D1682CC1} = {482C1FC7-4FD6-4381-8078-73BEBFAF4349} {31EB654C-B562-73B4-2456-78FA875515D2} = {0DDCFE67-7D4E-4709-9882-EC032A031789} - {068CD9AA-CEC3-CA68-1BAB-2B1B9FD711D3} = {8977A560-45C2-4EC2-A849-97335B382C74} - {4853A78A-4EC4-4D86-9F02-D0DDEAE85520} = {8977A560-45C2-4EC2-A849-97335B382C74} - {D5A8E20C-E8D2-4A57-906A-263994D8731D} = {8977A560-45C2-4EC2-A849-97335B382C74} + {6C816C16-D563-884A-D65B-5E68C6FB6659} = {8977A560-45C2-4EC2-A849-97335B382C74} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {604E6B91-7BC0-4126-AE07-D4D2FEFC3D29} @@ -2145,10 +2152,10 @@ Global src\Workspaces\SharedUtilitiesAndExtensions\Workspace\VisualBasic\VisualBasicWorkspaceExtensions.projitems*{0141285d-8f6c-42c7-baf3-3c0ccd61c716}*SharedItemsImports = 5 src\Compilers\CSharp\csc\CscCommandLine.projitems*{0161e25c-918a-4dc8-9648-30fdcc8e31e9}*SharedItemsImports = 5 src\Tools\ExternalAccess\Razor\Shared\Microsoft.CodeAnalysis.ExternalAccess.Razor.Shared.projitems*{068cd9aa-cec3-ca68-1bab-2b1b9fd711d3}*SharedItemsImports = 5 - src\Dependencies\Contracts\Microsoft.CodeAnalysis.Contracts.projitems*{0c2e1633-1462-4712-88f4-a0c945bad3a8}*SharedItemsImports = 5 src\RoslynAnalyzers\Utilities\Compiler\Analyzer.Utilities.projitems*{08735294-3e6b-4420-9916-e7b8c4eb874d}*SharedItemsImports = 13 src\RoslynAnalyzers\Utilities\Compiler\Analyzer.Utilities.projitems*{0a1267e9-52ff-b8de-8522-802be55f41da}*SharedItemsImports = 5 src\RoslynAnalyzers\Utilities\FlowAnalysis\FlowAnalysis.Utilities.projitems*{0a1267e9-52ff-b8de-8522-802be55f41da}*SharedItemsImports = 5 + src\Dependencies\Contracts\Microsoft.CodeAnalysis.Contracts.projitems*{0c2e1633-1462-4712-88f4-a0c945bad3a8}*SharedItemsImports = 5 src\Analyzers\Core\CodeFixes\CodeFixes.projitems*{1b6c4a1a-413b-41fb-9f85-5c09118e541b}*SharedItemsImports = 13 src\Compilers\Core\AnalyzerDriver\AnalyzerDriver.projitems*{1ee8cad3-55f9-4d91-96b2-084641da9a6c}*SharedItemsImports = 5 src\Dependencies\CodeAnalysis.Debugging\Microsoft.CodeAnalysis.Debugging.projitems*{1ee8cad3-55f9-4d91-96b2-084641da9a6c}*SharedItemsImports = 5 @@ -2177,9 +2184,9 @@ Global src\Analyzers\CSharp\Analyzers\CSharpAnalyzers.projitems*{3973b09a-4fbf-44a5-8359-3d22ceb71f71}*SharedItemsImports = 5 src\Analyzers\CSharp\CodeFixes\CSharpCodeFixes.projitems*{3973b09a-4fbf-44a5-8359-3d22ceb71f71}*SharedItemsImports = 5 src\Compilers\CSharp\CSharpAnalyzerDriver\CSharpAnalyzerDriver.projitems*{3973b09a-4fbf-44a5-8359-3d22ceb71f71}*SharedItemsImports = 5 - src\Dependencies\Contracts\Microsoft.CodeAnalysis.Contracts.projitems*{41ed1bfa-fdad-4fe4-8118-db23fb49b0b0}*SharedItemsImports = 5 src\RoslynAnalyzers\Utilities\Compiler\Analyzer.Utilities.projitems*{3f65d29d-ed95-2d6f-b927-47c5cf070aa5}*SharedItemsImports = 5 src\RoslynAnalyzers\Utilities\Workspaces\Workspaces.Utilities.projitems*{3f65d29d-ed95-2d6f-b927-47c5cf070aa5}*SharedItemsImports = 5 + src\Dependencies\Contracts\Microsoft.CodeAnalysis.Contracts.projitems*{41ed1bfa-fdad-4fe4-8118-db23fb49b0b0}*SharedItemsImports = 5 src\Workspaces\SharedUtilitiesAndExtensions\Workspace\CSharp\CSharpWorkspaceExtensions.projitems*{438db8af-f3f0-4ed9-80b5-13fddd5b8787}*SharedItemsImports = 13 src\Tools\ExternalAccess\Razor\Shared\Microsoft.CodeAnalysis.ExternalAccess.Razor.Shared.projitems*{4853a78a-4ec4-4d86-9f02-d0ddeae85520}*SharedItemsImports = 13 src\Compilers\CSharp\csc\CscCommandLine.projitems*{4b45ca0c-03a0-400f-b454-3d4bcb16af38}*SharedItemsImports = 5 @@ -2279,8 +2286,8 @@ Global src\Dependencies\CodeAnalysis.Debugging\Microsoft.CodeAnalysis.Debugging.projitems*{edc68a0e-c68d-4a74-91b7-bf38ec909888}*SharedItemsImports = 5 src\Dependencies\Contracts\Microsoft.CodeAnalysis.Contracts.projitems*{fa0e905d-ec46-466d-b7b2-3b5557f9428c}*SharedItemsImports = 5 src\ExpressionEvaluator\Core\Source\ResultProvider\ResultProvider.projitems*{fa0e905d-ec46-466d-b7b2-3b5557f9428c}*SharedItemsImports = 5 - src\Dependencies\Contracts\Microsoft.CodeAnalysis.Contracts.projitems*{fce88bbd-9bbd-4871-b9b0-de176d73a6b0}*SharedItemsImports = 5 src\RoslynAnalyzers\Utilities\FlowAnalysis\FlowAnalysis.Utilities.projitems*{fcb56cba-fa35-46a8-86b7-bae5433197d9}*SharedItemsImports = 13 src\RoslynAnalyzers\Utilities\Compiler\Analyzer.Utilities.projitems*{fce0046b-03f8-78f6-86a1-8ddcee8f4c9f}*SharedItemsImports = 5 + src\Dependencies\Contracts\Microsoft.CodeAnalysis.Contracts.projitems*{fce88bbd-9bbd-4871-b9b0-de176d73a6b0}*SharedItemsImports = 5 EndGlobalSection EndGlobal diff --git a/eng/config/PublishData.json b/eng/config/PublishData.json index 0de7127d64bac..8c5e46b15deee 100644 --- a/eng/config/PublishData.json +++ b/eng/config/PublishData.json @@ -71,6 +71,7 @@ "Microsoft.CodeAnalysis.ExternalAccess.CompilerDeveloperSDK": "vs-impl", "Microsoft.CodeAnalysis.ExternalAccess.Debugger": "vs-impl", "Microsoft.CodeAnalysis.ExternalAccess.EditorConfigGenerator": "vs-impl", + "Microsoft.CodeAnalysis.ExternalAccess.Extensions": "vs-impl", "Microsoft.CodeAnalysis.ExternalAccess.FSharp": "vs-impl", "Microsoft.CodeAnalysis.ExternalAccess.IntelliTrace": "vs-impl", "Microsoft.CodeAnalysis.ExternalAccess.ProjectSystem": "vs-impl", diff --git a/eng/targets/Services.props b/eng/targets/Services.props index 590cd0c2a1518..409f1b282f9a1 100644 --- a/eng/targets/Services.props +++ b/eng/targets/Services.props @@ -22,6 +22,7 @@ + diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptLifeCycleManager.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptLifeCycleManager.cs new file mode 100644 index 0000000000000..1aa9d2b7f0467 --- /dev/null +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptLifeCycleManager.cs @@ -0,0 +1,21 @@ +// 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.Composition; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler.ServerLifetime; + +namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; + +[ExportLspServiceFactory(typeof(LspServiceLifeCycleManager), ProtocolConstants.TypeScriptLanguageContract), Shared] +internal class VSTypeScriptLifeCycleManager : LspServiceLifeCycleManager.LspLifeCycleManagerFactory +{ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public VSTypeScriptLifeCycleManager(LspWorkspaceRegistrationService lspWorkspaceRegistrationService) : base(lspWorkspaceRegistrationService) + { + } +} diff --git a/src/Features/Core/Portable/Extensions/ExtensionFolder.cs b/src/Features/Core/Portable/Extensions/ExtensionFolder.cs new file mode 100644 index 0000000000000..9b03247f9de6a --- /dev/null +++ b/src/Features/Core/Portable/Extensions/ExtensionFolder.cs @@ -0,0 +1,183 @@ +// 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.Immutable; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.PooledObjects; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Extensions; + +internal sealed partial class ExtensionMessageHandlerServiceFactory +{ + private partial class ExtensionMessageHandlerService + { + /// + /// Represents a folder that many individual extension assemblies can be loaded from. + /// + private sealed class ExtensionFolder + { + private readonly ExtensionMessageHandlerService _extensionMessageHandlerService; + + /// + /// Lazily computed assembly loader for this particular folder. + /// + private readonly AsyncLazy _lazyAssemblyLoader; + + /// + /// Mapping from assembly file path to the handlers it contains. Should only be mutated while the lock is held by our parent . + /// + private ImmutableDictionary> _assemblyFilePathToHandlers = ImmutableDictionary>.Empty; + + public ExtensionFolder( + ExtensionMessageHandlerService extensionMessageHandlerService, + string assemblyFolderPath) + { + _extensionMessageHandlerService = extensionMessageHandlerService; + _lazyAssemblyLoader = AsyncLazy.Create(cancellationToken => + { +#if NET + var analyzerAssemblyLoaderProvider = _extensionMessageHandlerService._solutionServices.GetRequiredService(); + var analyzerAssemblyLoader = analyzerAssemblyLoaderProvider.CreateNewShadowCopyLoader(); + + // Allow this assembly loader to load any dll in assemblyFolderPath. + foreach (var dll in Directory.EnumerateFiles(assemblyFolderPath, "*.dll")) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + // Check if the file is a valid .NET assembly. + AssemblyName.GetAssemblyName(dll); + } + catch + { + // The file is not a valid .NET assembly, skip it. + continue; + } + + analyzerAssemblyLoader.AddDependencyLocation(dll); + } + + return (IAnalyzerAssemblyLoaderInternal?)analyzerAssemblyLoader; +#else + return (IAnalyzerAssemblyLoaderInternal?)null; +#endif + }); + } + + public void Unload() + { + // Only if we've created the assembly loader do we need to do anything. + _lazyAssemblyLoader.TryGetValue(out var loader); + loader?.Dispose(); + } + + private async Task CreateAssemblyHandlersAsync( + string assemblyFilePath, CancellationToken cancellationToken) + { + // On NetFramework do nothing. We have no way to load extensions safely. + var analyzerAssemblyLoader = await _lazyAssemblyLoader.GetValueAsync(cancellationToken).ConfigureAwait(false); + if (analyzerAssemblyLoader is null) + { + return new( + DocumentMessageHandlers: ImmutableDictionary.Empty, + WorkspaceMessageHandlers: ImmutableDictionary.Empty); + } + + var assembly = analyzerAssemblyLoader.LoadFromPath(assemblyFilePath); + var factory = _extensionMessageHandlerService._customMessageHandlerFactory; + Contract.ThrowIfNull(factory); + + var documentMessageHandlers = factory + .CreateDocumentMessageHandlers(assembly, extensionIdentifier: assemblyFilePath, cancellationToken) + .ToImmutableDictionary(h => h.Name, h => (IExtensionMessageHandlerWrapper)h); + var workspaceMessageHandlers = factory + .CreateWorkspaceMessageHandlers(assembly, extensionIdentifier: assemblyFilePath, cancellationToken) + .ToImmutableDictionary(h => h.Name, h => (IExtensionMessageHandlerWrapper)h); + + return new(documentMessageHandlers, workspaceMessageHandlers); + } + + public void RegisterAssembly(string assemblyFilePath) + { + // Must be called under our parent's lock to ensure we see a consistent state of things. + // This allows us to safely examine our current state, and then add the new item. + Contract.ThrowIfTrue(!Monitor.IsEntered(_extensionMessageHandlerService._gate)); + + if (_assemblyFilePathToHandlers.ContainsKey(assemblyFilePath)) + throw new InvalidOperationException($"Extension '{assemblyFilePath}' is already registered."); + + _assemblyFilePathToHandlers = _assemblyFilePathToHandlers.Add( + assemblyFilePath, + AsyncLazy.Create( + cancellationToken => this.CreateAssemblyHandlersAsync(assemblyFilePath, cancellationToken))); + } + + /// + /// Unregisters this assembly path from this extension folder. If this was the last registered path, then this + /// will return true so that this folder can be unloaded. + /// + public bool UnregisterAssembly(string assemblyFilePath) + { + // Must be called under our parent's lock to ensure we see a consistent state of things. This allows us + // to safely examine our current state, remove the existing item, and then return if we are now empty. + Contract.ThrowIfTrue(!Monitor.IsEntered(_extensionMessageHandlerService._gate)); + + if (!_assemblyFilePathToHandlers.ContainsKey(assemblyFilePath)) + throw new InvalidOperationException($"Extension '{assemblyFilePath}' was not registered."); + + _assemblyFilePathToHandlers = _assemblyFilePathToHandlers.Remove(assemblyFilePath); + return _assemblyFilePathToHandlers.Count == 0; + } + + public async ValueTask GetExtensionMessageNamesAsync(string assemblyFilePath, CancellationToken cancellationToken) + { + // This is safe to do as our general contract is that all handler operations should be called explicitly + // between calls to Register/Unregister the extension. So this cannot race with an extension being + // removed. + if (!_assemblyFilePathToHandlers.TryGetValue(assemblyFilePath, out var lazyHandlers)) + throw new InvalidOperationException($"Extension '{assemblyFilePath}' was not registered."); + + // If loading the assembly and getting the handlers failed, then we will throw that exception outwards + // for the client to hear about. + var handlers = await lazyHandlers.GetValueAsync(cancellationToken).ConfigureAwait(false); + + return new( + WorkspaceMessageHandlers: [.. handlers.WorkspaceMessageHandlers.Keys], + DocumentMessageHandlers: [.. handlers.DocumentMessageHandlers.Keys]); + } + + public async ValueTask AddHandlersAsync(string messageName, bool isSolution, ArrayBuilder result, CancellationToken cancellationToken) + { + foreach (var (_, lazyHandler) in _assemblyFilePathToHandlers) + { + cancellationToken.ThrowIfCancellationRequested(); + + AssemblyMessageHandlers handlers; + try + { + handlers = await lazyHandler.GetValueAsync(cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when (FatalError.ReportAndCatchUnlessCanceled(ex)) + { + // If loading the assembly and getting the handlers failed, then we will ignore this assembly + // and continue on to the next one. + continue; + } + + var specificHandlers = isSolution ? handlers.WorkspaceMessageHandlers : handlers.DocumentMessageHandlers; + if (specificHandlers.TryGetValue(messageName, out var handler)) + result.Add(handler); + } + } + } + } +} diff --git a/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerService.cs b/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerService.cs new file mode 100644 index 0000000000000..eed864b297da1 --- /dev/null +++ b/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerService.cs @@ -0,0 +1,194 @@ +// 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.IO; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Threading; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Extensions; + +internal sealed partial class ExtensionMessageHandlerServiceFactory +{ + private readonly record struct AssemblyMessageHandlers( + ImmutableDictionary DocumentMessageHandlers, + ImmutableDictionary WorkspaceMessageHandlers); + + private sealed partial class ExtensionMessageHandlerService( + SolutionServices solutionServices, + IExtensionMessageHandlerFactory? customMessageHandlerFactory) + : IExtensionMessageHandlerService + { + private readonly SolutionServices _solutionServices = solutionServices; + private readonly IExtensionMessageHandlerFactory? _customMessageHandlerFactory = customMessageHandlerFactory; + + /// + /// Lock for , , and . Note: this type is designed such that all time while this lock is held + /// should be minimal. Importantly, no async work or IO should be done while holding this lock. Instead, + /// all of that work should be pushed into AsyncLazy values that compute when asked, outside of this lock. + /// + private readonly object _gate = new(); + + /// + /// Extensions assembly load contexts and loaded handlers, indexed by extension folder path. + /// + private ImmutableDictionary _folderPathToExtensionFolder = ImmutableDictionary.Empty; + + /// + /// Cached handlers of document-related messages, indexed by handler message name. + /// + private readonly Dictionary>> _cachedDocumentHandlers_useOnlyUnderLock = new(); + + /// + /// Cached handlers of non-document-related messages, indexed by handler message name. + /// + private readonly Dictionary>> _cachedWorkspaceHandlers_useOnlyUnderLock = new(); + + private static string GetAssemblyFolderPath(string assemblyFilePath) + { + return Path.GetDirectoryName(assemblyFilePath) + ?? throw new InvalidOperationException($"Unable to get the directory name for {assemblyFilePath}."); + } + + private void ClearCachedHandlers_WhileUnderLock() + { + Contract.ThrowIfTrue(!Monitor.IsEntered(_gate)); + _cachedWorkspaceHandlers_useOnlyUnderLock.Clear(); + _cachedDocumentHandlers_useOnlyUnderLock.Clear(); + } + + private ValueTask RegisterExtensionInCurrentProcessAsync(string assemblyFilePath) + { + var assemblyFolderPath = GetAssemblyFolderPath(assemblyFilePath); + + // Take lock as we both want to update our state, and the state of the ExtensionFolder instance we get back. + lock (_gate) + { + // Clear out the cached handler names. They will be recomputed the next time we need them. + ClearCachedHandlers_WhileUnderLock(); + + var extensionFolder = ImmutableInterlocked.GetOrAdd( + ref _folderPathToExtensionFolder, + assemblyFolderPath, + assemblyFolderPath => new ExtensionFolder(this, assemblyFolderPath)); + + extensionFolder.RegisterAssembly(assemblyFilePath); + return default; + } + } + + private ValueTask UnregisterExtensionInCurrentProcessAsync(string assemblyFilePath) + { + var folderToUnload = Unregister(); + + // If we're done with the folder, ask it to unload any resources it is holding onto. This will ask it + // to unload all ALCs needed to load it and the extensions within. Unloading will happen once the + // runtime/gc determine the ALC is finally collectible. + folderToUnload?.Unload(); + return default; + + ExtensionFolder? Unregister() + { + var assemblyFolderPath = GetAssemblyFolderPath(assemblyFilePath); + + // Take lock as we both want to update our state, and the state of the ExtensionFolder instance we get back. + lock (_gate) + { + if (!_folderPathToExtensionFolder.TryGetValue(assemblyFolderPath, out var extensionFolder)) + throw new InvalidOperationException($"No extension registered as '{assemblyFolderPath}'"); + + // Clear out the cached handler names. They will be recomputed the next time we need them. + ClearCachedHandlers_WhileUnderLock(); + + // If we're not done with the folder. Return null so our caller doesn't unload anything. + if (!extensionFolder.UnregisterAssembly(assemblyFilePath)) + return null; + + // Last extension in the folder. Remove our folder registration entirely, and return it so the + // caller can unload it. + _folderPathToExtensionFolder = _folderPathToExtensionFolder.Remove(assemblyFolderPath); + return extensionFolder; + } + } + } + + private ValueTask ResetInCurrentProcessAsync() + { + ImmutableDictionary oldFolderPathToExtensionFolder; + lock (_gate) + { + oldFolderPathToExtensionFolder = _folderPathToExtensionFolder; + _folderPathToExtensionFolder = ImmutableDictionary.Empty; + ClearCachedHandlers_WhileUnderLock(); + } + + foreach (var (_, folderToUnload) in oldFolderPathToExtensionFolder) + folderToUnload.Unload(); + + return default; + } + + private async ValueTask GetExtensionMessageNamesInCurrentProcessAsync( + string assemblyFilePath, + CancellationToken cancellationToken) + { + var assemblyFolderPath = GetAssemblyFolderPath(assemblyFilePath); + + if (!_folderPathToExtensionFolder.TryGetValue(assemblyFolderPath, out var extensionFolder)) + throw new InvalidOperationException($"No extensions registered at '{assemblyFolderPath}'"); + + return await extensionFolder.GetExtensionMessageNamesAsync(assemblyFilePath, cancellationToken).ConfigureAwait(false); + } + + private async ValueTask HandleExtensionMessageInCurrentProcessAsync( + TArgument executeArgument, bool isSolution, string messageName, string jsonMessage, + Dictionary>> cachedHandlers, + CancellationToken cancellationToken) + { + AsyncLazy> lazyHandlers; + lock (_gate) + { + // May be called a lot. So we use the non-allocating form of this lookup pattern. + lazyHandlers = cachedHandlers.GetOrAdd( + messageName, + static (messageName, arg) => AsyncLazy.Create( + static (arg, cancellationToken) => arg.@this.ComputeHandlersAsync(arg.messageName, arg.isSolution, cancellationToken), + (messageName, arg.@this, arg.isSolution)), + (messageName, @this: this, isSolution)); + } + + var handlers = await lazyHandlers.GetValueAsync(cancellationToken).ConfigureAwait(false); + if (handlers.Length == 0) + throw new InvalidOperationException($"No handler found for message {messageName}."); + + if (handlers.Length > 1) + throw new InvalidOperationException($"Multiple handlers found for message {messageName}."); + + var handler = (IExtensionMessageHandlerWrapper)handlers[0]; + + var message = JsonSerializer.Deserialize(jsonMessage, handler.MessageType); + var result = await handler.ExecuteAsync(message, executeArgument, cancellationToken).ConfigureAwait(false); + return JsonSerializer.Serialize(result, handler.ResponseType); + } + + private async Task> ComputeHandlersAsync( + string messageName, bool isSolution, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var result); + + foreach (var (_, extensionFolder) in _folderPathToExtensionFolder) + await extensionFolder.AddHandlersAsync(messageName, isSolution, result, cancellationToken).ConfigureAwait(false); + + return result.ToImmutable(); + } + } +} diff --git a/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerServiceFactory.cs b/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerServiceFactory.cs new file mode 100644 index 0000000000000..323ae2edd4dfd --- /dev/null +++ b/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerServiceFactory.cs @@ -0,0 +1,21 @@ +// 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.Composition; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.Extensions; + +[ExportWorkspaceServiceFactory(typeof(IExtensionMessageHandlerService)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed partial class ExtensionMessageHandlerServiceFactory( + [Import(AllowDefault = true)] IExtensionMessageHandlerFactory? customMessageHandlerFactory) + : IWorkspaceServiceFactory +{ + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + => new ExtensionMessageHandlerService(workspaceServices.SolutionServices, customMessageHandlerFactory); +} diff --git a/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerService_RemoteOrLocal.cs b/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerService_RemoteOrLocal.cs new file mode 100644 index 0000000000000..e987ea2a97985 --- /dev/null +++ b/src/Features/Core/Portable/Extensions/ExtensionMessageHandlerService_RemoteOrLocal.cs @@ -0,0 +1,138 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Remote; +using Microsoft.CodeAnalysis.Threading; + +namespace Microsoft.CodeAnalysis.Extensions; + +internal sealed partial class ExtensionMessageHandlerServiceFactory +{ + private sealed partial class ExtensionMessageHandlerService + { + // Code for bifurcating calls to either the local or remote process. + + private async ValueTask ExecuteActionInRemoteOrCurrentProcessAsync( + Solution? solution, + Func executeInProcessAsync, + Func executeOutOfProcessAsync, + TArg arg, + CancellationToken cancellationToken) + { + await ExecuteFuncInRemoteOrCurrentProcessAsync( + solution, + static async (localService, tuple, cancellationToken) => + { + var (executeInProcessAsync, _, arg) = tuple; + await executeInProcessAsync(localService, arg, cancellationToken).ConfigureAwait(false); + return default(VoidResult); + }, + static async (service, tuple, checksum, cancellationToken) => + { + var (_, executeOutOfProcessAsync, arg) = tuple; + await executeOutOfProcessAsync(service, arg, checksum, cancellationToken).ConfigureAwait(false); + return default(VoidResult); + }, + (executeInProcessAsync, executeOutOfProcessAsync, arg), + cancellationToken).ConfigureAwait(false); + } + + private async ValueTask ExecuteFuncInRemoteOrCurrentProcessAsync( + Solution? solution, + Func> executeInProcessAsync, + Func> executeOutOfProcessAsync, + TArg arg, + CancellationToken cancellationToken) + { + var client = await RemoteHostClient.TryGetClientAsync(_solutionServices, cancellationToken).ConfigureAwait(false); + if (client is null) + return await executeInProcessAsync(this, arg, cancellationToken).ConfigureAwait(false); + + var result = solution is null + ? await client.TryInvokeAsync( + (remoteService, cancellationToken) => executeOutOfProcessAsync(remoteService, arg, null, cancellationToken), + cancellationToken).ConfigureAwait(false) + : await client.TryInvokeAsync( + solution, + (remoteService, checksum, cancellationToken) => executeOutOfProcessAsync(remoteService, arg, checksum, cancellationToken), + cancellationToken).ConfigureAwait(false); + + // If the remote call succeeded, this will have a valid value in it and can be returned. If it did not + // succeed then we will have already shown the user an error message stating there was an issue making + // the call, and it's fine for this to throw again, unwinding the stack up to the caller. + return result.Value; + } + + public ValueTask RegisterExtensionAsync(string assemblyFilePath, CancellationToken cancellationToken) + => ExecuteActionInRemoteOrCurrentProcessAsync( + solution: null, + static (localService, assemblyFilePath, _) => localService.RegisterExtensionInCurrentProcessAsync(assemblyFilePath), + static (remoteService, assemblyFilePath, _, cancellationToken) => remoteService.RegisterExtensionAsync(assemblyFilePath, cancellationToken), + assemblyFilePath, + cancellationToken); + + public ValueTask UnregisterExtensionAsync(string assemblyFilePath, CancellationToken cancellationToken) + => ExecuteActionInRemoteOrCurrentProcessAsync( + solution: null, + static (localService, assemblyFilePath, _) => localService.UnregisterExtensionInCurrentProcessAsync(assemblyFilePath), + static (remoteService, assemblyFilePath, _, cancellationToken) => remoteService.UnregisterExtensionAsync(assemblyFilePath, cancellationToken), + assemblyFilePath, + cancellationToken); + + public ValueTask GetExtensionMessageNamesAsync(string assemblyFilePath, CancellationToken cancellationToken) + => ExecuteFuncInRemoteOrCurrentProcessAsync( + solution: null, + static (localService, assemblyFilePath, cancellationToken) => localService.GetExtensionMessageNamesInCurrentProcessAsync(assemblyFilePath, cancellationToken), + static (remoteService, assemblyFilePath, _, cancellationToken) => remoteService.GetExtensionMessageNamesAsync(assemblyFilePath, cancellationToken), + assemblyFilePath, + cancellationToken); + + public ValueTask ResetAsync(CancellationToken cancellationToken) + => ExecuteActionInRemoteOrCurrentProcessAsync( + solution: null, + static (localService, _, _) => localService.ResetInCurrentProcessAsync(), + static (remoteService, _, _, cancellationToken) => remoteService.ResetAsync(cancellationToken), + default(VoidResult), + cancellationToken); + + public ValueTask HandleExtensionWorkspaceMessageAsync(Solution solution, string messageName, string jsonMessage, CancellationToken cancellationToken) + => ExecuteFuncInRemoteOrCurrentProcessAsync( + solution, + static (localService, arg, cancellationToken) => + { + var (solution, messageName, jsonMessage, handlers) = arg; + return localService.HandleExtensionMessageInCurrentProcessAsync( + executeArgument: solution, isSolution: true, messageName, jsonMessage, handlers, cancellationToken); + }, + static (remoteService, arg, checksum, cancellationToken) => + { + var (_, messageName, jsonMessage, _) = arg; + return remoteService.HandleExtensionWorkspaceMessageAsync( + checksum!.Value, messageName, jsonMessage, cancellationToken); + }, + (solution, messageName, jsonMessage, _cachedWorkspaceHandlers_useOnlyUnderLock), + cancellationToken); + + public ValueTask HandleExtensionDocumentMessageAsync(Document document, string messageName, string jsonMessage, CancellationToken cancellationToken) + => ExecuteFuncInRemoteOrCurrentProcessAsync( + document.Project.Solution, + static (localService, arg, cancellationToken) => + { + var (document, messageName, jsonMessage, handlers) = arg; + return localService.HandleExtensionMessageInCurrentProcessAsync( + executeArgument: document, isSolution: false, messageName, jsonMessage, handlers, cancellationToken); + }, + static (remoteService, arg, checksum, cancellationToken) => + { + var (document, messageName, jsonMessage, _) = arg; + return remoteService.HandleExtensionDocumentMessageAsync( + checksum!.Value, messageName, jsonMessage, document.Id, cancellationToken); + }, + (document, messageName, jsonMessage, _cachedDocumentHandlers_useOnlyUnderLock), + cancellationToken); + } +} diff --git a/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerFactory.cs b/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerFactory.cs new file mode 100644 index 0000000000000..77a56b42c9275 --- /dev/null +++ b/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerFactory.cs @@ -0,0 +1,35 @@ +// 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.Collections.Immutable; +using System.Reflection; +using System.Threading; + +namespace Microsoft.CodeAnalysis.Extensions; + +/// +/// Factory for creating instances of extension message handlers. +/// +internal interface IExtensionMessageHandlerFactory +{ + /// + /// Creates instances for each + /// IExtensionWorkspaceMessageHandler type in . + /// + /// The assembly to scan for handlers. + /// Unique identifier of the extension owning this handler. + /// May be called multiple times for the same instance. + ImmutableArray> CreateWorkspaceMessageHandlers( + Assembly assembly, string extensionIdentifier, CancellationToken cancellationToken); + + /// + /// Creates instances for each + /// IExtensionDocumentMessageHandler type in . + /// + /// The assembly to scan for handlers. + /// Unique identifier of the extension owning this handler. + /// May be called multiple times for the same instance. + ImmutableArray> CreateDocumentMessageHandlers( + Assembly assembly, string extensionIdentifier, CancellationToken cancellationToken); +} diff --git a/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerService.cs b/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerService.cs new file mode 100644 index 0000000000000..52902278d5d18 --- /dev/null +++ b/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerService.cs @@ -0,0 +1,57 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis.Extensions; + +/// +/// This service is used to register, unregister and execute extension message handlers. +/// +internal interface IExtensionMessageHandlerService : IWorkspaceService +{ + /// + /// Registers extension message handlers from the specified assembly. + /// + /// The assembly to register and create message handlers from. + ValueTask RegisterExtensionAsync(string assemblyFilePath, CancellationToken cancellationToken); + + /// + /// Unregisters extension message handlers previously registered from . + /// + /// The assembly for which handlers should be unregistered. + ValueTask UnregisterExtensionAsync(string assemblyFilePath, CancellationToken cancellationToken); + + /// + /// Gets the message names supported by the extension specified by . + /// + ValueTask GetExtensionMessageNamesAsync(string assemblyFilePath, CancellationToken cancellationToken); + + /// + /// Unregisters all extension message handlers. + /// + ValueTask ResetAsync(CancellationToken cancellationToken); + + /// + /// Executes a non-document-specific extension message handler with the given message and solution. + /// + /// The solution the message refers to. + /// The name of the handler to execute. This is generally the full name of the type implementing the handler. + /// The json message to be passed to the handler. + /// The json message returned by the handler. + ValueTask HandleExtensionWorkspaceMessageAsync( + Solution solution, string messageName, string jsonMessage, CancellationToken cancellationToken); + + /// + /// Executes a document-specific extension message handler with the given message and solution. + /// + /// The document the message refers to. + /// The name of the handler to execute. This is generally the full name of the type implementing the handler. + /// The json message to be passed to the handler. + /// The json message returned by the handler. + ValueTask HandleExtensionDocumentMessageAsync( + Document documentId, string messageName, string jsonMessage, CancellationToken cancellationToken); +} diff --git a/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerWrapper.cs b/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerWrapper.cs new file mode 100644 index 0000000000000..9d8defc5bada3 --- /dev/null +++ b/src/Features/Core/Portable/Extensions/IExtensionMessageHandlerWrapper.cs @@ -0,0 +1,51 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.Extensions; + +/// +/// Wrapper for an IExtensionWorkspaceMessageHandler or IExtensionDocumentMessageHandler +/// as returned by . +/// +internal interface IExtensionMessageHandlerWrapper +{ + /// + /// The type of object received as parameter by the extension message handler. + /// + Type MessageType { get; } + + /// + /// The type of object returned as result by the extension message handler. + /// + Type ResponseType { get; } + + /// + /// The name of the extension message handler. This is generally the full name of the class implementing the handler. + /// + string Name { get; } + + /// + /// The identifier of the extension that this message handler belongs to. + /// + string ExtensionIdentifier { get; } +} + +/// The type of object received as parameter by the extension message +/// handler. +internal interface IExtensionMessageHandlerWrapper : IExtensionMessageHandlerWrapper +{ + /// + /// Executes the extension message handler with the given message and document. + /// + /// An object of type to be passed + /// to the handler. + /// The argument the handler operates on. + /// An object of type returned by the message + /// handler. + Task ExecuteAsync(object? message, TArgument argument, CancellationToken cancellationToken); +} diff --git a/src/Features/Core/Portable/Extensions/IRemoteExtensionMessageHandlerService.cs b/src/Features/Core/Portable/Extensions/IRemoteExtensionMessageHandlerService.cs new file mode 100644 index 0000000000000..981be2c04c9db --- /dev/null +++ b/src/Features/Core/Portable/Extensions/IRemoteExtensionMessageHandlerService.cs @@ -0,0 +1,29 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Threading; + +namespace Microsoft.CodeAnalysis.Extensions; + +/// +/// Remote API for . +/// +internal interface IRemoteExtensionMessageHandlerService +{ + ValueTask RegisterExtensionAsync(string assemblyFilePath, CancellationToken cancellationToken); + + ValueTask UnregisterExtensionAsync(string assemblyFilePath, CancellationToken cancellationToken); + + ValueTask ResetAsync(CancellationToken cancellationToken); + + ValueTask GetExtensionMessageNamesAsync(string assemblyFilePath, CancellationToken cancellationToken); + + ValueTask HandleExtensionDocumentMessageAsync( + Checksum solutionChecksum, string messageName, string jsonMessage, DocumentId documentId, CancellationToken cancellationToken); + + ValueTask HandleExtensionWorkspaceMessageAsync( + Checksum solutionChecksum, string messageName, string jsonMessage, CancellationToken cancellationToken); +} diff --git a/src/Features/Core/Portable/Extensions/RegisterExtensionResponse.cs b/src/Features/Core/Portable/Extensions/RegisterExtensionResponse.cs new file mode 100644 index 0000000000000..7e56a1fc462dc --- /dev/null +++ b/src/Features/Core/Portable/Extensions/RegisterExtensionResponse.cs @@ -0,0 +1,13 @@ +// 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.Collections.Immutable; +using System.Runtime.Serialization; + +namespace Microsoft.CodeAnalysis.Extensions; + +[DataContract] +internal readonly record struct GetExtensionMessageNamesResponse( + [property: DataMember(Order = 0)] ImmutableArray WorkspaceMessageHandlers, + [property: DataMember(Order = 1)] ImmutableArray DocumentMessageHandlers); diff --git a/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj b/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj index 237de95690587..a017065103faf 100644 --- a/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj +++ b/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj @@ -41,6 +41,7 @@ + diff --git a/src/Features/Core/Portable/PublicAPI.Unshipped.txt b/src/Features/Core/Portable/PublicAPI.Unshipped.txt index 8b137891791fe..e69de29bb2d1d 100644 --- a/src/Features/Core/Portable/PublicAPI.Unshipped.txt +++ b/src/Features/Core/Portable/PublicAPI.Unshipped.txt @@ -1 +0,0 @@ - diff --git a/src/LanguageServer/Protocol/Handler/Extensions/AbstractExtensionHandler.cs b/src/LanguageServer/Protocol/Handler/Extensions/AbstractExtensionHandler.cs new file mode 100644 index 0000000000000..04a1bc67f72a4 --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/Extensions/AbstractExtensionHandler.cs @@ -0,0 +1,12 @@ +// 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. + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Extensions; + +internal abstract class AbstractExtensionHandler +{ + public bool MutatesSolutionState => false; + + public bool RequiresLSPSolution => true; +} diff --git a/src/LanguageServer/Protocol/Handler/Extensions/ExtensionDocumentMessageHandler.cs b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionDocumentMessageHandler.cs new file mode 100644 index 0000000000000..84e783c1f5b66 --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionDocumentMessageHandler.cs @@ -0,0 +1,39 @@ +// 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.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Extensions; +using Microsoft.CodeAnalysis.Host.Mef; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Extensions; + +[ExportCSharpVisualBasicStatelessLspService(typeof(ExtensionDocumentMessageHandler)), Shared] +[Method(MethodName)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class ExtensionDocumentMessageHandler() + : AbstractExtensionHandler, ILspServiceDocumentRequestHandler +{ + private const string MethodName = "roslyn/extensionDocumentMessage"; + + public TextDocumentIdentifier GetTextDocumentIdentifier(ExtensionDocumentMessageParams request) + => request.TextDocument; + + public async Task HandleRequestAsync(ExtensionDocumentMessageParams request, RequestContext context, CancellationToken cancellationToken) + { + Contract.ThrowIfNull(context.Document); + + var solution = context.Document.Project.Solution; + + var service = solution.Services.GetRequiredService(); + var response = await service.HandleExtensionDocumentMessageAsync( + context.Document, request.MessageName, request.Message, cancellationToken).ConfigureAwait(false); + + return new ExtensionMessageResponse(response); + } +} diff --git a/src/LanguageServer/Protocol/Handler/Extensions/ExtensionDocumentMessageParams.cs b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionDocumentMessageParams.cs new file mode 100644 index 0000000000000..0b7881f65199f --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionDocumentMessageParams.cs @@ -0,0 +1,19 @@ +// 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.Text.Json.Serialization; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Extensions; + +/// +/// Parameters for the roslyn/extensionDocumentMessage request. +/// +/// Name of the extension message to be invoked. +/// Json message to be passed to an extension message handler. +/// Text document the refers to. +internal readonly record struct ExtensionDocumentMessageParams( + [property: JsonPropertyName("messageName")] string MessageName, + [property: JsonPropertyName("message")] string Message, + [property: JsonPropertyName("textDocument")] TextDocumentIdentifier TextDocument); diff --git a/src/LanguageServer/Protocol/Handler/Extensions/ExtensionMessageResponse.cs b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionMessageResponse.cs new file mode 100644 index 0000000000000..a144ac7d4c087 --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionMessageResponse.cs @@ -0,0 +1,14 @@ +// 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.Text.Json.Serialization; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Extensions; + +/// +/// Return type for the roslyn/extensionWorkspaceMessage and roslyn/extensionDocumentMessage request. +/// +/// Json response returned by the extension message handler. +internal readonly record struct ExtensionMessageResponse( + [property: JsonPropertyName("response")] string Response); diff --git a/src/LanguageServer/Protocol/Handler/Extensions/ExtensionRegisterHandler.cs b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionRegisterHandler.cs new file mode 100644 index 0000000000000..8ebbd8548279a --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionRegisterHandler.cs @@ -0,0 +1,35 @@ +// 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.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Extensions; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Extensions; + +[ExportCSharpVisualBasicStatelessLspService(typeof(ExtensionRegisterHandler)), Shared] +[Method(MethodName)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class ExtensionRegisterHandler() + : AbstractExtensionHandler, ILspServiceRequestHandler +{ + private const string MethodName = "roslyn/extensionRegister"; + + public async Task HandleRequestAsync(ExtensionRegisterParams request, RequestContext context, CancellationToken cancellationToken) + { + Contract.ThrowIfNull(context.Solution); + + var solution = context.Solution; + var service = solution.Services.GetRequiredService(); + + await service.RegisterExtensionAsync(request.AssemblyFilePath, cancellationToken).ConfigureAwait(false); + var handlerNames = await service.GetExtensionMessageNamesAsync(request.AssemblyFilePath, cancellationToken).ConfigureAwait(false); + + return new(handlerNames.WorkspaceMessageHandlers, handlerNames.DocumentMessageHandlers); + } +} diff --git a/src/LanguageServer/Protocol/Handler/Extensions/ExtensionRegisterParams.cs b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionRegisterParams.cs new file mode 100644 index 0000000000000..2eea4e6003e19 --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionRegisterParams.cs @@ -0,0 +1,14 @@ +// 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.Text.Json.Serialization; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Extensions; + +/// +/// Parameters for the roslyn/extensionRegister request. +/// +/// Full path to the assembly that contains the message handlers to register. +internal readonly record struct ExtensionRegisterParams( + [property: JsonPropertyName("assemblyFilePath")] string AssemblyFilePath); diff --git a/src/LanguageServer/Protocol/Handler/Extensions/ExtensionRegisterResponse.cs b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionRegisterResponse.cs new file mode 100644 index 0000000000000..503e605797715 --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionRegisterResponse.cs @@ -0,0 +1,17 @@ +// 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.Collections.Immutable; +using System.Text.Json.Serialization; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Extensions; + +/// +/// Response for the roslyn/extensionRegister request. +/// +/// Names of the registered non-document-specific extension message handlers. +/// Names of the registered document-specific extension message handlers. +internal readonly record struct ExtensionRegisterResponse( + [property: JsonPropertyName("workspaceMessageHandlers")] ImmutableArray WorkspaceMessageHandlers, + [property: JsonPropertyName("documentMessageHandlers")] ImmutableArray DocumentMessageHandlers); diff --git a/src/LanguageServer/Protocol/Handler/Extensions/ExtensionUnregisterHandler.cs b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionUnregisterHandler.cs new file mode 100644 index 0000000000000..9903fb9153774 --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionUnregisterHandler.cs @@ -0,0 +1,30 @@ +// 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.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Extensions; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Extensions; + +[ExportCSharpVisualBasicStatelessLspService(typeof(ExtensionUnregisterHandler)), Shared] +[Method(MethodName)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class ExtensionUnregisterHandler() + : AbstractExtensionHandler, ILspServiceNotificationHandler +{ + private const string MethodName = "roslyn/extensionUnregister"; + + public async Task HandleNotificationAsync(ExtensionUnregisterParams request, RequestContext context, CancellationToken cancellationToken) + { + Contract.ThrowIfNull(context.Solution); + + var service = context.Solution.Services.GetRequiredService(); + await service.UnregisterExtensionAsync(request.AssemblyFilePath, cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/LanguageServer/Protocol/Handler/Extensions/ExtensionUnregisterParams.cs b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionUnregisterParams.cs new file mode 100644 index 0000000000000..6da8c8b12b204 --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionUnregisterParams.cs @@ -0,0 +1,14 @@ +// 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.Text.Json.Serialization; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Extensions; + +/// +/// Parameters for the roslyn/extensionUnregister request. +/// +/// Full path to the assembly that contains the message handlers to unregister. +internal readonly record struct ExtensionUnregisterParams( + [property: JsonPropertyName("assemblyFilePath")] string AssemblyFilePath); diff --git a/src/LanguageServer/Protocol/Handler/Extensions/ExtensionWorkspaceMessageHandler.cs b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionWorkspaceMessageHandler.cs new file mode 100644 index 0000000000000..050d88a53a9e9 --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionWorkspaceMessageHandler.cs @@ -0,0 +1,35 @@ +// 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.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Extensions; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Extensions; + +[ExportCSharpVisualBasicStatelessLspService(typeof(ExtensionWorkspaceMessageHandler)), Shared] +[Method(MethodName)] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class ExtensionWorkspaceMessageHandler() + : AbstractExtensionHandler, ILspServiceRequestHandler +{ + private const string MethodName = "roslyn/extensionWorkspaceMessage"; + + public async Task HandleRequestAsync(ExtensionWorkspaceMessageParams request, RequestContext context, CancellationToken cancellationToken) + { + Contract.ThrowIfNull(context.Solution); + + var solution = context.Solution; + + var service = solution.Services.GetRequiredService(); + var response = await service.HandleExtensionWorkspaceMessageAsync( + solution, request.MessageName, request.Message, cancellationToken).ConfigureAwait(false); + + return new ExtensionMessageResponse(response); + } +} diff --git a/src/LanguageServer/Protocol/Handler/Extensions/ExtensionWorkspaceMessageParams.cs b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionWorkspaceMessageParams.cs new file mode 100644 index 0000000000000..73f8d9665e09c --- /dev/null +++ b/src/LanguageServer/Protocol/Handler/Extensions/ExtensionWorkspaceMessageParams.cs @@ -0,0 +1,17 @@ +// 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.Text.Json.Serialization; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Extensions; + +/// +/// Parameters for the roslyn/extensionWorkspaceMessage request. +/// +/// Name of the extension message to be invoked. +/// Json message to be passed to an extension message handler. +internal readonly record struct ExtensionWorkspaceMessageParams( + [property: JsonPropertyName("messageName")] string MessageName, + [property: JsonPropertyName("message")] string Message); diff --git a/src/LanguageServer/Protocol/Handler/ServerLifetime/LspServiceLifeCycleManager.cs b/src/LanguageServer/Protocol/Handler/ServerLifetime/LspServiceLifeCycleManager.cs index ac54a6195d60d..94ca5ca32f332 100644 --- a/src/LanguageServer/Protocol/Handler/ServerLifetime/LspServiceLifeCycleManager.cs +++ b/src/LanguageServer/Protocol/Handler/ServerLifetime/LspServiceLifeCycleManager.cs @@ -3,8 +3,12 @@ // See the LICENSE file in the project root for more information. using System; +using System.Composition; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Extensions; +using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CommonLanguageServerProtocol.Framework; using Roslyn.LanguageServer.Protocol; using StreamJsonRpc; @@ -14,14 +18,38 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.ServerLifetime; internal sealed class LspServiceLifeCycleManager : ILifeCycleManager, ILspService { private readonly IClientLanguageServerManager _clientLanguageServerManager; + private readonly LspWorkspaceRegistrationService _lspWorkspaceRegistrationService; - public LspServiceLifeCycleManager(IClientLanguageServerManager clientLanguageServerManager) + [ExportCSharpVisualBasicLspServiceFactory(typeof(LspServiceLifeCycleManager)), Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + internal class LspLifeCycleManagerFactory(LspWorkspaceRegistrationService lspWorkspaceRegistrationService) : ILspServiceFactory + { + public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) + { + var clientLanguageServerManager = lspServices.GetRequiredService(); + return new LspServiceLifeCycleManager(clientLanguageServerManager, lspWorkspaceRegistrationService); + } + } + + private LspServiceLifeCycleManager(IClientLanguageServerManager clientLanguageServerManager, LspWorkspaceRegistrationService lspWorkspaceRegistrationService) { _clientLanguageServerManager = clientLanguageServerManager; + _lspWorkspaceRegistrationService = lspWorkspaceRegistrationService; } public async Task ShutdownAsync(string message = "Shutting down") { + // Shutting down is not cancellable. + var cancellationToken = CancellationToken.None; + + var hostWorkspace = _lspWorkspaceRegistrationService.GetAllRegistrations().SingleOrDefault(w => w.Kind == WorkspaceKind.Host); + if (hostWorkspace is not null) + { + var service = hostWorkspace.Services.GetRequiredService(); + await service.ResetAsync(cancellationToken).ConfigureAwait(false); + } + try { var messageParams = new LogMessageParams() @@ -29,7 +57,7 @@ public async Task ShutdownAsync(string message = "Shutting down") MessageType = MessageType.Info, Message = message }; - await _clientLanguageServerManager.SendNotificationAsync("window/logMessage", messageParams, CancellationToken.None).ConfigureAwait(false); + await _clientLanguageServerManager.SendNotificationAsync("window/logMessage", messageParams, cancellationToken).ConfigureAwait(false); } catch (Exception ex) when (ex is ObjectDisposedException or ConnectionLostException) { diff --git a/src/LanguageServer/Protocol/RoslynLanguageServer.cs b/src/LanguageServer/Protocol/RoslynLanguageServer.cs index 48e45911fb624..8d11be7d6c2f9 100644 --- a/src/LanguageServer/Protocol/RoslynLanguageServer.cs +++ b/src/LanguageServer/Protocol/RoslynLanguageServer.cs @@ -77,17 +77,16 @@ private FrozenDictionary> GetBaseServices( var baseServiceMap = new Dictionary(); var clientLanguageServerManager = new ClientLanguageServerManager(jsonRpc); - var lifeCycleManager = new LspServiceLifeCycleManager(clientLanguageServerManager); AddService(clientLanguageServerManager); AddService(logger); AddService(logger); AddService(capabilitiesProvider); - AddService(lifeCycleManager); + AddLazyService(lspServices => lspServices.GetRequiredService()); AddService(new ServerInfoProvider(serverKind, supportedLanguages)); - AddLazyService>((lspServices) => new RequestContextFactory(lspServices)); - AddLazyService((lspServices) => new TelemetryService(lspServices)); - AddLazyService((_) => HandlerProvider); + AddLazyService>(lspServices => new RequestContextFactory(lspServices)); + AddLazyService(lspServices => new TelemetryService(lspServices)); + AddLazyService(_ => HandlerProvider); AddService(new InitializeManager()); AddService(new InitializeHandler()); AddService(new InitializedHandler()); diff --git a/src/NuGet/VS.ExternalAPIs.Roslyn.Package/VS.ExternalAPIs.Roslyn.Package.csproj b/src/NuGet/VS.ExternalAPIs.Roslyn.Package/VS.ExternalAPIs.Roslyn.Package.csproj index ad6db8f1feb82..6d9141ef72539 100644 --- a/src/NuGet/VS.ExternalAPIs.Roslyn.Package/VS.ExternalAPIs.Roslyn.Package.csproj +++ b/src/NuGet/VS.ExternalAPIs.Roslyn.Package/VS.ExternalAPIs.Roslyn.Package.csproj @@ -26,51 +26,51 @@ - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - - + + + - + - + - - - - - - - - - - - - - + - <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.CSharp.EditorFeatures\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.CSharp.EditorFeatures.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.CSharp.Features\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.CSharp.Features.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.CSharp.Scripting\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.CSharp.Scripting.dll" TargetDir="" /> @@ -80,10 +80,11 @@ <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.EditorFeatures.Wpf\$(Configuration)\net472\Microsoft.CodeAnalysis.EditorFeatures.Wpf.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.EditorFeatures\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.EditorFeatures.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.ExternalAccess.Apex\$(Configuration)\net472\Microsoft.CodeAnalysis.ExternalAccess.Apex.dll" TargetDir="" /> + <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.ExternalAccess.AspNetCore\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.ExternalAccess.Copilot\$(Configuration)\net472\Microsoft.CodeAnalysis.ExternalAccess.Copilot.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.ExternalAccess.Debugger\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.ExternalAccess.Debugger.dll" TargetDir="" /> - <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.ExternalAccess.AspNetCore\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.ExternalAccess.EditorConfigGenerator\$(Configuration)\net472\Microsoft.CodeAnalysis.ExternalAccess.EditorConfigGenerator.dll" TargetDir="" /> + <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.ExternalAccess.Extensions\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.ExternalAccess.Extensions.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.ExternalAccess.FSharp\$(Configuration)\net472\Microsoft.CodeAnalysis.ExternalAccess.FSharp.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.ExternalAccess.Razor\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.ExternalAccess.Razor.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.Features\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.Features.dll" TargetDir="" /> @@ -96,10 +97,11 @@ <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.VisualBasic.Features\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.VisualBasic.Features.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.VisualBasic.Workspaces\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.VisualBasic.Workspaces.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.VisualBasic\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.VisualBasic.dll" TargetDir="" /> - <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.Workspaces.MSBuild\$(Configuration)\net472\Microsoft.CodeAnalysis.Workspaces.MSBuild.dll" TargetDir="" /> + <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.Workspaces.Desktop\$(Configuration)\net472\Microsoft.CodeAnalysis.Workspaces.Desktop.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost\$(Configuration)\net472\Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.exe" TargetDir="" /> + <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.Workspaces.MSBuild\$(Configuration)\net472\Microsoft.CodeAnalysis.Workspaces.MSBuild.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.Workspaces\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.Workspaces.dll" TargetDir="" /> - <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.Workspaces.Desktop\$(Configuration)\net472\Microsoft.CodeAnalysis.Workspaces.Desktop.dll" TargetDir="" /> + <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.VisualStudio.LanguageServices.CSharp\$(Configuration)\net472\Microsoft.VisualStudio.LanguageServices.CSharp.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.VisualStudio.LanguageServices.Implementation\$(Configuration)\net472\Microsoft.VisualStudio.LanguageServices.Implementation.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.VisualStudio.LanguageServices.LiveShare\$(Configuration)\net472\Microsoft.VisualStudio.LanguageServices.LiveShare.dll" TargetDir="" /> diff --git a/src/Tools/ExternalAccess/Extensions/External/DocumentLinePosition.cs b/src/Tools/ExternalAccess/Extensions/External/DocumentLinePosition.cs new file mode 100644 index 0000000000000..d4cb9f72cfa47 --- /dev/null +++ b/src/Tools/ExternalAccess/Extensions/External/DocumentLinePosition.cs @@ -0,0 +1,79 @@ +// 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.Text.Json.Serialization; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Extensions; + +/// +/// Immutable representation of a line number and position within a document. +/// +/// This type meant to be used exclusively by VisualStudio.Extensibility extensions. +public readonly struct DocumentLinePosition +{ + /// + /// Initializes a new instance of a with the given document and line position. + /// + /// The document. + /// The position within the document + /// When is . + /// When the document path is not available. + public DocumentLinePosition(Document document, LinePosition linePosition) + { + _ = document ?? throw new ArgumentNullException(nameof(document)); + + FilePath = document.FilePath ?? throw new InvalidOperationException("Missing document file path"); + Line = linePosition.Line; + Character = linePosition.Character; + } + + /// + /// Initializes a new instance of a with the given file path, line and character. + /// + /// The file path of the document. + /// The line number. + /// The character position within the line. + /// When is . + [JsonConstructor] + public DocumentLinePosition(string filePath, int line, int character) + { + FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); + Line = line; + Character = character; + } + + /// + /// Gets the file path of the document. + /// + [JsonPropertyName("filePath")] + public string FilePath { get; } + + /// + /// Gets the line number. The first line in a file is defined as line 0 (zero based line numbering). + /// + [JsonPropertyName("line")] + public int Line { get; } + + /// + /// Gets the character position within the line. + /// + [JsonPropertyName("character")] + public int Character { get; } + + /// + /// Converts this to a . + /// + /// The value. + public LinePosition ToLinePosition() + => new LinePosition(Line, Character); + + /// + /// Implicitly converts a to a . + /// + /// The value. + public static implicit operator LinePosition(DocumentLinePosition documentLinePosition) + => documentLinePosition.ToLinePosition(); +} diff --git a/src/Tools/ExternalAccess/Extensions/External/ExtensionMessageContext.cs b/src/Tools/ExternalAccess/Extensions/External/ExtensionMessageContext.cs new file mode 100644 index 0000000000000..ee18b9feb7153 --- /dev/null +++ b/src/Tools/ExternalAccess/Extensions/External/ExtensionMessageContext.cs @@ -0,0 +1,23 @@ +// 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; + +namespace Microsoft.CodeAnalysis.Extensions; + +/// +/// Represents the context of an extension message handler. +/// +public sealed class ExtensionMessageContext +{ + internal ExtensionMessageContext(Solution solution) + { + Solution = solution; + } + + /// + /// Gets the current solution state. + /// + public Solution Solution { get; } +} diff --git a/src/Tools/ExternalAccess/Extensions/External/IExtensionDocumentMessageHandler.cs b/src/Tools/ExternalAccess/Extensions/External/IExtensionDocumentMessageHandler.cs new file mode 100644 index 0000000000000..376f19ad06606 --- /dev/null +++ b/src/Tools/ExternalAccess/Extensions/External/IExtensionDocumentMessageHandler.cs @@ -0,0 +1,43 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.Extensions; + +/// +/// The base interface required to implement an extension message handler for document-specific messages. +/// +/// The type of the message received by the hander. must be +/// serializable using System.Text.Json. +/// The type of the response returned by the hander. must +/// be serializable using System.Text.Json. +/// +/// +/// Extension message handlers allow IDE extensions to send messages to the compiler and receive responses. This is +/// useful for scenarios where the IDE needs to query the compilation state or other information from the compiler. +/// +/// +/// The lifetime of a message handler object is tied to the solution: when the solution is closed, the assemblies that +/// contain the handler are unloaded. The state of any static variable will be lost. When a new solution is opened, a +/// new instance of the handler is created. Handlers that are defined in assemblies sharing the same folder are loaded +/// in the same assembly context and can share static state. +/// +/// +/// Multiple instances of a message handler may be created during the loading process. However, only one will actually +/// be used as the handler that receives messages for a particular solution. +/// +/// +public interface IExtensionDocumentMessageHandler +{ + /// + /// The method that receives the message and returns the response. + /// + /// The message sent by the IDE. + /// The context containing the current state of the solution. + /// The document object the message refers to. + /// The response to be returned to the IDE. + Task ExecuteAsync(TMessage message, ExtensionMessageContext context, Document document, CancellationToken cancellationToken); +} diff --git a/src/Tools/ExternalAccess/Extensions/External/IExtensionWorkspaceMessageHandler.cs b/src/Tools/ExternalAccess/Extensions/External/IExtensionWorkspaceMessageHandler.cs new file mode 100644 index 0000000000000..c33b9fd143a13 --- /dev/null +++ b/src/Tools/ExternalAccess/Extensions/External/IExtensionWorkspaceMessageHandler.cs @@ -0,0 +1,42 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.Extensions; + +/// +/// The base interface required to implement an extension message handler for non-document-specific messages. +/// +/// The type of the message received by the hander. must be +/// serializable using System.Text.Json. +/// The type of the response returned by the hander. must +/// be serializable using System.Text.Json. +/// +/// +/// Extension message handlers allow IDE extensions to send messages to the compiler and receive responses. This is +/// useful for scenarios where the IDE needs to query the compilation state or other information from the compiler. +/// +/// +/// The lifetime of a message handler object is tied to the solution: when the solution is closed, the assemblies that +/// contain the handler are unloaded. The state of any static variable will be lost. When a new solution is opened, a +/// new instance of the handler is created. Handlers that are defined in assemblies sharing the same folder are loaded +/// in the same assembly context and can share static state. +/// +/// +/// Multiple instances of a message handler may be created during the loading process. However, only one will actually +/// be used as the handler that receives messages for a particular solution. +/// +/// +public interface IExtensionWorkspaceMessageHandler +{ + /// + /// The method that receives the message and returns the response. + /// + /// The message sent by the IDE. + /// The context containing the current state of the solution. + /// The response to be returned to the IDE. + Task ExecuteAsync(TMessage message, ExtensionMessageContext context, CancellationToken cancellationToken); +} diff --git a/src/Tools/ExternalAccess/Extensions/Internal/ExtensionDocumentMessageHandlerWrapper.cs b/src/Tools/ExternalAccess/Extensions/Internal/ExtensionDocumentMessageHandlerWrapper.cs new file mode 100644 index 0000000000000..ea58e0a9d222f --- /dev/null +++ b/src/Tools/ExternalAccess/Extensions/Internal/ExtensionDocumentMessageHandlerWrapper.cs @@ -0,0 +1,18 @@ +// 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.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.Extensions; + +internal sealed class ExtensionDocumentMessageHandlerWrapper( + object handler, Type customMessageHandlerInterface, string extensionIdentifier) + : ExtensionHandlerWrapper(handler, customMessageHandlerInterface, extensionIdentifier) +{ + protected override Task ExecuteAsync(MethodInfo executeAsyncMethod, object handler, object? message, Document argument, CancellationToken cancellationToken) + => (Task)executeAsyncMethod.Invoke(handler, [message, new ExtensionMessageContext(argument.Project.Solution), argument, cancellationToken]); +} diff --git a/src/Tools/ExternalAccess/Extensions/Internal/ExtensionMessageHandlerFactory.cs b/src/Tools/ExternalAccess/Extensions/Internal/ExtensionMessageHandlerFactory.cs new file mode 100644 index 0000000000000..9031b975e04e6 --- /dev/null +++ b/src/Tools/ExternalAccess/Extensions/Internal/ExtensionMessageHandlerFactory.cs @@ -0,0 +1,75 @@ +// 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.Immutable; +using System.Composition; +using System.Reflection; +using System.Threading; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.Extensions; + +[Export(typeof(IExtensionMessageHandlerFactory)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class ExtensionMessageHandlerFactory() : IExtensionMessageHandlerFactory +{ + public ImmutableArray> CreateDocumentMessageHandlers( + Assembly assembly, string extensionIdentifier, CancellationToken cancellationToken) + => CreateWorkspaceHandlers( + assembly, + typeof(IExtensionDocumentMessageHandler<,>), + (handler, handlerInterface) => new ExtensionDocumentMessageHandlerWrapper(handler, handlerInterface, extensionIdentifier), + cancellationToken); + + public ImmutableArray> CreateWorkspaceMessageHandlers( + Assembly assembly, string extensionIdentifier, CancellationToken cancellationToken) + => CreateWorkspaceHandlers( + assembly, + typeof(IExtensionWorkspaceMessageHandler<,>), + (handler, handlerInterface) => new ExtensionWorkspaceMessageHandlerWrapper(handler, handlerInterface, extensionIdentifier), + cancellationToken); + + private static ImmutableArray> CreateWorkspaceHandlers( + Assembly assembly, + Type unboundInterfaceType, + Func> wrapperCreator, + CancellationToken cancellationToken) + { + var resultBuilder = ImmutableArray.CreateBuilder>(); + + foreach (var candidateType in assembly.GetTypes()) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (candidateType.IsAbstract || candidateType.IsGenericType) + continue; + + Type? boundInterfaceType = null; + foreach (var interfaceType in candidateType.GetInterfaces()) + { + if (interfaceType.IsGenericType && + !interfaceType.IsGenericTypeDefinition && + interfaceType.GetGenericTypeDefinition() == unboundInterfaceType) + { + if (boundInterfaceType is not null) + throw new InvalidOperationException($"Type {candidateType.FullName} implements interface {unboundInterfaceType.Name} more than once."); + + boundInterfaceType = interfaceType; + } + } + + if (boundInterfaceType == null) + continue; + + var handler = Activator.CreateInstance(candidateType) + ?? throw new InvalidOperationException($"Cannot create {candidateType.FullName}."); + + resultBuilder.Add(wrapperCreator(handler, boundInterfaceType)); + } + + return resultBuilder.ToImmutable(); + } +} diff --git a/src/Tools/ExternalAccess/Extensions/Internal/ExtensionMessageHandlerWrapper.cs b/src/Tools/ExternalAccess/Extensions/Internal/ExtensionMessageHandlerWrapper.cs new file mode 100644 index 0000000000000..e7d927fa189f7 --- /dev/null +++ b/src/Tools/ExternalAccess/Extensions/Internal/ExtensionMessageHandlerWrapper.cs @@ -0,0 +1,60 @@ +// 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.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.Extensions; + +internal abstract class ExtensionHandlerWrapper + : IExtensionMessageHandlerWrapper +{ + private readonly object _handler; + private readonly MethodInfo _executeAsyncMethod; + private readonly PropertyInfo _responseTaskResultProperty; + + protected ExtensionHandlerWrapper(object handler, Type customMessageHandlerInterface, string extensionIdentifier) + { + _handler = handler; + + Name = handler.GetType().FullName; + MessageType = customMessageHandlerInterface.GenericTypeArguments[0]; + ResponseType = customMessageHandlerInterface.GenericTypeArguments[1]; + ExtensionIdentifier = extensionIdentifier; + + _executeAsyncMethod = customMessageHandlerInterface.GetMethod(nameof(ExecuteAsync)); + _responseTaskResultProperty = typeof(Task<>).MakeGenericType(ResponseType).GetProperty(nameof(Task.Result)); + } + + public Type MessageType { get; } + + public Type ResponseType { get; } + + public string Name { get; } + + public string ExtensionIdentifier { get; } + + public async Task ExecuteAsync(object? message, TArgument argument, CancellationToken cancellationToken) + { + if ((message is null && MessageType.IsValueType) || (message is not null && !MessageType.IsAssignableFrom(message.GetType()))) + { + throw new InvalidOperationException($"The message type {message?.GetType().FullName ?? "null"} is not assignable to {MessageType.FullName}."); + } + + var responseTask = ExecuteAsync(_executeAsyncMethod, _handler, message, argument, cancellationToken); + await responseTask.ConfigureAwait(false); + var response = _responseTaskResultProperty.GetValue(responseTask); + + if ((response is null && ResponseType.IsValueType) || (response is not null && !ResponseType.IsAssignableFrom(response.GetType()))) + { + throw new InvalidOperationException($"The message type {response?.GetType().FullName ?? "null"} is not assignable to {ResponseType.FullName}."); + } + + return response; + } + + protected abstract Task ExecuteAsync(MethodInfo executeAsyncMethod, object handler, object? message, TArgument argument, CancellationToken cancellationToken); +} diff --git a/src/Tools/ExternalAccess/Extensions/Internal/ExtensionWorkspaceMessageHandlerWrapper.cs b/src/Tools/ExternalAccess/Extensions/Internal/ExtensionWorkspaceMessageHandlerWrapper.cs new file mode 100644 index 0000000000000..9255ae68776e1 --- /dev/null +++ b/src/Tools/ExternalAccess/Extensions/Internal/ExtensionWorkspaceMessageHandlerWrapper.cs @@ -0,0 +1,18 @@ +// 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.Reflection; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.Extensions; + +internal sealed class ExtensionWorkspaceMessageHandlerWrapper( + object handler, Type customMessageHandlerInterface, string extensionIdentifier) + : ExtensionHandlerWrapper(handler, customMessageHandlerInterface, extensionIdentifier) +{ + protected override Task ExecuteAsync(MethodInfo executeAsyncMethod, object handler, object? message, Solution argument, CancellationToken cancellationToken) + => (Task)executeAsyncMethod.Invoke(handler, [message, new ExtensionMessageContext(argument), cancellationToken]); +} diff --git a/src/Tools/ExternalAccess/Extensions/InternalAPI.Shipped.txt b/src/Tools/ExternalAccess/Extensions/InternalAPI.Shipped.txt new file mode 100644 index 0000000000000..7dc5c58110bfa --- /dev/null +++ b/src/Tools/ExternalAccess/Extensions/InternalAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Tools/ExternalAccess/Extensions/InternalAPI.Unshipped.txt b/src/Tools/ExternalAccess/Extensions/InternalAPI.Unshipped.txt new file mode 100644 index 0000000000000..d556753bb5665 --- /dev/null +++ b/src/Tools/ExternalAccess/Extensions/InternalAPI.Unshipped.txt @@ -0,0 +1,34 @@ +abstract Microsoft.CodeAnalysis.Extensions.ExtensionHandlerWrapper.ExecuteAsync(System.Reflection.MethodInfo! executeAsyncMethod, object! handler, object? message, TArgument argument, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.CodeAnalysis.CustomMessageHandler.CustomDocumentMessageHandlerWrapper +Microsoft.CodeAnalysis.CustomMessageHandler.CustomDocumentMessageHandlerWrapper.CustomDocumentMessageHandlerWrapper(object! handler, System.Type! iCustomMessageDocumentHandlerInterface) -> void +Microsoft.CodeAnalysis.CustomMessageHandler.CustomDocumentMessageHandlerWrapper.ExecuteAsync(object? message, Microsoft.CodeAnalysis.Document! document, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.CodeAnalysis.CustomMessageHandler.CustomDocumentMessageHandlerWrapper.MessageType.get -> System.Type! +Microsoft.CodeAnalysis.CustomMessageHandler.CustomDocumentMessageHandlerWrapper.Name.get -> string! +Microsoft.CodeAnalysis.CustomMessageHandler.CustomDocumentMessageHandlerWrapper.ResponseType.get -> System.Type! +Microsoft.CodeAnalysis.CustomMessageHandler.CustomMessageContext.CustomMessageContext(Microsoft.CodeAnalysis.Solution! solution) -> void +Microsoft.CodeAnalysis.CustomMessageHandler.CustomMessageHandlerFactory +Microsoft.CodeAnalysis.CustomMessageHandler.CustomMessageHandlerFactory.CreateDocumentMessageHandlers(System.Reflection.Assembly! assembly) -> System.Collections.Immutable.ImmutableArray +Microsoft.CodeAnalysis.CustomMessageHandler.CustomMessageHandlerFactory.CreateWorkspaceMessageHandlers(System.Reflection.Assembly! assembly) -> System.Collections.Immutable.ImmutableArray +Microsoft.CodeAnalysis.CustomMessageHandler.CustomMessageHandlerFactory.CustomMessageHandlerFactory() -> void +Microsoft.CodeAnalysis.CustomMessageHandler.CustomWorspaceMessageHandlerWrapper +Microsoft.CodeAnalysis.CustomMessageHandler.CustomWorspaceMessageHandlerWrapper.CustomWorspaceMessageHandlerWrapper(object! handler, System.Type! iCustomMessageHandlerInterface) -> void +Microsoft.CodeAnalysis.CustomMessageHandler.CustomWorspaceMessageHandlerWrapper.ExecuteAsync(object? message, Microsoft.CodeAnalysis.Solution! solution, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.CodeAnalysis.CustomMessageHandler.CustomWorspaceMessageHandlerWrapper.MessageType.get -> System.Type! +Microsoft.CodeAnalysis.CustomMessageHandler.CustomWorspaceMessageHandlerWrapper.Name.get -> string! +Microsoft.CodeAnalysis.CustomMessageHandler.CustomWorspaceMessageHandlerWrapper.ResponseType.get -> System.Type! +Microsoft.CodeAnalysis.Extensions.ExtensionDocumentMessageHandlerWrapper +Microsoft.CodeAnalysis.Extensions.ExtensionDocumentMessageHandlerWrapper.ExtensionDocumentMessageHandlerWrapper(object! handler, System.Type! customMessageHandlerInterface, string! extensionIdentifier) -> void +Microsoft.CodeAnalysis.Extensions.ExtensionHandlerWrapper +Microsoft.CodeAnalysis.Extensions.ExtensionHandlerWrapper.ExecuteAsync(object? message, TArgument argument, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.CodeAnalysis.Extensions.ExtensionHandlerWrapper.ExtensionHandlerWrapper(object! handler, System.Type! customMessageHandlerInterface, string! extensionIdentifier) -> void +Microsoft.CodeAnalysis.Extensions.ExtensionHandlerWrapper.ExtensionIdentifier.get -> string! +Microsoft.CodeAnalysis.Extensions.ExtensionHandlerWrapper.MessageType.get -> System.Type! +Microsoft.CodeAnalysis.Extensions.ExtensionHandlerWrapper.Name.get -> string! +Microsoft.CodeAnalysis.Extensions.ExtensionHandlerWrapper.ResponseType.get -> System.Type! +Microsoft.CodeAnalysis.Extensions.ExtensionMessageContext.ExtensionMessageContext(Microsoft.CodeAnalysis.Solution! solution) -> void +Microsoft.CodeAnalysis.Extensions.ExtensionMessageHandlerFactory +Microsoft.CodeAnalysis.Extensions.ExtensionMessageHandlerFactory.CreateDocumentMessageHandlers(System.Reflection.Assembly! assembly, string! extensionIdentifier, System.Threading.CancellationToken cancellationToken) -> System.Collections.Immutable.ImmutableArray!> +Microsoft.CodeAnalysis.Extensions.ExtensionMessageHandlerFactory.CreateWorkspaceMessageHandlers(System.Reflection.Assembly! assembly, string! extensionIdentifier, System.Threading.CancellationToken cancellationToken) -> System.Collections.Immutable.ImmutableArray!> +Microsoft.CodeAnalysis.Extensions.ExtensionMessageHandlerFactory.ExtensionMessageHandlerFactory() -> void +Microsoft.CodeAnalysis.Extensions.ExtensionWorkspaceMessageHandlerWrapper +Microsoft.CodeAnalysis.Extensions.ExtensionWorkspaceMessageHandlerWrapper.ExtensionWorkspaceMessageHandlerWrapper(object! handler, System.Type! customMessageHandlerInterface, string! extensionIdentifier) -> void \ No newline at end of file diff --git a/src/Tools/ExternalAccess/Extensions/Microsoft.CodeAnalysis.ExternalAccess.Extensions.csproj b/src/Tools/ExternalAccess/Extensions/Microsoft.CodeAnalysis.ExternalAccess.Extensions.csproj new file mode 100644 index 0000000000000..96439f3e9b9b6 --- /dev/null +++ b/src/Tools/ExternalAccess/Extensions/Microsoft.CodeAnalysis.ExternalAccess.Extensions.csproj @@ -0,0 +1,33 @@ + + + + + Library + Microsoft.CodeAnalysis.ExternalAccess.Extensions + netstandard2.0 + + + true + Microsoft.CodeAnalysis.ExternalAccess.Extensions + + A supporting package for VisualStudio.Extensibility extensions + + + + + + + + + + + + + + + + + + + + diff --git a/src/Tools/ExternalAccess/Extensions/PublicAPI.Shipped.txt b/src/Tools/ExternalAccess/Extensions/PublicAPI.Shipped.txt new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/Tools/ExternalAccess/Extensions/PublicAPI.Unshipped.txt b/src/Tools/ExternalAccess/Extensions/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000000..d80262503cf99 --- /dev/null +++ b/src/Tools/ExternalAccess/Extensions/PublicAPI.Unshipped.txt @@ -0,0 +1,15 @@ +Microsoft.CodeAnalysis.Extensions.DocumentLinePosition +Microsoft.CodeAnalysis.Extensions.DocumentLinePosition.Character.get -> int +Microsoft.CodeAnalysis.Extensions.DocumentLinePosition.DocumentLinePosition() -> void +Microsoft.CodeAnalysis.Extensions.DocumentLinePosition.DocumentLinePosition(Microsoft.CodeAnalysis.Document document, Microsoft.CodeAnalysis.Text.LinePosition linePosition) -> void +Microsoft.CodeAnalysis.Extensions.DocumentLinePosition.DocumentLinePosition(string filePath, int line, int character) -> void +Microsoft.CodeAnalysis.Extensions.DocumentLinePosition.FilePath.get -> string +Microsoft.CodeAnalysis.Extensions.DocumentLinePosition.Line.get -> int +Microsoft.CodeAnalysis.Extensions.DocumentLinePosition.ToLinePosition() -> Microsoft.CodeAnalysis.Text.LinePosition +Microsoft.CodeAnalysis.Extensions.ExtensionMessageContext +Microsoft.CodeAnalysis.Extensions.ExtensionMessageContext.Solution.get -> Microsoft.CodeAnalysis.Solution +Microsoft.CodeAnalysis.Extensions.IExtensionDocumentMessageHandler +Microsoft.CodeAnalysis.Extensions.IExtensionDocumentMessageHandler.ExecuteAsync(TMessage message, Microsoft.CodeAnalysis.Extensions.ExtensionMessageContext context, Microsoft.CodeAnalysis.Document document, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +Microsoft.CodeAnalysis.Extensions.IExtensionWorkspaceMessageHandler +Microsoft.CodeAnalysis.Extensions.IExtensionWorkspaceMessageHandler.ExecuteAsync(TMessage message, Microsoft.CodeAnalysis.Extensions.ExtensionMessageContext context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task +static Microsoft.CodeAnalysis.Extensions.DocumentLinePosition.implicit operator Microsoft.CodeAnalysis.Text.LinePosition(Microsoft.CodeAnalysis.Extensions.DocumentLinePosition documentLinePosition) -> Microsoft.CodeAnalysis.Text.LinePosition \ No newline at end of file diff --git a/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj b/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj index c2be591a52c53..5dc19530993a7 100644 --- a/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj +++ b/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj @@ -128,6 +128,7 @@ + diff --git a/src/Workspaces/Remote/Core/RemoteWorkspacesResources.resx b/src/Workspaces/Remote/Core/RemoteWorkspacesResources.resx index b95710fff4a67..d7a0780f01053 100644 --- a/src/Workspaces/Remote/Core/RemoteWorkspacesResources.resx +++ b/src/Workspaces/Remote/Core/RemoteWorkspacesResources.resx @@ -234,4 +234,7 @@ Related documents + + Extension message handler + \ No newline at end of file diff --git a/src/Workspaces/Remote/Core/ServiceDescriptors.cs b/src/Workspaces/Remote/Core/ServiceDescriptors.cs index 0d60f7395ea2f..376fe434d2b9f 100644 --- a/src/Workspaces/Remote/Core/ServiceDescriptors.cs +++ b/src/Workspaces/Remote/Core/ServiceDescriptors.cs @@ -17,6 +17,7 @@ using Microsoft.CodeAnalysis.DocumentHighlighting; using Microsoft.CodeAnalysis.EditAndContinue; using Microsoft.CodeAnalysis.EncapsulateField; +using Microsoft.CodeAnalysis.Extensions; using Microsoft.CodeAnalysis.ExternalAccess.UnitTesting; using Microsoft.CodeAnalysis.FindSymbols; using Microsoft.CodeAnalysis.FindUsages; @@ -63,6 +64,7 @@ internal sealed class ServiceDescriptors (typeof(IRemoteDocumentHighlightsService), null), (typeof(IRemoteEditAndContinueService), typeof(IRemoteEditAndContinueService.ICallback)), (typeof(IRemoteEncapsulateFieldService), null), + (typeof(IRemoteExtensionMessageHandlerService), null), (typeof(IRemoteExtensionMethodImportCompletionService), null), (typeof(IRemoteFindUsagesService), typeof(IRemoteFindUsagesService.ICallback)), (typeof(IRemoteFullyQualifyService), null), diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.cs.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.cs.xlf index d62020c15d81f..c13b28a824e53 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.cs.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.cs.xlf @@ -57,6 +57,11 @@ Zabalit refaktoring pole + + Extension message handler + Extension message handler + + Extension method import completion Dokončení importu metody rozšíření diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.de.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.de.xlf index 8bb309e1d00d5..7d6b8a5105019 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.de.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.de.xlf @@ -57,6 +57,11 @@ Feldrefactoring kapseln + + Extension message handler + Extension message handler + + Extension method import completion Erweiterungsmethodenimporte vervollständigen diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.es.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.es.xlf index 9d9bce54ca77b..2d89bd4c37442 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.es.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.es.xlf @@ -57,6 +57,11 @@ Encapsular la refactorización de campo + + Extension message handler + Extension message handler + + Extension method import completion Finalización de la importación del método de extensión diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.fr.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.fr.xlf index 4a39bd4627f35..8909739999915 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.fr.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.fr.xlf @@ -57,6 +57,11 @@ Refactorisation de l'encapsulation de champ + + Extension message handler + Extension message handler + + Extension method import completion Exécution de l'importation de méthode d'extension diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.it.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.it.xlf index b90d57e88f718..66074ff323f59 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.it.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.it.xlf @@ -57,6 +57,11 @@ Refactoring di Incapsula campo + + Extension message handler + Extension message handler + + Extension method import completion Completamento dell'importazione del metodo di estensione diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ja.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ja.xlf index 6578f074c30e8..9bd73d55d6e0a 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ja.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ja.xlf @@ -57,6 +57,11 @@ フィールドのカプセル化のリファクタリング + + Extension message handler + Extension message handler + + Extension method import completion 拡張メソッドのインポート完了 diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ko.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ko.xlf index 02d43a4f1dd04..df2ec3c901330 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ko.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ko.xlf @@ -57,6 +57,11 @@ 필드 리팩터링 캡슐화 + + Extension message handler + Extension message handler + + Extension method import completion 확장 메서드 가져오기 완료 diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.pl.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.pl.xlf index ce3bfa18af5f1..d034f1c2c3b99 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.pl.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.pl.xlf @@ -57,6 +57,11 @@ Refaktoryzacja hermetyzacji pola + + Extension message handler + Extension message handler + + Extension method import completion Uzupełnianie importu metody rozszerzenia diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.pt-BR.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.pt-BR.xlf index dc123a1d667aa..0371b6a72d63e 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.pt-BR.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.pt-BR.xlf @@ -57,6 +57,11 @@ Encapsular a refatoração de campo + + Extension message handler + Extension message handler + + Extension method import completion Conclusão da importação do método de extensão diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ru.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ru.xlf index 9aefb92600d36..5fe0ce251989c 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ru.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.ru.xlf @@ -57,6 +57,11 @@ Рефакторинг инкапсуляции поля + + Extension message handler + Extension message handler + + Extension method import completion Завершение импорта метода расширения diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.tr.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.tr.xlf index ce2c1d33eeb3d..c162e57f0d35e 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.tr.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.tr.xlf @@ -57,6 +57,11 @@ Alanı kapsülleme yeniden düzenlemesi + + Extension message handler + Extension message handler + + Extension method import completion Genişletme yöntemini içeri aktarma işlemi tamamlandı diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.zh-Hans.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.zh-Hans.xlf index 444eed20fcf2e..9d1e91d5f58aa 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.zh-Hans.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.zh-Hans.xlf @@ -57,6 +57,11 @@ 封装字段重构 + + Extension message handler + Extension message handler + + Extension method import completion 扩展方法导入完成 diff --git a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.zh-Hant.xlf b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.zh-Hant.xlf index 873caeb08c9f8..b155db43b50a5 100644 --- a/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.zh-Hant.xlf +++ b/src/Workspaces/Remote/Core/xlf/RemoteWorkspacesResources.zh-Hant.xlf @@ -57,6 +57,11 @@ 封裝欄位重構 + + Extension message handler + Extension message handler + + Extension method import completion 擴充方法匯入完成 diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs index 9710a71d7d5cf..274e260b057f2 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspaceManager.cs @@ -7,6 +7,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Extensions; using Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.Internal.EmbeddedLanguages; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Host.Mef; @@ -27,7 +28,8 @@ internal class RemoteWorkspaceManager .Add(typeof(AspNetCoreEmbeddedLanguageClassifier).Assembly) .Add(typeof(BrokeredServiceBase).Assembly) .Add(typeof(IRazorLanguageServerTarget).Assembly) - .Add(typeof(RemoteWorkspacesResources).Assembly); + .Add(typeof(RemoteWorkspacesResources).Assembly) + .Add(typeof(IExtensionWorkspaceMessageHandler<,>).Assembly); /// /// Default workspace manager used by the product. Tests may specify a custom + diff --git a/src/Workspaces/Remote/ServiceHub/Services/ExtensionMessageHandler/RemoteExtensionMessageHandlerService.cs b/src/Workspaces/Remote/ServiceHub/Services/ExtensionMessageHandler/RemoteExtensionMessageHandlerService.cs new file mode 100644 index 0000000000000..ff2804043c193 --- /dev/null +++ b/src/Workspaces/Remote/ServiceHub/Services/ExtensionMessageHandler/RemoteExtensionMessageHandlerService.cs @@ -0,0 +1,62 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Extensions; +using Microsoft.CodeAnalysis.Shared.Extensions; + +namespace Microsoft.CodeAnalysis.Remote; + +internal sealed partial class RemoteExtensionMessageHandlerService( + in BrokeredServiceBase.ServiceConstructionArguments arguments) + : BrokeredServiceBase(arguments), IRemoteExtensionMessageHandlerService +{ + internal sealed class Factory : FactoryBase + { + protected override IRemoteExtensionMessageHandlerService CreateService(in ServiceConstructionArguments arguments) + => new RemoteExtensionMessageHandlerService(arguments); + } + + private IExtensionMessageHandlerService GetExtensionService() + => this.GetWorkspaceServices().GetRequiredService(); + + public ValueTask RegisterExtensionAsync(string assemblyFilePath, CancellationToken cancellationToken) + => RunServiceAsync( + cancellationToken => GetExtensionService().RegisterExtensionAsync(assemblyFilePath, cancellationToken), + cancellationToken); + + public ValueTask UnregisterExtensionAsync(string assemblyFilePath, CancellationToken cancellationToken) + => RunServiceAsync( + cancellationToken => GetExtensionService().UnregisterExtensionAsync(assemblyFilePath, cancellationToken), + cancellationToken); + + public ValueTask ResetAsync(CancellationToken cancellationToken) + => RunServiceAsync( + cancellationToken => GetExtensionService().ResetAsync(cancellationToken), + cancellationToken); + + public ValueTask GetExtensionMessageNamesAsync(string assemblyFilePath, CancellationToken cancellationToken) + => RunServiceAsync( + cancellationToken => GetExtensionService().GetExtensionMessageNamesAsync(assemblyFilePath, cancellationToken), + cancellationToken); + + public ValueTask HandleExtensionDocumentMessageAsync( + Checksum solutionChecksum, string messageName, string jsonMessage, DocumentId documentId, CancellationToken cancellationToken) + => RunServiceAsync( + solutionChecksum, + async solution => + { + var document = await solution.GetRequiredDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); + return await GetExtensionService().HandleExtensionDocumentMessageAsync(document, messageName, jsonMessage, cancellationToken).ConfigureAwait(false); + }, + cancellationToken); + + public ValueTask HandleExtensionWorkspaceMessageAsync( + Checksum solutionChecksum, string messageName, string jsonMessage, CancellationToken cancellationToken) + => RunServiceAsync( + solutionChecksum, + solution => GetExtensionService().HandleExtensionWorkspaceMessageAsync(solution, messageName, jsonMessage, cancellationToken), + cancellationToken); +}