diff --git a/Roslyn.sln b/Roslyn.sln index e68230672e34d..290580925c9e5 100644 --- a/Roslyn.sln +++ b/Roslyn.sln @@ -523,10 +523,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.Exte EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.ExternalAccess.RazorCompiler.UnitTests", "src\Tools\ExternalAccess\RazorCompilerTest\Microsoft.CodeAnalysis.ExternalAccess.RazorCompiler.UnitTests.csproj", "{828FD0DB-9927-42AC-B6C2-D1514965D6C3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.LanguageServer", "src\Features\LanguageServer\Microsoft.CodeAnalysis.LanguageServer\Microsoft.CodeAnalysis.LanguageServer.csproj", "{2A3C94F7-5B5E-4CDC-B645-672815E61DEB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.LanguageServer.UnitTests", "src\Features\LanguageServer\Microsoft.CodeAnalysis.LanguageServer.UnitTests\Microsoft.CodeAnalysis.LanguageServer.UnitTests.csproj", "{9A90AA02-4275-40ED-B1F1-731AF17E675C}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Net.Compilers.Toolset.Framework.Package", "src\NuGet\Microsoft.Net.Compilers.Toolset\Framework\Microsoft.Net.Compilers.Toolset.Framework.Package.csproj", "{521ADC3E-CC15-414B-9356-D87C5BCF3A24}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LanguageServer", "LanguageServer", "{D449D505-CC6A-4E0B-AF1B-976E2D0AE67A}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.LanguageServices.DevKit", "src\VisualStudio\DevKit\Impl\Microsoft.VisualStudio.LanguageServices.DevKit.csproj", "{9B7AC5C2-293D-438D-B9A2-1EDDC2C6BF00}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.CSharp.Features.UnitTests", "src\Features\CSharpTest\Microsoft.CodeAnalysis.CSharp.Features.UnitTests.csproj", "{E645B517-5766-46FB-AA4A-D4D30C9E3BE6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.Features.UnitTests", "src\Features\Test\Microsoft.CodeAnalysis.Features.UnitTests.csproj", "{9296F799-5DE4-4E12-A68E-AAC39B0EB90A}" @@ -1295,10 +1301,22 @@ Global {828FD0DB-9927-42AC-B6C2-D1514965D6C3}.Debug|Any CPU.Build.0 = Debug|Any CPU {828FD0DB-9927-42AC-B6C2-D1514965D6C3}.Release|Any CPU.ActiveCfg = Release|Any CPU {828FD0DB-9927-42AC-B6C2-D1514965D6C3}.Release|Any CPU.Build.0 = Release|Any CPU + {2A3C94F7-5B5E-4CDC-B645-672815E61DEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A3C94F7-5B5E-4CDC-B645-672815E61DEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A3C94F7-5B5E-4CDC-B645-672815E61DEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A3C94F7-5B5E-4CDC-B645-672815E61DEB}.Release|Any CPU.Build.0 = Release|Any CPU + {9A90AA02-4275-40ED-B1F1-731AF17E675C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A90AA02-4275-40ED-B1F1-731AF17E675C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A90AA02-4275-40ED-B1F1-731AF17E675C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A90AA02-4275-40ED-B1F1-731AF17E675C}.Release|Any CPU.Build.0 = Release|Any CPU {521ADC3E-CC15-414B-9356-D87C5BCF3A24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {521ADC3E-CC15-414B-9356-D87C5BCF3A24}.Debug|Any CPU.Build.0 = Debug|Any CPU {521ADC3E-CC15-414B-9356-D87C5BCF3A24}.Release|Any CPU.ActiveCfg = Release|Any CPU {521ADC3E-CC15-414B-9356-D87C5BCF3A24}.Release|Any CPU.Build.0 = Release|Any CPU + {9B7AC5C2-293D-438D-B9A2-1EDDC2C6BF00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B7AC5C2-293D-438D-B9A2-1EDDC2C6BF00}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B7AC5C2-293D-438D-B9A2-1EDDC2C6BF00}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B7AC5C2-293D-438D-B9A2-1EDDC2C6BF00}.Release|Any CPU.Build.0 = Release|Any CPU {E645B517-5766-46FB-AA4A-D4D30C9E3BE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E645B517-5766-46FB-AA4A-D4D30C9E3BE6}.Debug|Any CPU.Build.0 = Debug|Any CPU {E645B517-5766-46FB-AA4A-D4D30C9E3BE6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1565,7 +1583,10 @@ Global {8BC50AFF-1EBF-4E9A-AEBB-04F387AA800F} = {FD0FAF5F-1DED-485C-99FA-84B97F3A8EEC} {E5E0BF73-95F7-4BC3-8443-2336C4FF4297} = {8977A560-45C2-4EC2-A849-97335B382C74} {828FD0DB-9927-42AC-B6C2-D1514965D6C3} = {8977A560-45C2-4EC2-A849-97335B382C74} + {2A3C94F7-5B5E-4CDC-B645-672815E61DEB} = {D449D505-CC6A-4E0B-AF1B-976E2D0AE67A} + {9A90AA02-4275-40ED-B1F1-731AF17E675C} = {D449D505-CC6A-4E0B-AF1B-976E2D0AE67A} {521ADC3E-CC15-414B-9356-D87C5BCF3A24} = {C52D8057-43AF-40E6-A01B-6CDBB7301985} + {9B7AC5C2-293D-438D-B9A2-1EDDC2C6BF00} = {8DBA5174-B0AA-4561-82B1-A46607697753} {E645B517-5766-46FB-AA4A-D4D30C9E3BE6} = {3E5FE3DB-45F7-4D83-9097-8F05D3B3AEC6} {9296F799-5DE4-4E12-A68E-AAC39B0EB90A} = {3E5FE3DB-45F7-4D83-9097-8F05D3B3AEC6} {57B7C0AA-E14A-41F6-AD06-FB3937F66FC2} = {3E5FE3DB-45F7-4D83-9097-8F05D3B3AEC6} @@ -1624,6 +1645,8 @@ Global src\Compilers\Server\VBCSCompiler\VBCSCompilerCommandLine.projitems*{9508f118-f62e-4c16-a6f4-7c3b56e166ad}*SharedItemsImports = 5 src\Compilers\VisualBasic\vbc\VbcCommandLine.projitems*{975cd834-45f4-4ea0-a395-cb60dbd0e214}*SharedItemsImports = 5 src\Workspaces\SharedUtilitiesAndExtensions\Workspace\Core\WorkspaceExtensions.projitems*{99f594b1-3916-471d-a761-a6731fc50e9a}*SharedItemsImports = 13 + src\Dependencies\Collections\Microsoft.CodeAnalysis.Collections.projitems*{9b7ac5c2-293d-438d-b9a2-1eddc2c6bf00}*SharedItemsImports = 5 + src\Dependencies\PooledObjects\Microsoft.CodeAnalysis.PooledObjects.projitems*{9b7ac5c2-293d-438d-b9a2-1eddc2c6bf00}*SharedItemsImports = 5 src\Analyzers\VisualBasic\CodeFixes\VisualBasicCodeFixes.projitems*{9f9ccc78-7487-4127-9d46-db23e501f001}*SharedItemsImports = 13 src\Analyzers\CSharp\CodeFixes\CSharpCodeFixes.projitems*{a07abcf5-bc43-4ee9-8fd8-b2d77fd54d73}*SharedItemsImports = 5 src\Workspaces\SharedUtilitiesAndExtensions\Workspace\CSharp\CSharpWorkspaceExtensions.projitems*{a07abcf5-bc43-4ee9-8fd8-b2d77fd54d73}*SharedItemsImports = 5 diff --git a/azure-pipelines-official.yml b/azure-pipelines-official.yml index d4c303b6fefaa..849d294e63e4e 100644 --- a/azure-pipelines-official.yml +++ b/azure-pipelines-official.yml @@ -5,6 +5,7 @@ trigger: - main-vs-deps - release/dev16.*-vs-deps - release/dev17.* + - features/lsp_tools_host exclude: - release/dev17.0 pr: none @@ -130,6 +131,12 @@ stages: - powershell: Write-Host "##vso[task.setvariable variable=VisualStudio.DropName]Products/$(System.TeamProject)/$(Build.Repository.Name)/$(SourceBranchName)/$(Build.BuildNumber)" displayName: Setting VisualStudio.DropName variable + - task: NodeTool@0 + inputs: + versionSpec: '16.x' + displayName: 'Install Node.js' + + - task: NuGetToolInstaller@0 inputs: versionSpec: '4.9.2' @@ -303,6 +310,32 @@ stages: ArtifactName: 'PackageArtifacts' condition: succeeded() + # Publish our language server executables as an artifact. + - task: NuGetCommand@2 + displayName: Publish Language Server Executables + inputs: + command: push + packagesToPush: '$(Build.SourcesDirectory)\artifacts\LanguageServer\*.nupkg' + allowPackageConflicts: false + nuGetFeedType: external + publishFeedCredentials: 'DevDiv - VS package feed' + condition: succeeded() + + # Publish language server package + - powershell: Write-Host "##vso[task.setvariable variable=NPMFileName]$((ls -file $(Build.SourcesDirectory)\artifacts\packages\Release\NPM\ | select -First 1).FullName)" + displayName: Setting NPM Package Variable + + # Authenticates the .npmrc file for publishing to the internal AzDO feed. + # See: https://learn.microsoft.com/azure/devops/pipelines/tasks/package/npm-authenticate?view=azure-devops + - task: npmAuthenticate@0 + displayName: Authenticate NPM Feed + inputs: + workingFile: $(Build.SourcesDirectory)/src/VisualStudio/DevKit/Impl/.npmrc + customEndpoint: devdiv-vs-green-npm-package-feed + + - script: npm publish --userconfig $(Build.SourcesDirectory)\src\VisualStudio\DevKit\Impl\.npmrc $(NPMFileName) + displayName: Publish Language Server NPM Package + # Publish Asset Manifests for Build Asset Registry job - task: PublishBuildArtifacts@1 displayName: Publish Asset Manifests diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e4f0fff4f588b..8282e102b2294 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -29,6 +29,61 @@ pr: - CONTRIBUTING.md - README.md +variables: + # This value is conditionally overriden by the variable group DotNet-HelixApi-Access. + # ADO only lets us conditionally include a variable group on individual stages. + - name: HelixApiAccessToken + value: '' + + # Set pool / queue name variables depending on which instance we're running in. + - name: PoolName + ${{ if eq(variables['System.TeamProject'], 'public') }}: + value: NetCore-Public + ${{ else }}: + value: NetCore1ESPool-Internal + + - name: Vs2022PreviewQueueName + ${{ if eq(variables['System.TeamProject'], 'public') }}: + value: windows.vs2022preview.amd64.open + ${{ else }}: + value: windows.vs2022preview.amd64 + + - name: Vs2022QueueName + ${{ if eq(variables['System.TeamProject'], 'public') }}: + value: windows.vs2022.amd64.open + ${{ else }}: + value: windows.vs2022.amd64 + + - name: UbuntuQueueName + ${{ if eq(variables['System.TeamProject'], 'public') }}: + value: Build.Ubuntu.1804.Amd64.Open + ${{ else }}: + value: Build.Ubuntu.1804.Amd64 + + - name: HelixWindowsQueueName + ${{ if eq(variables['System.TeamProject'], 'public') }}: + value: Windows.10.Amd64.Open + ${{ else }}: + value: Windows.10.Amd64 + + - name: HelixWindowsSpanishQueueName + ${{ if eq(variables['System.TeamProject'], 'public') }}: + value: Windows.10.Amd64.Server2022.ES.Open + ${{ else }}: + value: Windows.10.Amd64.Server2022.ES + + - name: HelixUbuntuQueueName + ${{ if eq(variables['System.TeamProject'], 'public') }}: + value: Ubuntu.1804.Amd64.Open + ${{ else }}: + value: Ubuntu.1804.Amd64 + + - name: HelixMacOsQueueName + ${{ if eq(variables['System.TeamProject'], 'public') }}: + value: OSX.1015.Amd64.Open + ${{ else }}: + value: OSX.1015.Amd64 + stages: - stage: Windows_Debug_Build dependsOn: [] @@ -38,7 +93,8 @@ stages: jobName: Build_Windows_Debug testArtifactName: Transport_Artifacts_Windows_Debug configuration: Debug - queueName: windows.vs2022preview.amd64.open + poolName: $(PoolName) + queueName: $(Vs2022PreviewQueueName) restoreArguments: -msbuildEngine dotnet /p:UsingToolVSSDK=false /p:GenerateSatelliteAssemblies=false buildArguments: -msbuildEngine dotnet /p:UsingToolVSSDK=false /p:GenerateSatelliteAssemblies=false /p:PublishReadyToRun=false @@ -50,7 +106,8 @@ stages: jobName: Build_Windows_Release testArtifactName: Transport_Artifacts_Windows_Release configuration: Release - queueName: windows.vs2022preview.amd64.open + poolName: $(PoolName) + queueName: $(Vs2022PreviewQueueName) restoreArguments: -msbuildEngine dotnet /p:UsingToolVSSDK=false /p:GenerateSatelliteAssemblies=false buildArguments: -msbuildEngine dotnet /p:UsingToolVSSDK=false /p:GenerateSatelliteAssemblies=false /p:PublishReadyToRun=false @@ -62,7 +119,8 @@ stages: jobName: Build_Unix_Debug testArtifactName: Transport_Artifacts_Unix_Debug configuration: Debug - queueName: Build.Ubuntu.1804.Amd64.Open + poolName: $(PoolName) + queueName: $(UbuntuQueueName) - stage: Source_Build dependsOn: [] @@ -90,6 +148,9 @@ stages: - stage: Windows_Debug_Desktop dependsOn: Windows_Debug_Build + variables: + - ${{ if ne(variables['System.TeamProject'], 'public') }}: + - group: DotNet-HelixApi-Access jobs: - ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: - template: eng/pipelines/test-windows-job.yml @@ -99,6 +160,8 @@ stages: testArtifactName: Transport_Artifacts_Windows_Debug configuration: Debug testArguments: -testDesktop -testArch x86 + helixQueueName: $(HelixWindowsQueueName) + helixApiAccessToken: $(HelixApiAccessToken) - template: eng/pipelines/test-windows-job.yml parameters: @@ -107,9 +170,14 @@ stages: testArtifactName: Transport_Artifacts_Windows_Debug configuration: Debug testArguments: -testDesktop -testArch x64 + helixQueueName: $(HelixWindowsQueueName) + helixApiAccessToken: $(HelixApiAccessToken) - stage: Windows_Release_Desktop dependsOn: Windows_Release_Build + variables: + - ${{ if ne(variables['System.TeamProject'], 'public') }}: + - group: DotNet-HelixApi-Access jobs: - template: eng/pipelines/test-windows-job.yml parameters: @@ -118,6 +186,8 @@ stages: testArtifactName: Transport_Artifacts_Windows_Release configuration: Release testArguments: -testDesktop -testArch x86 + helixQueueName: $(HelixWindowsQueueName) + helixApiAccessToken: $(HelixApiAccessToken) - ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: - template: eng/pipelines/test-windows-job.yml @@ -127,6 +197,8 @@ stages: testArtifactName: Transport_Artifacts_Windows_Release configuration: Release testArguments: -testDesktop -testArch x64 + helixQueueName: $(HelixWindowsQueueName) + helixApiAccessToken: $(HelixApiAccessToken) - template: eng/pipelines/test-windows-job.yml parameters: @@ -134,10 +206,15 @@ stages: jobName: Test_Windows_Desktop_Spanish_Release_64 testArtifactName: Transport_Artifacts_Windows_Release configuration: Release - testArguments: -testDesktop -testArch x64 -helixQueueName Windows.10.Amd64.Server2022.ES.Open + testArguments: -testDesktop -testArch x64 + helixQueueName: $(HelixWindowsSpanishQueueName) + helixApiAccessToken: $(HelixApiAccessToken) - stage: Windows_Debug_CoreClr dependsOn: Windows_Debug_Build + variables: + - ${{ if ne(variables['System.TeamProject'], 'public') }}: + - group: DotNet-HelixApi-Access jobs: - template: eng/pipelines/test-windows-job.yml parameters: @@ -146,6 +223,8 @@ stages: testArtifactName: Transport_Artifacts_Windows_Debug configuration: Debug testArguments: -testCoreClr + helixQueueName: $(HelixWindowsQueueName) + helixApiAccessToken: $(HelixApiAccessToken) - ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: - template: eng/pipelines/test-windows-job-single-machine.yml @@ -154,6 +233,8 @@ stages: jobName: Test_Windows_CoreClr_Debug_Single_Machine testArtifactName: Transport_Artifacts_Windows_Debug configuration: Debug + poolName: $(PoolName) + queueName: $(Vs2022QueueName) testArguments: -testCoreClr - template: eng/pipelines/test-windows-job.yml @@ -163,6 +244,8 @@ stages: testArtifactName: Transport_Artifacts_Windows_Debug configuration: Debug testArguments: -testCoreClr -testIOperation -testCompilerOnly + helixQueueName: $(HelixWindowsQueueName) + helixApiAccessToken: $(HelixApiAccessToken) # This leg runs almost all the compiler tests supported on CoreCLR, but # with additional validation for used assemblies and GetEmitDiagnostics @@ -173,9 +256,14 @@ stages: testArtifactName: Transport_Artifacts_Windows_Debug configuration: Debug testArguments: -testCoreClr -testUsedAssemblies -testCompilerOnly + helixQueueName: $(HelixWindowsQueueName) + helixApiAccessToken: $(HelixApiAccessToken) - stage: Windows_Release_CoreClr dependsOn: Windows_Release_Build + variables: + - ${{ if ne(variables['System.TeamProject'], 'public') }}: + - group: DotNet-HelixApi-Access jobs: - template: eng/pipelines/test-windows-job.yml parameters: @@ -184,9 +272,14 @@ stages: testArtifactName: Transport_Artifacts_Windows_Release configuration: Release testArguments: -testCoreClr + helixQueueName: $(HelixWindowsQueueName) + helixApiAccessToken: $(HelixApiAccessToken) - stage: Unix_Debug_CoreClr dependsOn: Unix_Build + variables: + - ${{ if ne(variables['System.TeamProject'], 'public') }}: + - group: DotNet-HelixApi-Access jobs: - template: eng/pipelines/test-unix-job.yml parameters: @@ -194,7 +287,9 @@ stages: jobName: Test_Linux_Debug testArtifactName: Transport_Artifacts_Unix_Debug configuration: Debug - testArguments: --testCoreClr --helixQueueName Ubuntu.1804.Amd64.Open + testArguments: --testCoreClr + helixQueueName: $(HelixUbuntuQueueName) + helixApiAccessToken: $(HelixApiAccessToken) - ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: - template: eng/pipelines/test-unix-job-single-machine.yml @@ -204,7 +299,8 @@ stages: testArtifactName: Transport_Artifacts_Unix_Debug configuration: Debug testArguments: --testCoreClr - queueName: Build.Ubuntu.1804.Amd64.Open + poolName: $(PoolName) + queueName: $(UbuntuQueueName) - ${{ if ne(variables['Build.Reason'], 'PullRequest') }}: - template: eng/pipelines/test-unix-job.yml @@ -213,7 +309,9 @@ stages: jobName: Test_macOS_Debug testArtifactName: Transport_Artifacts_Unix_Debug configuration: Debug - testArguments: --testCoreClr --helixQueueName OSX.1015.Amd64.Open + testArguments: --testCoreClr + helixQueueName: $(HelixMacOsQueueName) + helixApiAccessToken: $(HelixApiAccessToken) - stage: Correctness dependsOn: [] @@ -221,6 +319,7 @@ stages: - template: eng/pipelines/evaluate-changed-files.yml parameters: jobName: Determine_Changes + poolName: $(PoolName) vmImageName: ubuntu-latest paths: - subset: compilers @@ -234,8 +333,8 @@ stages: - job: Correctness_Build_Artifacts dependsOn: Determine_Changes pool: - name: NetCore-Public - demands: ImageOverride -equals windows.vs2022preview.amd64.open + name: $(PoolName) + demands: ImageOverride -equals $(Vs2022PreviewQueueName) timeoutInMinutes: 90 variables: - template: eng/pipelines/variables-build.yml @@ -284,8 +383,8 @@ stages: dependsOn: Determine_Changes condition: or(ne(variables['Build.Reason'], 'PullRequest'), eq(dependencies.Determine_Changes.outputs['SetPathVars_compilers.containsChange'], 'true')) pool: - name: NetCore-Public - demands: ImageOverride -equals windows.vs2022preview.amd64.open + name: $(PoolName) + demands: ImageOverride -equals $(Vs2022PreviewQueueName) timeoutInMinutes: 90 variables: - template: eng/pipelines/variables-build.yml @@ -306,8 +405,8 @@ stages: dependsOn: Determine_Changes condition: or(ne(variables['Build.Reason'], 'PullRequest'), eq(dependencies.Determine_Changes.outputs['SetPathVars_compilers.containsChange'], 'true')) pool: - name: NetCore-Public - demands: ImageOverride -equals windows.vs2022preview.amd64.open + name: $(PoolName) + demands: ImageOverride -equals $(Vs2022PreviewQueueName) timeoutInMinutes: 90 variables: - template: eng/pipelines/variables-build.yml @@ -322,8 +421,8 @@ stages: dependsOn: Determine_Changes condition: ne(variables['Build.Reason'], 'Pull Request') pool: - name: NetCore-Public - demands: ImageOverride -equals windows.vs2022preview.amd64.open + name: $(PoolName) + demands: ImageOverride -equals $(Vs2022PreviewQueueName) timeoutInMinutes: 90 variables: - template: eng/pipelines/variables-build.yml @@ -348,8 +447,8 @@ stages: dependsOn: Determine_Changes condition: or(ne(variables['Build.Reason'], 'PullRequest'), eq(dependencies.Determine_Changes.outputs['SetPathVars_compilers.containsChange'], 'true')) pool: - name: NetCore-Public - demands: ImageOverride -equals windows.vs2022preview.amd64.open + name: $(PoolName) + demands: ImageOverride -equals $(Vs2022PreviewQueueName) timeoutInMinutes: 90 variables: - template: eng/pipelines/variables-build.yml @@ -377,8 +476,8 @@ stages: - job: Correctness_Analyzers pool: - name: NetCore-Public - demands: ImageOverride -equals Build.Ubuntu.1804.Amd64.Open + name: $(PoolName) + demands: ImageOverride -equals $(UbuntuQueueName) timeoutInMinutes: 35 variables: - template: eng/pipelines/variables-build.yml diff --git a/eng/Versions.props b/eng/Versions.props index 95deeab853a94..df9eb9310b764 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -31,7 +31,7 @@ $(VisualStudioEditorPackagesVersion) 6.0.0-rtm.21518.12 6.0.0-rtm.21518.12 - 17.6.26-preview + 17.7.4-preview 17.7.8-preview-g8c33dc3a76 17.6.35829 16.10.0 @@ -78,6 +78,7 @@ $(RefOnlyMicrosoftBuildPackagesVersion) 6.0.0-preview.0.15 17.5.14-alpha + 7.0.0-preview.23251.2 diff --git a/eng/targets/Settings.props b/eng/targets/Settings.props index 62882152a5c7a..b6b963f87df58 100644 --- a/eng/targets/Settings.props +++ b/eng/targets/Settings.props @@ -51,6 +51,11 @@ false false true + + + $(ArtifactsDir)\packages\$(Configuration)\NPM\ + + false + true + true true diff --git a/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs b/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs index c326e8496d0a8..a6924a4b892b1 100644 --- a/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs +++ b/src/EditorFeatures/Core/EditAndContinue/EditAndContinueLanguageService.cs @@ -36,6 +36,7 @@ public NoSessionException() } private readonly PdbMatchingSourceTextProvider _sourceTextProvider; + private readonly IDiagnosticsRefresher _diagnosticRefresher; private readonly Lazy _debuggerService; private readonly IDiagnosticAnalyzerService _diagnosticService; private readonly EditAndContinueDiagnosticUpdateSource _diagnosticUpdateSource; @@ -63,13 +64,15 @@ public EditAndContinueLanguageService( Lazy debuggerService, IDiagnosticAnalyzerService diagnosticService, EditAndContinueDiagnosticUpdateSource diagnosticUpdateSource, - PdbMatchingSourceTextProvider sourceTextProvider) + PdbMatchingSourceTextProvider sourceTextProvider, + IDiagnosticsRefresher diagnosticRefresher) { WorkspaceProvider = workspaceProvider; _debuggerService = debuggerService; _diagnosticService = diagnosticService; _diagnosticUpdateSource = diagnosticUpdateSource; _sourceTextProvider = sourceTextProvider; + _diagnosticRefresher = diagnosticRefresher; } public void SetFileLoggingDirectory(string? logDirectory) @@ -160,6 +163,8 @@ public async ValueTask EnterBreakStateAsync(CancellationToken cancellationToken) try { await session.BreakStateOrCapabilitiesChangedAsync(_diagnosticService, _diagnosticUpdateSource, inBreakState: true, cancellationToken).ConfigureAwait(false); + + _diagnosticRefresher.RequestWorkspaceRefresh(); } catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { @@ -188,6 +193,8 @@ public async ValueTask ExitBreakStateAsync(CancellationToken cancellationToken) try { await session.BreakStateOrCapabilitiesChangedAsync(_diagnosticService, _diagnosticUpdateSource, inBreakState: false, cancellationToken).ConfigureAwait(false); + + _diagnosticRefresher.RequestWorkspaceRefresh(); GetActiveStatementTrackingService().EndTracking(); } catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) @@ -207,6 +214,8 @@ public async ValueTask OnCapabilitiesChangedAsync(CancellationToken cancellation try { await GetDebuggingSession().BreakStateOrCapabilitiesChangedAsync(_diagnosticService, _diagnosticUpdateSource, inBreakState: null, cancellationToken).ConfigureAwait(false); + + _diagnosticRefresher.RequestWorkspaceRefresh(); } catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { @@ -271,6 +280,8 @@ public async ValueTask EndSessionAsync(CancellationToken cancellationToken) { var solution = GetCurrentCompileTimeSolution(); await GetDebuggingSession().EndDebuggingSessionAsync(solution, _diagnosticUpdateSource, _diagnosticService, cancellationToken).ConfigureAwait(false); + + _diagnosticRefresher.RequestWorkspaceRefresh(); } catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) { @@ -345,6 +356,8 @@ public async ValueTask GetUpdatesAsync(CancellationToke _pendingUpdatedDesignTimeSolution = designTimeSolution; } + _diagnosticRefresher.RequestWorkspaceRefresh(); + var diagnostics = await EmitSolutionUpdateResults.GetHotReloadDiagnosticsAsync(solution, diagnosticData, rudeEdits, syntaxError, moduleUpdates.Status, cancellationToken).ConfigureAwait(false); return new ManagedHotReloadUpdates(moduleUpdates.Updates.FromContract(), diagnostics.FromContract()); } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptPullDiagnosticHandlerProvider.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptPullDiagnosticHandlerProvider.cs index 42a154aefbd1d..ec2b2cecb080e 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptPullDiagnosticHandlerProvider.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptPullDiagnosticHandlerProvider.cs @@ -3,16 +3,12 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Immutable; using System.Composition; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.EditAndContinue; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageServer; -using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; using Microsoft.CodeAnalysis.Options; -using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; @@ -23,8 +19,8 @@ internal class VSTypeScriptDocumentPullDiagnosticHandlerFactory : DocumentPullDi [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public VSTypeScriptDocumentPullDiagnosticHandlerFactory( IDiagnosticAnalyzerService analyzerService, - EditAndContinueDiagnosticUpdateSource editAndContinueDiagnosticUpdateSource, - IGlobalOptionService globalOptions) : base(analyzerService, editAndContinueDiagnosticUpdateSource, globalOptions) + IDiagnosticsRefresher diagnosticsRefresher, + IGlobalOptionService globalOptions) : base(analyzerService, diagnosticsRefresher, globalOptions) { } } @@ -36,8 +32,8 @@ internal class VSTypeScriptWorkspacePullDiagnosticHandler : WorkspacePullDiagnos [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public VSTypeScriptWorkspacePullDiagnosticHandler( IDiagnosticAnalyzerService analyzerService, - EditAndContinueDiagnosticUpdateSource editAndContinueDiagnosticUpdateSource, - IGlobalOptionService globalOptions) : base(analyzerService, editAndContinueDiagnosticUpdateSource, globalOptions) + IDiagnosticsRefresher diagnosticsRefresher, + IGlobalOptionService globalOptions) : base(analyzerService, diagnosticsRefresher, globalOptions) { } } diff --git a/src/EditorFeatures/Core/LanguageServer/EditorLspCompletionResultCreationService.cs b/src/EditorFeatures/Core/LanguageServer/EditorLspCompletionResultCreationService.cs index 9201b9f990db1..24dcd042d685e 100644 --- a/src/EditorFeatures/Core/LanguageServer/EditorLspCompletionResultCreationService.cs +++ b/src/EditorFeatures/Core/LanguageServer/EditorLspCompletionResultCreationService.cs @@ -20,7 +20,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer { [ExportWorkspaceService(typeof(ILspCompletionResultCreationService), ServiceLayer.Editor), Shared] - internal sealed class EditorLspCompletionResultCreationService : ILspCompletionResultCreationService + internal sealed class EditorLspCompletionResultCreationService : AbstractLspCompletionResultCreationService { [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] @@ -28,17 +28,20 @@ public EditorLspCompletionResultCreationService() { } - public async Task CreateAsync( + protected override async Task CreateItemAndPopulateTextEditAsync( Document document, SourceText documentText, bool snippetsSupported, bool itemDefaultsSupported, TextSpan defaultSpan, + string typedText, CompletionItem item, + CompletionService completionService, CancellationToken cancellationToken) { var lspItem = new LSP.VSInternalCompletionItem { + Label = item.GetEntireDisplayText(), Icon = new ImageElement(item.Tags.GetFirstGlyph().GetImageId()) }; @@ -55,37 +58,43 @@ public EditorLspCompletionResultCreationService() } else { - await DefaultLspCompletionResultCreationService.PopulateTextEditAsync( - document, documentText, itemDefaultsSupported, defaultSpan, item, lspItem, cancellationToken).ConfigureAwait(false); + await GetChangeAndPopulateSimpleTextEditAsync( + document, + documentText, + itemDefaultsSupported, + defaultSpan, + item, + lspItem, + completionService, + cancellationToken).ConfigureAwait(false); } return lspItem; } - public async Task ResolveAsync( - LSP.CompletionItem completionItem, - CompletionItem selectedItem, + public override async Task ResolveAsync( + LSP.CompletionItem lspItem, + CompletionItem roslynItem, + LSP.TextDocumentIdentifier textDocumentIdentifier, Document document, - LSP.ClientCapabilities clientCapabilities, + CompletionCapabilityHelper capabilityHelper, CompletionService completionService, CompletionOptions completionOptions, SymbolDescriptionOptions symbolDescriptionOptions, CancellationToken cancellationToken) { - var description = await completionService.GetDescriptionAsync(document, selectedItem, completionOptions, symbolDescriptionOptions, cancellationToken).ConfigureAwait(false)!; + var description = await completionService.GetDescriptionAsync(document, roslynItem, completionOptions, symbolDescriptionOptions, cancellationToken).ConfigureAwait(false)!; if (description != null) { - var supportsVSExtensions = clientCapabilities.HasVisualStudioLspCapability(); - if (supportsVSExtensions) + if (capabilityHelper.SupportVSInternalClientCapabilities) { - var vsCompletionItem = (LSP.VSInternalCompletionItem)completionItem; + var vsCompletionItem = (LSP.VSInternalCompletionItem)lspItem; vsCompletionItem.Description = new ClassifiedTextElement(description.TaggedParts .Select(tp => new ClassifiedTextRun(tp.Tag.ToClassificationTypeName(), tp.Text))); } else { - var clientSupportsMarkdown = clientCapabilities.TextDocument?.Completion?.CompletionItem?.DocumentationFormat?.Contains(LSP.MarkupKind.Markdown) == true; - completionItem.Documentation = ProtocolConversions.GetDocumentationMarkupContent(description.TaggedParts, document, clientSupportsMarkdown); + lspItem.Documentation = ProtocolConversions.GetDocumentationMarkupContent(description.TaggedParts, document, capabilityHelper.SupportsMarkdownDocumentation); } } @@ -94,64 +103,19 @@ await DefaultLspCompletionResultCreationService.PopulateTextEditAsync( // the LSP spec, but is currently supported by the VS client anyway. Once the VS client // adheres to the spec, this logic will need to change and VS will need to provide // official support for TextEdit resolution in some form. - if (selectedItem.IsComplexTextEdit) + if (roslynItem.IsComplexTextEdit) { - Contract.ThrowIfTrue(completionItem.InsertText != null); - Contract.ThrowIfTrue(completionItem.TextEdit != null); - - var snippetsSupported = clientCapabilities?.TextDocument?.Completion?.CompletionItem?.SnippetSupport ?? false; - - completionItem.TextEdit = await GenerateTextEditAsync( - document, completionService, selectedItem, snippetsSupported, cancellationToken).ConfigureAwait(false); - } - - return completionItem; - } - - // Internal for testing - internal static async Task GenerateTextEditAsync( - Document document, - CompletionService completionService, - CompletionItem selectedItem, - bool snippetsSupported, - CancellationToken cancellationToken) - { - var documentText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - - var completionChange = await completionService.GetChangeAsync( - document, selectedItem, cancellationToken: cancellationToken).ConfigureAwait(false); - var completionChangeSpan = completionChange.TextChange.Span; - var newText = completionChange.TextChange.NewText; - Contract.ThrowIfNull(newText); + Contract.ThrowIfTrue(lspItem.InsertText != null); + Contract.ThrowIfTrue(lspItem.TextEdit != null); - // If snippets are supported, that means we can move the caret (represented by $0) to - // a new location. - if (snippetsSupported) - { - var caretPosition = completionChange.NewPosition; - if (caretPosition.HasValue) - { - // caretPosition is the absolute position of the caret in the document. - // We want the position relative to the start of the snippet. - var relativeCaretPosition = caretPosition.Value - completionChangeSpan.Start; + var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + var (edit, _, _) = await GenerateComplexTextEditAsync( + document, completionService, roslynItem, capabilityHelper.SupportSnippets, insertNewPositionPlaceholder: true, cancellationToken).ConfigureAwait(false); - // The caret could technically be placed outside the bounds of the text - // being inserted. This situation is currently unsupported in LSP, so in - // these cases we won't move the caret. - if (relativeCaretPosition >= 0 && relativeCaretPosition <= newText.Length) - { - newText = newText.Insert(relativeCaretPosition, "$0"); - } - } + lspItem.TextEdit = edit; } - var textEdit = new LSP.TextEdit() - { - NewText = newText, - Range = ProtocolConversions.TextSpanToRange(completionChangeSpan, documentText), - }; - - return textEdit; + return lspItem; } } } diff --git a/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs b/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs index 1d50704a26538..728205e782d45 100644 --- a/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs +++ b/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs @@ -21,6 +21,7 @@ using Microsoft.CodeAnalysis.LanguageServer; using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CodeAnalysis.LanguageServer.Handler.CodeActions; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Completion; using Microsoft.CodeAnalysis.LanguageServer.UnitTests; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Extensions; @@ -49,7 +50,11 @@ protected AbstractLanguageServerProtocolTests(ITestOutputHelper? testOutputHelpe TestOutputLspLogger = testOutputHelper != null ? new TestOutputLspLogger(testOutputHelper) : NoOpLspLogger.Instance; } - private static readonly TestComposition s_composition = EditorTestCompositions.LanguageServerProtocolEditorFeatures + protected static readonly TestComposition EditorFeaturesLspComposition = EditorTestCompositions.LanguageServerProtocolEditorFeatures + .AddParts(typeof(TestDocumentTrackingService)) + .AddParts(typeof(TestWorkspaceRegistrationService)); + + protected static readonly TestComposition FeaturesLspComposition = EditorTestCompositions.LanguageServerProtocol .AddParts(typeof(TestDocumentTrackingService)) .AddParts(typeof(TestWorkspaceRegistrationService)); @@ -102,7 +107,7 @@ protected class OrderLocations : Comparer public override int Compare(LSP.Location x, LSP.Location y) => CompareLocations(x, y); } - protected virtual TestComposition Composition => s_composition; + protected virtual TestComposition Composition => EditorFeaturesLspComposition; private protected virtual TestAnalyzerReferenceByLanguage CreateTestAnalyzersReference() => new(DiagnosticExtensions.GetCompilerDiagnosticAnalyzersMap()); @@ -244,11 +249,12 @@ protected static LSP.CompletionParams CreateCompletionParams( bool preselect = false, ImmutableArray? commitCharacters = null, LSP.TextEdit? textEdit = null, - string? insertText = null, + string? textEditText = null, string? sortText = null, string? filterText = null, long resultId = 0, - bool vsResolveTextEditOnCommit = false) + bool vsResolveTextEditOnCommit = false, + LSP.CompletionItemLabelDetails? labelDetails = null) { var position = await document.GetPositionFromLinePositionAsync( ProtocolConversions.PositionToLinePosition(request.Position), CancellationToken.None).ConfigureAwait(false); @@ -258,7 +264,7 @@ protected static LSP.CompletionParams CreateCompletionParams( var item = new LSP.VSInternalCompletionItem() { TextEdit = textEdit, - InsertText = insertText, + TextEditText = textEditText, FilterText = filterText, Label = label, SortText = sortText, @@ -269,7 +275,8 @@ protected static LSP.CompletionParams CreateCompletionParams( ResultId = resultId, }), Preselect = preselect, - VsResolveTextEditOnCommit = vsResolveTextEditOnCommit + VsResolveTextEditOnCommit = vsResolveTextEditOnCommit, + LabelDetails = labelDetails }; if (tags != null) @@ -307,12 +314,12 @@ private protected Task CreateTestLspServerAsync(string[] markups, private protected Task CreateVisualBasicTestLspServerAsync(string markup, bool mutatingLspWorkspace, InitializationOptions? initializationOptions = null) => CreateTestLspServerAsync(new string[] { markup }, LanguageNames.VisualBasic, mutatingLspWorkspace, initializationOptions); - private Task CreateTestLspServerAsync( - string[] markups, string languageName, bool mutatingLspWorkspace, InitializationOptions? initializationOptions) + private protected Task CreateTestLspServerAsync( + string[] markups, string languageName, bool mutatingLspWorkspace, InitializationOptions? initializationOptions, List? excludedTypes = null, List? extraExportedTypes = null) { var lspOptions = initializationOptions ?? new InitializationOptions(); - var workspace = CreateWorkspace(lspOptions, workspaceKind: null, mutatingLspWorkspace); + var workspace = CreateWorkspace(lspOptions, workspaceKind: null, mutatingLspWorkspace, excludedTypes, extraExportedTypes); workspace.InitializeDocuments(TestWorkspace.CreateWorkspaceElement(languageName, files: markups, sourceGeneratedFiles: lspOptions.SourceGeneratedMarkups), openDocuments: false); @@ -372,10 +379,18 @@ private protected async Task CreateXmlTestLspServerAsync( } internal TestWorkspace CreateWorkspace( - InitializationOptions? options, string? workspaceKind, bool mutatingLspWorkspace) + InitializationOptions? options, string? workspaceKind, bool mutatingLspWorkspace, List? excludedTypes = null, List? extraExportedTypes = null) { + var composition = Composition; + + if (excludedTypes is not null) + composition = composition.AddExcludedPartTypes(excludedTypes); + + if (extraExportedTypes is not null) + composition = composition.AddParts(extraExportedTypes); + var workspace = new TestWorkspace( - Composition, workspaceKind, configurationOptions: new WorkspaceConfigurationOptions(EnableOpeningSourceGeneratedFiles: true), supportsLspMutation: mutatingLspWorkspace); + composition, workspaceKind, configurationOptions: new WorkspaceConfigurationOptions(EnableOpeningSourceGeneratedFiles: true), supportsLspMutation: mutatingLspWorkspace); options?.OptionUpdater?.Invoke(workspace.GetService()); workspace.GetService().Register(workspace); @@ -452,6 +467,16 @@ static LSP.Location ConvertTextSpanWithTextToLocation(TextSpan span, SourceText } } + protected static LSP.Location GetLocationPlusOne(LSP.Location originalLocation) + { + var newPosition = new LSP.Position { Character = originalLocation.Range.Start.Character + 1, Line = originalLocation.Range.Start.Line }; + return new LSP.Location + { + Uri = originalLocation.Uri, + Range = new LSP.Range { Start = newPosition, End = newPosition } + }; + } + private static string GetDocumentFilePathFromName(string documentName) => "C:\\" + documentName; diff --git a/src/EditorFeatures/TestUtilities/QuickInfo/ToolTipAssert.cs b/src/EditorFeatures/TestUtilities/QuickInfo/ToolTipAssert.cs index 9d7d81393bea8..330de55321e16 100644 --- a/src/EditorFeatures/TestUtilities/QuickInfo/ToolTipAssert.cs +++ b/src/EditorFeatures/TestUtilities/QuickInfo/ToolTipAssert.cs @@ -18,7 +18,7 @@ namespace Microsoft.CodeAnalysis.Test.Utilities.QuickInfo { public static class ToolTipAssert { - public static void EqualContent(object expected, object actual) + public static void EqualContent(object expected, object? actual) { try { @@ -26,25 +26,25 @@ public static void EqualContent(object expected, object actual) if (expected is ContainerElement containerElement) { - EqualContainerElement(containerElement, (ContainerElement)actual); + EqualContainerElement(containerElement, (ContainerElement)actual!); return; } if (expected is ImageElement imageElement) { - EqualImageElement(imageElement, (ImageElement)actual); + EqualImageElement(imageElement, (ImageElement)actual!); return; } if (expected is ClassifiedTextElement classifiedTextElement) { - EqualClassifiedTextElement(classifiedTextElement, (ClassifiedTextElement)actual); + EqualClassifiedTextElement(classifiedTextElement, (ClassifiedTextElement)actual!); return; } if (expected is ClassifiedTextRun classifiedTextRun) { - EqualClassifiedTextRun(classifiedTextRun, (ClassifiedTextRun)actual); + EqualClassifiedTextRun(classifiedTextRun, (ClassifiedTextRun)actual!); return; } @@ -53,7 +53,7 @@ public static void EqualContent(object expected, object actual) catch (Exception) { var renderedExpected = ContainerToString(expected); - var renderedActual = ContainerToString(actual); + var renderedActual = ContainerToString(actual!); AssertEx.EqualOrDiff(renderedExpected, renderedActual); // This is not expected to be hit, but it will be hit if the difference cannot be detected within the diff diff --git a/src/Features/Core/Portable/Common/FeaturesSessionTelemetry.cs b/src/Features/Core/Portable/Common/FeaturesSessionTelemetry.cs new file mode 100644 index 0000000000000..19fc302749750 --- /dev/null +++ b/src/Features/Core/Portable/Common/FeaturesSessionTelemetry.cs @@ -0,0 +1,25 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ChangeSignature; +using Microsoft.CodeAnalysis.Completion.Log; +using Microsoft.CodeAnalysis.Logging; + +namespace Microsoft.CodeAnalysis.Common +{ + internal static class FeaturesSessionTelemetry + { + public static void Report() + { + CompletionProvidersLogger.ReportTelemetry(); + SolutionLogger.ReportTelemetry(); + ChangeSignatureLogger.ReportTelemetry(); + } + } +} diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionProvider.cs index 7313c14f91517..41550239a3510 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/AbstractImportCompletionProvider.cs @@ -202,10 +202,11 @@ public override async Task GetChangeAsync( async Task ShouldCompleteWithFullyQualifyTypeName() { + if (ImportCompletionItem.ShouldAlwaysAddMissingImport(completionItem)) + return false; + if (!IsAddingImportsSupported(document)) - { return true; - } // We might need to qualify unimported types to use them in an import directive, because they only affect members of the containing // import container (e.g. namespace/class/etc. declarations). diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ImportCompletionItem.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ImportCompletionItem.cs index 93c01e94aae3a..c9ac87149b582 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ImportCompletionItem.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ImportCompletionItem.cs @@ -17,14 +17,15 @@ internal static class ImportCompletionItem { // Note the additional space as prefix to the System namespace, // to make sure items from System.* get sorted ahead. - private const string OtherNamespaceSortTextFormat = "{0} {1}"; - private const string SystemNamespaceSortTextFormat = "{0} {1}"; + private const string OtherNamespaceSortTextFormat = "~{0} {1}"; + private const string SystemNamespaceSortTextFormat = "~{0} {1}"; private const string TypeAritySuffixName = nameof(TypeAritySuffixName); private const string AttributeFullName = nameof(AttributeFullName); private const string MethodKey = nameof(MethodKey); private const string ReceiverKey = nameof(ReceiverKey); private const string OverloadCountKey = nameof(OverloadCountKey); + private const string AlwaysAddMissingImportKey = nameof(AlwaysAddMissingImportKey); public static CompletionItem Create( string name, @@ -209,5 +210,9 @@ private static (ISymbol? symbol, int overloadCount) GetSymbolAndOverloadCount(Co return (compilation.GetTypeByMetadataName(fullyQualifiedName), 0); } + + public static CompletionItem MarkItemToAlwaysAddMissingImport(CompletionItem item) => item.WithProperties(item.Properties.Add(AlwaysAddMissingImportKey, AlwaysAddMissingImportKey)); + + public static bool ShouldAlwaysAddMissingImport(CompletionItem item) => item.Properties.ContainsKey(AlwaysAddMissingImportKey); } } diff --git a/src/Features/Core/Portable/Contracts/Client/ISolutionSnapshotProvider.cs b/src/Features/Core/Portable/Contracts/Client/ISolutionSnapshotProvider.cs new file mode 100644 index 0000000000000..cdcdb86d3c14a --- /dev/null +++ b/src/Features/Core/Portable/Contracts/Client/ISolutionSnapshotProvider.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.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.Contracts.Client; + +[DataContract] +internal readonly record struct SolutionSnapshotId([property: DataMember] int Id); + +// brokered service implemented by the client +internal interface ISolutionSnapshotProvider +{ + ValueTask RegisterSolutionSnapshotAsync(CancellationToken cancellationToken); +} diff --git a/src/Features/Core/Portable/Diagnostics/IDiagnosticsRefresher.cs b/src/Features/Core/Portable/Diagnostics/IDiagnosticsRefresher.cs new file mode 100644 index 0000000000000..bd1b413174aa7 --- /dev/null +++ b/src/Features/Core/Portable/Diagnostics/IDiagnosticsRefresher.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.CodeAnalysis.Diagnostics; + +/// +/// Used to send request for diagnostic pull to the client. +/// +internal interface IDiagnosticsRefresher +{ + /// + /// Requests workspace diagnostics refresh. + /// Any component that maintains state whose change may affect reported diagnostics should call whenever that state changes. + /// Any component that reports diagnostics based on the value of a global option should also call whenever the option value changes. + /// + void RequestWorkspaceRefresh(); + + /// + /// Current version of global state that may affect diagnostics. Incremented on every refresh. + /// Used to determine whether any global state that might affect workspace diagnostics has changed. + /// + int GlobalStateVersion { get; } +} diff --git a/src/Features/Core/Portable/DocumentationComments/AbstractDocumentationCommentSnippetService.cs b/src/Features/Core/Portable/DocumentationComments/AbstractDocumentationCommentSnippetService.cs index ca42b299f8a77..199920bab8338 100644 --- a/src/Features/Core/Portable/DocumentationComments/AbstractDocumentationCommentSnippetService.cs +++ b/src/Features/Core/Portable/DocumentationComments/AbstractDocumentationCommentSnippetService.cs @@ -43,7 +43,8 @@ internal abstract class AbstractDocumentationCommentSnippetService? GetDocumentationCommentLines(SyntaxToken token, SourceText text, in DocumentationCommentOptions options, out string? indentText, out int caretOffset, out int spanToReplaceLength) { indentText = null; + + var lines = GetDocumentationStubLines(token, text, options, out caretOffset, out spanToReplaceLength, out var existingCommentText); + if (lines is null) + { + return lines; + } + + var documentationComment = token.GetAncestor(); + var line = text.Lines.GetLineFromPosition(documentationComment!.FullSpan.Start); + if (line.IsEmptyOrWhitespace()) + { + return null; + } + + // Add indents + var lineOffset = line.GetColumnOfFirstNonWhitespaceCharacterOrEndOfLine(options.TabSize); + indentText = lineOffset.CreateIndentationString(options.UseTabs, options.TabSize); + + IndentLines(lines, indentText); + + // We always want the caret text to be on the second line, with one space after the doc comment XML + // GetDocumentationCommentStubLines ensures that space is always there + caretOffset = lines[0].Length + indentText.Length + ExteriorTriviaText.Length + 1; + spanToReplaceLength = existingCommentText!.Length; + + return lines; + } + + private List? GetDocumentationCommentLinesNoIndentation(SyntaxToken token, SourceText text, in DocumentationCommentOptions options, out int caretOffset, out int spanToReplaceLength) + { + var lines = GetDocumentationStubLines(token, text, options, out caretOffset, out spanToReplaceLength, out var existingCommentText); + if (lines is null) + { + return lines; + } + + // We always want the caret text to be on the second line, with one space after the doc comment XML + // GetDocumentationCommentStubLines ensures that space is always there + caretOffset = lines[0].Length + ExteriorTriviaText.Length + 1; + spanToReplaceLength = existingCommentText!.Length; + + return lines; + } + + private List? GetDocumentationStubLines(SyntaxToken token, SourceText text, in DocumentationCommentOptions options, out int caretOffset, out int spanToReplaceLength, out string? existingCommentText) + { caretOffset = 0; spanToReplaceLength = 0; + existingCommentText = null; var documentationComment = token.GetAncestor(); - if (documentationComment == null || !IsSingleExteriorTrivia(documentationComment, out var existingCommentText)) + if (documentationComment == null || !IsSingleExteriorTrivia(documentationComment, out existingCommentText)) { return null; } @@ -112,17 +163,6 @@ internal abstract class AbstractDocumentationCommentSnippetService _pendingSolutionSnapshots = new(); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public SolutionSnapshotRegistry() + { + } + + /// + /// Called from LSP server. + /// + public SolutionSnapshotId RegisterSolutionSnapshot(Solution solution) + { + var id = new SolutionSnapshotId(Interlocked.Increment(ref s_solutionSnapshotId)); + + lock (_pendingSolutionSnapshots) + { + _pendingSolutionSnapshots.Add(id, solution); + } + + return id; + } + + public Solution GetRegisteredSolutionSnapshot(SolutionSnapshotId id) + { + lock (_pendingSolutionSnapshots) + { + Contract.ThrowIfFalse(_pendingSolutionSnapshots.TryGetValue(id, out var solution)); + Contract.ThrowIfFalse(_pendingSolutionSnapshots.Remove(id)); + return solution; + } + } + + public void Clear() + { + lock (_pendingSolutionSnapshots) + { + _pendingSolutionSnapshots.Clear(); + } + } +} diff --git a/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj b/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj index 3b57a428d8359..b20aa6906b3f6 100644 --- a/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj +++ b/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj @@ -27,6 +27,7 @@ + @@ -44,6 +45,13 @@ + + + + @@ -119,6 +127,7 @@ + diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/LspFileChangeWatcherTests.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/LspFileChangeWatcherTests.cs new file mode 100644 index 0000000000000..5a761f41b29d7 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/LspFileChangeWatcherTests.cs @@ -0,0 +1,146 @@ +// 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.Concurrent; +using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.FileWatching; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using StreamJsonRpc; +using Xunit.Abstractions; +using FileSystemWatcher = Microsoft.VisualStudio.LanguageServer.Protocol.FileSystemWatcher; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; + +public class LspFileChangeWatcherTests : AbstractLanguageServerHostTests +{ + private readonly ClientCapabilities _clientCapabilitiesWithFileWatcherSupport = new ClientCapabilities + { + Workspace = new WorkspaceClientCapabilities + { + DidChangeWatchedFiles = new DynamicRegistrationSetting { DynamicRegistration = true } + } + }; + + public LspFileChangeWatcherTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + } + + [Fact] + public async Task LspFileWatcherNotSupportedWithoutClientSupport() + { + await using var testLspServer = await TestLspServer.CreateAsync(new ClientCapabilities(), TestOutputLogger); + + Assert.False(LspFileChangeWatcher.SupportsLanguageServerHost(testLspServer.LanguageServerHost)); + } + + [Fact] + public async Task LspFileWatcherSupportedWithClientSupport() + { + await using var testLspServer = await TestLspServer.CreateAsync(_clientCapabilitiesWithFileWatcherSupport, TestOutputLogger); + + Assert.True(LspFileChangeWatcher.SupportsLanguageServerHost(testLspServer.LanguageServerHost)); + } + + [Fact] + public async Task CreatingDirectoryWatchRequestsDirectoryWatch() + { + AsynchronousOperationListenerProvider.Enable(enable: true); + + await using var testLspServer = await TestLspServer.CreateAsync(_clientCapabilitiesWithFileWatcherSupport, TestOutputLogger); + var lspFileChangeWatcher = new LspFileChangeWatcher( + testLspServer.LanguageServerHost, + testLspServer.ExportProvider.GetExportedValue()); + + var dynamicCapabilitiesRpcTarget = new DynamicCapabilitiesRpcTarget(); + testLspServer.AddClientLocalRpcTarget(dynamicCapabilitiesRpcTarget); + + using var tempRoot = new TempRoot(); + var tempDirectory = tempRoot.CreateDirectory(); + + // Try creating a context and ensure we created the registration + var context = lspFileChangeWatcher.CreateContext(new ProjectSystem.WatchedDirectory(tempDirectory.Path, extensionFilter: null)); + await WaitForFileWatcherAsync(testLspServer); + + var watcher = GetSingleFileWatcher(dynamicCapabilitiesRpcTarget); + + Assert.Equal(tempDirectory.Path + Path.DirectorySeparatorChar, watcher.GlobPattern.BaseUri); + Assert.Equal("**/*", watcher.GlobPattern.Pattern); + + // Get rid of the registration and it should be gone again + context.Dispose(); + await WaitForFileWatcherAsync(testLspServer); + Assert.Empty(dynamicCapabilitiesRpcTarget.Registrations); + } + + [Fact] + public async Task CreatingFileWatchRequestsFileWatch() + { + AsynchronousOperationListenerProvider.Enable(enable: true); + + await using var testLspServer = await TestLspServer.CreateAsync(_clientCapabilitiesWithFileWatcherSupport, TestOutputLogger); + var lspFileChangeWatcher = new LspFileChangeWatcher( + testLspServer.LanguageServerHost, + testLspServer.ExportProvider.GetExportedValue()); + + var dynamicCapabilitiesRpcTarget = new DynamicCapabilitiesRpcTarget(); + testLspServer.AddClientLocalRpcTarget(dynamicCapabilitiesRpcTarget); + + using var tempRoot = new TempRoot(); + var tempDirectory = tempRoot.CreateDirectory(); + + // Try creating a single file watch and ensure we created the registration + var context = lspFileChangeWatcher.CreateContext(); + var watchedFile = context.EnqueueWatchingFile("Z:\\SingleFile.txt"); + await WaitForFileWatcherAsync(testLspServer); + + var watcher = GetSingleFileWatcher(dynamicCapabilitiesRpcTarget); + + Assert.Equal("Z:\\", watcher.GlobPattern.BaseUri); + Assert.Equal("SingleFile.txt", watcher.GlobPattern.Pattern); + + // Get rid of the registration and it should be gone again + watchedFile.Dispose(); + await WaitForFileWatcherAsync(testLspServer); + Assert.Empty(dynamicCapabilitiesRpcTarget.Registrations); + } + + private static async Task WaitForFileWatcherAsync(TestLspServer testLspServer) + { + await testLspServer.ExportProvider.GetExportedValue().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync(); + } + + private static FileSystemWatcher GetSingleFileWatcher(DynamicCapabilitiesRpcTarget dynamicCapabilities) + { + var registrationJson = Assert.IsType(Assert.Single(dynamicCapabilities.Registrations).Value.RegisterOptions); + var registration = registrationJson.ToObject()!; + + return Assert.Single(registration.Watchers); + } + + private sealed class DynamicCapabilitiesRpcTarget + { + public readonly ConcurrentDictionary Registrations = new(); + + [JsonRpcMethod("client/registerCapability", UseSingleObjectParameterDeserialization = true)] + public Task RegisterCapabilityAsync(RegistrationParams registrationParams, CancellationToken _) + { + foreach (var registration in registrationParams.Registrations) + Assert.True(Registrations.TryAdd(registration.Id, registration)); + + return Task.CompletedTask; + } + + [JsonRpcMethod("client/unregisterCapability", UseSingleObjectParameterDeserialization = true)] + public Task UnregisterCapabilityAsync(UnregistrationParamsWithMisspelling unregistrationParams, CancellationToken _) + { + foreach (var unregistration in unregistrationParams.Unregistrations) + Assert.True(Registrations.TryRemove(unregistration.Id, out var _)); + + return Task.CompletedTask; + } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Microsoft.CodeAnalysis.LanguageServer.UnitTests.csproj b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Microsoft.CodeAnalysis.LanguageServer.UnitTests.csproj new file mode 100644 index 0000000000000..e6b567e3e8067 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Microsoft.CodeAnalysis.LanguageServer.UnitTests.csproj @@ -0,0 +1,38 @@ + + + + net7.0 + enable + enable + UnitTest + + false + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ServerInitializationTests.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ServerInitializationTests.cs new file mode 100644 index 0000000000000..817edcb942d2e --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/ServerInitializationTests.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 Microsoft.VisualStudio.LanguageServer.Protocol; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; + +public class ServerInitializationTests : AbstractLanguageServerHostTests +{ + public ServerInitializationTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + } + + [Fact] + public async Task TestServerHandlesTextSyncRequestsAsync() + { + await using var server = await CreateLanguageServerAsync(); + var document = new VersionedTextDocumentIdentifier { Uri = new Uri(@"C:\file.cs") }; + var response = await server.ExecuteRequestAsync(Methods.TextDocumentDidOpenName, new DidOpenTextDocumentParams + { + TextDocument = new TextDocumentItem + { + Uri = document.Uri, + Text = "Write" + } + }, CancellationToken.None); + + // These are notifications so we should get a null response (but no exceptions). + Assert.Null(response); + + response = await server.ExecuteRequestAsync(Methods.TextDocumentDidChangeName, new DidChangeTextDocumentParams + { + TextDocument = document, + ContentChanges = new[] + { + new TextDocumentContentChangeEvent + { + Range = new VisualStudio.LanguageServer.Protocol.Range { Start = new Position(0, 0), End = new Position(0, 0) }, + Text = "Console." + } + } + }, CancellationToken.None); + + // These are notifications so we should get a null response (but no exceptions). + Assert.Null(response); + + response = await server.ExecuteRequestAsync(Methods.TextDocumentDidCloseName, new DidCloseTextDocumentParams + { + TextDocument = document + }, CancellationToken.None); + + // These are notifications so we should get a null response (but no exceptions). + Assert.Null(response); + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/TelemetryReporterTests.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/TelemetryReporterTests.cs new file mode 100644 index 0000000000000..eb2918237c5f4 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/TelemetryReporterTests.cs @@ -0,0 +1,56 @@ +// 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 Microsoft.CodeAnalysis.Contracts.Telemetry; +using Microsoft.Extensions.Logging; +using Roslyn.Utilities; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; + +public sealed class TelemetryReporterTests : AbstractLanguageServerHostTests +{ + public TelemetryReporterTests(ITestOutputHelper testOutputHelper) + : base(testOutputHelper) + { + } + + private async Task CreateReporterAsync() + { + var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync(TestOutputLogger.Factory, includeDevKitComponents: true); + + var reporter = exportProvider.GetExport().Value; + Assert.NotNull(reporter); + + // Do not set default session in tests to enable test isolation: + reporter.InitializeSession("off", "test-session", isDefaultSession: false); + + return reporter; + } + + private static string GetEventName(string name) => $"test/event/{name}"; + + [Fact] + public async Task TestFault() + { + var service = await CreateReporterAsync(); + service.ReportFault(GetEventName(nameof(TestFault)), "test description", logLevel: 2, forceDump: false, processId: 0, new Exception()); + } + + [Fact] + public async Task TestBlockLogging() + { + var service = await CreateReporterAsync(); + service.LogBlockStart(GetEventName(nameof(TestBlockLogging)), kind: 0, blockId: 0); + service.LogBlockEnd(blockId: 0, ImmutableDictionary.Empty, CancellationToken.None); + } + + [Fact] + public async Task TestLog() + { + var service = await CreateReporterAsync(); + service.Log(GetEventName(nameof(TestLog)), ImmutableDictionary.Empty); + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/AbstractLanguageServerHostTests.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/AbstractLanguageServerHostTests.cs new file mode 100644 index 0000000000000..0d0e52c99dc8e --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/AbstractLanguageServerHostTests.cs @@ -0,0 +1,95 @@ +// 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 Microsoft.CodeAnalysis.LanguageServer.LanguageServer; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Composition; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Nerdbank.Streams; +using Roslyn.Utilities; +using StreamJsonRpc; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; + +public abstract class AbstractLanguageServerHostTests +{ + protected TestOutputLogger TestOutputLogger { get; } + + protected AbstractLanguageServerHostTests(ITestOutputHelper testOutputHelper) + { + TestOutputLogger = new TestOutputLogger(testOutputHelper); + } + + protected Task CreateLanguageServerAsync(bool includeDevKitComponents = true) + { + return TestLspServer.CreateAsync(new ClientCapabilities(), TestOutputLogger, includeDevKitComponents); + } + + protected sealed class TestLspServer : IAsyncDisposable + { + private readonly Task _languageServerHostCompletionTask; + private readonly JsonRpc _clientRpc; + + public static async Task CreateAsync(ClientCapabilities clientCapabilities, TestOutputLogger logger, bool includeDevKitComponents = true) + { + var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync(logger.Factory, includeDevKitComponents); + var testLspServer = new TestLspServer(exportProvider, logger); + var initializeResponse = await testLspServer.ExecuteRequestAsync(Methods.InitializeName, new InitializeParams { Capabilities = clientCapabilities }, CancellationToken.None); + Assert.NotNull(initializeResponse?.Capabilities); + + await testLspServer.ExecuteRequestAsync(Methods.InitializedName, new InitializedParams(), CancellationToken.None); + + return testLspServer; + } + + internal LanguageServerHost LanguageServerHost { get; } + public ExportProvider ExportProvider { get; } + + private TestLspServer(ExportProvider exportProvider, ILogger logger) + { + var (clientStream, serverStream) = FullDuplexStream.CreatePair(); + LanguageServerHost = new LanguageServerHost(serverStream, serverStream, exportProvider, logger); + + _clientRpc = new JsonRpc(new HeaderDelimitedMessageHandler(clientStream, clientStream, new JsonMessageFormatter())) + { + AllowModificationWhileListening = true, + ExceptionStrategy = ExceptionProcessing.ISerializable, + }; + + _clientRpc.StartListening(); + + // This task completes when the server shuts down. We store it so that we can wait for completion + // when we dispose of the test server. + LanguageServerHost.Start(); + + _languageServerHostCompletionTask = LanguageServerHost.WaitForExitAsync(); + ExportProvider = exportProvider; + } + + public async Task ExecuteRequestAsync(string methodName, TRequestType request, CancellationToken cancellationToken) where TRequestType : class + { + var result = await _clientRpc.InvokeWithParameterObjectAsync(methodName, request, cancellationToken: cancellationToken); + return result; + } + + public void AddClientLocalRpcTarget(object target) + { + _clientRpc.AddLocalRpcTarget(target); + } + + public async ValueTask DisposeAsync() + { + await _clientRpc.InvokeAsync(Methods.ShutdownName); + await _clientRpc.NotifyAsync(Methods.ExitName); + + // The language server host task should complete once shutdown and exit are called. +#pragma warning disable VSTHRD003 // Avoid awaiting foreign Tasks + await _languageServerHostCompletionTask; +#pragma warning restore VSTHRD003 // Avoid awaiting foreign Tasks + + _clientRpc.Dispose(); + } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/BrokeredServiceProxy.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/BrokeredServiceProxy.cs new file mode 100644 index 0000000000000..bff88eebd4630 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/BrokeredServiceProxy.cs @@ -0,0 +1,68 @@ +// 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 Nerdbank.Streams; +using StreamJsonRpc; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; + +/// +/// A wrapper which takes a service but actually sends calls to it through JsonRpc to ensure we can actually use the service across a wire. +/// +internal sealed class BrokeredServiceProxy : IAsyncDisposable where T : class +{ + /// + /// A task that cane awaited to assert the rest of the fields in this class being assigned and non-null. + /// + private readonly Task _createConnectionTask; + + private JsonRpc? _serverRpc; + private JsonRpc? _clientRpc; + private T? _clientFactoryProxy; + + public BrokeredServiceProxy(T service) + { + var (serverStream, clientStream) = FullDuplexStream.CreatePair(); + + var serverTask = Task.Run(async () => + { + var serverMultiplexingStream = await MultiplexingStream.CreateAsync(serverStream); + var serverChannel = await serverMultiplexingStream.AcceptChannelAsync(""); + + var serverFormatter = new MessagePackFormatter() { MultiplexingStream = serverMultiplexingStream }; + _serverRpc = new JsonRpc(new LengthHeaderMessageHandler(serverChannel, serverFormatter)); + + _serverRpc.AddLocalRpcTarget(service, options: null); + _serverRpc.StartListening(); + }); + + var clientTask = Task.Run(async () => + { + var clientMultiplexingStream = await MultiplexingStream.CreateAsync(clientStream); + var clientChannel = await clientMultiplexingStream.OfferChannelAsync(""); + + var clientFormatter = new MessagePackFormatter() { MultiplexingStream = clientMultiplexingStream }; + _clientRpc = new JsonRpc(new LengthHeaderMessageHandler(clientChannel, clientFormatter)); + + _clientFactoryProxy = _clientRpc.Attach(); + _clientRpc.StartListening(); + }); + + _createConnectionTask = Task.WhenAll(serverTask, clientTask); + } + + public async ValueTask DisposeAsync() + { + await _createConnectionTask; + + _serverRpc!.Dispose(); + _clientRpc!.Dispose(); + } + + public async Task GetServiceAsync() + { + await _createConnectionTask; + return _clientFactoryProxy!; + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs new file mode 100644 index 0000000000000..9d108c49ae614 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Composition; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; + +internal sealed class LanguageServerTestComposition +{ + /// + /// Build places DevKit files to this subdirectory. + /// + private const string DevKitExtensionSubdirectory = "DevKit"; + + private const string DevKitAssemblyFileName = "Microsoft.VisualStudio.LanguageServices.DevKit.dll"; + + private static string GetDevKitExtensionPath() + => Path.Combine(AppContext.BaseDirectory, DevKitExtensionSubdirectory, DevKitAssemblyFileName); + + public static Task CreateExportProviderAsync(ILoggerFactory loggerFactory, bool includeDevKitComponents) + => ExportProviderBuilder.CreateExportProviderAsync(extensionAssemblyPaths: includeDevKitComponents ? new[] { GetDevKitExtensionPath() } : Array.Empty(), sharedDependenciesPath: null, loggerFactory); +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/TestLoggerProvider.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/TestLoggerProvider.cs new file mode 100644 index 0000000000000..3ebda77fd10db --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/TestLoggerProvider.cs @@ -0,0 +1,25 @@ +// 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 Microsoft.Extensions.Logging; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; + +internal class TestLoggerProvider : ILoggerProvider +{ + private readonly ILogger _testLogger; + public TestLoggerProvider(ILogger testLogger) + { + _testLogger = testLogger; + } + + public ILogger CreateLogger(string categoryName) + { + return _testLogger; + } + + public void Dispose() + { + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/TestOutputLogger.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/TestOutputLogger.cs new file mode 100644 index 0000000000000..f26d1f89b9ee6 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/TestOutputLogger.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 Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; + +public class TestOutputLogger : ILogger +{ + private readonly ITestOutputHelper _testOutputHelper; + public readonly ILoggerFactory Factory; + + public TestOutputLogger(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + Factory = new LoggerFactory(new[] { new TestLoggerProvider(this) }); + } + + public IDisposable BeginScope(TState state) + { + return new NoOpDisposable(); + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + _testOutputHelper.WriteLine($"[{DateTime.UtcNow:hh:mm:ss.fff}][{logLevel}]{formatter(state, exception)}"); + } + + private sealed class NoOpDisposable : IDisposable + { + public void Dispose() + { + } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/Usings.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/Usings.cs new file mode 100644 index 0000000000000..349f347431670 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/Usings.cs @@ -0,0 +1,5 @@ +// 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. + +global using Xunit; \ No newline at end of file diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/WorkspaceProjectFactoryServiceTests.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/WorkspaceProjectFactoryServiceTests.cs new file mode 100644 index 0000000000000..e575d0879e230 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/WorkspaceProjectFactoryServiceTests.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 Microsoft.CodeAnalysis.LanguageServer.BrokeredServices; +using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; +using Microsoft.CodeAnalysis.LanguageServer.Logging; +using Microsoft.CodeAnalysis.Remote.ProjectSystem; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Shell.ServiceBroker; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; + +public class WorkspaceProjectFactoryServiceTests +{ + [Fact] + public async Task CreateProjectAndBatch() + { + using var exportProvider = await LanguageServerTestComposition.CreateExportProviderAsync(new LoggerFactory(), includeDevKitComponents: false); + await exportProvider.GetExportedValue().CreateAsync(); + + var workspaceFactory = exportProvider.GetExportedValue(); + var workspaceProjectFactoryServiceInstance = (WorkspaceProjectFactoryService)exportProvider + .GetExportedValues() + .Single(service => service.Descriptor == WorkspaceProjectFactoryServiceDescriptor.ServiceDescriptor); + + await using var brokeredServiceFactory = new BrokeredServiceProxy( + workspaceProjectFactoryServiceInstance); + + var workspaceProjectFactoryService = await brokeredServiceFactory.GetServiceAsync(); + using var workspaceProject = await workspaceProjectFactoryService.CreateAndAddProjectAsync( + new WorkspaceProjectCreationInfo(LanguageNames.CSharp, "DisplayName", FilePath: null, new Dictionary()), + CancellationToken.None); + + using var batch = await workspaceProject.StartBatchAsync(CancellationToken.None); + + var sourceFilePath = MakeAbsolutePath("SourceFile.cs"); + var additionalFilePath = MakeAbsolutePath("AdditionalFile.txt"); + + await workspaceProject.AddSourceFilesAsync(new[] { new SourceFileInfo(sourceFilePath, Array.Empty()) }, CancellationToken.None); + await workspaceProject.AddAdditionalFilesAsync(new[] { additionalFilePath }, CancellationToken.None); + await batch.ApplyAsync(CancellationToken.None); + + // Verify it actually did something; we won't exclusively test each method since those are tested at lower layers + var project = workspaceFactory.Workspace.CurrentSolution.Projects.Single(); + Assert.Equal(sourceFilePath, project.Documents.Single().FilePath); + Assert.Equal(additionalFilePath, project.AdditionalDocuments.Single().FilePath); + } + + private static string MakeAbsolutePath(string relativePath) + { + if (OperatingSystem.IsWindows()) + return Path.Combine("Z:\\", relativePath); + else + return Path.Combine("//", relativePath); + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/.editorconfig b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/.editorconfig new file mode 100644 index 0000000000000..e4c1e2f7a4fc5 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/.editorconfig @@ -0,0 +1,10 @@ +[*.cs] + +# CA2007: Consider calling ConfigureAwait on the awaited task +dotnet_diagnostic.CA2007.severity = none + +# CA2007: Do not directly await a Task +dotnet_diagnostic.CA2007.severity = none + +# VSTHRD003: Avoid awaiting foreign Tasks +dotnet_diagnostic.VSTHRD003.severity = none diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/BrokeredServiceBridgeProvider.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/BrokeredServiceBridgeProvider.cs new file mode 100644 index 0000000000000..eb420c2bc0f9a --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/BrokeredServiceBridgeProvider.cs @@ -0,0 +1,78 @@ +// 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.Composition; +using System.Diagnostics; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services; +using Microsoft.Extensions.Logging; +using Microsoft.ServiceHub.Framework; +using Microsoft.VisualStudio.Shell.ServiceBroker; +using Microsoft.VisualStudio.Utilities.ServiceBroker; +using Nerdbank.Streams; + +namespace Microsoft.CodeAnalysis.LanguageServer.BrokeredServices; + +[Export, Shared] +internal class BrokeredServiceBridgeProvider +{ + private const string ServiceBrokerChannelName = "serviceBroker"; + + private readonly ILogger _logger; + private readonly TraceSource _brokeredServiceTraceSource; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public BrokeredServiceBridgeProvider(ILoggerFactory loggerFactory, BrokeredServiceTraceListener brokeredServiceTraceListener) + { + _logger = loggerFactory.CreateLogger(); + _brokeredServiceTraceSource = brokeredServiceTraceListener.Source; + } + + /// + /// Creates the brokered service bridge to the remote process. + /// We expose the services from our container to the remote and consume services + /// from the remote by proffering them into our container. + /// + /// the pipe name we use for the connection. + /// our local container. + /// a cancellation token. + /// a task that represents the lifetime of the bridge. It will complete when the bridge closes. + public async Task SetupBrokeredServicesBridgeAsync(string brokeredServicePipeName, BrokeredServiceContainer container, CancellationToken cancellationToken) + { + _logger.LogDebug("Setting up brokered service bridge"); + using var bridgeStream = await ServerFactory.ConnectAsync(brokeredServicePipeName, cancellationToken); + using var bridgeMxStream = await MultiplexingStream.CreateAsync(bridgeStream, cancellationToken); + + // Wait until the connection ends (so we don't dispose of the stream before it ends). + await Task.WhenAll(ProfferServicesToRemoteAsync(), ConsumeServicesFromRemoteAsync()); + + async Task ProfferServicesToRemoteAsync() + { + using var profferedServiceBrokerChannel = await bridgeMxStream.OfferChannelAsync(ServiceBrokerChannelName, cancellationToken); + var serviceBroker = container.GetLimitedAccessServiceBroker(ServiceAudience.Local, ImmutableDictionary.Empty, ClientCredentialsPolicy.RequestOverridesDefault); + using IpcRelayServiceBroker relayServiceBroker = new(serviceBroker); + + FrameworkServices.RemoteServiceBroker + .WithTraceSource(_brokeredServiceTraceSource) + .ConstructRpc(relayServiceBroker, profferedServiceBrokerChannel); + + await relayServiceBroker.Completion; + } + + async Task ConsumeServicesFromRemoteAsync() + { + using var consumingServiceBrokerChannel = await bridgeMxStream.AcceptChannelAsync(ServiceBrokerChannelName, cancellationToken); + var remoteClient = FrameworkServices.RemoteServiceBroker + .WithTraceSource(_brokeredServiceTraceSource) + .ConstructRpc(consumingServiceBrokerChannel); + + using (container.ProfferRemoteBroker(remoteClient, bridgeMxStream, ServiceSource.OtherProcessOnSameMachine, Descriptors.RemoteServicesToRegister.Keys.ToImmutableHashSet())) + { + await consumingServiceBrokerChannel.Completion; + } + } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/BrokeredServiceContainer.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/BrokeredServiceContainer.cs new file mode 100644 index 0000000000000..e06bdafc0bb43 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/BrokeredServiceContainer.cs @@ -0,0 +1,78 @@ +// 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.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services; +using Microsoft.ServiceHub.Framework; +using Microsoft.ServiceHub.Framework.Services; +using Microsoft.VisualStudio.Composition; +using Microsoft.VisualStudio.Threading; +using Microsoft.VisualStudio.Utilities.ServiceBroker; + +namespace Microsoft.CodeAnalysis.LanguageServer.BrokeredServices; +internal class BrokeredServiceContainer : GlobalBrokeredServiceContainer +{ + public BrokeredServiceContainer(TraceSource traceSource) + : base(ImmutableDictionary.Empty, isClientOfExclusiveServer: false, joinableTaskFactory: null, traceSource) + { + } + + public override IReadOnlyDictionary LocalUserCredentials + => ImmutableDictionary.Empty; + + /// + internal new void RegisterServices(IReadOnlyDictionary services) + => base.RegisterServices(services); + + /// + internal new void UnregisterServices(IEnumerable services) + => base.UnregisterServices(services); + + internal ImmutableDictionary GetRegisteredServices() + => RegisteredServices; + + internal static async Task CreateAsync(ExportProvider exportProvider, CancellationToken cancellationToken) + { + var traceListener = exportProvider.GetExportedValue(); + var container = new BrokeredServiceContainer(traceListener.Source); + + container.ProfferIntrinsicService( + FrameworkServices.Authorization, + new ServiceRegistration(VisualStudio.Shell.ServiceBroker.ServiceAudience.Local, null, allowGuestClients: true), + (moniker, options, serviceBroker, cancellationToken) => new(new NoOpAuthorizationService())); + + var mefServiceBroker = exportProvider.GetExportedValue(); + mefServiceBroker.SetContainer(container); + + // Register local mef services. + await mefServiceBroker.RegisterAndProfferServicesAsync(cancellationToken); + + // Register the desired remote services + container.RegisterServices(Descriptors.RemoteServicesToRegister); + + return container; + } + + private class NoOpAuthorizationService : IAuthorizationService + { + public event EventHandler? CredentialsChanged; + + public event EventHandler? AuthorizationChanged; + + public ValueTask CheckAuthorizationAsync(ProtectedOperation operation, CancellationToken cancellationToken = default) + { + return new(true); + } + + public ValueTask> GetCredentialsAsync(CancellationToken cancellationToken = default) + { + return new(ImmutableDictionary.Empty); + } + + protected virtual void OnCredentialsChanged(EventArgs args) => this.CredentialsChanged?.Invoke(this, args); + + protected virtual void OnAuthorizationChanged(EventArgs args) => this.AuthorizationChanged?.Invoke(this, args); + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/BrokeredServiceTraceListener.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/BrokeredServiceTraceListener.cs new file mode 100644 index 0000000000000..5dbabf32c5dc0 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/BrokeredServiceTraceListener.cs @@ -0,0 +1,37 @@ +// 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.Composition; +using System.Diagnostics; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.Extensions.Logging; + +namespace Microsoft.CodeAnalysis.LanguageServer.BrokeredServices; + +[Export, Shared] +internal class BrokeredServiceTraceListener : TraceListener +{ + private readonly ILogger _logger; + + public TraceSource Source { get; } + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public BrokeredServiceTraceListener(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(nameof(BrokeredServiceTraceListener)); + Source = new TraceSource("ServiceBroker", SourceLevels.All); + Source.Listeners.Add(this); + } + + public override void Write(string? message) + { + _logger.LogDebug(message); + } + + public override void WriteLine(string? message) + { + _logger.LogDebug(message); + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/MefServiceBrokerOfExportedServices.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/MefServiceBrokerOfExportedServices.cs new file mode 100644 index 0000000000000..2e6436f80af37 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/MefServiceBrokerOfExportedServices.cs @@ -0,0 +1,34 @@ +// 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.Composition; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.VisualStudio.Shell.ServiceBroker; +using Microsoft.VisualStudio.Utilities.ServiceBroker; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.BrokeredServices; + +[Export, Shared] +internal class MefServiceBrokerOfExportedServices : ServiceBrokerOfExportedServices +{ + private Task? _containerTask; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public MefServiceBrokerOfExportedServices() + { + } + + public void SetContainer(GlobalBrokeredServiceContainer container) + { + _containerTask = Task.FromResult(container); + } + + protected override Task GetBrokeredServiceContainerAsync(CancellationToken cancellationToken) + { + Contract.ThrowIfNull(_containerTask, $"{nameof(SetContainer)} should have already been called."); + return _containerTask; + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/ServiceBrokerConnectHandler.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/ServiceBrokerConnectHandler.cs new file mode 100644 index 0000000000000..ea8dc39c00e83 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/ServiceBrokerConnectHandler.cs @@ -0,0 +1,47 @@ +// 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.Composition; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; + +namespace Microsoft.CodeAnalysis.LanguageServer.BrokeredServices; + +[ExportCSharpVisualBasicStatelessLspService(typeof(ServiceBrokerConnectHandler)), Shared] +[Method("serviceBroker/connect")] +internal class ServiceBrokerConnectHandler : ILspServiceNotificationHandler +{ + private readonly ServiceBrokerFactory _serviceBrokerFactory; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ServiceBrokerConnectHandler(ServiceBrokerFactory serviceBrokerFactory) + { + _serviceBrokerFactory = serviceBrokerFactory; + } + + public bool MutatesSolutionState => false; + + public bool RequiresLSPSolution => false; + + Task INotificationHandler.HandleNotificationAsync(NotificationParams request, RequestContext requestContext, CancellationToken cancellationToken) + { + return _serviceBrokerFactory.CreateAndConnectAsync(request.PipeName); + } + + [DataContract] + private class NotificationParams + { + [DataMember(Name = "pipeName")] + public required string PipeName { get; set; } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/ServiceBrokerFactory.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/ServiceBrokerFactory.cs new file mode 100644 index 0000000000000..001b513aeb641 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/ServiceBrokerFactory.cs @@ -0,0 +1,94 @@ +// 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.ComponentModel.Composition; +using System.ComponentModel.Composition.Hosting; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services.HelloWorld; +using Microsoft.CodeAnalysis.LanguageServer.StarredSuggestions; +using Microsoft.Extensions.Logging; +using Microsoft.ServiceHub.Framework; +using Microsoft.VisualStudio.Composition; +using Microsoft.VisualStudio.Shell.ServiceBroker; +using Roslyn.Utilities; +using ExportProvider = Microsoft.VisualStudio.Composition.ExportProvider; + +namespace Microsoft.CodeAnalysis.LanguageServer.BrokeredServices; + +/// +/// Exports an for convenient and potentially cross-IDE importing by other features. +/// +/// +/// Each import site gets its own instance to match the behavior of calling +/// which returns a private instance for everyone. +/// This is observable to callers in a few ways, including that they only get the events +/// based on their own service queries. +/// MEF will dispose of each instance as its lifetime comes to an end. +/// +#pragma warning disable RS0030 // This is intentionally using System.ComponentModel.Composition for compatibility with MEF service broker. +[Export] +internal class ServiceBrokerFactory +{ + private BrokeredServiceContainer? _container; + private readonly ExportProvider _exportProvider; + private Task _bridgeCompletionTask; + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ServiceBrokerFactory(ExportProvider exportProvider) + { + _exportProvider = exportProvider; + _bridgeCompletionTask = Task.CompletedTask; + } + + /// + /// Returns a full-access service broker, but will throw if we haven't yet connected to the Dev Kit broker. + /// + [Export(typeof(SVsFullAccessServiceBroker))] + public IServiceBroker FullAccessServiceBroker => this.GetRequiredServiceBrokerContainer().GetFullAccessServiceBroker(); + + /// + /// Returns a full-access service broker, but will return null if we haven't yet connected to the Dev Kit broker. + /// + public IServiceBroker? TryGetFullAccessServiceBroker() => _container?.GetFullAccessServiceBroker(); + + public BrokeredServiceContainer GetRequiredServiceBrokerContainer() + { + Contract.ThrowIfNull(_container); + return _container; + } + + /// + /// Creates a service broker instance without connecting via a pipe to another process. + /// + public async Task CreateAsync() + { + Contract.ThrowIfFalse(_container == null, "We should only create one container."); + + _container = await BrokeredServiceContainer.CreateAsync(_exportProvider, _cancellationTokenSource.Token); + } + + public async Task CreateAndConnectAsync(string brokeredServicePipeName) + { + await CreateAsync(); + + var bridgeProvider = _exportProvider.GetExportedValue(); + _bridgeCompletionTask = bridgeProvider.SetupBrokeredServicesBridgeAsync(brokeredServicePipeName, _container!, _cancellationTokenSource.Token); + + await _exportProvider.GetExportedValue().SayHelloToRemoteServerAsync(_cancellationTokenSource.Token); + } + + public Task ShutdownAndWaitForCompletionAsync() + { + _cancellationTokenSource.Cancel(); + + // Return the task we created when we created the bridge; if we never started it in the first place, we'll just return the + // completed task set in the constructor, so the waiter no-ops. + return _bridgeCompletionTask; + } +} +#pragma warning restore RS0030 // Do not used banned APIs diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/BrokeredServiceBridgeManifest/BrokeredServiceBridgeManifestService.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/BrokeredServiceBridgeManifest/BrokeredServiceBridgeManifestService.cs new file mode 100644 index 0000000000000..e98dd47d2f94e --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/BrokeredServiceBridgeManifest/BrokeredServiceBridgeManifestService.cs @@ -0,0 +1,59 @@ +// 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.ComponentModel.Composition; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.Extensions.Logging; +using Microsoft.ServiceHub.Framework; +using Microsoft.VisualStudio.Shell.ServiceBroker; + +namespace Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services.BrokeredServiceBridgeManifest; + +#pragma warning disable RS0030 // This is intentionally using System.ComponentModel.Composition for compatibility with MEF service broker. +[ExportBrokeredService(MonikerName, MonikerVersion, Audience = ServiceAudience.Local)] +internal class BrokeredServiceBridgeManifest : IBrokeredServiceBridgeManifest, IExportedBrokeredService +{ + internal const string MonikerName = "Microsoft.VisualStudio.Server.IBrokeredServiceBridgeManifest"; + internal const string MonikerVersion = "0.1"; + private static readonly ServiceMoniker s_serviceMoniker = new ServiceMoniker(MonikerName, new Version(MonikerVersion)); + private static readonly ServiceRpcDescriptor s_serviceDescriptor = new ServiceJsonRpcDescriptor( + s_serviceMoniker, + ServiceJsonRpcDescriptor.Formatters.UTF8, + ServiceJsonRpcDescriptor.MessageDelimiters.HttpLikeHeaders); + + private readonly ServiceBrokerFactory _serviceBrokerFactory; + private readonly ILogger _logger; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public BrokeredServiceBridgeManifest(ServiceBrokerFactory serviceBrokerFactory, ILoggerFactory loggerFactory) + { + _serviceBrokerFactory = serviceBrokerFactory; + _logger = loggerFactory.CreateLogger(); + } + + public ServiceRpcDescriptor Descriptor => s_serviceDescriptor; + + /// + /// Returns a subset of services registered to Microsoft.VisualStudio.Code.Server container that are proferred by the Language Server process. + /// + public ValueTask> GetAvailableServicesAsync(CancellationToken cancellationToken) + { + var services = (IReadOnlyCollection)_serviceBrokerFactory.GetRequiredServiceBrokerContainer().GetRegisteredServices() + .Select(s => s.Key) + .Where(s => s.Name.StartsWith("Microsoft.CodeAnalysis.LanguageServer.", StringComparison.Ordinal) || + s.Name.StartsWith("Microsoft.VisualStudio.LanguageServer.", StringComparison.Ordinal) || + s.Name.StartsWith("Microsoft.VisualStudio.LanguageServices.", StringComparison.Ordinal)) + .ToImmutableArray(); + _logger.LogDebug($"Proffered services: {string.Join(',', services.Select(s => s.ToString()))}"); + return ValueTask.FromResult(services); + } + + public Task InitializeAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} +#pragma warning restore RS0030 // Do not used banned APIs diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/BrokeredServiceBridgeManifest/IBrokeredServiceBridgeManifest.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/BrokeredServiceBridgeManifest/IBrokeredServiceBridgeManifest.cs new file mode 100644 index 0000000000000..8b4df206e01d9 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/BrokeredServiceBridgeManifest/IBrokeredServiceBridgeManifest.cs @@ -0,0 +1,25 @@ +// 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 Microsoft.ServiceHub.Framework; +using Microsoft.VisualStudio.Utilities.ServiceBroker; + +namespace Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services.BrokeredServiceBridgeManifest; + +/// +/// Defines a service to be used by the remote party to determine which services from this container +/// it should add to its container. This is useful in the case where the remote party connects to other processes +/// that proffer the same services as we do (e.g. intrinsic services). +/// Both are proffered as and therefore conflict. +/// +internal interface IBrokeredServiceBridgeManifest +{ + /// + /// Returns services that the container wishes to expose across the bridge. + /// + /// + /// + ValueTask> GetAvailableServicesAsync(CancellationToken cancellationToken); +} + diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/Definitions/IProjectInitializationStatusService.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/Definitions/IProjectInitializationStatusService.cs new file mode 100644 index 0000000000000..5b86a2e9c2d4e --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/Definitions/IProjectInitializationStatusService.cs @@ -0,0 +1,20 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using StreamJsonRpc; + +namespace Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services.Definitions; +/// +/// Copied from https://devdiv.visualstudio.com/DevDiv/_git/CPS?path=/src/Microsoft.VisualStudio.ProjectSystem.Server/BrokerServices/IProjectInitializationStatusService.cs +/// +internal interface IProjectInitializationStatusService +{ + [JsonRpcMethod("subscribeInitializationCompletion")] + ValueTask SubscribeInitializationCompletionAsync(IObserver observer, CancellationToken cancellationToken); +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/Definitions/ProjectInitializationCompleteState.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/Definitions/ProjectInitializationCompleteState.cs new file mode 100644 index 0000000000000..19f1de709f297 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/Definitions/ProjectInitializationCompleteState.cs @@ -0,0 +1,32 @@ +// 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.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using System.Xml.Linq; + +namespace Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services.Definitions; +/// +/// Copied from https://devdiv.visualstudio.com/DevDiv/_git/CPS?path=/src/Microsoft.VisualStudio.ProjectSystem.Server/ProjectInitializationCompletionState.cs +/// +[DataContract] +internal class ProjectInitializationCompletionState +{ + [DataMember(IsRequired = true, EmitDefaultValue = false, Name = "environmentStateVersion")] + public int EnvironmentStateVersion { get; set; } + + [DataMember(IsRequired = false, EmitDefaultValue = true, Name = "projectsLoadedCount")] + public int ProjectsLoadedCount { get; set; } + + [DataMember(IsRequired = false, EmitDefaultValue = true, Name = "projectsFailedCount")] + public int ProjectsFailedCount { get; set; } + + [DataMember(IsRequired = true, EmitDefaultValue = false, Name = "stateUpdateVersion")] + public int StateUpdateVersion { get; set; } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/Descriptors.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/Descriptors.cs new file mode 100644 index 0000000000000..a29bef7e51813 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/Descriptors.cs @@ -0,0 +1,56 @@ +// 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 Microsoft.CodeAnalysis.BrokeredServices; +using Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services.HelloWorld; +using Microsoft.ServiceHub.Framework; +using Microsoft.VisualStudio.Shell.ServiceBroker; +using Microsoft.VisualStudio.Utilities.ServiceBroker; +using Nerdbank.Streams; + +namespace Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services; + +internal class Descriptors +{ + // Descriptors for remote services. + // If adding services here, make sure to update RemoteServicesToRegister. + + public static readonly ServiceRpcDescriptor RemoteHelloWorldService = CreateDescriptor(new("helloServiceHubDotNetHost", new Version("0.1"))); + public static readonly ServiceRpcDescriptor RemoteModelService = CreateDescriptor(new("vs-intellicode-base-models", new Version("0.1"))); + + /// + /// See https://devdiv.visualstudio.com/DevDiv/_git/CPS?path=/src/Microsoft.VisualStudio.ProjectSystem.Server/BrokerServices/ProjectInitializationStatusServiceDescriptor.cs + /// + public static readonly ServiceRpcDescriptor RemoteProjectInitializationStatusService = new ServiceJsonRpcDescriptor( + new("Microsoft.VisualStudio.ProjectSystem.ProjectInitializationStatusService", new Version(0, 1)), + clientInterface: null, + ServiceJsonRpcDescriptor.Formatters.MessagePack, + ServiceJsonRpcDescriptor.MessageDelimiters.BigEndianInt32LengthHeader, + new MultiplexingStream.Options { ProtocolMajorVersion = 3 }); + + // Descriptors for local services. + + public static readonly ServiceRpcDescriptor LocalHelloWorldService = CreateDescriptor(new(HelloWorldService.MonikerName, new Version(HelloWorldService.MonikerVersion))); + + /// + /// The set of remote services that we register to our container. + /// + /// + /// Note that while today we only support static registration of services in the remote process it would be possible to implement dynamic registration + /// if we read the remote brokered service manifest. + /// + public static ImmutableDictionary RemoteServicesToRegister = new Dictionary + { + { RemoteHelloWorldService.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) }, + { RemoteModelService.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) }, + { RemoteProjectInitializationStatusService.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) }, + { BrokeredServiceDescriptors.SolutionSnapshotProvider.Moniker, new ServiceRegistration(ServiceAudience.Local, null, allowGuestClients: false) }, + }.ToImmutableDictionary(); + + public static ServiceJsonRpcDescriptor CreateDescriptor(ServiceMoniker serviceMoniker) => new( + serviceMoniker, + ServiceJsonRpcDescriptor.Formatters.UTF8, + ServiceJsonRpcDescriptor.MessageDelimiters.HttpLikeHeaders); +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/HelloWorld/HelloWorldService.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/HelloWorld/HelloWorldService.cs new file mode 100644 index 0000000000000..fd85111627ff8 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/HelloWorld/HelloWorldService.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.ComponentModel.Composition; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.ServiceHub.Framework; +using Microsoft.VisualStudio.Shell.ServiceBroker; + +namespace Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services.HelloWorld; + +#pragma warning disable RS0030 // This is intentionally using System.ComponentModel.Composition for compatibility with MEF service broker. +[ExportBrokeredService(MonikerName, MonikerVersion, Audience = ServiceAudience.AllClientsIncludingGuests | ServiceAudience.Local)] +internal class HelloWorldService : IHelloWorld, IExportedBrokeredService +{ + internal const string MonikerName = "Microsoft.CodeAnalysis.LanguageServer.IHelloWorld"; + internal const string MonikerVersion = "0.1"; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public HelloWorldService() + { + } + + public ServiceRpcDescriptor Descriptor => Descriptors.LocalHelloWorldService; + + public Task InitializeAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task SayHelloAsync(string name, CancellationToken cancellationToken) + { + return Task.FromResult($"Greetings {name}, welcome to the C# party :)"); + } + + public Task CallMeAsync(ServiceMoniker serviceMoniker, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} +#pragma warning restore RS0030 // Do not used banned APIs diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/HelloWorld/IHelloWorld.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/HelloWorld/IHelloWorld.cs new file mode 100644 index 0000000000000..1dc738ce759f5 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/HelloWorld/IHelloWorld.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 Microsoft.ServiceHub.Framework; +using StreamJsonRpc; + +namespace Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services.HelloWorld; + +/// +/// A simple interface used for sanity checking that the brokered service bridge works. +/// There is an implementation of the same service on the green side that we can talk to. +/// +internal interface IHelloWorld +{ + [JsonRpcMethod("sayHello")] + Task SayHelloAsync(string name, CancellationToken cancellationToken); + + [JsonRpcMethod("callMe")] + Task CallMeAsync(ServiceMoniker serviceMoniker, CancellationToken cancellationToken); +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/HelloWorld/RemoteHelloWorldProvider.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/HelloWorld/RemoteHelloWorldProvider.cs new file mode 100644 index 0000000000000..af6a15e5a79a2 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/BrokeredServices/Services/HelloWorld/RemoteHelloWorldProvider.cs @@ -0,0 +1,73 @@ +// 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.ComponentModel.Composition; +using System.Diagnostics; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.Extensions.Logging; +using Microsoft.ServiceHub.Framework; +using Microsoft.VisualStudio.Shell.ServiceBroker; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services.HelloWorld; + +#pragma warning disable RS0030 // This is intentionally using System.ComponentModel.Composition for compatibility with MEF service broker. +[Export] +internal class RemoteHelloWorldProvider +{ + private readonly IServiceBroker _serviceBroker; + private readonly TaskCompletionSource _serviceAvailable = new(); + private readonly ILogger _logger; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public RemoteHelloWorldProvider([Import(typeof(SVsFullAccessServiceBroker))] IServiceBroker serviceBroker, ILoggerFactory loggerFactory) + { + _serviceBroker = serviceBroker; + _logger = loggerFactory.CreateLogger(); + + _serviceBroker.AvailabilityChanged += ServiceBroker_AvailabilityChanged; + } + + public async Task SayHelloToRemoteServerAsync(CancellationToken cancellationToken) + { + var response = await TryGetHelloWorldAsync(cancellationToken); + if (!response) + { + await _serviceAvailable.Task; + Contract.ThrowIfFalse(await TryGetHelloWorldAsync(cancellationToken), "Was not able to get hello world response from remote"); + } + } + + private void ServiceBroker_AvailabilityChanged(object? sender, BrokeredServicesChangedEventArgs e) + { + if (e.ImpactedServices.Contains(Descriptors.RemoteHelloWorldService.Moniker)) + _serviceAvailable.SetResult(); + } + + private async Task TryGetHelloWorldAsync(CancellationToken cancellationToken) + { + var helloWorldService = await _serviceBroker.GetProxyAsync(Descriptors.RemoteHelloWorldService, cancellationToken); + using (helloWorldService as IDisposable) + { + if (helloWorldService is not null) + { + try + { + var response = await helloWorldService.SayHelloAsync("C#", cancellationToken); + _logger.LogDebug("Response from remote: " + response); + return true; + } + catch (Exception ex) + { + _logger.LogError($"Got exception when invoking callback function:{ex}"); + } + } + } + + return false; + } +} + +#pragma warning restore RS0030 // Do not used banned APIs diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Contracts/ITelemetryReporter.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Contracts/ITelemetryReporter.cs new file mode 100644 index 0000000000000..1ff7c10d1420c --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Contracts/ITelemetryReporter.cs @@ -0,0 +1,16 @@ +// 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; + +namespace Microsoft.CodeAnalysis.Contracts.Telemetry; + +internal interface ITelemetryReporter : IDisposable +{ + void InitializeSession(string telemetryLevel, string? sessionId, bool isDefaultSession); + void Log(string name, ImmutableDictionary properties); + void LogBlockStart(string eventName, int kind, int blockId); + void LogBlockEnd(int blockId, ImmutableDictionary properties, CancellationToken cancellationToken); + void ReportFault(string eventName, string description, int logLevel, bool forceDump, int processId, Exception exception); +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/CustomExportAssemblyLoader.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/CustomExportAssemblyLoader.cs new file mode 100644 index 0000000000000..9fb2d2657b0ea --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/CustomExportAssemblyLoader.cs @@ -0,0 +1,99 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Reflection; +using Microsoft.VisualStudio.Composition; + +namespace Microsoft.CodeAnalysis.LanguageServer; + +internal class CustomExportAssemblyLoader : IAssemblyLoader +{ + /// + /// Cache assemblies that are already loaded by AssemblyName comparison + /// + private readonly Dictionary _loadedAssemblies = new Dictionary(AssemblyNameComparer.Instance); + + /// + /// Base directory to search for if initial load fails + /// + private readonly string _baseDirectory; + + public CustomExportAssemblyLoader(string baseDirectory) + { + _baseDirectory = baseDirectory; + } + + public Assembly LoadAssembly(AssemblyName assemblyName) + { + Assembly? value; + lock (_loadedAssemblies) + { + _loadedAssemblies.TryGetValue(assemblyName, out value); + } + + if (value == null) + { + // Attempt to load the assembly normally, but fall back to Assembly.LoadFrom in the base + // directory if the assembly load fails + try + { + value = Assembly.Load(assemblyName); + } + catch (FileNotFoundException) when (assemblyName.Name is not null) + { + var filePath = Path.Combine(_baseDirectory, assemblyName.Name) + + (assemblyName.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) + ? "" + : ".dll"); + + value = Assembly.LoadFrom(filePath); + + if (value is null) + { + throw; + } + } + + lock (_loadedAssemblies) + { + _loadedAssemblies[assemblyName] = value; + return value; + } + } + + return value; + } + + public Assembly LoadAssembly(string assemblyFullName, string? codeBasePath) + { + var assemblyName = new AssemblyName(assemblyFullName); + return LoadAssembly(assemblyName); + } + + private class AssemblyNameComparer : IEqualityComparer + { + public static AssemblyNameComparer Instance = new AssemblyNameComparer(); + + public bool Equals(AssemblyName? x, AssemblyName? y) + { + if (x == null && y == null) + { + return true; + } + + if (x == null || y == null) + { + return false; + } + + return x.Name == y.Name; + } + + public int GetHashCode([DisallowNull] AssemblyName obj) + { + return obj.Name?.GetHashCode(StringComparison.Ordinal) ?? 0; + } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/ExportProviderBuilder.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/ExportProviderBuilder.cs new file mode 100644 index 0000000000000..3afa2dcc69c47 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/ExportProviderBuilder.cs @@ -0,0 +1,108 @@ +// 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.Runtime.Loader; +using Microsoft.CodeAnalysis.LanguageServer.Logging; +using Microsoft.CodeAnalysis.LanguageServer.Services; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Composition; + +namespace Microsoft.CodeAnalysis.LanguageServer; + +internal sealed class ExportProviderBuilder +{ + public static async Task CreateExportProviderAsync(IEnumerable extensionAssemblyPaths, string? sharedDependenciesPath, ILoggerFactory loggerFactory) + { + var logger = loggerFactory.CreateLogger(); + + var baseDirectory = AppContext.BaseDirectory; + + var resolver = new Resolver(new CustomExportAssemblyLoader(baseDirectory)); + + // Load any Roslyn assemblies from the extension directory + var assemblyPaths = Directory.EnumerateFiles(baseDirectory, "Microsoft.CodeAnalysis*.dll"); + assemblyPaths = assemblyPaths.Concat(Directory.EnumerateFiles(baseDirectory, "Microsoft.ServiceHub*.dll")); + + // Temporarily explicitly load the dlls we want to add to the MEF composition. This is due to a runtime bug + // in the 7.0.4 runtime where the APIs MEF uses to load assemblies break with R2R assemblies. + // See https://github.com/dotnet/runtime/issues/83526 + // + // Once a newer version of the runtime is widely available, we can remove this. + foreach (var path in assemblyPaths) + { + Assembly.LoadFrom(path); + } + + var discovery = PartDiscovery.Combine( + resolver, + new AttributedPartDiscovery(resolver, isNonPublicSupported: true), // "NuGet MEF" attributes (Microsoft.Composition) + new AttributedPartDiscoveryV1(resolver)); + + var assemblies = new List() + { + typeof(ExportProviderBuilder).Assembly + }; + + foreach (var extensionAssemblyPath in extensionAssemblyPaths) + { + if (AssemblyLoadContextWrapper.TryLoadExtension(extensionAssemblyPath, sharedDependenciesPath, logger, out var extensionAssembly)) + { + assemblies.Add(extensionAssembly); + } + } + + // TODO - we should likely cache the catalog so we don't have to rebuild it every time. + var catalog = ComposableCatalog.Create(resolver) + .AddParts(await discovery.CreatePartsAsync(assemblies)) + .AddParts(await discovery.CreatePartsAsync(assemblyPaths)) + .WithCompositionService(); // Makes an ICompositionService export available to MEF parts to import + + // Assemble the parts into a valid graph. + var config = CompositionConfiguration.Create(catalog); + + // Verify we only have expected errors. + ThrowOnUnexpectedErrors(config, logger); + + // Prepare an ExportProvider factory based on this graph. + var exportProviderFactory = config.CreateExportProviderFactory(); + + // Create an export provider, which represents a unique container of values. + // You can create as many of these as you want, but typically an app needs just one. + var exportProvider = exportProviderFactory.CreateExportProvider(); + + // Immediately set the logger factory, so that way it'll be available for the rest of the composition + exportProvider.GetExportedValue().SetFactory(loggerFactory); + + return exportProvider; + } + + private static void ThrowOnUnexpectedErrors(CompositionConfiguration configuration, ILogger logger) + { + // Verify that we have exactly the MEF errors that we expect. If we have less or more this needs to be updated to assert the expected behavior. + // Currently we are expecting the following: + // "----- CompositionError level 1 ------ + // Microsoft.CodeAnalysis.ExternalAccess.Pythia.PythiaSignatureHelpProvider.ctor(implementation): expected exactly 1 export matching constraints: + // Contract name: Microsoft.CodeAnalysis.ExternalAccess.Pythia.Api.IPythiaSignatureHelpProviderImplementation + // TypeIdentityName: Microsoft.CodeAnalysis.ExternalAccess.Pythia.Api.IPythiaSignatureHelpProviderImplementation + // but found 0. + // part definition Microsoft.CodeAnalysis.ExternalAccess.Pythia.PythiaSignatureHelpProvider + var erroredParts = configuration.CompositionErrors.FirstOrDefault()?.SelectMany(error => error.Parts).Select(part => part.Definition.Type.Name) ?? Enumerable.Empty(); + var expectedErroredParts = new string[] { "PythiaSignatureHelpProvider" }; + if (erroredParts.Count() != expectedErroredParts.Length || !erroredParts.All(part => expectedErroredParts.Contains(part))) + { + try + { + configuration.ThrowOnErrors(); + } + catch (CompositionFailedException ex) + { + // The ToString for the composition failed exception doesn't output a nice set of errors by default, so log it separately here. + logger.LogError($"Encountered errors in the MEF composition:{Environment.NewLine}{ex.ErrorsAsString}"); + throw; + } + } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostServicesProvider.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostServicesProvider.cs new file mode 100644 index 0000000000000..78ede56831cde --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostServicesProvider.cs @@ -0,0 +1,26 @@ +// 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.Composition; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.VisualStudio.Composition; + +namespace Microsoft.CodeAnalysis.LanguageServer; + +/// +/// A simple type to provide a single copy of for the MEF composition. +/// +[Export(typeof(HostServicesProvider)), Shared] +internal class HostServicesProvider +{ + public HostServices HostServices { get; } + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public HostServicesProvider(ExportProvider exportProvider) + { + HostServices = VisualStudioMefHostServices.Create(exportProvider); + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ExtensionManager.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ExtensionManager.cs new file mode 100644 index 0000000000000..86d6787244453 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ExtensionManager.cs @@ -0,0 +1,41 @@ +// 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.Composition; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Extensions; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.Extensions.Logging; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; + +[ExportWorkspaceService(typeof(IExtensionManager), ServiceLayer.Host), Shared] +internal class ExtensionManager : IExtensionManager +{ + private readonly ILogger _logger; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ExtensionManager(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(nameof(ExtensionManager)); + } + + public bool CanHandleException(object provider, Exception exception) => true; + + public void HandleException(object provider, Exception exception) + { + _logger.Log(LogLevel.Error, exception, $"{provider.GetType().ToString()} threw an exception."); + } + + public bool IsDisabled(object provider) + { + // We don't have an UI to allow disabling yet + return false; + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/DelegatingFileChangeWatcher.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/DelegatingFileChangeWatcher.cs new file mode 100644 index 0000000000000..ca98eb5944665 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/DelegatingFileChangeWatcher.cs @@ -0,0 +1,58 @@ +// 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.Composition; +using Microsoft.CodeAnalysis.LanguageServer.LanguageServer; +using Microsoft.CodeAnalysis.ProjectSystem; +using Microsoft.Extensions.Logging; +using Microsoft.CodeAnalysis.Host.Mef; +using Roslyn.Utilities; +using Microsoft.CodeAnalysis.Shared.TestHooks; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.FileWatching; + +/// +/// A MEF export for . This checks if we're able to create an if the client supports +/// file watching. If we do, we create that and delegate to it. Otherwise we use a . +/// +/// +/// LSP clients don't always support file watching; this allows us to be flexible and use it when we can, but fall back to something else if we can't. +/// +[Export(typeof(IFileChangeWatcher)), Shared] +internal sealed class DelegatingFileChangeWatcher : IFileChangeWatcher +{ + private readonly ILoggerFactory _loggerFactory; + private readonly IAsynchronousOperationListenerProvider _asynchronousOperationListenerProvider; + private readonly Lazy _underlyingFileWatcher; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public DelegatingFileChangeWatcher(ILoggerFactory loggerFactory, IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider) + { + _loggerFactory = loggerFactory; + _asynchronousOperationListenerProvider = asynchronousOperationListenerProvider; + _underlyingFileWatcher = new Lazy(CreateFileWatcher); + } + + private IFileChangeWatcher CreateFileWatcher() + { + // Do we already have an LSP client that we can confirm works for us? + var instance = LanguageServerHost.Instance; + + if (instance != null && LspFileChangeWatcher.SupportsLanguageServerHost(instance)) + { + return new LspFileChangeWatcher(instance, _asynchronousOperationListenerProvider); + } + else + { + _loggerFactory.CreateLogger().LogWarning("We are unable to use LSP file watching; falling back to our in-process watcher."); + return new SimpleFileChangeWatcher(); + } + } + + public IFileChangeContext CreateContext(params WatchedDirectory[] watchedDirectories) + { + return _underlyingFileWatcher.Value.CreateContext(watchedDirectories); + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspContractTypes.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspContractTypes.cs new file mode 100644 index 0000000000000..8633e910ab075 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspContractTypes.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.Runtime.Serialization; + +namespace Microsoft.VisualStudio.LanguageServer.Protocol; + +[DataContract] +internal class DidChangeWatchedFilesRegistrationOptions +{ + [DataMember(Name = "watchers")] + public required FileSystemWatcher[] Watchers { get; set; } +} + +[DataContract] +internal class FileSystemWatcher +{ + [DataMember(Name = "globPattern")] + public required RelativePattern GlobPattern { get; set; } + + [DataMember(Name = "kind")] + public WatchKind? Kind { get; set; } +} + +[DataContract] +internal class RelativePattern +{ + [DataMember(Name = "baseUri")] + public required string BaseUri { get; set; } + + [DataMember(Name = "pattern")] + public required string Pattern { get; set; } +} + +// The LSP specification has a spelling error in the protocol, but Microsoft.VisualStudio.LanguageServer.Protocol +// didn't carry that error along. This corrects that. +[DataContract] +internal class UnregistrationParamsWithMisspelling +{ + [DataMember(Name = "unregisterations")] + public required Unregistration[] Unregistrations { get; set; } +} + +[Flags] +internal enum WatchKind +{ + Create = 1, + Change = 2, + Delete = 4 +} \ No newline at end of file diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspDidChangeWatchedFilesHandler.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspDidChangeWatchedFilesHandler.cs new file mode 100644 index 0000000000000..04faf2d4433a3 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspDidChangeWatchedFilesHandler.cs @@ -0,0 +1,33 @@ +// 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.Composition; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.FileWatching; + +[ExportCSharpVisualBasicStatelessLspService(typeof(LspDidChangeWatchedFilesHandler)), Shared] +[Method("workspace/didChangeWatchedFiles")] +internal class LspDidChangeWatchedFilesHandler : ILspServiceNotificationHandler +{ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public LspDidChangeWatchedFilesHandler() + { + } + + public bool MutatesSolutionState => false; + public bool RequiresLSPSolution => false; + + Task INotificationHandler.HandleNotificationAsync(DidChangeWatchedFilesParams request, RequestContext requestContext, CancellationToken cancellationToken) + { + NotificationRaised?.Invoke(this, request); + return Task.CompletedTask; + } + + public event EventHandler? NotificationRaised; +} \ No newline at end of file diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspFileChangeWatcher.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspFileChangeWatcher.cs new file mode 100644 index 0000000000000..aec9573396aca --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspFileChangeWatcher.cs @@ -0,0 +1,248 @@ +// 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 Microsoft.CodeAnalysis.LanguageServer.LanguageServer; +using Microsoft.CodeAnalysis.ProjectSystem; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using System.Collections.Immutable; +using Roslyn.Utilities; +using FileSystemWatcher = Microsoft.VisualStudio.LanguageServer.Protocol.FileSystemWatcher; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.FileWatching; + +/// +/// An implementation of that delegates file watching through the LSP protocol to the client. +/// +internal sealed class LspFileChangeWatcher : IFileChangeWatcher +{ + private readonly LspDidChangeWatchedFilesHandler _didChangeWatchedFilesHandler; + private readonly IClientLanguageServerManager _clientLanguageServerManager; + private readonly IAsynchronousOperationListener _asynchronousOperationListener; + + public LspFileChangeWatcher(LanguageServerHost languageServerHost, IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider) + { + _didChangeWatchedFilesHandler = languageServerHost.GetRequiredLspService(); + _clientLanguageServerManager = languageServerHost.GetRequiredLspService(); + _asynchronousOperationListener = asynchronousOperationListenerProvider.GetListener(FeatureAttribute.Workspace); + + Contract.ThrowIfFalse(SupportsLanguageServerHost(languageServerHost)); + } + + public static bool SupportsLanguageServerHost(LanguageServerHost languageServerHost) + { + // We can only use the LSP client for doing file watching if we support dynamic registration for it + var clientCapabilitiesProvider = languageServerHost.GetRequiredLspService(); + return clientCapabilitiesProvider.GetClientCapabilities().Workspace?.DidChangeWatchedFiles?.DynamicRegistration ?? false; + } + + public IFileChangeContext CreateContext(params WatchedDirectory[] watchedDirectories) + { + return new FileChangeContext(watchedDirectories.ToImmutableArray(), this); + } + + private class FileChangeContext : IFileChangeContext + { + private readonly ImmutableArray _watchedDirectories; + private readonly LspFileChangeWatcher _lspFileChangeWatcher; + + /// + /// The registration for the directory being watched in this context, if some were given. + /// + private readonly LspFileWatchRegistration? _directoryWatchRegistration; + + /// + /// A lock to guard updates to . Using a reader/writer lock since file change notifications can be pretty chatty + /// and so we want to be able to process changes as fast as possible. + /// + private readonly ReaderWriterLockSlim _watchedFilesLock = new ReaderWriterLockSlim(); + + /// + /// The list of file paths we're watching manually that were outside the directories being watched. The count in this case counts + /// the number of + /// + private readonly Dictionary _watchedFiles = new Dictionary(StringComparer.Ordinal); + + public FileChangeContext(ImmutableArray watchedDirectories, LspFileChangeWatcher lspFileChangeWatcher) + { + _watchedDirectories = watchedDirectories; + _lspFileChangeWatcher = lspFileChangeWatcher; + + // If we have any watched directories, then watch those directories directly + if (watchedDirectories.Any()) + { + var directoryWatches = watchedDirectories.Select(d => new FileSystemWatcher + { + GlobPattern = new RelativePattern + { + BaseUri = d.Path, + Pattern = d.ExtensionFilter is not null ? "**/*" + d.ExtensionFilter : "**/*" + } + }).ToArray(); + + _directoryWatchRegistration = new LspFileWatchRegistration(lspFileChangeWatcher, directoryWatches); + } + + _lspFileChangeWatcher._didChangeWatchedFilesHandler.NotificationRaised += WatchedFilesHandler_OnNotificationRaised; + } + + private void WatchedFilesHandler_OnNotificationRaised(object? sender, DidChangeWatchedFilesParams e) + { + foreach (var changedFile in e.Changes) + { + var filePath = changedFile.Uri.LocalPath; + + // Unfortunately the LSP protocol doesn't give us any hint of which of the file watches we might have sent to the client + // was the one that registered for this change, so we have to check paths to see if this one we should respond to. + if (WatchedDirectory.FilePathCoveredByWatchedDirectories(_watchedDirectories, filePath, StringComparison.Ordinal)) + { + FileChanged?.Invoke(this, filePath); + } + else + { + bool isFileWatched; + using (_watchedFilesLock.DisposableRead()) + { + isFileWatched = _watchedFiles.ContainsKey(filePath); + } + + if (isFileWatched) + FileChanged?.Invoke(this, filePath); + } + } + } + + public event EventHandler? FileChanged; + + public void Dispose() + { + _lspFileChangeWatcher._didChangeWatchedFilesHandler.NotificationRaised -= WatchedFilesHandler_OnNotificationRaised; + _directoryWatchRegistration?.Dispose(); + } + + public IWatchedFile EnqueueWatchingFile(string filePath) + { + // If we already have this file under our path, we may not have to do additional watching + if (WatchedDirectory.FilePathCoveredByWatchedDirectories(_watchedDirectories, filePath, StringComparison.OrdinalIgnoreCase)) + return NoOpWatchedFile.Instance; + + // Record that we're now watching this file + using (_watchedFilesLock.DisposableWrite()) + { + _watchedFiles.TryGetValue(filePath, out var existingWatches); + _watchedFiles[filePath] = existingWatches + 1; + } + + var fileSystemWatcher = new FileSystemWatcher() + { + // TODO: figure out how I just can do an absolute path watch + GlobPattern = new RelativePattern + { + BaseUri = Path.GetDirectoryName(filePath)!, + Pattern = Path.GetFileName(filePath) + } + }; + + return new WatchedFile(filePath, new LspFileWatchRegistration(_lspFileChangeWatcher, fileSystemWatcher), this); + } + + private void RemoveFileFromWatchList(string filePath) + { + // Record that we're no longer watching this file + using (_watchedFilesLock.DisposableWrite()) + { + var existingWatches = _watchedFiles[filePath]; + if (existingWatches == 1) + _watchedFiles.Remove(filePath); + else + _watchedFiles[filePath] = existingWatches - 1; + } + } + + private class WatchedFile : IWatchedFile + { + private readonly string _filePath; + private readonly LspFileWatchRegistration _fileWatchRegistration; + private readonly FileChangeContext _fileChangeContext; + + public WatchedFile(string filePath, LspFileWatchRegistration fileWatchRegistration, FileChangeContext fileChangeContext) + { + _filePath = filePath; + _fileWatchRegistration = fileWatchRegistration; + _fileChangeContext = fileChangeContext; + } + + public void Dispose() + { + _fileWatchRegistration.Dispose(); + _fileChangeContext.RemoveFileFromWatchList(_filePath); + } + } + } + + /// + /// A small class to represent a registration that is sent to the client that we can cancel later. Since we send + /// registrations asynchronously, this tracks that so we don't send the unregister too early. + /// + private sealed class LspFileWatchRegistration : IDisposable + { + private readonly LspFileChangeWatcher _changeWatcher; + private readonly string _id; + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly Task _registrationTask; + + public LspFileWatchRegistration(LspFileChangeWatcher changeWatcher, params FileSystemWatcher[] fileSystemWatchers) + { + _changeWatcher = changeWatcher; + _id = Guid.NewGuid().ToString(); + _cancellationTokenSource = new CancellationTokenSource(); + + var registrationParams = new RegistrationParams() + { + Registrations = new Registration[] + { + new Registration + { + Id = _id, + Method = "workspace/didChangeWatchedFiles", + RegisterOptions = new DidChangeWatchedFilesRegistrationOptions + { + Watchers = fileSystemWatchers + } + } + } + }; + + var asyncToken = _changeWatcher._asynchronousOperationListener.BeginAsyncOperation(nameof(LspFileWatchRegistration)); + _registrationTask = changeWatcher._clientLanguageServerManager.SendRequestAsync("client/registerCapability", registrationParams, _cancellationTokenSource.Token).AsTask(); + _registrationTask.ReportNonFatalErrorUnlessCancelledAsync(_cancellationTokenSource.Token).CompletesAsyncOperation(asyncToken); + } + + public void Dispose() + { + // We need to remove our file watch. We'll run that once the previous work has completed. We'll run only if the registration completed successfully, since cancellation + // means it never actually made it to the client, and fault would mean it never was actually created. + _cancellationTokenSource.Cancel(); + + var asyncToken = _changeWatcher._asynchronousOperationListener.BeginAsyncOperation(nameof(LspFileWatchRegistration) + "." + nameof(Dispose)); + + _registrationTask.ContinueWith(async _ => + { + var unregistrationParams = new UnregistrationParamsWithMisspelling() + { + Unregistrations = new Unregistration[] + { + new Unregistration() + { + Id = _id, + Method = "workspace/didChangeWatchedFiles" + } + } + }; + + await _changeWatcher._clientLanguageServerManager.SendRequestAsync("client/unregisterCapability", unregistrationParams, CancellationToken.None); + }, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Default).Unwrap().ReportNonFatalErrorAsync().CompletesAsyncOperation(asyncToken); + } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/SimpleFileChangeWatcher.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/SimpleFileChangeWatcher.cs new file mode 100644 index 0000000000000..67efbd5933831 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/SimpleFileChangeWatcher.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.Collections.Immutable; +using Microsoft.CodeAnalysis.ProjectSystem; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.FileWatching; + +/// +/// A trivial implementation of that is built atop the framework . This is used if we can't +/// use the LSP one. +/// +/// +/// This implementation is not remotely efficient, but is available as a fallback implementation. If this needs to regularly be used, then this should get some improvements. +/// +internal sealed class SimpleFileChangeWatcher : IFileChangeWatcher +{ + public IFileChangeContext CreateContext(params WatchedDirectory[] watchedDirectories) + { + return new FileChangeContext(watchedDirectories.ToImmutableArray()); + } + + private class FileChangeContext : IFileChangeContext + { + private readonly ImmutableArray _watchedDirectories; + + /// + /// The directory watchers for the . + /// + private readonly ImmutableArray _directoryFileSystemWatchers; + private readonly ConcurrentSet _individualWatchedFiles = new ConcurrentSet(); + + public FileChangeContext(ImmutableArray watchedDirectories) + { + var watchedDirectoriesBuilder = ImmutableArray.CreateBuilder(watchedDirectories.Length); + var watcherBuilder = ImmutableArray.CreateBuilder(watchedDirectories.Length); + + foreach (var watchedDirectory in watchedDirectories) + { + // If the directory doesn't exist, we can't create a watcher for changes inside of it. In this case, we'll just skip this as a directory + // to watch; any requests for a watch within that directory will still create a one-off watcher for that specific file. That's not likely + // to be an issue in practice: directories that are missing would be things like global reference directories -- if it's not there, we + // probably won't ever see a watch for a file under there later anyways. + if (Directory.Exists(watchedDirectory.Path)) + { + var watcher = new FileSystemWatcher(watchedDirectory.Path); + watcher.IncludeSubdirectories = true; + + if (watchedDirectory.ExtensionFilter != null) + watcher.Filter = '*' + watchedDirectory.ExtensionFilter; + + watcher.Changed += RaiseEvent; + watcher.Created += RaiseEvent; + watcher.Deleted += RaiseEvent; + watcher.Renamed += RaiseEvent; + + watcher.EnableRaisingEvents = true; + + watchedDirectoriesBuilder.Add(watchedDirectory); + watcherBuilder.Add(watcher); + } + } + + _watchedDirectories = watchedDirectoriesBuilder.ToImmutable(); + _directoryFileSystemWatchers = watcherBuilder.ToImmutable(); + } + + public event EventHandler? FileChanged; + + public IWatchedFile EnqueueWatchingFile(string filePath) + { + // If this path is already covered by one of our directory watchers, nothing further to do + if (WatchedDirectory.FilePathCoveredByWatchedDirectories(_watchedDirectories, filePath, StringComparison.Ordinal)) + return NoOpWatchedFile.Instance; + + var individualWatchedFile = new IndividualWatchedFile(filePath, this); + _individualWatchedFiles.Add(individualWatchedFile); + return individualWatchedFile; + } + + private void RaiseEvent(object sender, FileSystemEventArgs e) + { + FileChanged?.Invoke(this, e.FullPath); + } + + public void Dispose() + { + foreach (var directoryWatcher in _directoryFileSystemWatchers) + directoryWatcher.Dispose(); + } + + private class IndividualWatchedFile : IWatchedFile + { + private readonly FileChangeContext _context; + private readonly FileSystemWatcher? _watcher; + + public IndividualWatchedFile(string filePath, FileChangeContext context) + { + _context = context; + + // We always must create a watch on an entire directory, so create that, filtered to the single file name + var directoryPath = Path.GetDirectoryName(filePath)!; + + // TODO: support missing directories properly + if (Directory.Exists(directoryPath)) + { + _watcher = new FileSystemWatcher(directoryPath, Path.GetFileName(filePath)); + _watcher.IncludeSubdirectories = false; + + _watcher.Changed += _context.RaiseEvent; + _watcher.Created += _context.RaiseEvent; + _watcher.Deleted += _context.RaiseEvent; + _watcher.Renamed += _context.RaiseEvent; + + _watcher.EnableRaisingEvents = true; + } + else + { + _watcher = null; + } + } + + public void Dispose() + { + if (_context._individualWatchedFiles.Remove(this) && _watcher != null) + { + _watcher.Changed -= _context.RaiseEvent; + _watcher.Created -= _context.RaiseEvent; + _watcher.Deleted -= _context.RaiseEvent; + _watcher.Renamed -= _context.RaiseEvent; + _watcher.Dispose(); + } + } + } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/HostDiagnosticAnalyzerProvider.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/HostDiagnosticAnalyzerProvider.cs new file mode 100644 index 0000000000000..8bbe02aa13604 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/HostDiagnosticAnalyzerProvider.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 Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; +internal class HostDiagnosticAnalyzerProvider : IHostDiagnosticAnalyzerProvider +{ + public ImmutableArray<(AnalyzerFileReference reference, string extensionId)> GetAnalyzerReferencesInExtensions() + { + // Right now we don't expose any way for the extensions in VS Code to provide analyzer references. + return ImmutableArray<(AnalyzerFileReference reference, string extensionId)>.Empty; + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs new file mode 100644 index 0000000000000..2847972cdccc5 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs @@ -0,0 +1,224 @@ +// 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.Concurrent; +using System.Collections.Immutable; +using System.Composition; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using Microsoft.Build.Locator; +using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler.DebugConfiguration; +using Microsoft.CodeAnalysis.LanguageServer.LanguageServer; +using Microsoft.CodeAnalysis.MSBuild; +using Microsoft.CodeAnalysis.MSBuild.Build; +using Microsoft.CodeAnalysis.MSBuild.Logging; +using Microsoft.CodeAnalysis.ProjectSystem; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Composition; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; + +[Export(typeof(LanguageServerProjectSystem)), Shared] +internal sealed class LanguageServerProjectSystem +{ + private readonly ProjectFileLoaderRegistry _projectFileLoaderRegistry; + + /// + /// A single gate for code that is adding work to and modifying . + /// This is just we don't have code simultaneously trying to load and unload solutions at once. + /// + private readonly SemaphoreSlim _gate = new SemaphoreSlim(initialCount: 1); + + private bool _msbuildLoaded = false; + + private readonly AsyncBatchingWorkQueue _projectsToLoadAndReload; + + private readonly LanguageServerWorkspaceFactory _workspaceFactory; + private readonly IFileChangeWatcher _fileChangeWatcher; + private readonly ILogger _logger; + + /// + /// The list of loaded projects in the workspace, keyed by project file path. The outer dictionary is a concurrent dictionary since we may be loading + /// multiple projects at once; the key is a single List we just have a single thread processing any given project file. This is only to be used + /// in and downstream calls; any other updating of this (like unloading projects) should be achieved by adding + /// things to the . + /// + private readonly ConcurrentDictionary> _loadedProjects = new(); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public LanguageServerProjectSystem( + LanguageServerWorkspaceFactory workspaceFactory, + IFileChangeWatcher fileChangeWatcher, + ILoggerFactory loggerFactory, + IAsynchronousOperationListenerProvider listenerProvider) + { + _workspaceFactory = workspaceFactory; + _fileChangeWatcher = fileChangeWatcher; + _logger = loggerFactory.CreateLogger(nameof(LanguageServerProjectSystem)); + + // TODO: remove the DiagnosticReporter that's coupled to the Workspace here + _projectFileLoaderRegistry = new ProjectFileLoaderRegistry(workspaceFactory.Workspace.Services.SolutionServices, new DiagnosticReporter(workspaceFactory.Workspace)); + + _projectsToLoadAndReload = new AsyncBatchingWorkQueue( + TimeSpan.FromMilliseconds(100), + LoadOrReloadProjectsAsync, + StringComparer.Ordinal, + listenerProvider.GetListener(FeatureAttribute.Workspace), + CancellationToken.None); // TODO: do we need to introduce a shutdown cancellation token for this? + } + + public async Task OpenSolutionAsync(string solutionFilePath) + { + await TryEnsureMSBuildLoadedAsync(Path.GetDirectoryName(solutionFilePath)!); + await OpenSolutionCoreAsync(solutionFilePath); + } + + [MethodImpl(MethodImplOptions.NoInlining)] // Don't inline; the caller needs to ensure MSBuild is loaded before we can use MSBuild types here + private async Task OpenSolutionCoreAsync(string solutionFilePath) + { + using (await _gate.DisposableWaitAsync()) + { + _logger.LogInformation($"Loading {solutionFilePath}..."); + var solutionFile = Microsoft.Build.Construction.SolutionFile.Parse(solutionFilePath); + _workspaceFactory.ProjectSystemProjectFactory.SolutionPath = solutionFilePath; + + foreach (var project in solutionFile.ProjectsInOrder) + { + if (project.ProjectType == Microsoft.Build.Construction.SolutionProjectType.SolutionFolder) + { + continue; + } + + _projectsToLoadAndReload.AddWork(project.AbsolutePath); + } + + // Wait for the in progress batch to complete and send a project initialized notification to the client. + await _projectsToLoadAndReload.WaitUntilCurrentBatchCompletesAsync(); + await ProjectInitializationHandler.SendProjectInitializationCompleteNotificationAsync(); + } + } + + private async Task TryEnsureMSBuildLoadedAsync(string workingDirectory) + { + using (await _gate.DisposableWaitAsync()) + { + if (_msbuildLoaded) + { + return true; + } + else + { + var msbuildDiscoveryOptions = new VisualStudioInstanceQueryOptions { DiscoveryTypes = DiscoveryType.DotNetSdk, WorkingDirectory = workingDirectory }; + var msbuildInstances = MSBuildLocator.QueryVisualStudioInstances(msbuildDiscoveryOptions); + var msbuildInstance = msbuildInstances.FirstOrDefault(); + + if (msbuildInstance != null) + { + MSBuildLocator.RegisterInstance(msbuildInstance); + _logger.LogInformation($"Loaded MSBuild at {msbuildInstance.MSBuildPath}"); + _msbuildLoaded = true; + + return true; + } + else + { + _logger.LogError($"Unable to find a MSBuild to use to load {workingDirectory}."); + return false; + } + } + } + } + + private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList projectPathsToLoadOrReload, CancellationToken cancellationToken) + { + var stopwatch = Stopwatch.StartNew(); + + // TODO: support configuration switching + var projectBuildManager = new ProjectBuildManager(additionalGlobalProperties: ImmutableDictionary.Empty); + + projectBuildManager.StartBatchBuild(); + + try + { + var tasks = new List(); + + foreach (var projectPathToLoadOrReload in projectPathsToLoadOrReload) + { + tasks.Add(Task.Run(() => LoadOrReloadProjectAsync(projectPathToLoadOrReload, projectBuildManager, cancellationToken), cancellationToken)); + } + + await Task.WhenAll(tasks); + } + finally + { + projectBuildManager.EndBatchBuild(); + + _logger.LogInformation($"Completed (re)load of all projects in {stopwatch.Elapsed}"); + } + } + + private async Task LoadOrReloadProjectAsync(string projectPath, ProjectBuildManager projectBuildManager, CancellationToken cancellationToken) + { + try + { + if (_projectFileLoaderRegistry.TryGetLoaderFromProjectPath(projectPath, out var loader)) + { + var loadedFile = await loader.LoadProjectFileAsync(projectPath, projectBuildManager, cancellationToken); + var loadedProjectInfos = await loadedFile.GetProjectFileInfosAsync(cancellationToken); + + var existingProjects = _loadedProjects.GetOrAdd(projectPath, static _ => new List()); + + foreach (var loadedProjectInfo in loadedProjectInfos) + { + // If we already have the project, just update it + var existingProject = existingProjects.Find(p => p.GetTargetFramework() == loadedProjectInfo.TargetFramework); + + if (existingProject != null) + { + await existingProject.UpdateWithNewProjectInfoAsync(loadedProjectInfo); + } + else + { + var projectSystemName = $"{projectPath} (${loadedProjectInfo.TargetFramework})"; + var projectCreationInfo = new ProjectSystemProjectCreationInfo { AssemblyName = projectSystemName, FilePath = projectPath }; + + var projectSystemProject = await _workspaceFactory.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync( + projectSystemName, + loadedProjectInfo.Language, + projectCreationInfo, + _workspaceFactory.ProjectSystemHostInfo); + + var loadedProject = new LoadedProject(projectSystemProject, _workspaceFactory.Workspace.Services.SolutionServices, _fileChangeWatcher, _workspaceFactory.TargetFrameworkManager); + loadedProject.NeedsReload += (_, _) => _projectsToLoadAndReload.AddWork(projectPath); + existingProjects.Add(loadedProject); + + await loadedProject.UpdateWithNewProjectInfoAsync(loadedProjectInfo); + } + } + + if (loadedFile.Log.Any()) + { + foreach (var logItem in loadedFile.Log) + { + _logger.LogWarning($"{logItem.Kind} while loading {logItem.ProjectFilePath}: {logItem.Message}"); + } + } + else + { + _logger.LogInformation($"Successfully completed load of {projectPath}"); + } + } + } + catch (Exception e) + { + _logger.LogError(e, $"Exception thrown while loading {projectPath}"); + } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspace.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspace.cs new file mode 100644 index 0000000000000..9a1526d3d49fe --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspace.cs @@ -0,0 +1,117 @@ +// 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 Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; +using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; + +/// +/// Mark this type as an so that LSP document changes are pushed into this instance, causing +/// our to stay in sync with all the document changes. +/// +/// There is a fundamental race with how solution snapshot data is stored in this type. Specifically, two entities push +/// changes into this workspace. First, the pushes changes into this relating to the +/// open/closed state of documents, and the current text contents that it knows about from events. Second, the project system may push changes about what files +/// actually exist in the workspace or not. As neither of these entities synchronizes on anything, we may hear about +/// things like changes to files that the project system has or has not told us about, or which it has added/removed +/// documents for already. +/// +/// Because of this, this type takes the stance that the actual presence/absence of files is dictated by the project +/// system. However, if the files are present, the open/closed state and the contents are dictated by the . This incongruity is not a problem due to how the +/// ends up working. For example, say the manager believes a file exists with some content, but the project system has +/// removed that file from this workspace. In that case, the manager will simply not see this type as containing the +/// document, and it will then add the changed doc to the misc workspace. Similarly, if the project system and +/// workspace manager ever disagree on document contents, that is never itself an issue as the workspace manager always +/// prefers the in-memory source text it is holding onto if the checksums of files change. +/// +/// Put another way, the lsp workspace manager will use the data in the workspace if it sees it is in alignment with +/// what it believes is the state of the world with respect to //. However, if it is not, +/// it will use the local information it has outside of the workspace to ensure it is always matched with the lsp +/// client. +/// +internal class LanguageServerWorkspace : Workspace, ILspWorkspace +{ + /// + /// Will be set by LanguageServerProjectSystem immediately after creating this instance. Can't be passed into the + /// constructor as the factory needs a reference to this type. + /// + public ProjectSystemProjectFactory ProjectSystemProjectFactory { private get; set; } = null!; + + public LanguageServerWorkspace(HostServices host) + : base(host, WorkspaceKind.Host) + { + } + + protected internal override bool PartialSemanticsEnabled => true; + + bool ILspWorkspace.SupportsMutation => true; + + ValueTask ILspWorkspace.UpdateTextIfPresentAsync(DocumentId documentId, SourceText sourceText, CancellationToken cancellationToken) + { + // We need to ensure that our changes, and the changes made by the ProjectSystemProjectFactory don't interleave. + // Specifically, ProjectSystemProjectFactory often makes several changes in a row that it thinks cannot be + // interrupted. To ensure this, we call into ProjectSystemProjectFactory to synchronize on the same lock that + // it has when making workspace changes. + // + // https://github.com/dotnet/roslyn/issues/67510 tracks cleaning up ProjectSystemProjectFactory so that it + // shares the same sync/lock/application code with the core workspace code. Once that happens, we won't need + // to do special coordination here. + return this.ProjectSystemProjectFactory.ApplyChangeToWorkspaceAsync( + _ => + { + this.OnDocumentTextChanged(documentId, sourceText, PreservationMode.PreserveIdentity, requireDocumentPresent: false); + return ValueTask.CompletedTask; + }, + cancellationToken); + } + + internal override ValueTask TryOnDocumentOpenedAsync(DocumentId documentId, SourceTextContainer textContainer, bool isCurrentContext, CancellationToken cancellationToken) + { + return this.ProjectSystemProjectFactory.ApplyChangeToWorkspaceAsync( + _ => + { + this.OnDocumentOpened(documentId, textContainer, isCurrentContext, requireDocumentPresentAndClosed: false); + return ValueTask.CompletedTask; + }, + cancellationToken); + } + + internal override ValueTask TryOnDocumentClosedAsync(DocumentId documentId, CancellationToken cancellationToken) + { + return this.ProjectSystemProjectFactory.ApplyChangeToWorkspaceAsync( + async w => + { + // TODO(cyrusn): This only works for normal documents currently. We'll have to rethink how things work + // in the world if we ever support additionalfiles/editorconfig in our language server. + var document = w.CurrentSolution.GetDocument(documentId); + + if (document is { FilePath: { } filePath }) + { + TextLoader loader; + if (document.DocumentState.Attributes.DesignTimeOnly) + { + // Dynamic files don't exist on disk so if we were to use the FileTextLoader we'd effectively be emptying out the document. + // We also assume they're not user editable, and hence can't have "unsaved" changes that are expected to go away on close. + // Instead we just maintain their current state as per the LSP view of the world. + var documentText = await document.GetTextAsync(cancellationToken); + loader = new SourceTextLoader(documentText, filePath); + } + else + { + loader = this.ProjectSystemProjectFactory.CreateFileTextLoader(filePath); + } + + this.OnDocumentClosedEx(documentId, loader, requireDocumentPresentAndOpen: false); + } + }, + cancellationToken); + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs new file mode 100644 index 0000000000000..29805c51d2a28 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerWorkspaceFactory.cs @@ -0,0 +1,81 @@ +// 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.Composition; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.ExternalAccess.VSCode.API; +using Microsoft.CodeAnalysis.LanguageServer.Handler.DebugConfiguration; +using Microsoft.CodeAnalysis.ProjectSystem; +using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Composition; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; + +[Export(typeof(LanguageServerWorkspaceFactory)), Shared] +internal sealed class LanguageServerWorkspaceFactory +{ + private readonly ILogger _logger; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public LanguageServerWorkspaceFactory( + HostServicesProvider hostServicesProvider, + VSCodeAnalyzerLoader analyzerLoader, + IFileChangeWatcher fileChangeWatcher, + [ImportMany] IEnumerable> dynamicFileInfoProviders, + ProjectTargetFrameworkManager projectTargetFrameworkManager, + ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(nameof(LanguageServerWorkspaceFactory)); + + var workspace = new LanguageServerWorkspace(hostServicesProvider.HostServices); + Workspace = workspace; + ProjectSystemProjectFactory = new ProjectSystemProjectFactory(Workspace, fileChangeWatcher, static (_, _) => Task.CompletedTask, _ => { }); + workspace.ProjectSystemProjectFactory = ProjectSystemProjectFactory; + + analyzerLoader.InitializeDiagnosticsServices(Workspace); + + ProjectSystemHostInfo = new ProjectSystemHostInfo( + DynamicFileInfoProviders: dynamicFileInfoProviders.ToImmutableArray(), + new ProjectSystemDiagnosticSource(), + new HostDiagnosticAnalyzerProvider()); + + TargetFrameworkManager = projectTargetFrameworkManager; + } + + public Workspace Workspace { get; } + + public ProjectSystemProjectFactory ProjectSystemProjectFactory { get; } + public ProjectSystemHostInfo ProjectSystemHostInfo { get; } + public ProjectTargetFrameworkManager TargetFrameworkManager { get; } + + public async Task InitializeSolutionLevelAnalyzersAsync(ImmutableArray analyzerPaths) + { + var references = new List(); + var analyzerLoader = VSCodeAnalyzerLoader.CreateAnalyzerAssemblyLoader(); + + foreach (var analyzerPath in analyzerPaths) + { + if (File.Exists(analyzerPath)) + { + references.Add(new AnalyzerFileReference(analyzerPath, analyzerLoader)); + _logger.LogDebug($"Solution-level analyzer at {analyzerPath} added to workspace."); + } + else + { + _logger.LogWarning($"Solution-level analyzer at {analyzerPath} could not be found."); + } + } + + await ProjectSystemProjectFactory.ApplyChangeToWorkspaceAsync(w => + { + w.SetCurrentSolution(s => s.WithAnalyzerReferences(references), WorkspaceChangeKind.SolutionChanged); + return ValueTask.CompletedTask; + }); + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs new file mode 100644 index 0000000000000..dcc1ef6697f14 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs @@ -0,0 +1,195 @@ +// 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 Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.LanguageServer.Handler.DebugConfiguration; +using Microsoft.CodeAnalysis.MSBuild; +using Microsoft.CodeAnalysis.ProjectSystem; +using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; + +/// +/// Represents a single project loaded for a single target. +/// +internal sealed class LoadedProject : IDisposable +{ + private readonly ProjectSystemProject _projectSystemProject; + private readonly ProjectSystemProjectOptionsProcessor _optionsProcessor; + private readonly IFileChangeContext _fileChangeContext; + private readonly ProjectTargetFrameworkManager _targetFrameworkManager; + + /// + /// The most recent version of the project design time build information; held onto so the next reload we can diff against this. + /// + private ProjectFileInfo? _mostRecentFileInfo; + private ImmutableArray _mostRecentMetadataReferences = ImmutableArray.Empty; + + public LoadedProject(ProjectSystemProject projectSystemProject, SolutionServices solutionServices, IFileChangeWatcher fileWatcher, ProjectTargetFrameworkManager targetFrameworkManager) + { + Contract.ThrowIfNull(projectSystemProject.FilePath); + + _projectSystemProject = projectSystemProject; + _optionsProcessor = new ProjectSystemProjectOptionsProcessor(projectSystemProject, solutionServices); + _targetFrameworkManager = targetFrameworkManager; + + // We'll watch the directory for all source file changes + // TODO: we only should listen for add/removals here, but we can't specify such a filter now + var projectDirectory = Path.GetDirectoryName(projectSystemProject.FilePath)!; + var watchedDirectories = new WatchedDirectory[] + { + new(projectDirectory, ".cs"), + new(projectDirectory, ".cshtml"), + new(projectDirectory, ".razor") + }; + + _fileChangeContext = fileWatcher.CreateContext(watchedDirectories); + _fileChangeContext.FileChanged += FileChangedContext_FileChanged; + + // Start watching for file changes for the project file as well + _fileChangeContext.EnqueueWatchingFile(projectSystemProject.FilePath); + } + + private void FileChangedContext_FileChanged(object? sender, string filePath) + { + NeedsReload?.Invoke(this, EventArgs.Empty); + } + + public event EventHandler? NeedsReload; + + public string? GetTargetFramework() + { + Contract.ThrowIfNull(_mostRecentFileInfo, "We haven't been given a loaded project yet, so we can't provide the existing TFM."); + return _mostRecentFileInfo.TargetFramework; + } + + public void Dispose() + { + _optionsProcessor.Dispose(); + _projectSystemProject.RemoveFromWorkspace(); + } + + public async ValueTask UpdateWithNewProjectInfoAsync(ProjectFileInfo newProjectInfo) + { + if (_mostRecentFileInfo != null) + { + // We should never be changing the fundamental identity of this project; if this happens we really should have done a full unload/reload. + Contract.ThrowIfFalse(newProjectInfo.FilePath == _mostRecentFileInfo.FilePath); + Contract.ThrowIfFalse(newProjectInfo.TargetFramework == _mostRecentFileInfo.TargetFramework); + } + + await using var batch = _projectSystemProject.CreateBatchScope(); + + var projectDisplayName = Path.GetFileNameWithoutExtension(newProjectInfo.FilePath); + + if (newProjectInfo.TargetFramework != null) + { + projectDisplayName += " (" + newProjectInfo.TargetFramework + ")"; + } + + _projectSystemProject.OutputFilePath = newProjectInfo.OutputFilePath; + _projectSystemProject.OutputRefFilePath = newProjectInfo.OutputRefFilePath; + + if (newProjectInfo.TargetFrameworkIdentifier != null) + { + _targetFrameworkManager.UpdateIdentifierForProject(_projectSystemProject.Id, newProjectInfo.TargetFrameworkIdentifier); + } + + _optionsProcessor.SetCommandLine(newProjectInfo.CommandLineArgs); + + UpdateProjectSystemProjectCollection( + newProjectInfo.Documents, + _mostRecentFileInfo?.Documents, + DocumentFileInfoComparer.Instance, + document => _projectSystemProject.AddSourceFile(document.FilePath), + document => _projectSystemProject.RemoveSourceFile(document.FilePath)); + + var metadataReferences = _optionsProcessor.GetParsedCommandLineArguments().MetadataReferences.Distinct(); + + UpdateProjectSystemProjectCollection( + metadataReferences, + _mostRecentMetadataReferences, + EqualityComparer.Default, // CommandLineReference already implements equality + reference => _projectSystemProject.AddMetadataReference(reference.Reference, reference.Properties), + reference => _projectSystemProject.RemoveMetadataReference(reference.Reference, reference.Properties)); + + // Now that we've updated it hold onto the old list of references so we can remove them if there's a later update + _mostRecentMetadataReferences = metadataReferences; + + UpdateProjectSystemProjectCollection( + newProjectInfo.AdditionalDocuments.Distinct(DocumentFileInfoComparer.Instance), // TODO: figure out why we have duplicates + _mostRecentFileInfo?.AdditionalDocuments.Distinct(DocumentFileInfoComparer.Instance), + DocumentFileInfoComparer.Instance, + document => _projectSystemProject.AddAdditionalFile(document.FilePath), + document => _projectSystemProject.RemoveAdditionalFile(document.FilePath)); + + UpdateProjectSystemProjectCollection( + newProjectInfo.AnalyzerConfigDocuments, + _mostRecentFileInfo?.AnalyzerConfigDocuments, + DocumentFileInfoComparer.Instance, + document => _projectSystemProject.AddAnalyzerConfigFile(document.FilePath), + document => _projectSystemProject.RemoveAnalyzerConfigFile(document.FilePath)); + + UpdateProjectSystemProjectCollection( + newProjectInfo.AdditionalDocuments.Where(TreatAsIsDynamicFile).Distinct(DocumentFileInfoComparer.Instance), // TODO: figure out why we have duplicates + _mostRecentFileInfo?.AdditionalDocuments.Where(TreatAsIsDynamicFile).Distinct(DocumentFileInfoComparer.Instance), + DocumentFileInfoComparer.Instance, + document => _projectSystemProject.AddDynamicSourceFile(document.FilePath, folders: ImmutableArray.Empty), + document => _projectSystemProject.RemoveDynamicSourceFile(document.FilePath)); + + _mostRecentFileInfo = newProjectInfo; + + return; + + static void UpdateProjectSystemProjectCollection(IEnumerable loadedCollection, IEnumerable? oldLoadedCollection, IEqualityComparer comparer, Action addItem, Action removeItem) + { + var oldItems = new HashSet(comparer); + + if (oldLoadedCollection != null) + { + foreach (var item in oldLoadedCollection) + oldItems.Add(item); + } + + foreach (var newItem in loadedCollection) + { + // If oldItems already has this, we don't need to add it again. We'll remove it, and what is left in oldItems is stuff to remove + if (!oldItems.Remove(newItem)) + addItem(newItem); + } + + foreach (var oldItem in oldItems) + { + removeItem(oldItem); + } + } + } + + private static bool TreatAsIsDynamicFile(DocumentFileInfo info) + { + var extension = Path.GetExtension(info.FilePath); + return extension is ".cshtml" or ".razor"; + } + + private sealed class DocumentFileInfoComparer : IEqualityComparer + { + public static IEqualityComparer Instance = new DocumentFileInfoComparer(); + + private DocumentFileInfoComparer() + { + } + + public bool Equals(DocumentFileInfo? x, DocumentFileInfo? y) + { + return StringComparer.Ordinal.Equals(x?.FilePath, y?.FilePath); + } + + public int GetHashCode(DocumentFileInfo obj) + { + return StringComparer.Ordinal.GetHashCode(obj.FilePath); + } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/MetadataService.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/MetadataService.cs new file mode 100644 index 0000000000000..7d8bcdd7ad732 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/MetadataService.cs @@ -0,0 +1,74 @@ +// 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.Composition; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; + +[ExportWorkspaceServiceFactory(typeof(IMetadataService), ServiceLayer.Host), Shared] +internal sealed class MetadataServiceFactory : IWorkspaceServiceFactory +{ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public MetadataServiceFactory() + { + } + + public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) + { + return new MetadataService(workspaceServices.GetRequiredService()); + } + + internal sealed class MetadataService : IMetadataService + { + private readonly IDocumentationProviderService _documentationProviderService; + + public MetadataService(IDocumentationProviderService documentationProviderService) + { + _documentationProviderService = documentationProviderService; + } + + public PortableExecutableReference GetReference(string resolvedPath, MetadataReferenceProperties properties) + { + // HACK: right now the FileWatchedPortableExecutableReferenceFactory in Roslyn presumes that each time it calls IMetadataService + // it gets a unique instance back; the default MetadataService at the workspace layer has a cache to encourage sharing, but that + // breaks the assumption. + try + { + return MetadataReference.CreateFromFile(resolvedPath, properties, _documentationProviderService.GetDocumentationProvider(resolvedPath)); + } + catch (IOException ex) + { + return new ThrowingExecutableReference(resolvedPath, properties, ex); + } + } + + private class ThrowingExecutableReference : PortableExecutableReference + { + private readonly IOException _ex; + + public ThrowingExecutableReference(string resolvedPath, MetadataReferenceProperties properties, IOException ex) : base(properties, resolvedPath) + { + _ex = ex; + } + + protected override DocumentationProvider CreateDocumentationProvider() + { + throw new NotImplementedException(); + } + + protected override Metadata GetMetadataImpl() + { + throw _ex; + } + + protected override PortableExecutableReference WithPropertiesImpl(MetadataReferenceProperties properties) + { + return new ThrowingExecutableReference(FilePath!, properties, _ex); + } + } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/OpenSolutionHandler.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/OpenSolutionHandler.cs new file mode 100644 index 0000000000000..da7bf9f0c814b --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/OpenSolutionHandler.cs @@ -0,0 +1,40 @@ +// 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.Composition; +using System.Runtime.Serialization; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CommonLanguageServerProtocol.Framework; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; + +[ExportCSharpVisualBasicStatelessLspService(typeof(OpenSolutionHandler)), Shared] +[Method("solution/open")] +internal class OpenSolutionHandler : ILspServiceNotificationHandler +{ + private readonly LanguageServerProjectSystem _projectSystem; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public OpenSolutionHandler(LanguageServerProjectSystem projectSystem) + { + _projectSystem = projectSystem; + } + + public bool MutatesSolutionState => false; + public bool RequiresLSPSolution => false; + + Task INotificationHandler.HandleNotificationAsync(NotificationParams request, RequestContext requestContext, CancellationToken cancellationToken) + { + return _projectSystem.OpenSolutionAsync(request.Solution.LocalPath); + } + + [DataContract] + private class NotificationParams + { + [DataMember(Name = "solution")] + public required Uri Solution { get; set; } + } +} \ No newline at end of file diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectInitializationHandler.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectInitializationHandler.cs new file mode 100644 index 0000000000000..7607906a2df8c --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectInitializationHandler.cs @@ -0,0 +1,119 @@ +// 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.ComponentModel.Composition; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services; +using Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services.Definitions; +using Microsoft.CodeAnalysis.LanguageServer.LanguageServer; +using Microsoft.Extensions.Logging; +using Microsoft.ServiceHub.Framework; +using Microsoft.VisualStudio.Shell.ServiceBroker; +using Roslyn.Utilities; +using StreamJsonRpc; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; + +#pragma warning disable RS0030 // This is intentionally using System.ComponentModel.Composition for compatibility with MEF service broker. +[Export] +internal class ProjectInitializationHandler : IDisposable +{ + private const string ProjectInitializationCompleteName = "workspace/projectInitializationComplete"; + + private readonly IServiceBroker _serviceBroker; + private readonly ServiceBrokerClient _serviceBrokerClient; + private readonly ILogger _logger; + + private readonly TaskCompletionSource _serviceAvailable = new(); + private readonly ProjectInitializationCompleteObserver _projectInitializationCompleteObserver; + + private IDisposable? _subscription; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ProjectInitializationHandler([Import(typeof(SVsFullAccessServiceBroker))] IServiceBroker serviceBroker, ILoggerFactory loggerFactory) + { + _serviceBroker = serviceBroker; + _serviceBroker.AvailabilityChanged += AvailabilityChanged; + _serviceBrokerClient = new ServiceBrokerClient(serviceBroker, joinableTaskFactory: null); + + _logger = loggerFactory.CreateLogger(); + _projectInitializationCompleteObserver = new ProjectInitializationCompleteObserver(_logger); + } + + public static async Task SendProjectInitializationCompleteNotificationAsync() + { + Contract.ThrowIfNull(LanguageServerHost.Instance, "We don't have an LSP channel yet to send this request through."); + var languageServerManager = LanguageServerHost.Instance.GetRequiredLspService(); + await languageServerManager.SendNotificationAsync(ProjectInitializationCompleteName, CancellationToken.None); + } + + public async Task SubscribeToInitializationCompleteAsync(CancellationToken cancellationToken) + { + // Use the ServiceBrokerClient so that we actually hold onto the instance of the service to prevent it from being disposed of until we're shutting down. + var didSubscribe = await TrySubscribeAsync(cancellationToken); + if (!didSubscribe) + { + // Service might be null the first time we try to access it - wait for it to become available on the remote side. + await _serviceAvailable.Task; + didSubscribe = await TrySubscribeAsync(cancellationToken); + Contract.ThrowIfFalse(didSubscribe, $"Unable to subscribe to {Descriptors.RemoteProjectInitializationStatusService.Moniker}"); + } + } + + private async Task TrySubscribeAsync(CancellationToken cancellationToken) + { + using var rental = await _serviceBrokerClient.GetProxyAsync(Descriptors.RemoteProjectInitializationStatusService, cancellationToken); + if (rental.Proxy is not null) + { + _subscription = await rental.Proxy.SubscribeInitializationCompletionAsync(_projectInitializationCompleteObserver, cancellationToken); + return true; + } + + return false; + } + + private void AvailabilityChanged(object? sender, BrokeredServicesChangedEventArgs e) + { + if (e.ImpactedServices.Contains(Descriptors.RemoteProjectInitializationStatusService.Moniker)) + _serviceAvailable.SetResult(); + } + + public void Dispose() + { + _serviceBroker.AvailabilityChanged -= AvailabilityChanged; + _subscription?.Dispose(); + _serviceBrokerClient.Dispose(); + } + + internal class ProjectInitializationCompleteObserver : IObserver + { + private readonly ILogger _logger; + + public ProjectInitializationCompleteObserver(ILogger logger) + { + _logger = logger; + } + + [JsonRpcMethod("onCompleted")] + public void OnCompleted() + { + // NoOp - OnNext is the only method that will be called upon completion of initial project load. + } + + [JsonRpcMethod("onError", UseSingleObjectParameterDeserialization = true)] + public void OnError(Exception error) + { + _logger.LogError(error, "Devkit project initialization observer failed"); + } + + [JsonRpcMethod("onNext", UseSingleObjectParameterDeserialization = true)] + public void OnNext(ProjectInitializationCompletionState value) + { + _logger.LogDebug("Devkit project initialization completed"); + _ = SendProjectInitializationCompleteNotificationAsync().ReportNonFatalErrorAsync(); + } + } +} +#pragma warning restore RS0030 // Do not used banned APIs diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectSystemDiagnosticSource.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectSystemDiagnosticSource.cs new file mode 100644 index 0000000000000..57a11e0431565 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectSystemDiagnosticSource.cs @@ -0,0 +1,32 @@ +// 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 Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; +internal class ProjectSystemDiagnosticSource : IProjectSystemDiagnosticSource +{ + public void ClearAllDiagnosticsForProject(ProjectId projectId) + { + } + + public void ClearAnalyzerReferenceDiagnostics(AnalyzerFileReference fileReference, string language, ProjectId projectId) + { + } + + public void ClearDiagnosticsForProject(ProjectId projectId, object key) + { + } + + public DiagnosticData CreateAnalyzerLoadFailureDiagnostic(AnalyzerLoadFailureEventArgs e, string fullPath, ProjectId projectId, string language) + { + return DocumentAnalysisExecutor.CreateAnalyzerLoadFailureDiagnostic(e, fullPath, projectId, language); + } + + public void UpdateDiagnosticsForProject(ProjectId projectId, object key, IEnumerable items) + { + // TODO: actually store the diagnostics + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/RazorDynamicFileInfoProvider.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/RazorDynamicFileInfoProvider.cs new file mode 100644 index 0000000000000..627a50e7be348 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/RazorDynamicFileInfoProvider.cs @@ -0,0 +1,105 @@ +// 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.Composition; +using System.Runtime.Serialization; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.LanguageServer; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; + +[Export(typeof(IDynamicFileInfoProvider)), Shared] +[ExportMetadata("Extensions", new string[] { "cshtml", "razor", })] +internal class RazorDynamicFileInfoProvider : IDynamicFileInfoProvider +{ + private const string ProvideRazorDynamicFileInfoMethodName = "razor/provideDynamicFileInfo"; + + [DataContract] + private class ProvideDynamicFileParams + { + [DataMember(Name = "razorFiles")] + public required Uri[] RazorFiles { get; set; } + } + + [DataContract] + private class ProvideDynamicFileResponse + { + [DataMember(Name = "generatedFiles")] + public required Uri[] GeneratedFiles { get; set; } + } + + private const string RemoveRazorDynamicFileInfoMethodName = "razor/removeDynamicFileInfo"; + + [DataContract] + private class RemoveDynamicFileParams + { + [DataMember(Name = "razorFiles")] + public required Uri[] RazorFiles { get; set; } + } + +#pragma warning disable CS0067 // We won't fire the Updated event -- we expect Razor to send us textual changes via didChange instead + public event EventHandler? Updated; +#pragma warning restore CS0067 + + private readonly Lazy _razorWorkspaceListenerInitializer; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public RazorDynamicFileInfoProvider(Lazy razorWorkspaceListenerInitializer) + { + _razorWorkspaceListenerInitializer = razorWorkspaceListenerInitializer; + } + + public async Task GetDynamicFileInfoAsync(ProjectId projectId, string? projectFilePath, string filePath, CancellationToken cancellationToken) + { + _razorWorkspaceListenerInitializer.Value.NotifyDynamicFile(projectId); + + var requestParams = new ProvideDynamicFileParams { RazorFiles = new[] { ProtocolConversions.GetUriFromFilePath(filePath) } }; + + Contract.ThrowIfNull(LanguageServerHost.Instance, "We don't have an LSP channel yet to send this request through."); + var clientLanguageServerManager = LanguageServerHost.Instance.GetRequiredLspService(); + + var response = await clientLanguageServerManager.SendRequestAsync( + ProvideRazorDynamicFileInfoMethodName, requestParams, cancellationToken); + + // Since we only sent one file over, we should get either zero or one URI back + var responseUri = response.GeneratedFiles.SingleOrDefault(); + + if (responseUri == null) + { + return null; + } + else + { + var dynamicFileInfoFilePath = ProtocolConversions.GetDocumentFilePathFromUri(responseUri); + return new DynamicFileInfo(dynamicFileInfoFilePath, SourceCodeKind.Regular, EmptyStringTextLoader.Instance, designTimeOnly: true, documentServiceProvider: null); + } + } + + public Task RemoveDynamicFileInfoAsync(ProjectId projectId, string? projectFilePath, string filePath, CancellationToken cancellationToken) + { + var notificationParams = new RemoveDynamicFileParams { RazorFiles = new[] { ProtocolConversions.GetUriFromFilePath(filePath) } }; + + Contract.ThrowIfNull(LanguageServerHost.Instance, "We don't have an LSP channel yet to send this request through."); + var clientLanguageServerManager = LanguageServerHost.Instance.GetRequiredLspService(); + + return clientLanguageServerManager.SendNotificationAsync( + RemoveRazorDynamicFileInfoMethodName, notificationParams, cancellationToken).AsTask(); + } + + private sealed class EmptyStringTextLoader : TextLoader + { + public static readonly TextLoader Instance = new EmptyStringTextLoader(); + + private EmptyStringTextLoader() { } + + public override Task LoadTextAndVersionAsync(LoadTextOptions options, CancellationToken cancellationToken) + { + return Task.FromResult(TextAndVersion.Create(SourceText.From(""), VersionStamp.Default)); + } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/RazorInitializeHandler.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/RazorInitializeHandler.cs new file mode 100644 index 0000000000000..dd1dd878f1102 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/RazorInitializeHandler.cs @@ -0,0 +1,34 @@ +// 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.Composition; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CommonLanguageServerProtocol.Framework; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; + +[ExportCSharpVisualBasicStatelessLspService(typeof(RazorInitializeHandler)), Shared] +[Method("razor/initialize")] +internal class RazorInitializeHandler : ILspServiceNotificationHandler +{ + private readonly Lazy _razorWorkspaceListenerInitializer; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public RazorInitializeHandler(Lazy razorWorkspaceListenerInitializer) + { + _razorWorkspaceListenerInitializer = razorWorkspaceListenerInitializer; + } + + public bool MutatesSolutionState => false; + public bool RequiresLSPSolution => false; + + Task INotificationHandler.HandleNotificationAsync(object request, RequestContext requestContext, CancellationToken cancellationToken) + { + _razorWorkspaceListenerInitializer.Value.Initialize(); + + return Task.CompletedTask; + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/RazorWorkspaceListenerInitializer.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/RazorWorkspaceListenerInitializer.cs new file mode 100644 index 0000000000000..9b7cdf022a986 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/RazorWorkspaceListenerInitializer.cs @@ -0,0 +1,84 @@ +// 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.Composition; +using Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.Extensions.Logging; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; + +[Export(typeof(RazorWorkspaceListenerInitializer)), Shared] +internal sealed class RazorWorkspaceListenerInitializer +{ + // This should be moved to the Razor side once things are announced, so defaults are all in one + // place, in case things ever need to change + private const string _projectRazorJsonFileName = "project.razor.vscode.json"; + + private readonly ILogger _logger; + private readonly Workspace _workspace; + private readonly ILoggerFactory _loggerFactory; + + // Locks all access to _razorWorkspaceListener and _projectIdWithDynamicFiles + private readonly object _initializeGate = new(); + private HashSet _projectIdWithDynamicFiles = new(); + + private RazorWorkspaceListener? _razorWorkspaceListener; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public RazorWorkspaceListenerInitializer(LanguageServerWorkspaceFactory workspaceFactory, ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(nameof(RazorWorkspaceListenerInitializer)); + + _workspace = workspaceFactory.Workspace; + _loggerFactory = loggerFactory; + } + + internal void Initialize() + { + HashSet projectsToInitialize; + lock (_initializeGate) + { + // Only initialize once + if (_razorWorkspaceListener is not null) + { + return; + } + + _logger.LogTrace("Initializing the Razor workspace listener"); + _razorWorkspaceListener = new RazorWorkspaceListener(_loggerFactory); + _razorWorkspaceListener.EnsureInitialized(_workspace, _projectRazorJsonFileName); + + projectsToInitialize = _projectIdWithDynamicFiles; + // May as well clear out the collection, it will never get used again anyway. + _projectIdWithDynamicFiles = new(); + } + + foreach (var projectId in projectsToInitialize) + { + _logger.LogTrace("{projectId} notifying a dynamic file for the first time", projectId); + _razorWorkspaceListener.NotifyDynamicFile(projectId); + } + } + + internal void NotifyDynamicFile(ProjectId projectId) + { + lock (_initializeGate) + { + if (_razorWorkspaceListener is null) + { + // We haven't been initialized by the extension yet, so just store the project id, to tell Razor later + _logger.LogTrace("{projectId} queuing up a dynamic file notify for later", projectId); + _projectIdWithDynamicFiles.Add(projectId); + + return; + } + } + + // We've been initialized, so just pass the information along + _logger.LogTrace("{projectId} forwarding on a dynamic file notification because we're initialized", projectId); + _razorWorkspaceListener.NotifyDynamicFile(projectId); + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProject.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProject.cs new file mode 100644 index 0000000000000..f450e28c1cab9 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProject.cs @@ -0,0 +1,209 @@ +// 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 Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.LanguageServer.Handler.DebugConfiguration; +using Microsoft.CodeAnalysis.Remote.ProjectSystem; +using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; + +internal class WorkspaceProject : IWorkspaceProject +{ + private readonly ProjectSystemProject _project; + private readonly ProjectSystemProjectOptionsProcessor _optionsProcessor; + private readonly ProjectTargetFrameworkManager _targetFrameworkManager; + + public WorkspaceProject(ProjectSystemProject project, SolutionServices solutionServices, ProjectTargetFrameworkManager targetFrameworkManager) + { + _project = project; + _optionsProcessor = new ProjectSystemProjectOptionsProcessor(_project, solutionServices); + _targetFrameworkManager = targetFrameworkManager; + } + + public async Task AddAdditionalFilesAsync(IReadOnlyList additionalFilePaths, CancellationToken _) + { + await using var batchScope = _project.CreateBatchScope(); + + foreach (var additionalFilePath in additionalFilePaths) + _project.AddAdditionalFile(additionalFilePath); + } + + public async Task AddAnalyzerConfigFilesAsync(IReadOnlyList analyzerConfigPaths, CancellationToken _) + { + await using var batchScope = _project.CreateBatchScope(); + + foreach (var analyzerConfigPath in analyzerConfigPaths) + _project.AddAnalyzerConfigFile(analyzerConfigPath); + } + + public async Task AddAnalyzerReferencesAsync(IReadOnlyList analyzerPaths, CancellationToken _) + { + await using var batchScope = _project.CreateBatchScope(); + + foreach (var analyzerPath in analyzerPaths) + _project.AddAnalyzerReference(analyzerPath); + } + + public async Task AddDynamicFilesAsync(IReadOnlyList dynamicFilePaths, CancellationToken _) + { + await using var batchScope = _project.CreateBatchScope(); + + foreach (var dynamicFilePath in dynamicFilePaths) + _project.AddDynamicSourceFile(dynamicFilePath, folders: ImmutableArray.Empty); + } + + public async Task AddMetadataReferencesAsync(IReadOnlyList metadataReferences, CancellationToken _) + { + await using var batchScope = _project.CreateBatchScope(); + + foreach (var metadataReference in metadataReferences) + _project.AddMetadataReference(metadataReference.FilePath, metadataReference.CreateProperties()); + } + + public async Task AddSourceFilesAsync(IReadOnlyList sourceFiles, CancellationToken _) + { + await using var batchScope = _project.CreateBatchScope(); + + foreach (var sourceFile in sourceFiles) + _project.AddSourceFile(sourceFile.FilePath, folders: sourceFile.FolderNames.ToImmutableArray()); + } + + public void Dispose() + { + _project.RemoveFromWorkspace(); + } + + public async Task RemoveAdditionalFilesAsync(IReadOnlyList additionalFilePaths, CancellationToken _) + { + await using var batchScope = _project.CreateBatchScope(); + + foreach (var additionalFilePath in additionalFilePaths) + _project.RemoveAdditionalFile(additionalFilePath); + } + + public async Task RemoveAnalyzerConfigFilesAsync(IReadOnlyList analyzerConfigPaths, CancellationToken _) + { + await using var batchScope = _project.CreateBatchScope(); + + foreach (var analyzerConfigPath in analyzerConfigPaths) + _project.RemoveAnalyzerConfigFile(analyzerConfigPath); + } + + public async Task RemoveAnalyzerReferencesAsync(IReadOnlyList analyzerPaths, CancellationToken _) + { + await using var batchScope = _project.CreateBatchScope(); + + foreach (var analyzerPath in analyzerPaths) + _project.RemoveAnalyzerReference(analyzerPath); + } + + public async Task RemoveDynamicFilesAsync(IReadOnlyList dynamicFilePaths, CancellationToken _) + { + await using var batchScope = _project.CreateBatchScope(); + + foreach (var dynamicFilePath in dynamicFilePaths) + _project.RemoveDynamicSourceFile(dynamicFilePath); + } + + public async Task RemoveMetadataReferencesAsync(IReadOnlyList metadataReferences, CancellationToken _) + { + await using var batchScope = _project.CreateBatchScope(); + + foreach (var metadataReference in metadataReferences) + _project.RemoveMetadataReference(metadataReference.FilePath, metadataReference.CreateProperties()); + } + + public async Task RemoveSourceFilesAsync(IReadOnlyList sourceFiles, CancellationToken _) + { + await using var batchScope = _project.CreateBatchScope(); + + foreach (var sourceFile in sourceFiles) + _project.RemoveSourceFile(sourceFile); + } + + public async Task SetBuildSystemPropertiesAsync(IReadOnlyDictionary properties, CancellationToken _) + { + // Create a batch scope, just so we have asynchronous closing and application of the batch. + await using var batchScope = _project.CreateBatchScope(); + + foreach (var (name, value) in properties) + { + var valueOrNull = string.IsNullOrEmpty(value) ? null : value; + + switch (name) + { + case "AssemblyName": _project.AssemblyName = value; break; + case "MaxSupportedLangVersion": _project.MaxLangVersion = value; break; + case "RootNamespace": _project.DefaultNamespace = valueOrNull; break; + case "RunAnalyzers": _project.RunAnalyzers = bool.Parse(valueOrNull ?? bool.TrueString); break; + case "RunAnalyzersDuringLiveAnalysis": _project.RunAnalyzersDuringLiveAnalysis = bool.Parse(valueOrNull ?? bool.TrueString); break; + case "TargetPath": _project.OutputFilePath = GetFullyQualifiedPath(valueOrNull); break; + case "TargetRefPath": _project.OutputRefFilePath = GetFullyQualifiedPath(valueOrNull); break; + case "TargetFrameworkIdentifier": _targetFrameworkManager.UpdateIdentifierForProject(_project.Id, valueOrNull); break; + } + } + + string? GetFullyQualifiedPath(string? propertyValue) + { + Contract.ThrowIfNull(_project.FilePath, "We don't have a project path at this point."); + + if (propertyValue is not null) + return Path.Combine(_project.FilePath, propertyValue); + else + return null; + } + } + + public async Task SetCommandLineArgumentsAsync(IReadOnlyList arguments, CancellationToken _) + { + // Create a batch scope, just so we have asynchronous closing and application of the batch. + await using var batchScope = _project.CreateBatchScope(); + _optionsProcessor.SetCommandLine(arguments.ToImmutableArray()); + } + + public async Task SetDisplayNameAsync(string displayName, CancellationToken _) + { + // Create a batch scope, just so we have asynchronous closing and application of the batch. + await using var batchScope = _project.CreateBatchScope(); + _project.DisplayName = displayName; + } + + public async Task SetProjectHasAllInformationAsync(bool hasAllInformation, CancellationToken _) + { + // Create a batch scope, just so we have asynchronous closing and application of the batch. + await using var batchScope = _project.CreateBatchScope(); + _project.HasAllInformation = hasAllInformation; + } + + public Task StartBatchAsync(CancellationToken cancellationToken) + { + return Task.FromResult(new WorkspaceProjectBatch(_project.CreateBatchScope())); + } + + private class WorkspaceProjectBatch : IWorkspaceProjectBatch + { + private IAsyncDisposable? _batch; + + public WorkspaceProjectBatch(IAsyncDisposable batch) + { + _batch = batch; + } + + public async Task ApplyAsync(CancellationToken cancellationToken) + { + if (_batch == null) + throw new InvalidOperationException("The batch has already been applied."); + + await _batch.DisposeAsync().ConfigureAwait(false); + _batch = null; + } + + public void Dispose() + { + } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProjectFactoryService.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProjectFactoryService.cs new file mode 100644 index 0000000000000..906994584d08a --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProjectFactoryService.cs @@ -0,0 +1,65 @@ +// 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.ComponentModel.Composition; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Remote.ProjectSystem; +using Microsoft.ServiceHub.Framework; +using Microsoft.VisualStudio.Shell.ServiceBroker; + +namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; +#pragma warning disable RS0030 // This is intentionally using System.ComponentModel.Composition for compatibility with MEF service broker. +/// +/// An implementation of the brokered service that just maps calls to the underlying project system. +/// +[ExportBrokeredService("Microsoft.VisualStudio.LanguageServices.WorkspaceProjectFactoryService", null, Audience = ServiceAudience.Local)] +internal class WorkspaceProjectFactoryService : IWorkspaceProjectFactoryService, IExportedBrokeredService +{ + private readonly LanguageServerWorkspaceFactory _workspaceFactory; + private readonly ProjectInitializationHandler _projectInitializationHandler; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public WorkspaceProjectFactoryService(LanguageServerWorkspaceFactory workspaceFactory, ProjectInitializationHandler projectInitializationHandler) + { + _workspaceFactory = workspaceFactory; + _projectInitializationHandler = projectInitializationHandler; + } + + ServiceRpcDescriptor IExportedBrokeredService.Descriptor => WorkspaceProjectFactoryServiceDescriptor.ServiceDescriptor; + + async Task IExportedBrokeredService.InitializeAsync(CancellationToken cancellationToken) + { + await _projectInitializationHandler.SubscribeToInitializationCompleteAsync(cancellationToken); + } + + public async Task CreateAndAddProjectAsync(WorkspaceProjectCreationInfo creationInfo, CancellationToken _) + { + if (creationInfo.BuildSystemProperties.TryGetValue("SolutionPath", out var solutionPath)) + { + _workspaceFactory.ProjectSystemProjectFactory.SolutionPath = solutionPath; + } + + var project = await _workspaceFactory.ProjectSystemProjectFactory.CreateAndAddToWorkspaceAsync( + creationInfo.DisplayName, + creationInfo.Language, + new Workspaces.ProjectSystem.ProjectSystemProjectCreationInfo { FilePath = creationInfo.FilePath }, + _workspaceFactory.ProjectSystemHostInfo); + + var workspaceProject = new WorkspaceProject(project, _workspaceFactory.Workspace.Services.SolutionServices, _workspaceFactory.TargetFrameworkManager); + + // We've created a new project, so initialize properties we have + await workspaceProject.SetBuildSystemPropertiesAsync(creationInfo.BuildSystemProperties, CancellationToken.None); + + return workspaceProject; + } + + public Task> GetSupportedBuildSystemPropertiesAsync(CancellationToken _) + { + // TODO: implement + return Task.FromResult((IReadOnlyCollection)ImmutableArray.Empty); + } +} +#pragma warning restore RS0030 // Do not used banned APIs diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/ProjectDebugConfiguration.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/ProjectDebugConfiguration.cs new file mode 100644 index 0000000000000..82e2ab5fa530c --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/ProjectDebugConfiguration.cs @@ -0,0 +1,40 @@ +// 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.Runtime.Serialization; +using Newtonsoft.Json; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.DebugConfiguration; + +[DataContract] +internal class ProjectDebugConfiguration +{ + public ProjectDebugConfiguration(string projectPath, string outputPath, string projectName, bool targetsDotnetCore, bool isExe, string? solutionPath) + { + ProjectPath = projectPath; + OutputPath = outputPath; + ProjectName = projectName; + TargetsDotnetCore = targetsDotnetCore; + IsExe = isExe; + SolutionPath = solutionPath; + } + + [JsonProperty(PropertyName = "projectPath")] + public string ProjectPath { get; } + + [JsonProperty(PropertyName = "outputPath")] + public string OutputPath { get; } + + [JsonProperty(PropertyName = "projectName")] + public string ProjectName { get; } + + [JsonProperty(PropertyName = "targetsDotnetCore")] + public bool TargetsDotnetCore { get; } + + [JsonProperty(PropertyName = "isExe")] + public bool IsExe { get; } + + [JsonProperty(PropertyName = "solutionPath")] + public string? SolutionPath { get; } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/ProjectTargetFrameworkManager.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/ProjectTargetFrameworkManager.cs new file mode 100644 index 0000000000000..be3a3b21ae97b --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/ProjectTargetFrameworkManager.cs @@ -0,0 +1,46 @@ +// 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.Concurrent; +using System.Composition; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.DebugConfiguration; + +/// +/// Keeps track of which project uses what TFM. +/// +[Export, Shared] +internal class ProjectTargetFrameworkManager +{ + private readonly ConcurrentDictionary _projectToTargetFrameworkIdentifer = new(); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ProjectTargetFrameworkManager() + { + } + + public void UpdateIdentifierForProject(ProjectId projectId, string? identifier) + { + _ = _projectToTargetFrameworkIdentifer.AddOrUpdate(projectId, identifier, (project, oldIdentifier) => identifier); + } + + public bool IsDotnetCoreProject(ProjectId projectId) + { + if (_projectToTargetFrameworkIdentifer.TryGetValue(projectId, out var identifier) && identifier != null) + { + return IsDotnetCoreIdentifier(identifier); + } + + return false; + } + + private static bool IsDotnetCoreIdentifier(string identifier) + { + // This is the condition suggested by the SDK/MSBuild for determining if a project targets .net core. + return identifier.StartsWith(".NETCoreApp") || identifier.StartsWith(".NETStandard"); + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/WorkspaceDebugConfigurationHandler.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/WorkspaceDebugConfigurationHandler.cs new file mode 100644 index 0000000000000..5042fb751b12d --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/WorkspaceDebugConfigurationHandler.cs @@ -0,0 +1,72 @@ +// 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.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host.Mef; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.DebugConfiguration; + +[ExportCSharpVisualBasicStatelessLspService(typeof(WorkspaceDebugConfigurationHandler)), Shared] +[Method(MethodName)] +internal class WorkspaceDebugConfigurationHandler : ILspServiceRequestHandler +{ + private const string MethodName = "workspace/debugConfiguration"; + + private readonly ProjectTargetFrameworkManager _targetFrameworkManager; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public WorkspaceDebugConfigurationHandler(ProjectTargetFrameworkManager targetFrameworkManager) + { + _targetFrameworkManager = targetFrameworkManager; + } + + public bool MutatesSolutionState => false; + + public bool RequiresLSPSolution => true; + + public Task HandleRequestAsync(WorkspaceDebugConfigurationParams request, RequestContext context, CancellationToken cancellationToken) + { + Contract.ThrowIfNull(context.Solution, nameof(context.Solution)); + + var projects = context.Solution.Projects + .Where(p => p.FilePath != null && p.OutputFilePath != null) + .Where(p => IsProjectInWorkspace(request.WorkspacePath, p)) + .Select(GetProjectDebugConfiguration).ToArray(); + return Task.FromResult(projects); + } + + private static bool IsProjectInWorkspace(Uri workspacePath, Project project) + { + return PathUtilities.IsSameDirectoryOrChildOf(project.FilePath!, workspacePath.LocalPath); + } + + private ProjectDebugConfiguration GetProjectDebugConfiguration(Project project) + { + var isExe = project.CompilationOptions?.OutputKind is OutputKind.ConsoleApplication or OutputKind.WindowsApplication; + var targetsDotnetCore = _targetFrameworkManager.IsDotnetCoreProject(project.Id); + return new ProjectDebugConfiguration(project.FilePath!, project.OutputFilePath!, GetProjectName(project), targetsDotnetCore, isExe, project.Solution.FilePath); + } + + private static string GetProjectName(Project project) + { + var (_, flavor) = project.State.NameAndFlavor; + if (string.IsNullOrEmpty(flavor)) + { + return project.Name; + } + else + { + var projectPath = project.FilePath!; + var projectFileName = Path.GetFileName(projectPath); + return $"{projectFileName} ({flavor}) - {projectPath}"; + } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/WorkspaceDebugConfigurationParams.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/WorkspaceDebugConfigurationParams.cs new file mode 100644 index 0000000000000..a286f9bdf9f4a --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/DebugConfiguration/WorkspaceDebugConfigurationParams.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; +using System.Runtime.Serialization; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Newtonsoft.Json; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.DebugConfiguration; + +[DataContract] +internal record WorkspaceDebugConfigurationParams( + [JsonProperty(PropertyName = "workspacePath"), JsonConverter(typeof(DocumentUriConverter))] Uri WorkspacePath); diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/LanguageServerHost.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/LanguageServerHost.cs new file mode 100644 index 0000000000000..03803c10aad83 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/LanguageServerHost.cs @@ -0,0 +1,68 @@ +// 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 Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServer.Logging; +using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Composition; +using StreamJsonRpc; + +namespace Microsoft.CodeAnalysis.LanguageServer.LanguageServer; + +#pragma warning disable CA1001 // The JsonRpc instance is disposed of by the AbstractLanguageServer during shutdown +internal sealed class LanguageServerHost +#pragma warning restore CA1001 // The JsonRpc instance is disposed of by the AbstractLanguageServer during shutdown +{ + // TODO: replace this with a MEF part instead + /// + /// A static reference to the server instance. + /// Used by components to send notifications and requests back to the client. + /// + internal static LanguageServerHost? Instance { get; private set; } + + private readonly ILogger _logger; + private readonly AbstractLanguageServer _roslynLanguageServer; + private readonly JsonRpc _jsonRpc; + + public LanguageServerHost(Stream inputStream, Stream outputStream, ExportProvider exportProvider, ILogger logger) + { + var handler = new HeaderDelimitedMessageHandler(outputStream, inputStream, new JsonMessageFormatter()); + + // If there is a jsonrpc disconnect or server shutdown, that is handled by the AbstractLanguageServer. No need to do anything here. + _jsonRpc = new JsonRpc(handler) + { + ExceptionStrategy = ExceptionProcessing.CommonErrorData, + }; + + var roslynLspFactory = exportProvider.GetExportedValue(); + var capabilitiesProvider = new ServerCapabilitiesProvider(exportProvider.GetExportedValue()); + + _logger = logger; + var lspLogger = new LspServiceLogger(_logger); + + var hostServices = exportProvider.GetExportedValue().HostServices; + _roslynLanguageServer = roslynLspFactory.Create(_jsonRpc, capabilitiesProvider, WellKnownLspServerKinds.CSharpVisualBasicLspServer, lspLogger, hostServices); + } + + public void Start() + { + _logger.LogInformation("Starting server..."); + _jsonRpc.StartListening(); + + // Now that the server is started, update the our instance reference + Instance = this; + } + + public async Task WaitForExitAsync() + { + await _jsonRpc.Completion; + await _roslynLanguageServer.WaitForExitAsync(); + } + + public T GetRequiredLspService() where T : ILspService + { + return _roslynLanguageServer.GetLspServices().GetRequiredService(); + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/ServerCapabilitiesProvider.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/ServerCapabilitiesProvider.cs new file mode 100644 index 0000000000000..1b8b495b941ef --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/ServerCapabilitiesProvider.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 Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.LanguageServer; + +internal sealed class ServerCapabilitiesProvider : ICapabilitiesProvider +{ + private readonly ExperimentalCapabilitiesProvider _roslynCapabilities; + + public ServerCapabilitiesProvider(ExperimentalCapabilitiesProvider roslynCapabilities) + { + _roslynCapabilities = roslynCapabilities; + } + + public ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) + { + var roslynCapabilities = _roslynCapabilities.GetCapabilities(clientCapabilities); + return roslynCapabilities; + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/WorkspaceRegistrationService.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/WorkspaceRegistrationService.cs new file mode 100644 index 0000000000000..fed1eeb89123f --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/WorkspaceRegistrationService.cs @@ -0,0 +1,22 @@ +// 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.Composition; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.LanguageServer.LanguageServer; + +/// +/// Implements the workspace registration service so that any new workspaces we +/// create are automatically registered by +/// +[Export(typeof(LspWorkspaceRegistrationService)), Shared] +internal class WorkspaceRegistrationService : LspWorkspaceRegistrationService +{ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public WorkspaceRegistrationService() + { + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Logging/LspLogMessageLogger.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Logging/LspLogMessageLogger.cs new file mode 100644 index 0000000000000..a461af8c5ecf1 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Logging/LspLogMessageLogger.cs @@ -0,0 +1,80 @@ +// 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.Composition; +using Microsoft.CodeAnalysis.LanguageServer.LanguageServer; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using StreamJsonRpc; + +namespace Microsoft.CodeAnalysis.LanguageServer.Logging; + +/// +/// Implements an ILogger that seamlessly switches from a fallback logger +/// to LSP log messages as soon as the server initializes. +/// +internal sealed class LspLogMessageLogger : ILogger +{ + private readonly string _categoryName; + private readonly ILogger _fallbackLogger; + + public LspLogMessageLogger(string categoryName, ILoggerFactory fallbackLoggerFactory) + { + _categoryName = categoryName; + _fallbackLogger = fallbackLoggerFactory.CreateLogger(categoryName); + } + + public IDisposable BeginScope(TState state) + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + var server = LanguageServerHost.Instance; + if (server == null) + { + // If the language server has not been initialized yet, log using the fallback logger. + _fallbackLogger.Log(logLevel, eventId, state, exception, formatter); + return; + } + + var message = formatter(state, exception); + + // HACK: work around https://github.com/dotnet/runtime/issues/67597: the formatter function we passed the exception to completely ignores the exception, + // we'll add an exception message back in. If we didn't have a message, we'll just replace it with the exception text. + if (exception != null) + { + var exceptionString = exception.ToString(); + if (message == "[null]") // https://github.com/dotnet/runtime/blob/013ca673f6316dbbe71c7b327d7b8fa41cf8c992/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/FormattedLogValues.cs#L19 + message = exceptionString; + else + message += " " + exceptionString; + } + + if (message != null && logLevel != LogLevel.None) + { + message = $"[{_categoryName}]{message}"; + var _ = server.GetRequiredLspService().SendNotificationAsync(Methods.WindowLogMessageName, new LogMessageParams() + { + Message = message, + MessageType = logLevel switch + { + LogLevel.Trace => MessageType.Log, + LogLevel.Debug => MessageType.Log, + LogLevel.Information => MessageType.Info, + LogLevel.Warning => MessageType.Warning, + LogLevel.Error => MessageType.Error, + LogLevel.Critical => MessageType.Error, + _ => throw new InvalidOperationException($"Unexpected logLevel argument {logLevel}"), + } + }, CancellationToken.None); + } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Logging/LspLogMessageLoggerProvider.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Logging/LspLogMessageLoggerProvider.cs new file mode 100644 index 0000000000000..1f216c21c4550 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Logging/LspLogMessageLoggerProvider.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.Collections.Concurrent; +using Microsoft.Extensions.Logging; + +namespace Microsoft.CodeAnalysis.LanguageServer.Logging; +internal class LspLogMessageLoggerProvider : ILoggerProvider +{ + private readonly ILoggerFactory _fallbackLoggerFactory; + private readonly ConcurrentDictionary _loggers = new(StringComparer.OrdinalIgnoreCase); + + public LspLogMessageLoggerProvider(ILoggerFactory fallbackLoggerFactory) + { + _fallbackLoggerFactory = fallbackLoggerFactory; + } + + public ILogger CreateLogger(string categoryName) + { + return _loggers.GetOrAdd(categoryName, new LspLogMessageLogger(categoryName, _fallbackLoggerFactory)); + } + + public void Dispose() + { + _loggers.Clear(); + _fallbackLoggerFactory.Dispose(); + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Logging/LspServiceLogger.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Logging/LspServiceLogger.cs new file mode 100644 index 0000000000000..5da7012ea5e5d --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Logging/LspServiceLogger.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 Microsoft.Extensions.Logging; + +namespace Microsoft.CodeAnalysis.LanguageServer.Logging; + +/// +/// Implements by sending LSP log messages back to the client. +/// +internal sealed class LspServiceLogger : ILspServiceLogger +{ + private readonly ILogger _hostLogger; + + public LspServiceLogger(ILogger hostLogger) + { + _hostLogger = hostLogger; + } + + public void LogEndContext(string message, params object[] @params) => _hostLogger.LogDebug($"[{DateTime.UtcNow:hh:mm:ss.fff}][End]{message}", @params); + + public void LogError(string message, params object[] @params) => _hostLogger.LogError(message, @params); + + public void LogException(Exception exception, string? message = null, params object[] @params) => _hostLogger.LogError(exception, message, @params); + + /// + /// TODO - This should call LogInformation, however we need to introduce a LogDebug call in clasp first. + /// + public void LogInformation(string message, params object[] @params) => _hostLogger.LogDebug(message, @params); + + public void LogStartContext(string message, params object[] @params) => _hostLogger.LogDebug($"[{DateTime.UtcNow:hh:mm:ss.fff}][Start]{message}", @params); + + public void LogWarning(string message, params object[] @params) => _hostLogger.LogWarning(message, @params); +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Logging/RoslynLogger.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Logging/RoslynLogger.cs new file mode 100644 index 0000000000000..957cd1f146d0c --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Logging/RoslynLogger.cs @@ -0,0 +1,235 @@ +// 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.Concurrent; +using System.Collections.Immutable; +using System.Diagnostics; +using Microsoft.CodeAnalysis.Common; +using Microsoft.CodeAnalysis.Contracts.Telemetry; +using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.Internal.Log; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Logging +{ + internal class RoslynLogger : ILogger + { + private static RoslynLogger? _instance; + private static readonly ConcurrentDictionary s_eventMap = new(); + private static readonly ConcurrentDictionary<(FunctionId id, string name), string> s_propertyMap = new(); + + private readonly ConcurrentDictionary _pendingScopes = new(concurrencyLevel: 2, capacity: 10); + private static ITelemetryReporter? _telemetryReporter; + + private RoslynLogger() + { + } + + public static void Initialize(ITelemetryReporter? reporter, string? telemetryLevel, string? sessionId) + { + Contract.ThrowIfTrue(_instance is not null); + + FatalError.Handler = ReportFault; + FatalError.CopyHandlerTo(typeof(Compilation).Assembly); + + if (reporter is not null && telemetryLevel is not null) + { + reporter.InitializeSession(telemetryLevel, sessionId, isDefaultSession: true); + _telemetryReporter = reporter; + } + + _instance = new(); + + var currentLogger = Logger.GetLogger(); + if (currentLogger is null) + { + Logger.SetLogger(_instance); + } + else + { + Logger.SetLogger(AggregateLogger.Create(currentLogger, _instance)); + } + } + + private static void ReportFault(Exception exception, ErrorSeverity severity, bool forceDump) + { + try + { + if (exception is OperationCanceledException { InnerException: { } oceInnerException }) + { + ReportFault(oceInnerException, severity, forceDump); + return; + } + + if (exception is AggregateException aggregateException) + { + // We (potentially) have multiple exceptions; let's just report each of them + foreach (var innerException in aggregateException.Flatten().InnerExceptions) + ReportFault(innerException, severity, forceDump); + + return; + } + + if (_telemetryReporter is not null) + { + var eventName = GetEventName(FunctionId.NonFatalWatson); + var description = GetDescription(exception); + var currentProcess = Process.GetCurrentProcess(); + _telemetryReporter.ReportFault(eventName, description, (int)severity, forceDump, currentProcess.Id, exception); + } + } + catch (OutOfMemoryException) + { + FailFast.OnFatalException(exception); + } + catch (Exception e) + { + FailFast.OnFatalException(e); + } + } + + public bool IsEnabled(FunctionId functionId) + => _telemetryReporter is not null; + + public void Log(FunctionId functionId, LogMessage logMessage) + { + var name = GetEventName(functionId); + var properties = GetProperties(functionId, logMessage, delta: null); + + try + { + _telemetryReporter?.Log(name, properties); + } + catch + { + } + } + + public void LogBlockStart(FunctionId functionId, LogMessage logMessage, int blockId, CancellationToken cancellationToken) + { + var eventName = GetEventName(functionId); + var kind = GetKind(logMessage); + + try + { + _telemetryReporter?.LogBlockStart(eventName, (int)kind, blockId); + } + catch + { + } + } + + public void LogBlockEnd(FunctionId functionId, LogMessage logMessage, int blockId, int delta, CancellationToken cancellationToken) + { + var properties = GetProperties(functionId, logMessage, delta); + try + { + _telemetryReporter?.LogBlockEnd(blockId, properties, cancellationToken); + } + catch + { + } + } + + public static void ShutdownAndReportSessionTelemetry() + { + if (_instance is null) + { + return; + } + + FeaturesSessionTelemetry.Report(); + + (var currentReporter, _telemetryReporter) = (_telemetryReporter, null); + currentReporter?.Dispose(); + _instance = null; + } + + private static string GetDescription(Exception exception) + { + const string CodeAnalysisNamespace = nameof(Microsoft) + "." + nameof(CodeAnalysis); + + // Be resilient to failing here. If we can't get a suitable name, just fall back to the standard name we + // used to report. + try + { + // walk up the stack looking for the first call from a type that isn't in the ErrorReporting namespace. + var frames = new StackTrace(exception).GetFrames(); + + // On the .NET Framework, GetFrames() can return null even though it's not documented as such. + // At least one case here is if the exception's stack trace itself is null. + if (frames != null) + { + foreach (var frame in frames) + { + var method = frame?.GetMethod(); + var methodName = method?.Name; + if (methodName == null) + continue; + + var declaringTypeName = method?.DeclaringType?.FullName; + if (declaringTypeName == null) + continue; + + if (!declaringTypeName.StartsWith(CodeAnalysisNamespace)) + continue; + + return declaringTypeName + "." + methodName; + } + } + } + catch + { + } + + // If we couldn't get a stack, do this + return exception.Message; + } + + private const string EventPrefix = "vs/ide/vbcs/"; + private const string PropertyPrefix = "vs.ide.vbcs."; + + private static string GetEventName(FunctionId id) + => s_eventMap.GetOrAdd(id, id => EventPrefix + GetTelemetryName(id, separator: '/')); + + private static string GetPropertyName(FunctionId id, string name) + => s_propertyMap.GetOrAdd((id, name), key => PropertyPrefix + GetTelemetryName(id, separator: '.') + "." + key.name.ToLowerInvariant()); + + private static string GetTelemetryName(FunctionId id, char separator) + => Enum.GetName(typeof(FunctionId), id)!.Replace('_', separator).ToLowerInvariant(); + + private static LogType GetKind(LogMessage logMessage) + => logMessage is KeyValueLogMessage kvLogMessage + ? kvLogMessage.Kind + : logMessage.LogLevel switch + { + >= LogLevel.Information => LogType.UserAction, + _ => LogType.Trace + }; + + private static ImmutableDictionary GetProperties(FunctionId id, LogMessage logMessage, int? delta) + { + var builder = ImmutableDictionary.CreateBuilder(); + + if (logMessage is KeyValueLogMessage kvLogMessage) + { + foreach (var (name, val) in kvLogMessage.Properties) + { + builder.Add(GetPropertyName(id, name), val); + } + } + else + { + builder.Add(GetPropertyName(id, "Message"), logMessage.GetMessage()); + } + + if (delta.HasValue) + { + builder.Add(GetPropertyName(id, "Delta"), delta.Value); + } + + return builder.ToImmutableDictionary(); + } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Logging/ServerLoggerFactory.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Logging/ServerLoggerFactory.cs new file mode 100644 index 0000000000000..84ea12bffbd89 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Logging/ServerLoggerFactory.cs @@ -0,0 +1,47 @@ +// 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.Composition; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.Extensions.Logging; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Logging; + +[Export(typeof(ILoggerFactory))] +[Export(typeof(ServerLoggerFactory))] +[Shared] +internal class ServerLoggerFactory : ILoggerFactory +{ + private ILoggerFactory? _loggerFactory; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ServerLoggerFactory() + { + } + + public void SetFactory(ILoggerFactory loggerFactory) + { + Contract.ThrowIfTrue(_loggerFactory is not null); + _loggerFactory = loggerFactory; + } + + void ILoggerFactory.AddProvider(ILoggerProvider provider) + { + Contract.ThrowIfNull(_loggerFactory); + _loggerFactory.AddProvider(provider); + } + + ILogger ILoggerFactory.CreateLogger(string categoryName) + { + Contract.ThrowIfNull(_loggerFactory); + return _loggerFactory.CreateLogger(categoryName); + } + + void IDisposable.Dispose() + { + _loggerFactory?.Dispose(); + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Microsoft.CodeAnalysis.LanguageServer.csproj b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Microsoft.CodeAnalysis.LanguageServer.csproj new file mode 100644 index 0000000000000..e629e6d4a436b --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Microsoft.CodeAnalysis.LanguageServer.csproj @@ -0,0 +1,133 @@ + + + Exe + net7.0 + enable + enable + + true + + true + + PublishAllRids;$(BeforePack) + + NU5100 + + $(ArtifactsDir)/LanguageServer + + + false + $(ArtifactsDir)/LanguageServer/$(Configuration)/$(TargetFramework)/$(RuntimeIdentifier) + $(ArtifactsDir)/LanguageServer/$(Configuration)/$(TargetFramework)/neutral + + 7.0.0-preview.7.22362.8 + + + win-x64;win-x86;win-arm64;linux-x64;linux-arm64;alpine-x64;alpine-arm64;osx-x64;osx-arm64 + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + RuntimeIdentifier=%(RuntimeIdentifierForPublish.Identity) + + + + + + + + + + + + + true + content\LanguageServer + false + None + + + + diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs new file mode 100644 index 0000000000000..42b6a40e1866b --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs @@ -0,0 +1,201 @@ +// 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.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.Parsing; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Microsoft.CodeAnalysis.Contracts.Telemetry; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.BrokeredServices; +using Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services.HelloWorld; +using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; +using Microsoft.CodeAnalysis.LanguageServer.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Logging; +using Microsoft.CodeAnalysis.LanguageServer.StarredSuggestions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Console; + +// Setting the title can fail if the process is run without a window, such +// as when launched detached from nodejs +try +{ + Console.Title = "Microsoft.CodeAnalysis.LanguageServer"; +} +catch (IOException) +{ +} + +WindowsErrorReporting.SetErrorModeOnWindows(); + +var parser = CreateCommandLineParser(); +return await parser.InvokeAsync(args); + +static async Task RunAsync( + bool launchDebugger, + LogLevel minimumLogLevel, + string? starredCompletionPath, + string? telemetryLevel, + string? sessionId, + string? sharedDependenciesPath, + IEnumerable extensionAssemblyPaths, + CancellationToken cancellationToken) +{ + // Before we initialize the LSP server we can't send LSP log messages. + // Create a console logger as a fallback to use before the LSP server starts. + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(minimumLogLevel); + builder.AddProvider(new LspLogMessageLoggerProvider(fallbackLoggerFactory: + // Add a console logger as a fallback for when the LSP server has not finished initializing. + LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(minimumLogLevel); + builder.AddConsole(options => options.LogToStandardErrorThreshold = LogLevel.Trace); + // The console logger outputs control characters on unix for colors which don't render correctly in VSCode. + builder.AddSimpleConsole(formatterOptions => formatterOptions.ColorBehavior = LoggerColorBehavior.Disabled); + }) + )); + }); + + if (launchDebugger) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Debugger.Launch() only works on Windows. + _ = Debugger.Launch(); + } + else + { + var logger = loggerFactory.CreateLogger(); + var timeout = TimeSpan.FromMinutes(1); + logger.LogCritical($"Server started with process ID {Environment.ProcessId}"); + logger.LogCritical($"Waiting {timeout:g} for a debugger to attach"); + using var timeoutSource = new CancellationTokenSource(timeout); + while (!Debugger.IsAttached && !timeoutSource.Token.IsCancellationRequested) + { + await Task.Delay(100, CancellationToken.None); + } + } + } + + using var exportProvider = await ExportProviderBuilder.CreateExportProviderAsync(extensionAssemblyPaths, sharedDependenciesPath, loggerFactory); + + // Initialize the fault handler if it's available + var telemetryReporter = exportProvider.GetExports().SingleOrDefault()?.Value; + RoslynLogger.Initialize(telemetryReporter, telemetryLevel, sessionId); + + // Create the workspace first, since right now the language server will assume there's at least one Workspace + var workspaceFactory = exportProvider.GetExportedValue(); + + var analyzerPaths = new DirectoryInfo(AppContext.BaseDirectory).GetFiles("*.dll") + .Where(f => f.Name.StartsWith("Microsoft.CodeAnalysis.", StringComparison.Ordinal) && !f.Name.Contains("LanguageServer", StringComparison.Ordinal)) + .Select(f => f.FullName) + .ToImmutableArray(); + + await workspaceFactory.InitializeSolutionLevelAnalyzersAsync(analyzerPaths); + + var serviceBrokerFactory = exportProvider.GetExportedValue(); + StarredCompletionAssemblyHelper.InitializeInstance(starredCompletionPath, loggerFactory, serviceBrokerFactory); + + var server = new LanguageServerHost(Console.OpenStandardInput(), Console.OpenStandardOutput(), exportProvider, loggerFactory.CreateLogger(nameof(LanguageServerHost))); + server.Start(); + + try + { + await server.WaitForExitAsync(); + } + finally + { + // After the LSP server shutdown, report session wide telemetry + RoslynLogger.ShutdownAndReportSessionTelemetry(); + + // Server has exited, cancel our service broker service + await serviceBrokerFactory.ShutdownAndWaitForCompletionAsync(); + } +} + +static Parser CreateCommandLineParser() +{ + var debugOption = new Option("--debug", getDefaultValue: () => false) + { + Description = "Flag indicating if the debugger should be launched on startup.", + IsRequired = false, + }; + var brokeredServicePipeNameOption = new Option("--brokeredServicePipeName") + { + Description = "The name of the pipe used to connect to a remote process (if one exists).", + IsRequired = false, + }; + + var logLevelOption = new Option("--logLevel", description: "The minimum log verbosity.", parseArgument: result => + { + var value = result.Tokens.Single().Value; + return !Enum.TryParse(value, out var logLevel) + ? throw new InvalidOperationException($"Unexpected logLevel argument {result}") + : logLevel; + }) + { + IsRequired = true, + }; + var starredCompletionsPathOption = new Option("--starredCompletionComponentPath") + { + Description = "The location of the starred completion component (if one exists).", + IsRequired = false, + }; + + var telemetryLevelOption = new Option("--telemetryLevel") + { + Description = "Telemetry level, Defaults to 'off'. Example values: 'all', 'crash', 'error', or 'off'.", + IsRequired = false, + }; + + var sessionIdOption = new Option("--sessionId") + { + Description = "Session Id to use for telemetry", + IsRequired = false + }; + + var sharedDependenciesOption = new Option("--sharedDependencies") + { + Description = "Full path of the directory containing shared assemblies (optional).", + IsRequired = false + }; + + var extensionAssemblyPathsOption = new Option(new string[] { "--extension", "--extensions" }) // TODO: remove plural form + { + Description = "Full paths of extension assemblies to load (optional).", + IsRequired = false + }; + + var rootCommand = new RootCommand() + { + debugOption, + brokeredServicePipeNameOption, + logLevelOption, + starredCompletionsPathOption, + telemetryLevelOption, + sessionIdOption, + sharedDependenciesOption, + extensionAssemblyPathsOption, + }; + rootCommand.SetHandler(context => + { + var cancellationToken = context.GetCancellationToken(); + var launchDebugger = context.ParseResult.GetValueForOption(debugOption); + var logLevel = context.ParseResult.GetValueForOption(logLevelOption); + var starredCompletionsPath = context.ParseResult.GetValueForOption(starredCompletionsPathOption); + var telemetryLevel = context.ParseResult.GetValueForOption(telemetryLevelOption); + var sessionId = context.ParseResult.GetValueForOption(sessionIdOption); + var sharedDependenciesPath = context.ParseResult.GetValueForOption(sharedDependenciesOption); + var extensionAssemblyPaths = context.ParseResult.GetValueForOption(extensionAssemblyPathsOption) ?? Array.Empty(); + + return RunAsync(launchDebugger, logLevel, starredCompletionsPath, telemetryLevel, sessionId, sharedDependenciesPath, extensionAssemblyPaths, cancellationToken); + }); + + return new CommandLineBuilder(rootCommand).UseDefaults().Build(); +} + diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/AssemblyLoadContextWrapper.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/AssemblyLoadContextWrapper.cs new file mode 100644 index 0000000000000..d61012c2aafb7 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/AssemblyLoadContextWrapper.cs @@ -0,0 +1,173 @@ +// 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.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Services +{ + internal sealed class AssemblyLoadContextWrapper : IDisposable + { + private static readonly ConcurrentDictionary s_loadedSharedAssemblies = new(AssemblyNameComparer.Default); + + private AssemblyLoadContext? _assemblyLoadContext; + private readonly ImmutableDictionary _loadedAssemblies; + private readonly ILogger? _logger; + + private AssemblyLoadContextWrapper(AssemblyLoadContext assemblyLoadContext, ImmutableDictionary loadedFiles, ILogger? logger) + { + _assemblyLoadContext = assemblyLoadContext; + _loadedAssemblies = loadedFiles; + _logger = logger; + } + + public static bool TryLoadExtension(string assemblyFilePath, string? sharedDependenciesPath, ILogger? logger, [NotNullWhen(true)] out Assembly? assembly) + { + var dir = Path.GetDirectoryName(assemblyFilePath); + var fileName = Path.GetFileName(assemblyFilePath); + var fileNameNoExt = Path.GetFileNameWithoutExtension(assemblyFilePath); + + Contract.ThrowIfNull(dir); + Contract.ThrowIfNull(fileName); + Contract.ThrowIfNull(fileNameNoExt); + + var loadContext = TryCreate(fileNameNoExt, dir, sharedDependenciesPath, logger); + if (loadContext != null) + { + assembly = loadContext.GetAssembly(fileName); + return true; + } + + assembly = null; + return false; + } + + public static AssemblyLoadContextWrapper? TryCreate(string name, string assembliesDirectoryPath, string? sharedDependenciesPath, ILogger? logger) + { + try + { + var loadContext = CreateLoadContext(name, sharedDependenciesPath); + var directory = new DirectoryInfo(assembliesDirectoryPath); + var builder = new Dictionary(); + foreach (var file in directory.GetFiles("*.dll")) + { + builder.Add(file.Name, loadContext.LoadFromAssemblyPath(file.FullName)); + } + + return new AssemblyLoadContextWrapper(loadContext, builder.ToImmutableDictionary(), logger); + } + catch (Exception ex) + { + logger?.LogError(ex, "Failed to initialize AssemblyLoadContext {name}", name); + return null; + } + } + + private static AssemblyLoadContext CreateLoadContext(string name, string? sharedDependenciesPath) + { + var loadContext = new AssemblyLoadContext(name); + + if (sharedDependenciesPath != null) + { + loadContext.Resolving += (_, assemblyName) => + { + if (assemblyName.Name is null) + { + return null; + } + + if (s_loadedSharedAssemblies.TryGetValue(assemblyName, out var loadedAssembly)) + { + return loadedAssembly; + } + + var candidatePath = assemblyName.CultureName is not null + ? Path.Combine(sharedDependenciesPath, assemblyName.CultureName, $"{assemblyName.Name}.dll") + : Path.Combine(sharedDependenciesPath, $"{assemblyName.Name}.dll"); + + if (File.Exists(candidatePath)) + { + loadedAssembly = loadContext.LoadFromAssemblyPath(candidatePath); + } + + s_loadedSharedAssemblies.TryAdd(assemblyName, loadedAssembly); + + return s_loadedSharedAssemblies[assemblyName]; + }; + } + + return loadContext; + } + + public Assembly GetAssembly(string name) => _loadedAssemblies[name]; + + public MethodInfo? TryGetMethodInfo(string assemblyName, string className, string methodName) + { + try + { + return GetMethodInfo(assemblyName, className, methodName); + } + catch (Exception ex) + { + _logger?.LogError(ex, "Failed to get method information from {assembly} for {class}.{method}", assemblyName, className, methodName); + return null; + } + } + + public MethodInfo GetMethodInfo(string assemblyName, string className, string methodName) + { + var assembly = GetAssembly(assemblyName); + var completionHelperType = assembly.GetType(className); + if (completionHelperType == null) + { + throw new ArgumentException($"{assembly.FullName} assembly did not contain {className} class"); + } + var createCompletionProviderMethodInto = completionHelperType?.GetMethod(methodName); + if (createCompletionProviderMethodInto == null) + { + throw new ArgumentException($"{className} from {assembly.FullName} assembly did not contain {methodName} method"); + } + return createCompletionProviderMethodInto; + } + + public void Dispose() + { + _assemblyLoadContext?.Unload(); + _assemblyLoadContext = null; + } + + private sealed class AssemblyNameComparer : IEqualityComparer + { + public static readonly AssemblyNameComparer Default = new(); + + public bool Equals(AssemblyName? x, AssemblyName? y) + { + if (ReferenceEquals(x, y)) + return true; + + if (x == null || y == null) + return false; + + return string.Equals(x.Name, y.Name, StringComparison.OrdinalIgnoreCase) + && string.Equals(x.CultureName, y.CultureName, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode(AssemblyName obj) + => HashCode.Combine( + StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Name ?? string.Empty), + StringComparer.OrdinalIgnoreCase.GetHashCode(obj.CultureName ?? string.Empty)); + } + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/StarredCompletions/StarredCompletionProvider.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/StarredCompletions/StarredCompletionProvider.cs new file mode 100644 index 0000000000000..2fceb3ddfa578 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/StarredCompletions/StarredCompletionProvider.cs @@ -0,0 +1,46 @@ +// 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.Composition; +using System.Threading; +using Microsoft.CodeAnalysis.Completion; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; +using Microsoft.CodeAnalysis.LanguageService; + +namespace Microsoft.CodeAnalysis.LanguageServer.StarredSuggestions; + +[ExportCompletionProvider("CSharpStarredCompletionProvider", LanguageNames.CSharp), Shared] +internal class StarredCompletionProvider : CompletionProvider +{ + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public StarredCompletionProvider() { } + + public override async Task ProvideCompletionsAsync(CompletionContext context) + { + var provider = await StarredCompletionAssemblyHelper.GetCompletionProviderAsync(context.CancellationToken); + if (provider == null) + { + return; //no-op if provider cannot be retrieved from assembly + } + await provider.ProvideCompletionsAsync(context); + } + + public override async Task GetChangeAsync(Document document, CompletionItem item, char? commitKey = null, CancellationToken cancellationToken = default) + { + var provider = await StarredCompletionAssemblyHelper.GetCompletionProviderAsync(cancellationToken); + Contract.ThrowIfNull(provider, "ProvideCompletionsAsync must have completed successfully for GetChangeAsync to be called"); + return await provider.GetChangeAsync(document, item, commitKey, cancellationToken).ConfigureAwait(false); + } + + internal override async Task GetDescriptionAsync(Document document, CompletionItem item, CompletionOptions options, SymbolDescriptionOptions displayOptions, CancellationToken cancellationToken) + { + var provider = await StarredCompletionAssemblyHelper.GetCompletionProviderAsync(cancellationToken); + Contract.ThrowIfNull(provider, "ProvideCompletionsAsync must have completed successfully for GetDescriptionAsync to be called"); + return await provider.GetDescriptionAsync(document, item, options, displayOptions, cancellationToken); + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/StarredCompletions/StarredCompletionsAssemblyHelper.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/StarredCompletions/StarredCompletionsAssemblyHelper.cs new file mode 100644 index 0000000000000..03a581634f047 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/StarredCompletions/StarredCompletionsAssemblyHelper.cs @@ -0,0 +1,122 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.Loader; +using Microsoft.CodeAnalysis.Completion; +using Microsoft.CodeAnalysis.LanguageServer.BrokeredServices; +using Microsoft.CodeAnalysis.LanguageServer.BrokeredServices.Services; +using Microsoft.CodeAnalysis.LanguageServer.Services; +using Microsoft.Extensions.Logging; +using Microsoft.ServiceHub.Framework; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.StarredSuggestions; +internal static class StarredCompletionAssemblyHelper +{ + private const string CompletionsDllName = "Microsoft.VisualStudio.IntelliCode.CSharp.dll"; + private const string ALCName = "IntelliCode-ALC"; + private const string CompletionHelperClassFullName = "PythiaCSDevKit.CSDevKitCompletionHelper"; + private const string CreateCompletionProviderMethodName = "CreateCompletionProviderAsync"; + + // The following fields are only set as a part of the call to InitializeInstance, which is only called once for the lifetime of the process. Thus, it is safe to assume that once + // set, they will never change again. + private static string? s_completionsAssemblyLocation; + private static ILogger? s_logger; + private static ServiceBrokerFactory? s_serviceBrokerFactory; + + /// + /// A gate to guard the actual creation of . This just prevents us from trying to create the provider more than once; once the field is set it + /// won't change again. + /// + private static readonly SemaphoreSlim s_gate = new SemaphoreSlim(initialCount: 1); + private static bool s_previousCreationFailed = false; + private static CompletionProvider? s_completionProvider; + + /// + /// Initializes CompletionsAssemblyHelper singleton + /// + /// Location of dll for starred completion + /// Factory for creating new logger + /// Service broker with access to necessary remote services + internal static void InitializeInstance(string? completionsAssemblyLocation, ILoggerFactory loggerFactory, ServiceBrokerFactory serviceBrokerFactory) + { + // No location provided means it wasn't passed through from C# Dev Kit, so we don't need to initialize anything further + if (string.IsNullOrEmpty(completionsAssemblyLocation)) + { + return; + } + + // C# Dev Kit must be installed, so we should be able to provide this; however we may not yet have a connection to the Dev Kit service broker, so we need to defer the actual creation + // until that point. + s_completionsAssemblyLocation = completionsAssemblyLocation; + s_logger = loggerFactory.CreateLogger(typeof(StarredCompletionAssemblyHelper)); + s_serviceBrokerFactory = serviceBrokerFactory; + } + + internal static async Task GetCompletionProviderAsync(CancellationToken cancellationToken) + { + // Short cut: if we already have a provider, return it + if (s_completionProvider is CompletionProvider completionProvider) + return completionProvider; + + // If we don't have one because we previously failed to create one, then just return failure + if (s_previousCreationFailed) + return null; + + // If we were never initialized with any information from Dev Kit, we can't create one + if (s_completionsAssemblyLocation is null || s_logger is null || s_serviceBrokerFactory is null) + return null; + + // If we don't have a connection to a service broker yet, we also can't create one + var serviceBroker = s_serviceBrokerFactory.TryGetFullAccessServiceBroker(); + if (serviceBroker is null) + return null; + + // At this point, we have everything we need to go and create the provider, so let's do it + using (await s_gate.DisposableWaitAsync(cancellationToken)) + { + // Re-check this inside the lock, since we could have had a success between the earlier check and now + if (s_completionProvider is CompletionProvider completionProviderInsideLock) + return completionProviderInsideLock; + + // Re-check this inside the lock, since we could have had a failure between the earlier check and now + if (s_previousCreationFailed) + return null; + + try + { + var alc = AssemblyLoadContextWrapper.TryCreate(ALCName, s_completionsAssemblyLocation, sharedDependenciesPath: null, s_logger); + if (alc is null) + { + s_previousCreationFailed = true; + return null; + } + + var createCompletionProviderMethodInfo = alc.GetMethodInfo(CompletionsDllName, CompletionHelperClassFullName, CreateCompletionProviderMethodName); + + s_completionProvider = await CreateCompletionProviderAsync(createCompletionProviderMethodInfo, serviceBroker, s_completionsAssemblyLocation, s_logger); + return s_completionProvider; + } + catch (Exception ex) + { + s_previousCreationFailed = true; + s_logger.LogError(ex, "Unable to create the StarredCompletionProvider."); + throw; + } + } + } + + private static async Task CreateCompletionProviderAsync(MethodInfo createCompletionProviderMethodInfo, IServiceBroker serviceBroker, string modelBasePath, ILogger logger) + { + var completionProviderObj = createCompletionProviderMethodInfo.Invoke(null, new object[4] { serviceBroker, BrokeredServices.Services.Descriptors.RemoteModelService, modelBasePath, logger }); + if (completionProviderObj == null) + { + throw new NotSupportedException($"{createCompletionProviderMethodInfo.Name} method could not be invoked"); + } + var completionProvider = (Task)completionProviderObj; + return await completionProvider; + } +} diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/WindowsErrorReporting.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/WindowsErrorReporting.cs new file mode 100644 index 0000000000000..f11f0b62d8cb4 --- /dev/null +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/WindowsErrorReporting.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.Diagnostics; +using System.Runtime.InteropServices; + +internal class WindowsErrorReporting +{ + internal static void SetErrorModeOnWindows() + { + if (!OperatingSystem.IsWindows()) + { + return; + } + + SetErrorMode(ErrorModes.SYSTEM_DEFAULT); + + // There have been reports that SetErrorMode wasn't working correctly, so double + // check in Debug builds that it actually set + Debug.Assert(GetErrorMode() == (uint)ErrorModes.SYSTEM_DEFAULT); + } + + [DllImport("kernel32.dll")] + private static extern ErrorModes SetErrorMode(ErrorModes uMode); + + [DllImport("kernel32.dll")] + private static extern uint GetErrorMode(); + + [Flags] + private enum ErrorModes : uint + { + SYSTEM_DEFAULT = 0x0, + SEM_FAILCRITICALERRORS = 0x0001, + SEM_NOGPFAULTERRORBOX = 0x0002, + SEM_NOALIGNMENTFAULTEXCEPT = 0x0004, + SEM_NOOPENFILEERRORBOX = 0x8000 + } +} diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/QueueItem.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/QueueItem.cs index 8dcb9e1f04a41..6874d32e77673 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/QueueItem.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/QueueItem.cs @@ -147,10 +147,7 @@ public async Task StartRequestAsync(TRequestContext? context, CancellationToken } else { - throw new NotImplementedException( - $"Unrecognized {nameof(IMethodHandler)} implementation {_handler.GetType()}. " + - $"TRequest is {typeof(TRequest)}. " + - $"TResponse is {typeof(TResponse)}."); + throw new NotImplementedException($"Unrecognized {nameof(IMethodHandler)} implementation {_handler.GetType()}. "); } } catch (OperationCanceledException ex) diff --git a/src/Features/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs b/src/Features/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs index 803821542f104..7e530a737459d 100644 --- a/src/Features/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs +++ b/src/Features/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs @@ -11,6 +11,7 @@ using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Completion.Providers; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Completion; using Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -45,9 +46,9 @@ public void Initialize() public ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) { var supportsVsExtensions = clientCapabilities.HasVisualStudioLspCapability(); - var capabilities = supportsVsExtensions ? GetVSServerCapabilities() : new ServerCapabilities(); + var capabilities = supportsVsExtensions ? GetVSServerCapabilities() : new VSInternalServerCapabilities(); - var commitCharacters = CompletionRules.Default.DefaultCommitCharacters.Select(c => c.ToString()).ToArray(); + var commitCharacters = AbstractLspCompletionResultCreationService.DefaultCommitCharactersArray; var triggerCharacters = _completionProviders.SelectMany( lz => CommonCompletionUtilities.GetTriggerCharacters(lz.Value)).Distinct().Select(c => c.ToString()).ToArray(); @@ -112,6 +113,9 @@ public ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) WorkDoneProgress = false, }; + // Using VS server capabilities because we have our own custom client. + capabilities.OnAutoInsertProvider = new VSInternalDocumentOnAutoInsertOptions { TriggerCharacters = new[] { "'", "/", "\n" } }; + if (!supportsVsExtensions) { capabilities.DiagnosticOptions = new DiagnosticOptions @@ -125,10 +129,9 @@ public ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) return capabilities; } - private static VSServerCapabilities GetVSServerCapabilities() - => new VSInternalServerCapabilities + private static VSInternalServerCapabilities GetVSServerCapabilities() + => new() { - OnAutoInsertProvider = new VSInternalDocumentOnAutoInsertOptions { TriggerCharacters = new[] { "'", "/", "\n" } }, DocumentHighlightProvider = true, ProjectContextProvider = true, BreakableRangeProvider = true, diff --git a/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs b/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs index 170bb5a2285ae..53da6d3deb3e2 100644 --- a/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs +++ b/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs @@ -77,16 +77,23 @@ public static ImmutableArray GetDocumentIds(this Solution solution, // For now we do our best to handle as many cases as we can. // Tracking issue - https://github.com/dotnet/roslyn/issues/68083 - var documentIds = solution.GetDocumentIdsWithFilePath(documentUri.AbsolutePath); - if (documentIds.Any()) - return documentIds; + // If the uri is a file then use the simplified absolute or local path. + // In other cases (e.g. git files) use the full uri string. This ensures documents + // with the same local path (e.g. git://someFilePath and file://someFilePath) are differentiated. + if (documentUri.IsFile) + { + var fileDocumentIds = solution.GetDocumentIdsWithFilePath(documentUri.AbsolutePath); + if (fileDocumentIds.Any()) + return fileDocumentIds; - documentIds = solution.GetDocumentIdsWithFilePath(documentUri.LocalPath); - if (documentIds.Any()) + fileDocumentIds = solution.GetDocumentIdsWithFilePath(documentUri.LocalPath); + return fileDocumentIds; + } + else + { + var documentIds = solution.GetDocumentIdsWithFilePath(documentUri.OriginalString); return documentIds; - - documentIds = solution.GetDocumentIdsWithFilePath(documentUri.OriginalString); - return documentIds; + } } public static Document? GetDocument(this Solution solution, TextDocumentIdentifier documentIdentifier) diff --git a/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs b/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs index b214ff92d3204..54eb27da30665 100644 --- a/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs +++ b/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs @@ -151,6 +151,12 @@ static async Task GetInsertionCharacterAsync(Document document, int positi } } + // If we have a file:///xyz URI, we'll store the actual file path string in the document file path. + // Otherwise we have a URI that doesn't point to an actual file. In such a scenario, we'll store the full URI string (including schema). + // This will allow correct round-tripping of the URI for features that need it until we support URI as a first class document concept. + // Tracking issue - https://github.com/dotnet/roslyn/issues/68083 + public static string GetDocumentFilePathFromUri(Uri uri) => uri.IsFile ? uri.LocalPath : uri.OriginalString; + public static Uri GetUriFromFilePath(string filePath) { if (filePath is null) @@ -193,7 +199,6 @@ public static LSP.VersionedTextDocumentIdentifier DocumentToVersionedTextDocumen public static LinePosition PositionToLinePosition(LSP.Position position) => new LinePosition(position.Line, position.Character); - public static LinePositionSpan RangeToLinePositionSpan(LSP.Range range) => new(PositionToLinePosition(range.Start), PositionToLinePosition(range.End)); diff --git a/src/Features/LanguageServer/Protocol/ExternalAccess/VSCode/API/VSCodeTelemetryLogger.cs b/src/Features/LanguageServer/Protocol/ExternalAccess/VSCode/API/VSCodeTelemetryLogger.cs deleted file mode 100644 index 4d7a0cb5ff504..0000000000000 --- a/src/Features/LanguageServer/Protocol/ExternalAccess/VSCode/API/VSCodeTelemetryLogger.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.Generic; -using System.Threading; -using Microsoft.CodeAnalysis.Internal.Log; - -namespace Microsoft.CodeAnalysis.LanguageServer.ExternalAccess.VSCode.API; - -/// -/// Allows VSCode to implement a telemetry logger for events. -/// -internal abstract class VSCodeTelemetryLogger : ILogger -{ - bool ILogger.IsEnabled(FunctionId functionId) - { - return IsEnabled(functionId.Convert()); - } - - void ILogger.Log(FunctionId functionId, LogMessage logMessage) - { - Log(functionId.Convert(), GetProperties(logMessage), CancellationToken.None); - } - - void ILogger.LogBlockStart(FunctionId functionId, LogMessage logMessage, int uniquePairId, CancellationToken cancellationToken) - { - LogBlockStart(functionId.Convert(), GetProperties(logMessage), cancellationToken); - } - - void ILogger.LogBlockEnd(FunctionId functionId, LogMessage logMessage, int uniquePairId, int delta, CancellationToken cancellationToken) - { - LogBlockEnd(functionId.Convert(), GetProperties(logMessage), cancellationToken); - } - - public void Register() - { - Logger.SetLogger(this); - } - - public abstract bool IsEnabled(string functionId); - - public abstract void Log(string functionId, IEnumerable>? properties, CancellationToken cancellationToken); - - public abstract void LogBlockStart(string functionId, IEnumerable>? properties, CancellationToken cancellationToken); - - public abstract void LogBlockEnd(string functionId, IEnumerable>? properties, CancellationToken cancellationToken); - - private static IEnumerable>? GetProperties(LogMessage logMessage) - { - return logMessage is KeyValueLogMessage kvMessage ? kvMessage.Properties : null; - } -} diff --git a/src/Features/LanguageServer/Protocol/Features/Diagnostics/DiagnosticAnalyzerService.cs b/src/Features/LanguageServer/Protocol/Features/Diagnostics/DiagnosticAnalyzerService.cs index 3d76bdd469550..7185f7b4aca8a 100644 --- a/src/Features/LanguageServer/Protocol/Features/Diagnostics/DiagnosticAnalyzerService.cs +++ b/src/Features/LanguageServer/Protocol/Features/Diagnostics/DiagnosticAnalyzerService.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.Diagnostics.EngineV2; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Options; @@ -45,7 +46,8 @@ public DiagnosticAnalyzerService( IDiagnosticUpdateSourceRegistrationService registrationService, IAsynchronousOperationListenerProvider listenerProvider, DiagnosticAnalyzerInfoCache.SharedGlobalCache globalCache, - IGlobalOptionService globalOptions) + IGlobalOptionService globalOptions, + IDiagnosticsRefresher diagnosticsRefresher) { AnalyzerInfoCache = globalCache.AnalyzerInfoCache; Listener = listenerProvider.GetListener(FeatureAttribute.DiagnosticService); @@ -56,9 +58,23 @@ public DiagnosticAnalyzerService( _eventQueue = new TaskQueue(Listener, TaskScheduler.Default); registrationService.Register(this); - GlobalOptions = globalOptions; + + globalOptions.AddOptionChangedHandler(this, (_, e) => + { + if (IsGlobalOptionAffectingDiagnostics(e.Option)) + { + diagnosticsRefresher.RequestWorkspaceRefresh(); + } + }); } + public static bool IsGlobalOptionAffectingDiagnostics(IOption2 option) + => option == NamingStyleOptions.NamingPreferences || + option.Definition.Group.Parent == CodeStyleOptionGroups.CodeStyle || + option == SolutionCrawlerOptionsStorage.BackgroundAnalysisScopeOption || + option == SolutionCrawlerOptionsStorage.SolutionBackgroundAnalysisScopeOption || + option == SolutionCrawlerOptionsStorage.CompilerDiagnosticsScopeOption; + public void Reanalyze(Workspace workspace, IEnumerable? projectIds, IEnumerable? documentIds, bool highPriority) { var service = workspace.Services.GetService(); @@ -83,6 +99,8 @@ public void Reanalyze(Workspace workspace, IEnumerable? projectIds, I // always make sure that analyzer is called on background thread. return Task.Run(async () => { + priorityProvider ??= new DefaultCodeActionRequestPriorityProvider(); + using var _ = ArrayBuilder.GetInstance(out var diagnostics); var upToDate = await analyzer.TryAppendDiagnosticsForSpanAsync( document, range, diagnostics, shouldIncludeDiagnostic, @@ -109,6 +127,8 @@ public Task> GetDiagnosticsForSpanAsync( { if (_map.TryGetValue(document.Project.Solution.Workspace, out var analyzer)) { + priorityProvider ??= new DefaultCodeActionRequestPriorityProvider(); + // always make sure that analyzer is called on background thread. return Task.Run(() => analyzer.GetDiagnosticsForSpanAsync( document, range, shouldIncludeDiagnostic, includeSuppressedDiagnostics, includeCompilerDiagnostics, diff --git a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs index a161c98f4e887..218e1e8ebbe3a 100644 --- a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs +++ b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.cs @@ -70,17 +70,11 @@ public DiagnosticIncrementalAnalyzer( private void OnGlobalOptionChanged(object? sender, OptionChangedEventArgs e) { - if (e.Option == NamingStyleOptions.NamingPreferences || - e.Option.Definition.Group.Parent == CodeStyleOptionGroups.CodeStyle || - e.Option == SolutionCrawlerOptionsStorage.BackgroundAnalysisScopeOption || - e.Option == SolutionCrawlerOptionsStorage.SolutionBackgroundAnalysisScopeOption || - e.Option == SolutionCrawlerOptionsStorage.CompilerDiagnosticsScopeOption) + if (DiagnosticAnalyzerService.IsGlobalOptionAffectingDiagnostics(e.Option) && + GlobalOptions.GetOption(SolutionCrawlerRegistrationService.EnableSolutionCrawler)) { - if (GlobalOptions.GetOption(SolutionCrawlerRegistrationService.EnableSolutionCrawler)) - { - var service = Workspace.Services.GetService(); - service?.Reanalyze(Workspace, this, projectIds: null, documentIds: null, highPriority: false); - } + var service = Workspace.Services.GetService(); + service?.Reanalyze(Workspace, this, projectIds: null, documentIds: null, highPriority: false); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/AbstractRefreshQueue.cs b/src/Features/LanguageServer/Protocol/Handler/AbstractRefreshQueue.cs new file mode 100644 index 0000000000000..0935cf7cbbfea --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/AbstractRefreshQueue.cs @@ -0,0 +1,127 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Collections; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler +{ + internal abstract class AbstractRefreshQueue : + IOnInitialized, + ILspService, + IDisposable + { + private AsyncBatchingWorkQueue? _refreshQueue; + + private readonly LspWorkspaceManager _lspWorkspaceManager; + private readonly IClientLanguageServerManager _notificationManager; + + private readonly IAsynchronousOperationListener _asyncListener; + private readonly CancellationTokenSource _disposalTokenSource; + private readonly LspWorkspaceRegistrationService _lspWorkspaceRegistrationService; + + protected bool _isQueueCreated; + + protected abstract string GetFeatureAttribute(); + protected abstract bool? GetRefreshSupport(ClientCapabilities clientCapabilities); + protected abstract string GetWorkspaceRefreshName(); + + public AbstractRefreshQueue( + IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider, + LspWorkspaceRegistrationService lspWorkspaceRegistrationService, + LspWorkspaceManager lspWorkspaceManager, + IClientLanguageServerManager notificationManager) + { + _isQueueCreated = false; + _asyncListener = asynchronousOperationListenerProvider.GetListener(GetFeatureAttribute()); + _lspWorkspaceRegistrationService = lspWorkspaceRegistrationService; + _disposalTokenSource = new(); + _lspWorkspaceManager = lspWorkspaceManager; + _notificationManager = notificationManager; + } + + public Task OnInitializedAsync(ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + { + if (_refreshQueue is null && GetRefreshSupport(clientCapabilities) is true) + { + // Only send a refresh notification to the client every 2s (if needed) in order to avoid + // sending too many notifications at once. This ensures we batch up workspace notifications, + // but also means we send soon enough after a compilation-computation to not make the user wait + // an enormous amount of time. + _refreshQueue = new AsyncBatchingWorkQueue( + delay: TimeSpan.FromMilliseconds(2000), + processBatchAsync: (documentUris, cancellationToken) + => FilterLspTrackedDocumentsAsync(_lspWorkspaceManager, _notificationManager, documentUris, cancellationToken), + equalityComparer: EqualityComparer.Default, + asyncListener: _asyncListener, + _disposalTokenSource.Token); + _isQueueCreated = true; + _lspWorkspaceRegistrationService.LspSolutionChanged += OnLspSolutionChanged; + } + + return Task.CompletedTask; + } + + private void OnLspSolutionChanged(object? sender, WorkspaceChangeEventArgs e) + { + if (e.DocumentId is not null && e.Kind is WorkspaceChangeKind.DocumentChanged) + { + var document = e.NewSolution.GetRequiredDocument(e.DocumentId); + var documentUri = document.GetURI(); + + // We enqueue the URI since there's a chance the client is already tracking the + // document, in which case we don't need to send a refresh notification. + // We perform the actual check when processing the batch to ensure we have the + // most up-to-date list of tracked documents. + EnqueueRefreshNotification(documentUri); + } + else + { + EnqueueRefreshNotification(documentUri: null); + } + } + + protected void EnqueueRefreshNotification(Uri? documentUri) + { + if (_isQueueCreated) + { + Contract.ThrowIfNull(_refreshQueue); + _refreshQueue.AddWork(documentUri); + } + } + + private ValueTask FilterLspTrackedDocumentsAsync( + LspWorkspaceManager lspWorkspaceManager, + IClientLanguageServerManager notificationManager, + ImmutableSegmentedList documentUris, + CancellationToken cancellationToken) + { + var trackedDocuments = lspWorkspaceManager.GetTrackedLspText(); + foreach (var documentUri in documentUris) + { + if (documentUri is null || !trackedDocuments.ContainsKey(documentUri)) + { + return notificationManager.SendRequestAsync(GetWorkspaceRefreshName(), cancellationToken); + } + } + + // LSP is already tracking all changed documents so we don't need to send a refresh request. + return ValueTaskFactory.CompletedTask; + } + + public virtual void Dispose() + { + _lspWorkspaceRegistrationService.LspSolutionChanged -= OnLspSolutionChanged; + _disposalTokenSource.Cancel(); + _disposalTokenSource.Dispose(); + } + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensRefreshQueue.cs b/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensRefreshQueue.cs new file mode 100644 index 0000000000000..890c9f94ef16d --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensRefreshQueue.cs @@ -0,0 +1,34 @@ +// 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 Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.CodeLens +{ + internal class CodeLensRefreshQueue : AbstractRefreshQueue + { + public CodeLensRefreshQueue( + IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider, + LspWorkspaceRegistrationService lspWorkspaceRegistrationService, + LspWorkspaceManager lspWorkspaceManager, + IClientLanguageServerManager notificationManager) + : base(asynchronousOperationListenerProvider, lspWorkspaceRegistrationService, lspWorkspaceManager, notificationManager) + { + } + + protected override string GetFeatureAttribute() + => FeatureAttribute.CodeLens; + + protected override bool? GetRefreshSupport(ClientCapabilities clientCapabilities) + { + return clientCapabilities.Workspace?.CodeLens?.RefreshSupport; + } + + protected override string GetWorkspaceRefreshName() + { + return Methods.WorkspaceCodeLensRefreshName; + } + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensRefreshQueueFactory.cs b/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensRefreshQueueFactory.cs new file mode 100644 index 0000000000000..1102b80c71eb6 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/CodeLens/CodeLensRefreshQueueFactory.cs @@ -0,0 +1,38 @@ +// 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.Options; +using Microsoft.CodeAnalysis.Shared.TestHooks; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.CodeLens +{ + [ExportCSharpVisualBasicLspServiceFactory(typeof(CodeLensRefreshQueue)), Shared] + internal sealed class CodeLensRefreshQueueFactory : ILspServiceFactory + { + private readonly IAsynchronousOperationListenerProvider _asyncListenerProvider; + private readonly LspWorkspaceRegistrationService _lspWorkspaceRegistrationService; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public CodeLensRefreshQueueFactory( + IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider, + LspWorkspaceRegistrationService lspWorkspaceRegistrationService, + IGlobalOptionService globalOptionService) + { + _asyncListenerProvider = asynchronousOperationListenerProvider; + _lspWorkspaceRegistrationService = lspWorkspaceRegistrationService; + } + + public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) + { + var notificationManager = lspServices.GetRequiredService(); + var lspWorkspaceManager = lspServices.GetRequiredService(); + + return new CodeLensRefreshQueue(_asyncListenerProvider, _lspWorkspaceRegistrationService, lspWorkspaceManager, notificationManager); + } + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Completion/AbstractLspCompletionResultCreationService.cs b/src/Features/LanguageServer/Protocol/Handler/Completion/AbstractLspCompletionResultCreationService.cs new file mode 100644 index 0000000000000..231522562851f --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Completion/AbstractLspCompletionResultCreationService.cs @@ -0,0 +1,455 @@ +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Completion; +using Microsoft.CodeAnalysis.Completion.Providers; +using Microsoft.CodeAnalysis.Completion.Providers.Snippets; +using Microsoft.CodeAnalysis.LanguageService; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; +using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Completion +{ + internal abstract class AbstractLspCompletionResultCreationService : ILspCompletionResultCreationService + { + protected abstract Task CreateItemAndPopulateTextEditAsync(Document document, SourceText documentText, bool snippetsSupported, bool itemDefaultsSupported, TextSpan defaultSpan, string typedText, CompletionItem item, CompletionService completionService, CancellationToken cancellationToken); + public abstract Task ResolveAsync(LSP.CompletionItem lspItem, CompletionItem roslynItem, LSP.TextDocumentIdentifier textDocumentIdentifier, Document document, CompletionCapabilityHelper capabilityHelper, CompletionService completionService, CompletionOptions completionOptions, SymbolDescriptionOptions symbolDescriptionOptions, CancellationToken cancellationToken); + + public static string[] DefaultCommitCharactersArray { get; } = CreateCommitCharacterArrayFromRules(CompletionItemRules.Default); + + public async Task ConvertToLspCompletionListAsync( + Document document, + CompletionCapabilityHelper capabilityHelper, + CompletionList list, bool isIncomplete, long resultId, + CancellationToken cancellationToken) + { + if (list.ItemsList.Count == 0) + { + return new LSP.VSInternalCompletionList + { + Items = Array.Empty(), + // If we have a suggestion mode item, we just need to keep the list in suggestion mode. + // We don't need to return the fake suggestion mode item. + SuggestionMode = list.SuggestionModeItem is not null, + IsIncomplete = isIncomplete, + }; + } + + var lspVSClientCapability = capabilityHelper.SupportVSInternalClientCapabilities; + var defaultEditRangeSupported = capabilityHelper.SupportDefaultEditRange; + + // We use the default completion list span as our comparison point for optimization when generating the TextEdits later on. + var defaultSpan = list.Span; + var documentText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + + // Set resolve data on list if the client supports it, otherwise set it on each item. + var resolveData = new CompletionResolveData() { ResultId = resultId }; + var completionItemResolveData = capabilityHelper.SupportCompletionListData || capabilityHelper.SupportVSInternalCompletionListData + ? null : resolveData; + + using var _ = ArrayBuilder.GetInstance(out var lspCompletionItems); + var commitCharactersRuleCache = new Dictionary, string[]>(CommitCharacterArrayComparer.Instance); + + var creationService = document.Project.Solution.Services.GetRequiredService(); + var completionService = document.GetRequiredLanguageService(); + + var typedText = documentText.GetSubText(defaultSpan).ToString(); + foreach (var item in list.ItemsList) + lspCompletionItems.Add(await CreateLSPCompletionItemAsync(item, typedText).ConfigureAwait(false)); + + var completionList = new LSP.VSInternalCompletionList + { + // public LSP + Items = lspCompletionItems.ToArray(), + IsIncomplete = isIncomplete, + ItemDefaults = new LSP.CompletionListItemDefaults + { + EditRange = capabilityHelper.SupportDefaultEditRange ? ProtocolConversions.TextSpanToRange(defaultSpan, documentText) : null, + Data = capabilityHelper.SupportCompletionListData ? resolveData : null + }, + + // VS internal + // + // If we have a suggestion mode item, we just need to keep the list in suggestion mode. + // We don't need to return the fake suggestion mode item. + SuggestionMode = list.SuggestionModeItem != null, + Data = capabilityHelper.SupportVSInternalCompletionListData ? resolveData : null, + }; + + PromoteCommonCommitCharactersOntoList(); + + if (completionList.ItemDefaults.EditRange is null && completionList.ItemDefaults.CommitCharacters is null && completionList.ItemDefaults.Data is null) + completionList.ItemDefaults = null; + + return capabilityHelper.SupportVSInternalClientCapabilities + ? new LSP.OptimizedVSCompletionList(completionList) + : completionList; + + async Task CreateLSPCompletionItemAsync(CompletionItem item, string typedText) + { + // Defer to host to create the actual completion item (including potential subclasses), and add any + // custom information. + var lspItem = await CreateItemAndPopulateTextEditAsync( + document, documentText, capabilityHelper.SupportSnippets, defaultEditRangeSupported, defaultSpan, typedText, item, completionService, cancellationToken).ConfigureAwait(false); + + if (!item.InlineDescription.IsEmpty()) + lspItem.LabelDetails = new() { Description = item.InlineDescription }; + + // Now add data common to all hosts. + lspItem.Data = completionItemResolveData; + + if (!lspItem.Label.Equals(item.SortText, StringComparison.Ordinal)) + lspItem.SortText = item.SortText; + + if (!lspItem.Label.Equals(item.FilterText, StringComparison.Ordinal)) + lspItem.FilterText = item.FilterText; + + lspItem.Kind = GetCompletionKind(item.Tags, capabilityHelper.SupportedItemKinds); + lspItem.Preselect = item.Rules.MatchPriority == MatchPriority.Preselect; + + if (!lspItem.Preselect && + !lspVSClientCapability && + typedText.Length == 0 && + item.Rules.SelectionBehavior != CompletionItemSelectionBehavior.HardSelection) + { + // VSCode does not have the concept of soft selection, the list is always hard selected. + // In order to emulate soft selection behavior for things like argument completion, regex completion, + // datetime completion, etc. we create a completion item without any specific commit characters. + // This means only tab / enter will commit. VS supports soft selection, so we only do this for non-VS clients. + // + // Note this only applies when user hasn't actually typed anything and completion provider does not request the item + // to be hard-selected. Otherwise, we set its commit characters as normal. This also means we'd need to set IsIncomplete to true + // to make sure the client will ask us again when user starts typing so we can provide items with proper commit characters. + lspItem.CommitCharacters = Array.Empty(); + isIncomplete = true; + } + else + { + lspItem.CommitCharacters = GetCommitCharacters(item, commitCharactersRuleCache); + } + + return lspItem; + } + + static LSP.CompletionItemKind GetCompletionKind( + ImmutableArray tags, + ISet supportedClientKinds) + { + foreach (var tag in tags) + { + if (ProtocolConversions.RoslynTagToCompletionItemKinds.TryGetValue(tag, out var completionItemKinds)) + { + // Always at least pick the core kind provided. + var kind = completionItemKinds[0]; + + // If better kinds are preferred, return them if the client supports them. + for (var i = 1; i < completionItemKinds.Length; i++) + { + var preferredKind = completionItemKinds[i]; + if (supportedClientKinds.Contains(preferredKind)) + kind = preferredKind; + } + + return kind; + } + } + + return LSP.CompletionItemKind.Text; + } + + static string[] GetCommitCharacters( + CompletionItem item, + Dictionary, string[]> currentRuleCache) + { + if (item.Rules.CommitCharacterRules.IsEmpty) + return DefaultCommitCharactersArray; + + if (!currentRuleCache.TryGetValue(item.Rules.CommitCharacterRules, out var cachedCommitCharacters)) + { + cachedCommitCharacters = CreateCommitCharacterArrayFromRules(item.Rules); + currentRuleCache.Add(item.Rules.CommitCharacterRules, cachedCommitCharacters); + } + + return cachedCommitCharacters; + } + + void PromoteCommonCommitCharactersOntoList() + { + // If client doesn't support default commit characters on list, we want to set commit characters for each item with default to null. + // This way client will default to the commit chars server provided in ServerCapabilities.CompletionProvider.AllCommitCharacters. + if (!(capabilityHelper.SupportDefaultCommitCharacters || capabilityHelper.SupportVSInternalDefaultCommitCharacters)) + { + foreach (var completionItem in completionList.Items) + { + if (completionItem.CommitCharacters == DefaultCommitCharactersArray) + completionItem.CommitCharacters = null; + } + + return; + } + + if (completionList.Items.IsEmpty()) + return; + + var commitCharacterReferences = new Dictionary(); + var mostUsedCount = 0; + string[]? mostUsedCommitCharacters = null; + + for (var i = 0; i < completionList.Items.Length; i++) + { + var completionItem = completionList.Items[i]; + var commitCharacters = completionItem.CommitCharacters; + + Contract.ThrowIfNull(commitCharacters); + + commitCharacterReferences.TryGetValue(commitCharacters, out var existingCount); + existingCount++; + + if (existingCount > mostUsedCount) + { + // Capture the most used commit character counts so we don't need to re-iterate the array later + mostUsedCommitCharacters = commitCharacters; + mostUsedCount = existingCount; + } + + commitCharacterReferences[commitCharacters] = existingCount; + } + + // Promoted the most used commit characters onto the list and then remove these from child items. + // public LSP + if (capabilityHelper.SupportDefaultCommitCharacters) + { + completionList.ItemDefaults.CommitCharacters = mostUsedCommitCharacters; + } + + // VS internal + if (capabilityHelper.SupportVSInternalDefaultCommitCharacters) + { + completionList.CommitCharacters = mostUsedCommitCharacters; + } + + foreach (var completionItem in completionList.Items) + { + if (completionItem.CommitCharacters == mostUsedCommitCharacters) + { + completionItem.CommitCharacters = null; + } + } + } + } + + public static string[] CreateCommitCharacterArrayFromRules(CompletionItemRules rules) + { + using var _ = PooledHashSet.GetInstance(out var commitCharacters); + commitCharacters.AddAll(CompletionRules.Default.DefaultCommitCharacters); + foreach (var rule in rules.CommitCharacterRules) + { + switch (rule.Kind) + { + case CharacterSetModificationKind.Add: + commitCharacters.UnionWith(rule.Characters); + continue; + case CharacterSetModificationKind.Remove: + commitCharacters.ExceptWith(rule.Characters); + continue; + case CharacterSetModificationKind.Replace: + commitCharacters.Clear(); + commitCharacters.AddRange(rule.Characters); + break; + } + } + + return commitCharacters.Select(c => c.ToString()).ToArray(); + } + + private sealed class CommitCharacterArrayComparer : IEqualityComparer> + { + public static readonly CommitCharacterArrayComparer Instance = new(); + + private CommitCharacterArrayComparer() + { + } + + public bool Equals([AllowNull] ImmutableArray x, [AllowNull] ImmutableArray y) + { + if (x == y) + return true; + + for (var i = 0; i < x.Length; i++) + { + var xKind = x[i].Kind; + var yKind = y[i].Kind; + if (xKind != yKind) + { + return false; + } + + var xCharacters = x[i].Characters; + var yCharacters = y[i].Characters; + if (xCharacters.Length != yCharacters.Length) + { + return false; + } + + for (var j = 0; j < xCharacters.Length; j++) + { + if (xCharacters[j] != yCharacters[j]) + { + return false; + } + } + } + + return true; + } + + public int GetHashCode([DisallowNull] ImmutableArray obj) + { + var combinedHash = Hash.CombineValues(obj); + return combinedHash; + } + } + + protected static void PopulateTextEdit( + LSP.CompletionItem lspItem, + TextSpan completionChangeSpan, + string completionChangeNewText, + SourceText documentText, + bool itemDefaultsSupported, + TextSpan defaultSpan) + { + if (itemDefaultsSupported && completionChangeSpan == defaultSpan) + { + // We only need to store the new text as the text edit text when it differs from Label. + if (!lspItem.Label.Equals(completionChangeNewText, StringComparison.Ordinal)) + lspItem.TextEditText = completionChangeNewText; + } + else + { + lspItem.TextEdit = new LSP.TextEdit() + { + NewText = completionChangeNewText, + Range = ProtocolConversions.TextSpanToRange(completionChangeSpan, documentText), + }; + } + } + + protected static async Task GetChangeAndPopulateSimpleTextEditAsync( + Document document, + SourceText documentText, + bool itemDefaultsSupported, + TextSpan defaultSpan, + CompletionItem item, + LSP.CompletionItem lspItem, + CompletionService completionService, + CancellationToken cancellationToken) + { + Contract.ThrowIfTrue(item.IsComplexTextEdit); + Contract.ThrowIfNull(lspItem.Label); + + var completionChange = await completionService.GetChangeAsync(document, item, cancellationToken: cancellationToken).ConfigureAwait(false); + var change = completionChange.TextChange; + + // If the change's span is different from default, then the item should be mark as IsComplexTextEdit. + // But since we don't have a way to enforce this, we'll just check for it here. + Debug.Assert(change.Span == defaultSpan); + PopulateTextEdit(lspItem, change.Span, change.NewText ?? string.Empty, documentText, itemDefaultsSupported, defaultSpan); + } + + public static async Task GenerateAdditionalTextEditForImportCompletionAsync( + CompletionItem selectedItem, + Document document, + CompletionService completionService, + CancellationToken cancellationToken) + { + Debug.Assert(selectedItem.Flags.IsExpanded()); + selectedItem = ImportCompletionItem.MarkItemToAlwaysAddMissingImport(selectedItem); + var completionChange = await completionService.GetChangeAsync(document, selectedItem, cancellationToken: cancellationToken).ConfigureAwait(false); + + var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + using var _ = ArrayBuilder.GetInstance(out var builder); + foreach (var change in completionChange.TextChanges) + { + if (change.NewText == selectedItem.DisplayText) + continue; + + builder.Add(new LSP.TextEdit() + { + NewText = change.NewText!, + Range = ProtocolConversions.TextSpanToRange(change.Span, sourceText), + }); + } + + return builder.ToArray(); + } + + public static async Task<(LSP.TextEdit edit, bool isSnippetString, int? newPosition)> GenerateComplexTextEditAsync( + Document document, + CompletionService completionService, + CompletionItem selectedItem, + bool snippetsSupported, + bool insertNewPositionPlaceholder, + CancellationToken cancellationToken) + { + Debug.Assert(selectedItem.IsComplexTextEdit); + + var completionChange = await completionService.GetChangeAsync(document, selectedItem, cancellationToken: cancellationToken).ConfigureAwait(false); + var completionChangeSpan = completionChange.TextChange.Span; + var newText = completionChange.TextChange.NewText; + Contract.ThrowIfNull(newText); + + var documentText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + var textEdit = new LSP.TextEdit() + { + NewText = newText, + Range = ProtocolConversions.TextSpanToRange(completionChangeSpan, documentText), + }; + + var isSnippetString = false; + var newPosition = completionChange.NewPosition; + + if (snippetsSupported) + { + if (SnippetCompletionItem.IsSnippet(selectedItem) + && completionChange.Properties.TryGetValue(SnippetCompletionItem.LSPSnippetKey, out var lspSnippetChangeText)) + { + textEdit.NewText = lspSnippetChangeText; + isSnippetString = true; + newPosition = null; + } + else if (insertNewPositionPlaceholder) + { + var caretPosition = completionChange.NewPosition; + if (caretPosition.HasValue) + { + // caretPosition is the absolute position of the caret in the document. + // We want the position relative to the start of the snippet. + var relativeCaretPosition = caretPosition.Value - completionChangeSpan.Start; + + // The caret could technically be placed outside the bounds of the text + // being inserted. This situation is currently unsupported in LSP, so in + // these cases we won't move the caret. + if (relativeCaretPosition >= 0 && relativeCaretPosition <= newText.Length) + { + textEdit.NewText = textEdit.NewText.Insert(relativeCaretPosition, "$0"); + } + } + } + } + + return (textEdit, isSnippetString, newPosition); + } + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionCapabilityHelper.cs b/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionCapabilityHelper.cs new file mode 100644 index 0000000000000..9892f33d66cd1 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionCapabilityHelper.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; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Completion +{ + internal sealed class CompletionCapabilityHelper + { + public const string CommitCharactersPropertyName = "commitCharacters"; + public const string DataPropertyName = "data"; + public const string EditRangePropertyName = "editRange"; + + private readonly CompletionSetting? _completionSetting; + private readonly VSInternalCompletionSetting? _vsCompletionSetting; + + public bool SupportVSInternalClientCapabilities { get; } + public bool SupportDefaultEditRange { get; } + public bool SupportCompletionListData { get; } + public bool SupportVSInternalCompletionListData { get; } + public bool SupportDefaultCommitCharacters { get; } + public bool SupportVSInternalDefaultCommitCharacters { get; } + public bool SupportSnippets { get; } + public bool SupportsMarkdownDocumentation { get; } + public ISet SupportedItemKinds { get; } + + public CompletionCapabilityHelper(ClientCapabilities clientCapabilities) + { + // public LSP + _completionSetting = clientCapabilities.TextDocument?.Completion; + + SupportSnippets = _completionSetting?.CompletionItem?.SnippetSupport ?? false; + SupportDefaultEditRange = _completionSetting?.CompletionListSetting?.ItemDefaults?.Contains(EditRangePropertyName) == true; + SupportsMarkdownDocumentation = _completionSetting?.CompletionItem?.DocumentationFormat?.Contains(MarkupKind.Markdown) == true; + SupportCompletionListData = _completionSetting?.CompletionListSetting?.ItemDefaults?.Contains(DataPropertyName) == true; + SupportDefaultCommitCharacters = _completionSetting?.CompletionListSetting?.ItemDefaults?.Contains(CommitCharactersPropertyName) == true; + SupportedItemKinds = _completionSetting?.CompletionItemKind?.ValueSet?.ToSet() ?? SpecializedCollections.EmptySet(); + + // internal VS LSP + if (clientCapabilities.HasVisualStudioLspCapability()) + { + SupportVSInternalClientCapabilities = true; + _vsCompletionSetting = ((VSInternalClientCapabilities)clientCapabilities).TextDocument?.Completion as VSInternalCompletionSetting; + } + else + { + SupportVSInternalClientCapabilities = false; + _vsCompletionSetting = null; + } + + SupportVSInternalCompletionListData = SupportVSInternalClientCapabilities && _vsCompletionSetting?.CompletionList?.Data == true; + SupportVSInternalDefaultCommitCharacters = SupportVSInternalClientCapabilities && _vsCompletionSetting?.CompletionList?.CommitCharacters == true; + } + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionHandler.cs index 84dbb97bd1a92..3613ef5c62576 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionHandler.cs @@ -3,14 +3,12 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Completion; -using Microsoft.CodeAnalysis.Completion.Providers; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageServer.Handler.Completion; using Microsoft.CodeAnalysis.Options; @@ -29,10 +27,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler [Method(LSP.Methods.TextDocumentCompletionName)] internal sealed partial class CompletionHandler : ILspServiceDocumentRequestHandler { - private readonly IGlobalOptionService _globalOptions; - private readonly ImmutableHashSet _csharpTriggerCharacters; - private readonly ImmutableHashSet _vbTriggerCharacters; public bool MutatesSolutionState => false; public bool RequiresLSPSolution => true; @@ -40,15 +35,9 @@ internal sealed partial class CompletionHandler : ILspServiceDocumentRequestHand [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public CompletionHandler( - IGlobalOptionService globalOptions, - [ImportMany] IEnumerable> completionProviders) + IGlobalOptionService globalOptions) { _globalOptions = globalOptions; - - _csharpTriggerCharacters = completionProviders.Where(lz => lz.Metadata.Language == LanguageNames.CSharp).SelectMany( - lz => CommonCompletionUtilities.GetTriggerCharacters(lz.Value)).ToImmutableHashSet(); - _vbTriggerCharacters = completionProviders.Where(lz => lz.Metadata.Language == LanguageNames.VisualBasic).SelectMany( - lz => CommonCompletionUtilities.GetTriggerCharacters(lz.Value)).ToImmutableHashSet(); } public LSP.TextDocumentIdentifier GetTextDocumentIdentifier(LSP.CompletionParams request) => request.TextDocument; @@ -56,51 +45,34 @@ public CompletionHandler( public async Task HandleRequestAsync( LSP.CompletionParams request, RequestContext context, CancellationToken cancellationToken) { - var document = context.Document; - Contract.ThrowIfNull(document); + Contract.ThrowIfNull(context.Document); Contract.ThrowIfNull(context.Solution); - var clientCapabilities = context.GetRequiredClientCapabilities(); - - // C# and VB share the same LSP language server, and thus share the same default trigger characters. - // We need to ensure the trigger character is valid in the document's language. For example, the '{' - // character, while a trigger character in VB, is not a trigger character in C#. - if (request.Context != null && - request.Context.TriggerKind == LSP.CompletionTriggerKind.TriggerCharacter && - !char.TryParse(request.Context.TriggerCharacter, out var triggerCharacter) && - !char.IsLetterOrDigit(triggerCharacter) && - !IsValidTriggerCharacterForDocument(document, triggerCharacter)) + + var document = context.Document; + var documentText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + var capabilityHelper = new CompletionCapabilityHelper(context.GetRequiredClientCapabilities()); + + var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(false); + var completionTrigger = await ProtocolConversions.LSPToRoslynCompletionTriggerAsync(request.Context, document, position, cancellationToken).ConfigureAwait(false); + var completionOptions = GetCompletionOptions(document, capabilityHelper); + var completionService = document.GetRequiredLanguageService(); + + // Let CompletionService decide if we should trigger completion, unless the request is for incomplete results, in which case we always trigger. + if (request.Context?.TriggerKind is not LSP.CompletionTriggerKind.TriggerForIncompleteCompletions + && !completionService.ShouldTriggerCompletion(document.Project, document.Project.Services, documentText, position, completionTrigger, completionOptions, document.Project.Solution.Options, roles: null)) { return null; } - var completionOptions = GetCompletionOptions(document); - var completionService = document.GetRequiredLanguageService(); - var documentText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var completionListResult = await GetFilteredCompletionListAsync(request, context, documentText, document, completionOptions, completionService, cancellationToken).ConfigureAwait(false); if (completionListResult == null) return null; var (list, isIncomplete, resultId) = completionListResult.Value; - return await ConvertToLspCompletionListAsync(document, clientCapabilities, list, isIncomplete, resultId, cancellationToken) - .ConfigureAwait(false); - - // Local function - bool IsValidTriggerCharacterForDocument(Document document, char triggerCharacter) - { - if (document.Project.Language == LanguageNames.CSharp) - { - return _csharpTriggerCharacters.Contains(triggerCharacter); - } - else if (document.Project.Language == LanguageNames.VisualBasic) - { - return _vbTriggerCharacters.Contains(triggerCharacter); - } - // Typescript still calls into this for completion. - // Since we don't know what their trigger characters are, just return true. - return true; - } + var creationService = document.Project.Solution.Services.GetRequiredService(); + return await creationService.ConvertToLspCompletionListAsync(document, capabilityHelper, list, isIncomplete, resultId, cancellationToken) + .ConfigureAwait(false); } private async Task<(CompletionList CompletionList, bool IsIncomplete, long ResultId)?> GetFilteredCompletionListAsync( @@ -173,7 +145,11 @@ private static (CompletionList CompletionList, bool IsIncomplete) FilterCompleti CompletionTrigger completionTrigger, SourceText sourceText) { + if (completionListMaxSize < 0 || completionListMaxSize >= completionList.ItemsList.Count) + return (completionList, false); + var filterText = sourceText.GetSubText(completionList.Span).ToString(); + var filterReason = GetFilterReason(completionTrigger); // Use pattern matching to determine which items are most relevant out of the calculated items. using var _ = ArrayBuilder.GetInstance(out var matchResultsBuilder); @@ -184,7 +160,7 @@ private static (CompletionList CompletionList, bool IsIncomplete) FilterCompleti if (helper.TryCreateMatchResult( item, completionTrigger.Kind, - GetFilterReason(completionTrigger), + filterReason, recentItemIndex: -1, includeMatchSpans: false, index, @@ -201,10 +177,10 @@ private static (CompletionList CompletionList, bool IsIncomplete) FilterCompleti // Finally, truncate the list to 1000 items plus any preselected items that occur after the first 1000. var filteredList = matchResultsBuilder .Take(completionListMaxSize) - .Concat(matchResultsBuilder.Skip(completionListMaxSize).Where(match => ShouldItemBePreselected(match.CompletionItem))) + .Concat(matchResultsBuilder.Skip(completionListMaxSize).Where(match => match.CompletionItem.Rules.MatchPriority == MatchPriority.Preselect)) .Select(matchResult => matchResult.CompletionItem) .ToImmutableArray(); - var newCompletionList = completionList.WithItems(filteredList); + var newCompletionList = completionList.WithItemsList(filteredList); // Per the LSP spec, the completion list should be marked with isIncomplete = false when further insertions will // not generate any more completion items. This means that we should be checking if the matchedResults is larger @@ -233,32 +209,36 @@ static CompletionFilterReason GetFilterReason(CompletionTrigger trigger) } } - public static bool ShouldItemBePreselected(CompletionItem completionItem) + private CompletionOptions GetCompletionOptions(Document document, CompletionCapabilityHelper capabilityHelper) { - // An item should be preselected for LSP when the match priority is preselect and the item is hard selected. - // LSP does not support soft preselection, so we do not preselect in that scenario to avoid interfering with - // typing. - return completionItem.Rules.MatchPriority == MatchPriority.Preselect && completionItem.Rules.SelectionBehavior == CompletionItemSelectionBehavior.HardSelection; - } + var options = _globalOptions.GetCompletionOptions(document.Project.Language); - internal CompletionOptions GetCompletionOptions(Document document) - { - // Filter out unimported types for now as there are two issues with providing them: - // 1. LSP client does not currently provide a way to provide detail text on the completion item to show the namespace. - // https://dev.azure.com/devdiv/DevDiv/_workitems/edit/1076759 - // 2. We need to figure out how to provide the text edits along with the completion item or provide them in the resolve request. - // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/985860/ - // 3. LSP client should support completion filters / expanders - // - // Also don't trigger completion in argument list automatically, since LSP currently has no concept of soft selection. - // We want to avoid committing selected item with commit chars like `"` and `)`. - return _globalOptions.GetCompletionOptions(document.Project.Language) with + if (capabilityHelper.SupportVSInternalClientCapabilities) { - ShowItemsFromUnimportedNamespaces = false, - ExpandedCompletionBehavior = ExpandedCompletionMode.NonExpandedItemsOnly, - UpdateImportCompletionCacheInBackground = false, - TriggerInArgumentLists = false - }; + // Filter out unimported types for now as there are two issues with providing them: + // 1. LSP client does not currently provide a way to provide detail text on the completion item to show the namespace. + // https://dev.azure.com/devdiv/DevDiv/_workitems/edit/1076759 + // 2. We need to figure out how to provide the text edits along with the completion item or provide them in the resolve request. + // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/985860/ + // 3. LSP client should support completion filters / expanders + options = options with + { + ShowItemsFromUnimportedNamespaces = false, + ExpandedCompletionBehavior = ExpandedCompletionMode.NonExpandedItemsOnly, + UpdateImportCompletionCacheInBackground = false, + }; + } + else + { + var updateImportCompletionCacheInBackground = options.ShowItemsFromUnimportedNamespaces is true; + options = options with + { + ShowNewSnippetExperienceUserOption = false, + UpdateImportCompletionCacheInBackground = updateImportCompletionCacheInBackground + }; + } + + return options; } } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionHandler_CreateLspList.cs b/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionHandler_CreateLspList.cs deleted file mode 100644 index 128c78b40acb7..0000000000000 --- a/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionHandler_CreateLspList.cs +++ /dev/null @@ -1,281 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Completion; -using Microsoft.CodeAnalysis.LanguageServer.Handler.Completion; -using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Shared.Extensions; -using Roslyn.Utilities; -using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; - -namespace Microsoft.CodeAnalysis.LanguageServer.Handler -{ - internal sealed partial class CompletionHandler - { - internal const string EditRangeSetting = "editRange"; - - private static async Task ConvertToLspCompletionListAsync( - Document document, - LSP.ClientCapabilities clientCapabilities, - CompletionList list, bool isIncomplete, long resultId, - CancellationToken cancellationToken) - { - if (list.ItemsList.Count == 0) - { - return new LSP.VSInternalCompletionList - { - Items = Array.Empty(), - // If we have a suggestion mode item, we just need to keep the list in suggestion mode. - // We don't need to return the fake suggestion mode item. - SuggestionMode = list.SuggestionModeItem is not null, - IsIncomplete = isIncomplete, - }; - } - - var lspVSClientCapability = clientCapabilities.HasVisualStudioLspCapability() == true; - - var completionCapabilities = clientCapabilities.TextDocument?.Completion; - var supportedKinds = completionCapabilities?.CompletionItemKind?.ValueSet?.ToSet() ?? new HashSet(); - var itemDefaultsSupported = completionCapabilities?.CompletionListSetting?.ItemDefaults?.Contains(EditRangeSetting) == true; - - // We use the default completion list span as our comparison point for optimization when generating the TextEdits later on. - var defaultSpan = list.Span; - var documentText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - var defaultRange = ProtocolConversions.TextSpanToRange(defaultSpan, documentText); - - // Set resolve data on list if the client supports it, otherwise set it on each item. - var resolveData = new CompletionResolveData() { ResultId = resultId }; - var (completionItemResolveData, completionListResolvedData) = clientCapabilities.HasCompletionListDataCapability() - ? (null as CompletionResolveData, resolveData) - : (resolveData, null); - - using var _ = ArrayBuilder.GetInstance(out var lspCompletionItems); - var commitCharactersRuleCache = new Dictionary, string[]>(CommitCharacterArrayComparer.Instance); - - foreach (var item in list.ItemsList) - lspCompletionItems.Add(await CreateLSPCompletionItemAsync(item).ConfigureAwait(false)); - - var completionList = new LSP.VSInternalCompletionList - { - Items = lspCompletionItems.ToArray(), - SuggestionMode = list.SuggestionModeItem != null, - IsIncomplete = isIncomplete, - Data = completionListResolvedData, - }; - - if (clientCapabilities.HasCompletionListCommitCharactersCapability()) - PromoteCommonCommitCharactersOntoList(completionList); - - if (itemDefaultsSupported) - { - completionList.ItemDefaults = new LSP.CompletionListItemDefaults - { - EditRange = defaultRange, - }; - } - - return new LSP.OptimizedVSCompletionList(completionList); - - async Task CreateLSPCompletionItemAsync(CompletionItem item) - { - var snippetsSupported = completionCapabilities?.CompletionItem?.SnippetSupport ?? false; - var creationService = document.Project.Solution.Services.GetRequiredService(); - - // Defer to host to create the actual completion item (including potential subclasses), and add any - // custom information. - var lspItem = await creationService.CreateAsync( - document, documentText, snippetsSupported, itemDefaultsSupported, defaultSpan, item, cancellationToken).ConfigureAwait(false); - - // Now add data common to all hosts. - lspItem.Data = completionItemResolveData; - lspItem.Label = $"{item.DisplayTextPrefix}{item.DisplayText}{item.DisplayTextSuffix}"; - - lspItem.SortText = item.SortText; - lspItem.FilterText = item.FilterText; - - lspItem.Kind = GetCompletionKind(item.Tags, supportedKinds); - lspItem.Preselect = ShouldItemBePreselected(item); - - lspItem.CommitCharacters = GetCommitCharacters(item, commitCharactersRuleCache, lspVSClientCapability); - - return lspItem; - } - - static LSP.CompletionItemKind GetCompletionKind( - ImmutableArray tags, - ISet supportedClientKinds) - { - foreach (var tag in tags) - { - if (ProtocolConversions.RoslynTagToCompletionItemKinds.TryGetValue(tag, out var completionItemKinds)) - { - // Always at least pick the core kind provided. - var kind = completionItemKinds[0]; - - // If better kinds are preferred, return them if the client supports them. - for (var i = 1; i < completionItemKinds.Length; i++) - { - var preferredKind = completionItemKinds[i]; - if (supportedClientKinds.Contains(preferredKind)) - kind = preferredKind; - } - - return kind; - } - } - - return LSP.CompletionItemKind.Text; - } - - static string[]? GetCommitCharacters( - CompletionItem item, - Dictionary, string[]> currentRuleCache, - bool supportsVSExtensions) - { - // VSCode does not have the concept of soft selection, the list is always hard selected. - // In order to emulate soft selection behavior for things like argument completion, regex completion, datetime completion, etc - // we create a completion item without any specific commit characters. This means only tab / enter will commit. - // VS supports soft selection, so we only do this for non-VS clients. - if (!supportsVSExtensions && item.Rules.SelectionBehavior == CompletionItemSelectionBehavior.SoftSelection) - return Array.Empty(); - - var commitCharacterRules = item.Rules.CommitCharacterRules; - - // VS will use the default commit characters if no items are specified on the completion item. - // However, other clients like VSCode do not support this behavior so we must specify - // commit characters on every completion item - https://github.com/microsoft/vscode/issues/90987 - if (supportsVSExtensions && commitCharacterRules.IsEmpty) - return null; - - if (!currentRuleCache.TryGetValue(commitCharacterRules, out var cachedCommitCharacters)) - { - using var _ = PooledHashSet.GetInstance(out var commitCharacters); - commitCharacters.AddAll(CompletionRules.Default.DefaultCommitCharacters); - foreach (var rule in commitCharacterRules) - { - switch (rule.Kind) - { - case CharacterSetModificationKind.Add: - commitCharacters.UnionWith(rule.Characters); - continue; - case CharacterSetModificationKind.Remove: - commitCharacters.ExceptWith(rule.Characters); - continue; - case CharacterSetModificationKind.Replace: - commitCharacters.Clear(); - commitCharacters.AddRange(rule.Characters); - break; - } - } - - cachedCommitCharacters = commitCharacters.Select(c => c.ToString()).ToArray(); - currentRuleCache.Add(item.Rules.CommitCharacterRules, cachedCommitCharacters); - } - - return cachedCommitCharacters; - } - - static void PromoteCommonCommitCharactersOntoList(LSP.VSInternalCompletionList completionList) - { - if (completionList.Items.IsEmpty()) - { - return; - } - - var defaultCommitCharacters = CompletionRules.Default.DefaultCommitCharacters.Select(c => c.ToString()).ToArray(); - var commitCharacterReferences = new Dictionary(); - var mostUsedCount = 0; - string[]? mostUsedCommitCharacters = null; - for (var i = 0; i < completionList.Items.Length; i++) - { - var completionItem = completionList.Items[i]; - var commitCharacters = completionItem.CommitCharacters; - // The commit characters on the item are null, this means the commit characters are actually - // the default commit characters we passed in the initialize request. - commitCharacters ??= defaultCommitCharacters; - - commitCharacterReferences.TryGetValue(commitCharacters, out var existingCount); - existingCount++; - - if (existingCount > mostUsedCount) - { - // Capture the most used commit character counts so we don't need to re-iterate the array later - mostUsedCommitCharacters = commitCharacters; - mostUsedCount = existingCount; - } - - commitCharacterReferences[commitCharacters] = existingCount; - } - - Contract.ThrowIfNull(mostUsedCommitCharacters); - - // Promoted the most used commit characters onto the list and then remove these from child items. - completionList.CommitCharacters = mostUsedCommitCharacters; - for (var i = 0; i < completionList.Items.Length; i++) - { - var completionItem = completionList.Items[i]; - if (completionItem.CommitCharacters == mostUsedCommitCharacters) - { - completionItem.CommitCharacters = null; - } - } - } - } - - private sealed class CommitCharacterArrayComparer : IEqualityComparer> - { - public static readonly CommitCharacterArrayComparer Instance = new(); - - private CommitCharacterArrayComparer() - { - } - - public bool Equals([AllowNull] ImmutableArray x, [AllowNull] ImmutableArray y) - { - if (x == y) - return true; - - for (var i = 0; i < x.Length; i++) - { - var xKind = x[i].Kind; - var yKind = y[i].Kind; - if (xKind != yKind) - { - return false; - } - - var xCharacters = x[i].Characters; - var yCharacters = y[i].Characters; - if (xCharacters.Length != yCharacters.Length) - { - return false; - } - - for (var j = 0; j < xCharacters.Length; j++) - { - if (xCharacters[j] != yCharacters[j]) - { - return false; - } - } - } - - return true; - } - - public int GetHashCode([DisallowNull] ImmutableArray obj) - { - var combinedHash = Hash.CombineValues(obj); - return combinedHash; - } - } - } -} diff --git a/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionResolveData.cs b/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionResolveData.cs index 65e3842672af9..ae7d7f77a6e55 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionResolveData.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionResolveData.cs @@ -2,16 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.CodeAnalysis.LanguageServer.Handler.Completion; using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; -namespace Microsoft.CodeAnalysis.LanguageServer.Handler +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Completion { /// /// Provides the intermediate data passed from CompletionHandler to CompletionResolveHandler. /// Passed along via . /// - internal class CompletionResolveData + internal sealed class CompletionResolveData { /// /// ID associated with the item's completion list. diff --git a/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionResolveHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionResolveHandler.cs index 000bc5ea483b9..5160438651564 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionResolveHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Completion/CompletionResolveHandler.cs @@ -2,7 +2,6 @@ // 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.Linq; using System.Threading; using System.Threading.Tasks; @@ -44,11 +43,6 @@ public CompletionResolveHandler(IGlobalOptionService globalOptions, CompletionLi public async Task HandleRequestAsync(LSP.CompletionItem completionItem, RequestContext context, CancellationToken cancellationToken) { - var document = context.GetRequiredDocument(); - var clientCapabilities = context.GetRequiredClientCapabilities(); - - var completionService = document.Project.Services.GetRequiredService(); - var cacheEntry = GetCompletionListCacheEntry(completionItem); if (cacheEntry == null) { @@ -57,6 +51,9 @@ public CompletionResolveHandler(IGlobalOptionService globalOptions, CompletionLi return completionItem; } + var document = context.GetRequiredDocument(); + var completionService = document.Project.Services.GetRequiredService(); + // Find the matching completion item in the completion list var selectedItem = cacheEntry.CompletionList.ItemsList.FirstOrDefault(cachedCompletionItem => MatchesLSPCompletionItem(completionItem, cachedCompletionItem)); @@ -69,8 +66,9 @@ public CompletionResolveHandler(IGlobalOptionService globalOptions, CompletionLi await creationService.ResolveAsync( completionItem, selectedItem, + cacheEntry.TextDocument, document, - clientCapabilities, + new CompletionCapabilityHelper(context.GetRequiredClientCapabilities()), completionService, completionOptions, symbolDescriptionOptions, @@ -82,24 +80,7 @@ await creationService.ResolveAsync( private static bool MatchesLSPCompletionItem(LSP.CompletionItem lspCompletionItem, CompletionItem completionItem) { - if (!lspCompletionItem.Label.StartsWith(completionItem.DisplayTextPrefix, StringComparison.Ordinal)) - { - return false; - } - - // The prefix matches, consume the matching prefix from the lsp completion item label. - var displayTextWithSuffix = lspCompletionItem.Label[completionItem.DisplayTextPrefix.Length..]; - if (!displayTextWithSuffix.EndsWith(completionItem.DisplayTextSuffix, StringComparison.Ordinal)) - { - return false; - } - - // The suffix matches, consume the matching suffix from the lsp completion item label. - var originalDisplayText = displayTextWithSuffix[..^completionItem.DisplayTextSuffix.Length]; - - // Now we're left with what should be the original display text for the lsp completion item. - // Check to make sure it matches the cached completion item label. - return string.Equals(originalDisplayText, completionItem.DisplayText); + return lspCompletionItem.Label == completionItem.GetEntireDisplayText(); } private CompletionListCache.CacheEntry? GetCompletionListCacheEntry(LSP.CompletionItem request) diff --git a/src/Features/LanguageServer/Protocol/Handler/Completion/DefaultLspCompletionResultCreationService.cs b/src/Features/LanguageServer/Protocol/Handler/Completion/DefaultLspCompletionResultCreationService.cs new file mode 100644 index 0000000000000..cf0262481470a --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Completion/DefaultLspCompletionResultCreationService.cs @@ -0,0 +1,114 @@ +// 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.Completion; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageService; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; +using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Completion +{ + [ExportWorkspaceService(typeof(ILspCompletionResultCreationService), ServiceLayer.Default), Shared] + internal sealed class DefaultLspCompletionResultCreationService : AbstractLspCompletionResultCreationService + { + /// + /// Command name implemented by the client and invoked when an item with complex edit is committed. + /// + public const string CompleteComplexEditCommand = "roslyn.client.completionComplexEdit"; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public DefaultLspCompletionResultCreationService() + { + } + + protected override async Task CreateItemAndPopulateTextEditAsync(Document document, + SourceText documentText, + bool snippetsSupported, + bool itemDefaultsSupported, + TextSpan defaultSpan, + string typedText, + CompletionItem item, + CompletionService completionService, + CancellationToken cancellationToken) + { + var lspItem = new LSP.CompletionItem() { Label = item.GetEntireDisplayText() }; + + if (item.IsComplexTextEdit) + { + //await completionService.GetChangeAsync(document, item, cancellationToken: cancellationToken).ConfigureAwait(false); + // For unimported item, we use display text (type or method name) as the text edit text, and rely on resolve handler to add missing import as additional edit. + // For other complex edit item, we return a no-op edit and rely on resolve handler to compute the actual change and provide the command to apply it. + var completionChangeNewText = item.Flags.IsExpanded() ? item.DisplayText : typedText; + PopulateTextEdit(lspItem, completionChangeSpan: defaultSpan, completionChangeNewText, documentText, itemDefaultsSupported, defaultSpan: defaultSpan); + } + else + { + await GetChangeAndPopulateSimpleTextEditAsync( + document, + documentText, + itemDefaultsSupported, + defaultSpan, + item, + lspItem, + completionService, + cancellationToken).ConfigureAwait(false); + } + + return lspItem; + } + + public override async Task ResolveAsync( + LSP.CompletionItem lspItem, + CompletionItem roslynItem, + LSP.TextDocumentIdentifier textDocumentIdentifier, + Document document, + CompletionCapabilityHelper capabilityHelper, + CompletionService completionService, + CompletionOptions completionOptions, + SymbolDescriptionOptions symbolDescriptionOptions, + CancellationToken cancellationToken) + { + var description = await completionService.GetDescriptionAsync(document, roslynItem, completionOptions, symbolDescriptionOptions, cancellationToken).ConfigureAwait(false)!; + if (description != null) + { + lspItem.Documentation = ProtocolConversions.GetDocumentationMarkupContent(description.TaggedParts, document, capabilityHelper.SupportsMarkdownDocumentation); + } + + if (roslynItem.IsComplexTextEdit) + { + if (roslynItem.Flags.IsExpanded()) + { + var additionalEdits = await GenerateAdditionalTextEditForImportCompletionAsync(roslynItem, document, completionService, cancellationToken).ConfigureAwait(false); + lspItem.AdditionalTextEdits = additionalEdits; + } + else + { + var (textEdit, isSnippetString, newPosition) = await GenerateComplexTextEditAsync( + document, completionService, roslynItem, capabilityHelper.SupportSnippets, insertNewPositionPlaceholder: false, cancellationToken).ConfigureAwait(false); + + var lspOffset = newPosition is null ? -1 : newPosition.Value; + + lspItem.Command = lspItem.Command = new LSP.Command() + { + CommandIdentifier = CompleteComplexEditCommand, + Title = nameof(CompleteComplexEditCommand), + Arguments = new object[] { textDocumentIdentifier.Uri, textEdit, isSnippetString, lspOffset } + }; + } + } + + if (!roslynItem.InlineDescription.IsEmpty()) + lspItem.LabelDetails = new() { Description = roslynItem.InlineDescription }; + + return lspItem; + } + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Completion/ILspCompletionResultCreationService.cs b/src/Features/LanguageServer/Protocol/Handler/Completion/ILspCompletionResultCreationService.cs index b955a56962ec4..7bbfca2cca8d2 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Completion/ILspCompletionResultCreationService.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Completion/ILspCompletionResultCreationService.cs @@ -2,119 +2,33 @@ // 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.Diagnostics; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageService; -using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Completion { internal interface ILspCompletionResultCreationService : IWorkspaceService { - Task CreateAsync( + Task ConvertToLspCompletionListAsync( Document document, - SourceText documentText, - bool snippetsSupported, - bool itemDefaultsSupported, - TextSpan defaultSpan, - CompletionItem item, + CompletionCapabilityHelper capabilityHelper, + CompletionList list, bool isIncomplete, long resultId, CancellationToken cancellationToken); Task ResolveAsync( - LSP.CompletionItem completionItem, - CompletionItem selectedItem, + LSP.CompletionItem lspItem, + CompletionItem roslynItem, + LSP.TextDocumentIdentifier textDocumentIdentifier, Document document, - LSP.ClientCapabilities clientCapabilities, + CompletionCapabilityHelper capabilityHelper, CompletionService completionService, CompletionOptions completionOptions, SymbolDescriptionOptions symbolDescriptionOptions, CancellationToken cancellationToken); } - - [ExportWorkspaceService(typeof(ILspCompletionResultCreationService)), Shared] - internal sealed class DefaultLspCompletionResultCreationService : ILspCompletionResultCreationService - { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public DefaultLspCompletionResultCreationService() - { - } - - public async Task CreateAsync( - Document document, - SourceText documentText, - bool snippetsSupported, - bool itemDefaultsSupported, - TextSpan defaultSpan, - CompletionItem item, - CancellationToken cancellationToken) - { - var completionItem = new LSP.CompletionItem(); - await PopulateTextEditAsync(document, documentText, itemDefaultsSupported, defaultSpan, item, completionItem, cancellationToken).ConfigureAwait(false); - return completionItem; - } - - public async Task ResolveAsync( - LSP.CompletionItem completionItem, - CompletionItem selectedItem, - Document document, - LSP.ClientCapabilities clientCapabilities, - CompletionService completionService, - CompletionOptions completionOptions, - SymbolDescriptionOptions symbolDescriptionOptions, - CancellationToken cancellationToken) - { - var description = await completionService.GetDescriptionAsync(document, selectedItem, completionOptions, symbolDescriptionOptions, cancellationToken).ConfigureAwait(false)!; - if (description != null) - { - var clientSupportsMarkdown = clientCapabilities.TextDocument?.Completion?.CompletionItem?.DocumentationFormat?.Contains(LSP.MarkupKind.Markdown) == true; - completionItem.Documentation = ProtocolConversions.GetDocumentationMarkupContent(description.TaggedParts, document, clientSupportsMarkdown); - } - - return completionItem; - } - - public static async Task PopulateTextEditAsync( - Document document, - SourceText documentText, - bool itemDefaultsSupported, - TextSpan defaultSpan, - CompletionItem item, - LSP.CompletionItem lspItem, - CancellationToken cancellationToken) - { - var completionService = document.GetRequiredLanguageService(); - - var completionChange = await completionService.GetChangeAsync( - document, item, cancellationToken: cancellationToken).ConfigureAwait(false); - var completionChangeSpan = completionChange.TextChange.Span; - var newText = completionChange.TextChange.NewText ?? ""; - - if (itemDefaultsSupported && completionChangeSpan == defaultSpan) - { - // The span is the same as the default, we just need to store the new text as - // the insert text so the client can create the text edit from it and the default range. - lspItem.InsertText = newText; - } - else - { - Debug.Assert(completionChangeSpan == defaultSpan || item.IsComplexTextEdit); - lspItem.TextEdit = new LSP.TextEdit() - { - NewText = newText, - Range = ProtocolConversions.TextSpanToRange(completionChangeSpan, documentText), - }; - } - } - } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Configuration/DidChangeConfigurationNotificationHandler_OptionList.cs b/src/Features/LanguageServer/Protocol/Handler/Configuration/DidChangeConfigurationNotificationHandler_OptionList.cs index ba6470f52af46..646690539eaba 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Configuration/DidChangeConfigurationNotificationHandler_OptionList.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Configuration/DidChangeConfigurationNotificationHandler_OptionList.cs @@ -5,7 +5,9 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.DocumentHighlighting; +using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.ImplementType; +using Microsoft.CodeAnalysis.Indentation; using Microsoft.CodeAnalysis.InlineHints; using Microsoft.CodeAnalysis.MetadataAsSource; using Microsoft.CodeAnalysis.Options; @@ -26,6 +28,7 @@ internal partial class DidChangeConfigurationNotificationHandler CompletionOptionsStorage.ShowNameSuggestions, CompletionOptionsStorage.ShowItemsFromUnimportedNamespaces, CompletionOptionsStorage.ProvideRegexCompletions, + CompletionOptionsStorage.ShowItemsFromUnimportedNamespaces, QuickInfoOptionsStorage.ShowRemarksInQuickInfo, // Go to definition MetadataAsSourceOptionsStorage.NavigateToDecompiledSources, @@ -44,6 +47,12 @@ internal partial class DidChangeConfigurationNotificationHandler InlineHintsOptionsStorage.EnabledForTypes, InlineHintsOptionsStorage.ForImplicitVariableTypes, InlineHintsOptionsStorage.ForLambdaParameterTypes, - InlineHintsOptionsStorage.ForImplicitObjectCreation); + InlineHintsOptionsStorage.ForImplicitObjectCreation, + // EditorConfig + FormattingOptions2.TabSize, + FormattingOptions2.IndentationSize, + FormattingOptions2.UseTabs, + FormattingOptions2.NewLine, + FormattingOptions2.InsertFinalNewLine); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractPullDiagnosticHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractPullDiagnosticHandler.cs index 8a25b1a252647..6f0a770f4fadb 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractPullDiagnosticHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractPullDiagnosticHandler.cs @@ -6,10 +6,10 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.EditAndContinue; using Microsoft.CodeAnalysis.LanguageServer.Features.Diagnostics; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PooledObjects; @@ -25,8 +25,8 @@ internal abstract class AbstractDocumentPullDiagnosticHandler /// Cache where we store the data produced by prior requests so that they can be returned if nothing of significance - /// changed. The VersionStamp is produced by while the - /// Checksum is produced by . The former is faster + /// changed. The is produced by while the + /// is produced by . The former is faster /// and works well for us in the normal case. The latter still allows us to reuse diagnostics when changes happen that /// update the version stamp but not the content (for example, forking LSP text). /// - private readonly ConcurrentDictionary> _categoryToVersionedCache = new(); + private readonly ConcurrentDictionary> _categoryToVersionedCache = new(); public bool MutatesSolutionState => false; public bool RequiresLSPSolution => true; protected AbstractPullDiagnosticHandler( IDiagnosticAnalyzerService diagnosticAnalyzerService, - EditAndContinueDiagnosticUpdateSource editAndContinueDiagnosticUpdateSource, + IDiagnosticsRefresher diagnosticRefresher, IGlobalOptionService globalOptions) { DiagnosticAnalyzerService = diagnosticAnalyzerService; - _editAndContinueDiagnosticUpdateSource = editAndContinueDiagnosticUpdateSource; + _diagnosticRefresher = diagnosticRefresher; GlobalOptions = globalOptions; } @@ -163,7 +163,7 @@ protected virtual Task WaitForChangesAsync(RequestContext context, CancellationT foreach (var diagnosticSource in orderedSources) { - var encVersion = _editAndContinueDiagnosticUpdateSource.Version; + var globalStateVersion = _diagnosticRefresher.GlobalStateVersion; var project = diagnosticSource.GetProject(); @@ -171,8 +171,8 @@ protected virtual Task WaitForChangesAsync(RequestContext context, CancellationT documentToPreviousDiagnosticParams, diagnosticSource.GetId(), project, - computeCheapVersionAsync: async () => (encVersion, await project.GetDependentVersionAsync(cancellationToken).ConfigureAwait(false)), - computeExpensiveVersionAsync: async () => (encVersion, await project.GetDependentChecksumAsync(cancellationToken).ConfigureAwait(false)), + computeCheapVersionAsync: async () => (globalStateVersion, await project.GetDependentVersionAsync(cancellationToken).ConfigureAwait(false)), + computeExpensiveVersionAsync: async () => (globalStateVersion, await project.GetDependentChecksumAsync(cancellationToken).ConfigureAwait(false)), cancellationToken).ConfigureAwait(false); if (newResultId != null) { @@ -317,10 +317,7 @@ private void HandleRemovedDocuments(RequestContext context, ImmutableArray ConvertDiagnostic(IDiagnosticSource diagnosticSource, DiagnosticData diagnosticData, ClientCapabilities capabilities) { - // VSCode throws on hidden diagnostics without a message (all hint diagnostic messages are rendered on hover). - // Roslyn creates these for example in remove unnecessary imports, see RemoveUnnecessaryImportsConstants.DiagnosticFixableId. - // TODO - We should probably not be creating these as separate diagnostics or have a 'really really' hidden tag. - if (!capabilities.HasVisualStudioLspCapability() && string.IsNullOrEmpty(diagnosticData.Message) && diagnosticData.Severity == DiagnosticSeverity.Hidden) + if (!ShouldIncludeHiddenDiagnostic(diagnosticData, capabilities)) { return ImmutableArray.Empty; } @@ -350,20 +347,38 @@ private void HandleRemovedDocuments(RequestContext context, ImmutableArray(diagnostic); } - // Roslyn produces unnecessary diagnostics by using additional locations, however LSP doesn't support tagging - // additional locations separately. Instead we just create multiple hidden diagnostics for unnecessary squiggling. - using var _ = ArrayBuilder.GetInstance(out var diagnosticsBuilder); - diagnosticsBuilder.Add(diagnostic); - foreach (var location in unnecessaryLocations) + if (capabilities.HasVisualStudioLspCapability()) { - var additionalDiagnostic = CreateLspDiagnostic(diagnosticData, project, capabilities); - additionalDiagnostic.Severity = LSP.DiagnosticSeverity.Hint; - additionalDiagnostic.Range = GetRange(location); - additionalDiagnostic.Tags = new DiagnosticTag[] { DiagnosticTag.Unnecessary, VSDiagnosticTags.HiddenInEditor, VSDiagnosticTags.HiddenInErrorList, VSDiagnosticTags.SuppressEditorToolTip }; - diagnosticsBuilder.Add(additionalDiagnostic); - } + // Roslyn produces unnecessary diagnostics by using additional locations, however LSP doesn't support tagging + // additional locations separately. Instead we just create multiple hidden diagnostics for unnecessary squiggling. + using var _ = ArrayBuilder.GetInstance(out var diagnosticsBuilder); + diagnosticsBuilder.Add(diagnostic); + foreach (var location in unnecessaryLocations) + { + var additionalDiagnostic = CreateLspDiagnostic(diagnosticData, project, capabilities); + additionalDiagnostic.Severity = LSP.DiagnosticSeverity.Hint; + additionalDiagnostic.Range = GetRange(location); + additionalDiagnostic.Tags = new DiagnosticTag[] { DiagnosticTag.Unnecessary, VSDiagnosticTags.HiddenInEditor, VSDiagnosticTags.HiddenInErrorList, VSDiagnosticTags.SuppressEditorToolTip }; + diagnosticsBuilder.Add(additionalDiagnostic); + } - return diagnosticsBuilder.ToImmutableArray(); + return diagnosticsBuilder.ToImmutableArray(); + } + else + { + diagnostic.Tags = diagnostic.Tags != null ? diagnostic.Tags.Append(DiagnosticTag.Unnecessary) : new DiagnosticTag[] { DiagnosticTag.Unnecessary }; + var diagnosticRelatedInformation = unnecessaryLocations.Value.Select(l => new DiagnosticRelatedInformation + { + Location = new LSP.Location + { + Range = GetRange(l), + Uri = ProtocolConversions.GetUriFromFilePath(l.UnmappedFileSpan.Path) + }, + Message = diagnostic.Message + }).ToArray(); + diagnostic.RelatedInformation = diagnosticRelatedInformation; + return ImmutableArray.Create(diagnostic); + } LSP.VSDiagnostic CreateLspDiagnostic( DiagnosticData diagnosticData, @@ -435,6 +450,39 @@ static LSP.Range GetRange(DiagnosticDataLocation dataLocation) } }; } + + static bool ShouldIncludeHiddenDiagnostic(DiagnosticData diagnosticData, ClientCapabilities capabilities) + { + // VS can handle us reporting any kind of diagnostic using VS custom tags. + if (capabilities.HasVisualStudioLspCapability() == true) + { + return true; + } + + // Diagnostic isn't hidden - we should report this diagnostic in all scenarios. + if (diagnosticData.Severity != DiagnosticSeverity.Hidden) + { + return true; + } + + // Roslyn creates these for example in remove unnecessary imports, see RemoveUnnecessaryImportsConstants.DiagnosticFixableId. + // These aren't meant to be visible in anyway, so we can safely exclude them. + // TODO - We should probably not be creating these as separate diagnostics or have a 'really really' hidden tag. + if (string.IsNullOrEmpty(diagnosticData.Message)) + { + return false; + } + + // Hidden diagnostics that are unnecessary are visible to the user in the form of fading. + // We can report these diagnostics. + if (diagnosticData.CustomTags.Contains(WellKnownDiagnosticTags.Unnecessary)) + { + return true; + } + + // We have a hidden diagnostic that has no fading. This diagnostic can't be visible so don't send it to the client. + return false; + } } private static VSDiagnosticRank? ConvertRank(DiagnosticData diagnosticData) @@ -459,7 +507,7 @@ private static LSP.DiagnosticSeverity ConvertDiagnosticSeverity(DiagnosticSeveri // Hidden is translated in ConvertTags to pass along appropriate _ms tags // that will hide the item in a client that knows about those tags. DiagnosticSeverity.Hidden => LSP.DiagnosticSeverity.Hint, - DiagnosticSeverity.Info => LSP.DiagnosticSeverity.Hint, + DiagnosticSeverity.Info => LSP.DiagnosticSeverity.Information, DiagnosticSeverity.Warning => LSP.DiagnosticSeverity.Warning, DiagnosticSeverity.Error => LSP.DiagnosticSeverity.Error, _ => throw ExceptionUtilities.UnexpectedValue(severity), diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticsRefreshQueue.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticsRefreshQueue.cs new file mode 100644 index 0000000000000..c06bc461d1415 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticsRefreshQueue.cs @@ -0,0 +1,107 @@ +// 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 Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; + +internal sealed class DiagnosticsRefreshQueue : AbstractRefreshQueue +{ + [ExportCSharpVisualBasicLspServiceFactory(typeof(DiagnosticsRefreshQueue)), Shared] + internal sealed class Factory : ILspServiceFactory + { + private readonly IAsynchronousOperationListenerProvider _asyncListenerProvider; + private readonly LspWorkspaceRegistrationService _lspWorkspaceRegistrationService; + private readonly Refresher _refresher; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public Factory( + IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider, + LspWorkspaceRegistrationService lspWorkspaceRegistrationService, + Refresher refresher) + { + _asyncListenerProvider = asynchronousOperationListenerProvider; + _lspWorkspaceRegistrationService = lspWorkspaceRegistrationService; + _refresher = refresher; + } + + public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) + { + var notificationManager = lspServices.GetRequiredService(); + var lspWorkspaceManager = lspServices.GetRequiredService(); + + return new DiagnosticsRefreshQueue(_asyncListenerProvider, _lspWorkspaceRegistrationService, lspWorkspaceManager, notificationManager, _refresher); + } + } + + [Shared] + [Export(typeof(Refresher))] + [Export(typeof(IDiagnosticsRefresher))] + internal sealed class Refresher : IDiagnosticsRefresher + { + /// + /// Incremented every time a refresh is requested. + /// + private int _globalStateVersion; + + public event Action? WorkspaceRefreshRequested; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public Refresher() + { + } + + public void RequestWorkspaceRefresh() + { + // bump version before sending the request to the client: + Interlocked.Increment(ref _globalStateVersion); + + WorkspaceRefreshRequested?.Invoke(); + } + + public int GlobalStateVersion + => _globalStateVersion; + } + + private readonly Refresher _refresher; + + private DiagnosticsRefreshQueue( + IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider, + LspWorkspaceRegistrationService lspWorkspaceRegistrationService, + LspWorkspaceManager lspWorkspaceManager, + IClientLanguageServerManager notificationManager, + Refresher refresher) + : base(asynchronousOperationListenerProvider, lspWorkspaceRegistrationService, lspWorkspaceManager, notificationManager) + { + _refresher = refresher; + + refresher.WorkspaceRefreshRequested += WorkspaceRefreshRequested; + } + + public override void Dispose() + { + base.Dispose(); + _refresher.WorkspaceRefreshRequested -= WorkspaceRefreshRequested; + } + + private void WorkspaceRefreshRequested() + => EnqueueRefreshNotification(documentUri: null); + + protected override string GetFeatureAttribute() + => FeatureAttribute.DiagnosticService; + + protected override bool? GetRefreshSupport(ClientCapabilities clientCapabilities) + => clientCapabilities.Workspace?.Diagnostics?.RefreshSupport; + + protected override string GetWorkspaceRefreshName() + => Methods.WorkspaceDiagnosticRefreshName; +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs index 17a924111bbb7..97cfd5ead2fb0 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs @@ -20,9 +20,9 @@ internal partial class DocumentPullDiagnosticHandler : AbstractDocumentPullDiagn { public DocumentPullDiagnosticHandler( IDiagnosticAnalyzerService analyzerService, - EditAndContinueDiagnosticUpdateSource editAndContinueDiagnosticUpdateSource, + IDiagnosticsRefresher diagnosticRefresher, IGlobalOptionService globalOptions) - : base(analyzerService, editAndContinueDiagnosticUpdateSource, globalOptions) + : base(analyzerService, diagnosticRefresher, globalOptions) { } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandlerFactory.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandlerFactory.cs index 78e885a953ef5..73300e9cc99dd 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandlerFactory.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandlerFactory.cs @@ -5,7 +5,6 @@ using System; using System.Composition; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.EditAndContinue; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Options; @@ -15,22 +14,22 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics internal class DocumentPullDiagnosticHandlerFactory : ILspServiceFactory { private readonly IDiagnosticAnalyzerService _analyzerService; - private readonly EditAndContinueDiagnosticUpdateSource _editAndContinueDiagnosticUpdateSource; + private readonly IDiagnosticsRefresher _diagnosticsRefresher; private readonly IGlobalOptionService _globalOptions; [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public DocumentPullDiagnosticHandlerFactory( IDiagnosticAnalyzerService analyzerService, - EditAndContinueDiagnosticUpdateSource editAndContinueDiagnosticUpdateSource, + IDiagnosticsRefresher diagnosticsRefresher, IGlobalOptionService globalOptions) { _analyzerService = analyzerService; - _editAndContinueDiagnosticUpdateSource = editAndContinueDiagnosticUpdateSource; + _diagnosticsRefresher = diagnosticsRefresher; _globalOptions = globalOptions; } public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) - => new DocumentPullDiagnosticHandler(_analyzerService, _editAndContinueDiagnosticUpdateSource, _globalOptions); + => new DocumentPullDiagnosticHandler(_analyzerService, _diagnosticsRefresher, _globalOptions); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticHandlerFactory.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticHandlerFactory.cs index fe9355b3b15e7..baade1214bde0 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticHandlerFactory.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticHandlerFactory.cs @@ -16,24 +16,24 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; // by n DocumentDiagnosticPartialResult literals. // See https://github.com/microsoft/vscode-languageserver-node/blob/main/protocol/src/common/proposed.diagnostics.md#textDocument_diagnostic [ExportCSharpVisualBasicLspServiceFactory(typeof(PublicDocumentPullDiagnosticsHandler)), Shared] -internal class PublicDocumentPullDiagnosticHandlerFactory : ILspServiceFactory +internal sealed class PublicDocumentPullDiagnosticHandlerFactory : ILspServiceFactory { private readonly IDiagnosticAnalyzerService _analyzerService; - private readonly EditAndContinueDiagnosticUpdateSource _editAndContinueDiagnosticUpdateSource; + private readonly IDiagnosticsRefresher _diagnosticRefresher; private readonly IGlobalOptionService _globalOptions; [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public PublicDocumentPullDiagnosticHandlerFactory( IDiagnosticAnalyzerService analyzerService, - EditAndContinueDiagnosticUpdateSource editAndContinueDiagnosticUpdateSource, + IDiagnosticsRefresher diagnosticRefresher, IGlobalOptionService globalOptions) { _analyzerService = analyzerService; - _editAndContinueDiagnosticUpdateSource = editAndContinueDiagnosticUpdateSource; + _diagnosticRefresher = diagnosticRefresher; _globalOptions = globalOptions; } public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) - => new PublicDocumentPullDiagnosticsHandler(_analyzerService, _editAndContinueDiagnosticUpdateSource, _globalOptions); + => new PublicDocumentPullDiagnosticsHandler(_analyzerService, _diagnosticRefresher, _globalOptions); } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler.cs index 882ca44e152c7..af74b572008ac 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler.cs @@ -23,13 +23,13 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; using DocumentDiagnosticPartialReport = SumType; [Method(Methods.TextDocumentDiagnosticName)] -internal class PublicDocumentPullDiagnosticsHandler : AbstractDocumentPullDiagnosticHandler +internal sealed class PublicDocumentPullDiagnosticsHandler : AbstractDocumentPullDiagnosticHandler { public PublicDocumentPullDiagnosticsHandler( IDiagnosticAnalyzerService analyzerService, - EditAndContinueDiagnosticUpdateSource editAndContinueDiagnosticUpdateSource, + IDiagnosticsRefresher diagnosticsRefresher, IGlobalOptionService globalOptions) - : base(analyzerService, editAndContinueDiagnosticUpdateSource, globalOptions) + : base(analyzerService, diagnosticsRefresher, globalOptions) { } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticHandlerFactory.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticHandlerFactory.cs index dfca2c14ca1ac..de2a1e0e78290 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticHandlerFactory.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticHandlerFactory.cs @@ -12,11 +12,11 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; [ExportCSharpVisualBasicLspServiceFactory(typeof(PublicWorkspacePullDiagnosticsHandler)), Shared] -internal class PublicWorkspacePullDiagnosticHandlerFactory : ILspServiceFactory +internal sealed class PublicWorkspacePullDiagnosticHandlerFactory : ILspServiceFactory { private readonly LspWorkspaceRegistrationService _registrationService; private readonly IDiagnosticAnalyzerService _analyzerService; - private readonly EditAndContinueDiagnosticUpdateSource _editAndContinueDiagnosticUpdateSource; + private readonly IDiagnosticsRefresher _diagnosticsRefresher; private readonly IGlobalOptionService _globalOptions; [ImportingConstructor] @@ -24,18 +24,18 @@ internal class PublicWorkspacePullDiagnosticHandlerFactory : ILspServiceFactory public PublicWorkspacePullDiagnosticHandlerFactory( LspWorkspaceRegistrationService registrationService, IDiagnosticAnalyzerService analyzerService, - EditAndContinueDiagnosticUpdateSource editAndContinueDiagnosticUpdateSource, + IDiagnosticsRefresher diagnosticsRefresher, IGlobalOptionService globalOptions) { _registrationService = registrationService; _analyzerService = analyzerService; - _editAndContinueDiagnosticUpdateSource = editAndContinueDiagnosticUpdateSource; + _diagnosticsRefresher = diagnosticsRefresher; _globalOptions = globalOptions; } public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) { var workspaceManager = lspServices.GetRequiredService(); - return new PublicWorkspacePullDiagnosticsHandler(workspaceManager, _registrationService, _analyzerService, _editAndContinueDiagnosticUpdateSource, _globalOptions); + return new PublicWorkspacePullDiagnosticsHandler(workspaceManager, _registrationService, _analyzerService, _diagnosticsRefresher, _globalOptions); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler.cs index 4815fd9c3bc54..96149017fbc0c 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler.cs @@ -22,7 +22,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; using WorkspaceDiagnosticPartialReport = SumType; [Method(Methods.WorkspaceDiagnosticName)] -internal class PublicWorkspacePullDiagnosticsHandler : AbstractPullDiagnosticHandler, IDisposable +internal sealed class PublicWorkspacePullDiagnosticsHandler : AbstractPullDiagnosticHandler, IDisposable { private readonly LspWorkspaceRegistrationService _workspaceRegistrationService; private readonly LspWorkspaceManager _workspaceManager; @@ -39,9 +39,9 @@ public PublicWorkspacePullDiagnosticsHandler( LspWorkspaceManager workspaceManager, LspWorkspaceRegistrationService registrationService, IDiagnosticAnalyzerService analyzerService, - EditAndContinueDiagnosticUpdateSource editAndContinueDiagnosticUpdateSource, + IDiagnosticsRefresher diagnosticRefresher, IGlobalOptionService globalOptions) - : base(analyzerService, editAndContinueDiagnosticUpdateSource, globalOptions) + : base(analyzerService, diagnosticRefresher, globalOptions) { _workspaceManager = workspaceManager; _workspaceRegistrationService = registrationService; diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandler.cs index 62b117d747003..b108c3cab4464 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandler.cs @@ -8,7 +8,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.EditAndContinue; using Microsoft.CodeAnalysis.ExternalAccess.Razor.Api; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Options; @@ -23,8 +22,8 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics [Method(VSInternalMethods.WorkspacePullDiagnosticName)] internal sealed partial class WorkspacePullDiagnosticHandler : AbstractPullDiagnosticHandler { - public WorkspacePullDiagnosticHandler(IDiagnosticAnalyzerService analyzerService, EditAndContinueDiagnosticUpdateSource editAndContinueDiagnosticUpdateSource, IGlobalOptionService globalOptions) - : base(analyzerService, editAndContinueDiagnosticUpdateSource, globalOptions) + public WorkspacePullDiagnosticHandler(IDiagnosticAnalyzerService analyzerService, IDiagnosticsRefresher diagnosticsRefresher, IGlobalOptionService globalOptions) + : base(analyzerService, diagnosticsRefresher, globalOptions) { } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandlerFactory.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandlerFactory.cs index 04f24be5da13f..70430c10ec498 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandlerFactory.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandlerFactory.cs @@ -5,7 +5,6 @@ using System; using System.Composition; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.EditAndContinue; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Options; @@ -15,22 +14,22 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics internal class WorkspacePullDiagnosticHandlerFactory : ILspServiceFactory { private readonly IDiagnosticAnalyzerService _analyzerService; - private readonly EditAndContinueDiagnosticUpdateSource _editAndContinueDiagnosticUpdateSource; + private readonly IDiagnosticsRefresher _diagnosticsRefresher; private readonly IGlobalOptionService _globalOptions; [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public WorkspacePullDiagnosticHandlerFactory( IDiagnosticAnalyzerService analyzerService, - EditAndContinueDiagnosticUpdateSource editAndContinueDiagnosticUpdateSource, + IDiagnosticsRefresher diagnosticsRefresher, IGlobalOptionService globalOptions) { _analyzerService = analyzerService; - _editAndContinueDiagnosticUpdateSource = editAndContinueDiagnosticUpdateSource; + _diagnosticsRefresher = diagnosticsRefresher; _globalOptions = globalOptions; } public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) - => new WorkspacePullDiagnosticHandler(_analyzerService, _editAndContinueDiagnosticUpdateSource, _globalOptions); + => new WorkspacePullDiagnosticHandler(_analyzerService, _diagnosticsRefresher, _globalOptions); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/EditAndContinue/RegisterSolutionSnapshotHandler.cs b/src/Features/LanguageServer/Protocol/Handler/EditAndContinue/RegisterSolutionSnapshotHandler.cs new file mode 100644 index 0000000000000..bc2a111664bc3 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/EditAndContinue/RegisterSolutionSnapshotHandler.cs @@ -0,0 +1,41 @@ +// 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.Runtime.Serialization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.EditAndContinue; +using Microsoft.CodeAnalysis.Host.Mef; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.EditAndContinue; + +[DataContract] +internal readonly record struct LspSolutionSnapshotId([property: DataMember(Name = "id")] int Id); + +[ExportCSharpVisualBasicStatelessLspService(typeof(RegisterSolutionSnapshotHandler)), Shared] +[Method("workspace/_vs_registerSolutionSnapshot")] +internal sealed class RegisterSolutionSnapshotHandler : ILspServiceRequestHandler +{ + private readonly ISolutionSnapshotRegistry _registry; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public RegisterSolutionSnapshotHandler(ISolutionSnapshotRegistry registry) + { + _registry = registry; + } + + public bool MutatesSolutionState => false; + public bool RequiresLSPSolution => true; + + public Task HandleRequestAsync(RequestContext context, CancellationToken cancellationToken) + { + Contract.ThrowIfNull(context.Solution); + var id = _registry.RegisterSolutionSnapshot(context.Solution); + return Task.FromResult(new LspSolutionSnapshotId(id.Id)); + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintRefreshQueue.cs b/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintRefreshQueue.cs new file mode 100644 index 0000000000000..8e1c0703cf7d4 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintRefreshQueue.cs @@ -0,0 +1,66 @@ +// 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 Microsoft.CodeAnalysis.InlineHints; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.InlayHint +{ + internal class InlayHintRefreshQueue : AbstractRefreshQueue + { + private readonly IGlobalOptionService _globalOptionService; + + public InlayHintRefreshQueue( + IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider, + LspWorkspaceRegistrationService lspWorkspaceRegistrationService, + IGlobalOptionService globalOptionService, + LspWorkspaceManager lspWorkspaceManager, + IClientLanguageServerManager notificationManager) + : base(asynchronousOperationListenerProvider, lspWorkspaceRegistrationService, lspWorkspaceManager, notificationManager) + { + _globalOptionService = globalOptionService; + _globalOptionService.AddOptionChangedHandler(this, OnOptionChanged); + } + + public override void Dispose() + { + base.Dispose(); + _globalOptionService.RemoveOptionChangedHandler(this, OnOptionChanged); + } + + private void OnOptionChanged(object? sender, OptionChangedEventArgs e) + { + if (e.Option.Equals(InlineHintsOptionsStorage.EnabledForParameters) || + e.Option.Equals(InlineHintsOptionsStorage.ForIndexerParameters) || + e.Option.Equals(InlineHintsOptionsStorage.ForLiteralParameters) || + e.Option.Equals(InlineHintsOptionsStorage.ForOtherParameters) || + e.Option.Equals(InlineHintsOptionsStorage.ForObjectCreationParameters) || + e.Option.Equals(InlineHintsOptionsStorage.SuppressForParametersThatDifferOnlyBySuffix) || + e.Option.Equals(InlineHintsOptionsStorage.SuppressForParametersThatMatchArgumentName) || + e.Option.Equals(InlineHintsOptionsStorage.SuppressForParametersThatMatchMethodIntent) || + e.Option.Equals(InlineHintsOptionsStorage.EnabledForTypes) || + e.Option.Equals(InlineHintsOptionsStorage.ForImplicitVariableTypes) || + e.Option.Equals(InlineHintsOptionsStorage.ForLambdaParameterTypes) || + e.Option.Equals(InlineHintsOptionsStorage.ForImplicitObjectCreation)) + { + EnqueueRefreshNotification(documentUri: null); + } + } + + protected override string GetFeatureAttribute() + => FeatureAttribute.InlineHints; + + protected override bool? GetRefreshSupport(ClientCapabilities clientCapabilities) + { + return clientCapabilities.Workspace?.InlayHint?.RefreshSupport; + } + + protected override string GetWorkspaceRefreshName() + { + return Methods.WorkspaceInlayHintRefreshName; + } + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintRefreshQueueFactory.cs b/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintRefreshQueueFactory.cs new file mode 100644 index 0000000000000..232a9858c26ac --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/InlayHint/InlayHintRefreshQueueFactory.cs @@ -0,0 +1,41 @@ +// 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.Handler.InlayHint; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Shared.TestHooks; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.InlayHint +{ + [ExportCSharpVisualBasicLspServiceFactory(typeof(InlayHintRefreshQueue)), Shared] + internal sealed class InlayHintRefreshQueueFactory : ILspServiceFactory + { + private readonly IAsynchronousOperationListenerProvider _asyncListenerProvider; + private readonly LspWorkspaceRegistrationService _lspWorkspaceRegistrationService; + private readonly IGlobalOptionService _globalOptionService; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public InlayHintRefreshQueueFactory( + IAsynchronousOperationListenerProvider asynchronousOperationListenerProvider, + LspWorkspaceRegistrationService lspWorkspaceRegistrationService, + IGlobalOptionService globalOptionService) + { + _asyncListenerProvider = asynchronousOperationListenerProvider; + _lspWorkspaceRegistrationService = lspWorkspaceRegistrationService; + _globalOptionService = globalOptionService; + } + + public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) + { + var notificationManager = lspServices.GetRequiredService(); + var lspWorkspaceManager = lspServices.GetRequiredService(); + + return new InlayHintRefreshQueue(_asyncListenerProvider, _lspWorkspaceRegistrationService, _globalOptionService, lspWorkspaceManager, notificationManager); + } + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/OnAutoInsert/OnAutoInsertHandler.cs b/src/Features/LanguageServer/Protocol/Handler/OnAutoInsert/OnAutoInsertHandler.cs index 09c5631ba41c9..4abc6158ac136 100644 --- a/src/Features/LanguageServer/Protocol/Handler/OnAutoInsert/OnAutoInsertHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/OnAutoInsert/OnAutoInsertHandler.cs @@ -70,6 +70,7 @@ public OnAutoInsertHandler( var documentationCommentResponse = await GetDocumentationCommentResponseAsync( request, document, service, docCommentOptions, cancellationToken).ConfigureAwait(false); + if (documentationCommentResponse != null) { return documentationCommentResponse; @@ -112,7 +113,7 @@ public OnAutoInsertHandler( var result = autoInsertParams.Character == "\n" ? service.GetDocumentationCommentSnippetOnEnterTyped(syntaxTree, sourceText, position, options, cancellationToken) - : service.GetDocumentationCommentSnippetOnCharacterTyped(syntaxTree, sourceText, position, options, cancellationToken); + : service.GetDocumentationCommentSnippetOnCharacterTyped(syntaxTree, sourceText, position, options, cancellationToken, addIndentation: false); if (result == null) { diff --git a/src/Features/LanguageServer/Protocol/LspOptionsStorage.cs b/src/Features/LanguageServer/Protocol/LspOptionsStorage.cs index 995a0880fc0bb..cedb7e097c9bb 100644 --- a/src/Features/LanguageServer/Protocol/LspOptionsStorage.cs +++ b/src/Features/LanguageServer/Protocol/LspOptionsStorage.cs @@ -11,6 +11,7 @@ internal sealed class LspOptionsStorage /// /// This sets the max list size we will return in response to a completion request. /// If there are more than this many items, we will set the isIncomplete flag on the returned completion list. + /// If set to negative value, we will always return the full list. /// public static readonly Option2 MaxCompletionListSize = new("dotnet_lsp_max_completion_list_size", defaultValue: 1000); diff --git a/src/Features/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj b/src/Features/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj index 0c624c6d95306..cbdbe98365b8e 100644 --- a/src/Features/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj +++ b/src/Features/LanguageServer/Protocol/Microsoft.CodeAnalysis.LanguageServer.Protocol.csproj @@ -4,7 +4,7 @@ Library Microsoft.CodeAnalysis.LanguageServer - net6.0;netstandard2.0 + net7.0;netstandard2.0 true .NET Compiler Platform ("Roslyn") support for Language Server Protocol. @@ -42,7 +42,7 @@ - + @@ -68,10 +68,10 @@ + - + - diff --git a/src/Tools/BuildValidator/BuildValidator.csproj b/src/Tools/BuildValidator/BuildValidator.csproj index f07af0728e425..c9518866ce352 100644 --- a/src/Tools/BuildValidator/BuildValidator.csproj +++ b/src/Tools/BuildValidator/BuildValidator.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Tools/BuildValidator/Program.cs b/src/Tools/BuildValidator/Program.cs index 179d2de624eaf..4d91d066b0db3 100644 --- a/src/Tools/BuildValidator/Program.cs +++ b/src/Tools/BuildValidator/Program.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.CommandLine; -using System.CommandLine.Invocation; +using System.CommandLine.NamingConventionBinder; using System.IO; using System.Linq; using System.Reflection.PortableExecutable; @@ -33,18 +33,18 @@ static int Main(string[] args) System.Diagnostics.Trace.Listeners.Clear(); var rootCommand = new RootCommand { - new Option( + new Option( "--assembliesPath", BuildValidatorResources.Path_to_assemblies_to_rebuild_can_be_specified_one_or_more_times - ) { IsRequired = true, Argument = { Arity = ArgumentArity.OneOrMore } }, - new Option( + ) { IsRequired = true, Arity = ArgumentArity.OneOrMore }, + new Option( "--exclude", BuildValidatorResources.Assemblies_to_be_excluded_substring_match - ) { Argument = { Arity = ArgumentArity.ZeroOrMore } }, + ) { Arity = ArgumentArity.ZeroOrMore }, new Option( "--sourcePath", BuildValidatorResources.Path_to_sources_to_use_in_rebuild ) { IsRequired = true }, - new Option( + new Option( "--referencesPath", BuildValidatorResources.Path_to_referenced_assemblies_can_be_specified_zero_or_more_times - ) { Argument = { Arity = ArgumentArity.ZeroOrMore } }, + ) { Arity = ArgumentArity.ZeroOrMore }, new Option( "--verbose", BuildValidatorResources.Output_verbose_log_information ), diff --git a/src/Tools/IdeBenchmarks/Lsp/LspCompletionSerializationBenchmarks.cs b/src/Tools/IdeBenchmarks/Lsp/LspCompletionSerializationBenchmarks.cs new file mode 100644 index 0000000000000..d171ac617d04b --- /dev/null +++ b/src/Tools/IdeBenchmarks/Lsp/LspCompletionSerializationBenchmarks.cs @@ -0,0 +1,178 @@ +// 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.IO; +using System.Linq; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using Microsoft.CodeAnalysis.LanguageServer.UnitTests.Completion; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Test.Utilities; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json; +using Roslyn.Test.Utilities; +using Xunit; +using LSP = Microsoft.VisualStudio.LanguageServer.Protocol; +using static System.Windows.Forms.VisualStyles.VisualStyleElement.TextBox; +using static Roslyn.Test.Utilities.AbstractLanguageServerProtocolTests; +using System.Threading; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.PooledObjects; + +namespace IdeBenchmarks.Lsp +{ + [MemoryDiagnoser] + public class LspCompletionSerializationBenchmarks : AbstractLanguageServerProtocolTests + { + protected override TestComposition Composition => FeaturesLspComposition; + private readonly UseExportProviderAttribute _useExportProviderAttribute = new(); + + private LSP.CompletionList? _list; + + public LspCompletionSerializationBenchmarks() : base(null) + { + } + + [GlobalSetup] + public void GlobalSetup() + { + } + + [IterationSetup] + public void IterationSetup() + { + _useExportProviderAttribute.Before(null); + LoadSolutionAsync().Wait(); + } + + [IterationCleanup] + public void CleanupAsync() + { + _useExportProviderAttribute.Before(null); + } + + private async Task LoadSolutionAsync() + { + var markup = +@"using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Buffers.Text; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.ComponentModel.Design; +using System.Configuration; +using System.Data; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using System.Dynamic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Media; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +class A +{ + void M() + { + {|caret:|} + } +}"; + await using var testServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace: false, new LSP.VSInternalClientCapabilities + { + TextDocument = new LSP.TextDocumentClientCapabilities + { + Completion = new LSP.CompletionSetting + { + CompletionListSetting = new LSP.CompletionListSetting + { + ItemDefaults = new string[] { "editRange", "commitCharacters", "data" }, + } + } + } + }).ConfigureAwait(false); + + var globalOptions = testServer.TestWorkspace.GetService(); + globalOptions.SetGlobalOption(LspOptionsStorage.MaxCompletionListSize, -1); + + var caret = testServer.GetLocations("caret").Single(); + var completionParams = new LSP.CompletionParams() + { + TextDocument = CreateTextDocumentIdentifier(caret.Uri), + Position = caret.Range.Start, + Context = new LSP.CompletionContext() + { + TriggerKind = LSP.CompletionTriggerKind.Invoked, + } + }; + + var document = testServer.GetCurrentSolution().Projects.First().Documents.First(); + var results = await testServer.ExecuteRequestAsync(LSP.Methods.TextDocumentCompletionName, completionParams, CancellationToken.None); + + var list = (await CompletionTests.RunGetCompletionsAsync(testServer, completionParams)); + if (list.Items.Length == 0) + throw new System.Exception(); + + using var _ = ArrayBuilder.GetInstance(out var builder); + while (builder.Count < 10000) + { + foreach (var item in list.Items) + { + builder.Add(item); + if (item.CommitCharacters is not null || item.Data is not null) + throw new InvalidDataException(); + + if (builder.Count == 10000) + break; + } + } + + list.Items = builder.ToArray(); + _list = list; + } + + [Benchmark] + public async Task Serialization() + { + var serializer = new JsonSerializer(); + serializer.Formatting = Formatting.None; + serializer.NullValueHandling = NullValueHandling.Ignore; + serializer.DefaultValueHandling = DefaultValueHandling.Ignore; + + using var stream = new MemoryStream(); + var sw = new StreamWriter(stream); + var jsonWriter = new JsonTextWriter(sw); + { + serializer.Serialize(jsonWriter, _list); + await jsonWriter.FlushAsync(); + } + + stream.Seek(0, SeekOrigin.Begin); + + using (var sr = new StreamReader(stream)) + using (var jsonReader = new JsonTextReader(sr)) + { + var list = serializer.Deserialize(jsonReader); + if (list!.Items.Length != _list!.Items.Length) + throw new System.Exception(); + } + } + + [Fact] + public async Task Test() + { + await LoadSolutionAsync(); + await Serialization(); + } + } +} diff --git a/src/Tools/Source/RunTests/Options.cs b/src/Tools/Source/RunTests/Options.cs index c2d4a47731523..d3ce7af3ed56d 100644 --- a/src/Tools/Source/RunTests/Options.cs +++ b/src/Tools/Source/RunTests/Options.cs @@ -87,6 +87,12 @@ internal class Options /// public string? HelixQueueName { get; set; } + /// + /// Access token to send jobs to helix (only valid when is ). + /// This should only be set when using internal helix queues. + /// + public string? HelixApiAccessToken { get; set; } + /// /// Path to the dotnet executable we should use for running dotnet test /// @@ -142,6 +148,7 @@ public Options( var sequential = false; var helix = false; var helixQueueName = "Windows.10.Amd64.Open"; + string? helixApiAccessToken = null; var retry = false; string? testFilter = null; int? timeout = null; @@ -168,6 +175,7 @@ public Options( { "sequential", "Run tests sequentially", o => sequential = o is object }, { "helix", "Run tests on Helix", o => helix = o is object }, { "helixQueueName=", "Name of the Helix queue to run tests on", (string s) => helixQueueName = s }, + { "helixApiAccessToken=", "Access token for internal helix queues", (string s) => helixApiAccessToken = s }, { "testfilter=", "xUnit string to pass to --filter, e.g. FullyQualifiedName~TestClass1|Category=CategoryA", (string s) => testFilter = s }, { "timeout=", "Minute timeout to limit the tests to", (int i) => timeout = i }, { "out=", "Test result file directory (when running on Helix, this is relative to the Helix work item directory)", (string s) => resultFileDirectory = s }, @@ -254,6 +262,7 @@ public Options( Sequential = sequential, UseHelix = helix, HelixQueueName = helixQueueName, + HelixApiAccessToken = helixApiAccessToken, IncludeHtml = includeHtml, TestFilter = testFilter, Timeout = timeout is { } t ? TimeSpan.FromMinutes(t) : null, diff --git a/src/Tools/Source/RunTests/TestRunner.cs b/src/Tools/Source/RunTests/TestRunner.cs index 573174594f74a..4907b8db5a1b6 100644 --- a/src/Tools/Source/RunTests/TestRunner.cs +++ b/src/Tools/Source/RunTests/TestRunner.cs @@ -108,7 +108,6 @@ internal async Task RunAllOnHelixAsync(ImmutableArraytest " + buildNumber + @" " + _options.HelixQueueName + @" - " + queuedBy + @" true " + globalJson.sdk.version + @" sdk @@ -123,9 +122,22 @@ internal async Task RunAllOnHelixAsync(ImmutableArray { Debug.Assert(e.Data is not null); ConsoleUtil.WriteLine(e.Data); }, cancellationToken: cancellationToken); diff --git a/src/VisualStudio/Core/Def/ProjectSystem/FileChangeWatcher.cs b/src/VisualStudio/Core/Def/ProjectSystem/FileChangeWatcher.cs index 645fcac519b40..0aa18f88ed9c4 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/FileChangeWatcher.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/FileChangeWatcher.cs @@ -7,7 +7,6 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Linq; -using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Collections; @@ -417,19 +416,8 @@ public void Dispose() public IWatchedFile EnqueueWatchingFile(string filePath) { // If we already have this file under our path, we may not have to do additional watching - foreach (var watchedDirectory in _watchedDirectories) - { - if (filePath.StartsWith(watchedDirectory.Path, StringComparison.OrdinalIgnoreCase)) - { - // If ExtensionFilter is null, then we're watching for all files in the directory so the prior check - // of the directory containment was sufficient. If it isn't null, then we have to check the extension - // matches. - if (watchedDirectory.ExtensionFilter == null || filePath.EndsWith(watchedDirectory.ExtensionFilter, StringComparison.OrdinalIgnoreCase)) - { - return NoOpWatchedFile.Instance; - } - } - } + if (WatchedDirectory.FilePathCoveredByWatchedDirectories(_watchedDirectories, filePath, StringComparison.OrdinalIgnoreCase)) + return NoOpWatchedFile.Instance; var token = new RegularWatchedFile(this); @@ -510,23 +498,6 @@ int IVsFileChangeEvents.FilesChanged(uint cChanges, string[] rgpszFile, uint[] r int IVsFileChangeEvents.DirectoryChanged(string pszDirectory) => VSConstants.E_NOTIMPL; - /// - /// When a FileChangeWatcher already has a watch on a directory, a request to watch a specific file is a no-op. In that case, we return this token, - /// which when disposed also does nothing. - /// - internal sealed class NoOpWatchedFile : IWatchedFile - { - public static readonly IWatchedFile Instance = new NoOpWatchedFile(); - - private NoOpWatchedFile() - { - } - - public void Dispose() - { - } - } - public sealed class RegularWatchedFile : IWatchedFile { public RegularWatchedFile(Context context) diff --git a/src/VisualStudio/Core/Def/RoslynPackage.cs b/src/VisualStudio/Core/Def/RoslynPackage.cs index 805f5f17d2c45..0f222dec7040b 100644 --- a/src/VisualStudio/Core/Def/RoslynPackage.cs +++ b/src/VisualStudio/Core/Def/RoslynPackage.cs @@ -9,13 +9,11 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.ChangeSignature; using Microsoft.CodeAnalysis.ColorSchemes; -using Microsoft.CodeAnalysis.Completion.Log; +using Microsoft.CodeAnalysis.Common; using Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.ErrorReporting; -using Microsoft.CodeAnalysis.Logging; using Microsoft.CodeAnalysis.Notification; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Remote.ProjectSystem; @@ -322,11 +320,9 @@ protected override void Dispose(bool disposing) private void ReportSessionWideTelemetry() { - SolutionLogger.ReportTelemetry(); AsyncCompletionLogger.ReportTelemetry(); - CompletionProvidersLogger.ReportTelemetry(); - ChangeSignatureLogger.ReportTelemetry(); InheritanceMarginLogger.ReportTelemetry(); + FeaturesSessionTelemetry.Report(); ComponentModel.GetService().ReportOtherWorkspaceTelemetry(); } diff --git a/src/VisualStudio/DevKit/Impl/.npmrc b/src/VisualStudio/DevKit/Impl/.npmrc new file mode 100644 index 0000000000000..816fed5f83098 --- /dev/null +++ b/src/VisualStudio/DevKit/Impl/.npmrc @@ -0,0 +1,3 @@ +# This must be located directly next to the package.json for compliance. +registry=https://pkgs.dev.azure.com/dnceng/internal/_packaging/dotnet-tools-internal/npm/registry/ +always-auth=true \ No newline at end of file diff --git a/src/VisualStudio/DevKit/Impl/EditAndContinue/ManagedHotReloadLanguageServiceBridge.cs b/src/VisualStudio/DevKit/Impl/EditAndContinue/ManagedHotReloadLanguageServiceBridge.cs new file mode 100644 index 0000000000000..fe716761b9f7a --- /dev/null +++ b/src/VisualStudio/DevKit/Impl/EditAndContinue/ManagedHotReloadLanguageServiceBridge.cs @@ -0,0 +1,69 @@ +// 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.ComponentModel.Composition; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.BrokeredServices; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.ServiceHub.Framework; +using Microsoft.VisualStudio.Debugger.Contracts.HotReload; +using Microsoft.VisualStudio.Shell.ServiceBroker; +using InternalContracts = Microsoft.CodeAnalysis.Contracts.EditAndContinue; + +namespace Microsoft.CodeAnalysis.EditAndContinue; + +[ExportBrokeredService(MonikerName, ServiceVersion, Audience = ServiceAudience.Local)] +internal sealed partial class ManagedHotReloadLanguageServiceBridge : IManagedHotReloadLanguageService, IExportedBrokeredService +{ + private const string ServiceName = "ManagedHotReloadLanguageService"; + private const string ServiceVersion = "0.1"; + private const string MonikerName = BrokeredServiceDescriptors.LanguageServerComponentNamespace + "." + BrokeredServiceDescriptors.LanguageServerComponentName + "." + ServiceName; + + public static readonly ServiceJsonRpcDescriptor ServiceDescriptor = BrokeredServiceDescriptors.CreateServerServiceDescriptor(ServiceName, new(ServiceVersion)); + private readonly InternalContracts.IManagedHotReloadLanguageService _service; + + static ManagedHotReloadLanguageServiceBridge() + => Debug.Assert(ServiceDescriptor.Moniker.Name == MonikerName); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ManagedHotReloadLanguageServiceBridge(InternalContracts.IManagedHotReloadLanguageService service) + => _service = service; + + ServiceRpcDescriptor IExportedBrokeredService.Descriptor + => ServiceDescriptor; + + public Task InitializeAsync(CancellationToken cancellationToken) + => Task.CompletedTask; + + public ValueTask StartSessionAsync(CancellationToken cancellationToken) + => _service.StartSessionAsync(cancellationToken); + + public ValueTask EndSessionAsync(CancellationToken cancellationToken) + => _service.EndSessionAsync(cancellationToken); + + public ValueTask EnterBreakStateAsync(CancellationToken cancellationToken) + => _service.EnterBreakStateAsync(cancellationToken); + + public ValueTask ExitBreakStateAsync(CancellationToken cancellationToken) + => _service.ExitBreakStateAsync(cancellationToken); + + public ValueTask OnCapabilitiesChangedAsync(CancellationToken cancellationToken) + => _service.OnCapabilitiesChangedAsync(cancellationToken); + + public async ValueTask GetUpdatesAsync(CancellationToken cancellationToken) + => (await _service.GetUpdatesAsync(cancellationToken).ConfigureAwait(false)).FromContract(); + + public ValueTask CommitUpdatesAsync(CancellationToken cancellationToken) + => _service.CommitUpdatesAsync(cancellationToken); + + public ValueTask DiscardUpdatesAsync(CancellationToken cancellationToken) + => _service.DiscardUpdatesAsync(cancellationToken); + + public ValueTask HasChangesAsync(string? sourceFilePath, CancellationToken cancellationToken) + => _service.HasChangesAsync(sourceFilePath, cancellationToken); +} diff --git a/src/VisualStudio/DevKit/Impl/Logging/VSCodeTelemetryLogger.cs b/src/VisualStudio/DevKit/Impl/Logging/VSCodeTelemetryLogger.cs new file mode 100644 index 0000000000000..99a3ba5c79839 --- /dev/null +++ b/src/VisualStudio/DevKit/Impl/Logging/VSCodeTelemetryLogger.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.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Diagnostics; +using System.Text; +using System.Threading; +using Microsoft.CodeAnalysis.Contracts.Telemetry; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.VisualStudio.Telemetry; + +namespace Microsoft.CodeAnalysis.LanguageServer.Logging; + +[Export(typeof(ITelemetryReporter)), Shared] +internal sealed class VSCodeTelemetryLogger : ITelemetryReporter +{ + private TelemetrySession? _telemetrySession; + private const string CollectorApiKey = "0c6ae279ed8443289764825290e4f9e2-1a736e7c-1324-4338-be46-fc2a58ae4d14-7255"; + private static int _dumpsSubmitted = 0; + + private static readonly ConcurrentDictionary _pendingScopes = new(concurrencyLevel: 2, capacity: 10); + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public VSCodeTelemetryLogger() + { + } + + public void InitializeSession(string telemetryLevel, string? sessionId, bool isDefaultSession) + { + Debug.Assert(_telemetrySession == null); + + var sessionSettingsJson = CreateSessionSettingsJson(telemetryLevel, sessionId); + var session = new TelemetrySession($"{{{sessionSettingsJson}}}"); + + if (isDefaultSession) + { + TelemetryService.SetDefaultSession(session); + } + + session.Start(); + session.RegisterForReliabilityEvent(); + + _telemetrySession = session; + } + + public void Log(string name, ImmutableDictionary properties) + { + Debug.Assert(_telemetrySession != null); + + var telemetryEvent = new TelemetryEvent(name); + SetProperties(telemetryEvent, properties); + _telemetrySession.PostEvent(telemetryEvent); + } + + public void LogBlockStart(string eventName, int kind, int blockId) + { + Debug.Assert(_telemetrySession != null); + + _pendingScopes[blockId] = kind switch + { + 0 => _telemetrySession.StartOperation(eventName), // LogType.Trace + 1 => _telemetrySession.StartUserTask(eventName), // LogType.UserAction + _ => new InvalidOperationException($"Unknown BlockStart kind: {kind}") + }; + } + + public void LogBlockEnd(int blockId, ImmutableDictionary properties, CancellationToken cancellationToken) + { + var found = _pendingScopes.TryRemove(blockId, out var scope); + Debug.Assert(found); + + var endEvent = GetEndEvent(scope); + SetProperties(endEvent, properties); + + var result = cancellationToken.IsCancellationRequested ? TelemetryResult.UserCancel : TelemetryResult.Success; + + if (scope is TelemetryScope operation) + operation.End(result); + else if (scope is TelemetryScope userTask) + userTask.End(result); + else + throw new InvalidCastException($"Unexpected value for scope: {scope}"); + } + + public void ReportFault(string eventName, string description, int logLevel, bool forceDump, int processId, Exception exception) + { + Debug.Assert(_telemetrySession != null); + + var faultEvent = new FaultEvent( + eventName: eventName, + description: description, + (FaultSeverity)logLevel, + exceptionObject: exception, + gatherEventDetails: faultUtility => + { + if (forceDump) + { + // Let's just send a maximum of three; number chosen arbitrarily + if (Interlocked.Increment(ref _dumpsSubmitted) <= 3) + faultUtility.AddProcessDump(processId); + } + + if (faultUtility is FaultEvent { IsIncludedInWatsonSample: true }) + { + // if needed, add any extra logs here + } + + // Returning "0" signals that, if sampled, we should send data to Watson. + // Any other value will cancel the Watson report. We never want to trigger a process dump manually, + // we'll let TargetedNotifications determine if a dump should be collected. + // See https://aka.ms/roslynnfwdocs for more details + return 0; + }); + + _telemetrySession.PostEvent(faultEvent); + } + + public void Dispose() + { + _telemetrySession?.Dispose(); + _telemetrySession = null; + } + + private static string CreateSessionSettingsJson(string telemetryLevel, string? sessionId) + { + sessionId ??= Guid.NewGuid().ToString(); + + // Generate a new startTime for process to be consumed by Telemetry Settings + using var curProcess = Process.GetCurrentProcess(); + var processStartTime = curProcess.StartTime.ToFileTimeUtc().ToString(); + + var sb = new StringBuilder(); + + var kvp = new Dictionary + { + { "Id", StringToJsonValue(sessionId) }, + { "HostName", StringToJsonValue("Default") }, + + // Insert Telemetry Level instead of Opt-Out status. The telemetry service handles + // validation of this value so there is no need to do so on this end. If it's invalid, + // it defaults to off. + { "TelemetryLevel", StringToJsonValue(telemetryLevel) }, + + // this sets the Telemetry Session Created by LSP Server to be the Root Initial session + // This means that the SessionID set here by "Id" will be the SessionID used by cloned session + // further down stream + { "IsInitialSession", "true" }, + { "CollectorApiKey", StringToJsonValue(CollectorApiKey) }, + + // using 1010 to indicate VS Code and not to match it to devenv 1000 + { "AppId", "1010" }, + { "ProcessStartTime", processStartTime }, + }; + + foreach (var keyValue in kvp) + { + sb.AppendFormat("\"{0}\":{1},", keyValue.Key, keyValue.Value); + } + + return sb.ToString().TrimEnd(','); + + static string StringToJsonValue(string? value) + { + if (value == null) + { + return "null"; + } + + return '"' + value + '"'; + } + } + + private static TelemetryEvent GetEndEvent(object? scope) + => scope switch + { + TelemetryScope operation => operation.EndEvent, + TelemetryScope userTask => userTask.EndEvent, + _ => throw new InvalidCastException($"Unexpected value for scope: {scope}") + }; + + private static void SetProperties(TelemetryEvent telemetryEvent, ImmutableDictionary properties) + { + foreach (var (name, value) in properties) + { + telemetryEvent.Properties.Add(name, value); + } + } +} diff --git a/src/VisualStudio/DevKit/Impl/Microsoft.VisualStudio.LanguageServices.DevKit.csproj b/src/VisualStudio/DevKit/Impl/Microsoft.VisualStudio.LanguageServices.DevKit.csproj new file mode 100644 index 0000000000000..2788a1c7d4357 --- /dev/null +++ b/src/VisualStudio/DevKit/Impl/Microsoft.VisualStudio.LanguageServices.DevKit.csproj @@ -0,0 +1,105 @@ + + + + + Library + net7.0 + enable + true + + .NET Compiler Platform ("Roslyn") Language Server Protocol internal. + + true + + + + + + + + + + + + + + + + + + + + + + DoNotUse + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $(StageForNpmPackDependsOn);CollectNpmInputs + + + + + $(PackageVersion) + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/VisualStudio/DevKit/Impl/package.json b/src/VisualStudio/DevKit/Impl/package.json new file mode 100644 index 0000000000000..cf3f56c469c13 --- /dev/null +++ b/src/VisualStudio/DevKit/Impl/package.json @@ -0,0 +1,4 @@ +{ + "name": "@microsoft/visualstudio-languageservices-devkit", + "version": "0.0.1" +} \ No newline at end of file diff --git a/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/EditorVerifierInProcess.cs b/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/EditorVerifierInProcess.cs index a71c8f225550f..593c9dfcc5cb7 100644 --- a/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/EditorVerifierInProcess.cs +++ b/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/EditorVerifierInProcess.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -272,16 +272,20 @@ await TestServices.Workspace.WaitForAllAsyncOperationsAsync( Assert.Equal(expectedTag.tooltipText, actualTooltipText); } - static string CollectTextInRun(ContainerElement containerElement) + static string CollectTextInRun(ContainerElement? containerElement) { var builder = new StringBuilder(); - foreach (var element in containerElement.Elements) + + if (containerElement is not null) { - if (element is ClassifiedTextElement classifiedTextElement) + foreach (var element in containerElement.Elements) { - foreach (var run in classifiedTextElement.Runs) + if (element is ClassifiedTextElement classifiedTextElement) { - builder.Append(run.Text); + foreach (var run in classifiedTextElement.Runs) + { + builder.Append(run.Text); + } } } } diff --git a/src/Workspaces/Core/MSBuild/MSBuild/Constants/PropertyNames.cs b/src/Workspaces/Core/MSBuild/MSBuild/Constants/PropertyNames.cs index 2691c30f7e5e5..84fadee0836c9 100644 --- a/src/Workspaces/Core/MSBuild/MSBuild/Constants/PropertyNames.cs +++ b/src/Workspaces/Core/MSBuild/MSBuild/Constants/PropertyNames.cs @@ -60,6 +60,7 @@ internal static class PropertyNames public const string TargetCompactFramework = nameof(TargetCompactFramework); public const string TargetFramework = nameof(TargetFramework); public const string TargetFrameworks = nameof(TargetFrameworks); + public const string TargetFrameworkIdentifier = nameof(TargetFrameworkIdentifier); public const string TargetPath = nameof(TargetPath); public const string TargetRefPath = nameof(TargetRefPath); public const string TreatWarningsAsErrors = nameof(TreatWarningsAsErrors); diff --git a/src/Workspaces/Core/MSBuild/MSBuild/ProjectFile/ProjectFile.cs b/src/Workspaces/Core/MSBuild/MSBuild/ProjectFile/ProjectFile.cs index 62c2ac0ad3ad4..708e01d35a78e 100644 --- a/src/Workspaces/Core/MSBuild/MSBuild/ProjectFile/ProjectFile.cs +++ b/src/Workspaces/Core/MSBuild/MSBuild/ProjectFile/ProjectFile.cs @@ -146,6 +146,8 @@ private ProjectFileInfo CreateProjectFileInfo(MSB.Execution.ProjectInstance proj targetFramework = null; } + var targetFrameworkIdentifier = project.ReadPropertyString(PropertyNames.TargetFrameworkIdentifier); + var docs = project.GetDocuments() .Where(IsNotTemporaryGeneratedFile) .Select(MakeDocumentFileInfo) @@ -167,6 +169,7 @@ private ProjectFileInfo CreateProjectFileInfo(MSB.Execution.ProjectInstance proj intermediateOutputFilePath, defaultNamespace, targetFramework, + targetFrameworkIdentifier, commandLineArgs, docs, additionalDocs, diff --git a/src/Workspaces/Core/MSBuild/MSBuild/ProjectFile/ProjectFileInfo.cs b/src/Workspaces/Core/MSBuild/MSBuild/ProjectFile/ProjectFileInfo.cs index 561322651a523..71128b114258b 100644 --- a/src/Workspaces/Core/MSBuild/MSBuild/ProjectFile/ProjectFileInfo.cs +++ b/src/Workspaces/Core/MSBuild/MSBuild/ProjectFile/ProjectFileInfo.cs @@ -62,6 +62,12 @@ internal sealed class ProjectFileInfo /// public string? TargetFramework { get; } + /// + /// The target framework identifier of this project. + /// Used to determine if a project is targeting .net core. + /// + public string? TargetFrameworkIdentifier { get; } + /// /// The command line args used to compile the project. /// @@ -107,6 +113,7 @@ private ProjectFileInfo( string? intermediateOutputFilePath, string? defaultNamespace, string? targetFramework, + string? targetFrameworkIdentifier, ImmutableArray commandLineArgs, ImmutableArray documents, ImmutableArray additionalDocuments, @@ -124,6 +131,7 @@ private ProjectFileInfo( this.IntermediateOutputFilePath = intermediateOutputFilePath; this.DefaultNamespace = defaultNamespace; this.TargetFramework = targetFramework; + this.TargetFrameworkIdentifier = targetFrameworkIdentifier; this.CommandLineArgs = commandLineArgs; this.Documents = documents; this.AdditionalDocuments = additionalDocuments; @@ -140,6 +148,7 @@ public static ProjectFileInfo Create( string? intermediateOutputFilePath, string? defaultNamespace, string? targetFramework, + string? targetFrameworkIdentifier, ImmutableArray commandLineArgs, ImmutableArray documents, ImmutableArray additionalDocuments, @@ -155,6 +164,7 @@ public static ProjectFileInfo Create( intermediateOutputFilePath, defaultNamespace, targetFramework, + targetFrameworkIdentifier, commandLineArgs, documents, additionalDocuments, @@ -172,6 +182,7 @@ public static ProjectFileInfo CreateEmpty(string language, string? filePath, Dia intermediateOutputFilePath: null, defaultNamespace: null, targetFramework: null, + targetFrameworkIdentifier: null, commandLineArgs: ImmutableArray.Empty, documents: ImmutableArray.Empty, additionalDocuments: ImmutableArray.Empty, diff --git a/src/Workspaces/Core/MSBuild/Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj b/src/Workspaces/Core/MSBuild/Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj index 075262492f489..7d0bdc9c66649 100644 --- a/src/Workspaces/Core/MSBuild/Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj +++ b/src/Workspaces/Core/MSBuild/Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj @@ -39,7 +39,7 @@ - + diff --git a/src/Workspaces/Core/Portable/Log/KeyValueLogMessage.cs b/src/Workspaces/Core/Portable/Log/KeyValueLogMessage.cs index dbbcc1029dbdc..e6b23bd66a43d 100644 --- a/src/Workspaces/Core/Portable/Log/KeyValueLogMessage.cs +++ b/src/Workspaces/Core/Portable/Log/KeyValueLogMessage.cs @@ -176,11 +176,11 @@ internal enum LogType /// /// Log some traces of an activity (default) /// - Trace, + Trace = 0, /// /// Log an user explicit action /// - UserAction, + UserAction = 1, } } diff --git a/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj b/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj index 27fa7e51c329c..a1f280799b925 100644 --- a/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj +++ b/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj @@ -55,13 +55,15 @@ + - + + diff --git a/src/Workspaces/Core/Portable/Shared/TestHooks/FeatureAttribute.cs b/src/Workspaces/Core/Portable/Shared/TestHooks/FeatureAttribute.cs index 08f2b9ac04510..fc2cba96afb4a 100644 --- a/src/Workspaces/Core/Portable/Shared/TestHooks/FeatureAttribute.cs +++ b/src/Workspaces/Core/Portable/Shared/TestHooks/FeatureAttribute.cs @@ -14,6 +14,7 @@ internal static class FeatureAttribute public const string CallHierarchy = nameof(CallHierarchy); public const string Classification = nameof(Classification); public const string CodeDefinitionWindow = nameof(CodeDefinitionWindow); + public const string CodeLens = nameof(CodeLens); public const string CodeModel = nameof(CodeModel); public const string CompletionSet = nameof(CompletionSet); public const string DesignerAttributes = nameof(DesignerAttributes); diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/FileWatchedPortableExecutableReferenceFactory.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/FileWatchedPortableExecutableReferenceFactory.cs index a49d837f98f6a..3aaef92c7a5f4 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/FileWatchedPortableExecutableReferenceFactory.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/FileWatchedPortableExecutableReferenceFactory.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; @@ -19,9 +21,10 @@ internal sealed class FileWatchedPortableExecutableReferenceFactory private readonly SolutionServices _solutionServices; /// - /// A file change context used to watch metadata references. + /// A file change context used to watch metadata references. This is lazy to avoid creating this immediately during our LSP process startup, when we + /// don't yet know the LSP client's capabilities. /// - private readonly IFileChangeContext _fileReferenceChangeContext; + private readonly Lazy _fileReferenceChangeContext; /// /// File watching tokens from that are watching metadata references. These are only created once we are actually applying a batch because @@ -42,21 +45,49 @@ public FileWatchedPortableExecutableReferenceFactory( { _solutionServices = solutionServices; - var watchedDirectories = new List(); - - if (PlatformInformation.IsWindows) + _fileReferenceChangeContext = new Lazy(() => { - // We will do a single directory watch on the Reference Assemblies folder to avoid having to create separate file - // watches on individual .dlls that effectively never change. - var referenceAssembliesPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Reference Assemblies", "Microsoft", "Framework"); - watchedDirectories.Add(new WatchedDirectory(referenceAssembliesPath, ".dll")); - } + var referenceDirectories = new HashSet(); + + // On each platform, there is a place that reference assemblies for the framework are installed. These are rarely going to be changed + // but are the most common places that we're going to create file watches. Rather than either creating a huge number of file watchers + // for every single file, or eventually realizing we should just watch these directories, we just create the single directory watchers now. + // We'll collect this from two places: constructing it from known environment variables, and also for the defaults where those environment + // variables would usually point, as a fallback. + + if (Environment.GetEnvironmentVariable("DOTNET_ROOT") is string dotnetRoot && !string.IsNullOrEmpty(dotnetRoot)) + { + referenceDirectories.Add(Path.Combine(dotnetRoot, "packs")); + } - // TODO: set this to watch the NuGet directory as well; there's some concern that watching the entire directory - // might make restores take longer because we'll be watching changes that may not impact your project. + if (PlatformInformation.IsWindows) + { + referenceDirectories.Add(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Reference Assemblies", "Microsoft", "Framework")); + referenceDirectories.Add(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "dotnet", "packs")); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + referenceDirectories.Add("/usr/lib/dotnet/packs"); + referenceDirectories.Add("/usr/share/dotnet/packs"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + referenceDirectories.Add("/usr/local/share/dotnet/packs"); + } + + // Also watch the NuGet restore path; we don't do this (yet) on Windows due to potential concerns about whether + // this creates additional overhead responding to changes during a restore. + // TODO: remove this condition + if (!PlatformInformation.IsWindows) + { + referenceDirectories.Add(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages")); + } - _fileReferenceChangeContext = fileChangeWatcher.CreateContext(watchedDirectories.ToArray()); - _fileReferenceChangeContext.FileChanged += FileReferenceChangeContext_FileChanged; + var directoriesToWatch = referenceDirectories.Select(static d => new WatchedDirectory(d, ".dll")).ToArray(); + var fileReferenceChangeContext = fileChangeWatcher.CreateContext(directoriesToWatch); + fileReferenceChangeContext.FileChanged += FileReferenceChangeContext_FileChanged; + return fileReferenceChangeContext; + }); } public event EventHandler? ReferenceChanged; @@ -66,7 +97,7 @@ public PortableExecutableReference CreateReferenceAndStartWatchingFile(string fu lock (_gate) { var reference = _solutionServices.GetRequiredService().GetReference(fullFilePath, properties); - var fileWatchingToken = _fileReferenceChangeContext.EnqueueWatchingFile(fullFilePath); + var fileWatchingToken = _fileReferenceChangeContext.Value.EnqueueWatchingFile(fullFilePath); _metadataReferenceFileWatchingTokens.Add(reference, fileWatchingToken); diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/IFileChangeWatcher.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/IFileChangeWatcher.cs index 358b4ed163703..fb9727e2d6936 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/IFileChangeWatcher.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/IFileChangeWatcher.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Immutable; namespace Microsoft.CodeAnalysis.ProjectSystem { @@ -27,8 +28,8 @@ internal sealed class WatchedDirectory { public WatchedDirectory(string path, string? extensionFilter) { - // We are doing string comparisons with this path, so ensure it has a trailing \ so we don't get confused with sibling - // paths that won't actually be covered. + // We are doing string comparisons with this path, so ensure it has a trailing directory separator so we don't get confused with sibling + // paths that won't actually be covered. For example, if we're watching C:\Directory we wouldn't see changes to C:\DirectorySibling\Foo.txt. if (!path.EndsWith(System.IO.Path.DirectorySeparatorChar.ToString())) { path += System.IO.Path.DirectorySeparatorChar; @@ -49,6 +50,25 @@ public WatchedDirectory(string path, string? extensionFilter) /// If non-null, only watch the directory for changes to a specific extension. String always starts with a period. /// public string? ExtensionFilter { get; } + + public static bool FilePathCoveredByWatchedDirectories(ImmutableArray watchedDirectories, string filePath, StringComparison stringComparison) + { + foreach (var watchedDirectory in watchedDirectories) + { + if (filePath.StartsWith(watchedDirectory.Path, stringComparison)) + { + // If ExtensionFilter is null, then we're watching for all files in the directory so the prior check + // of the directory containment was sufficient. If it isn't null, then we have to check the extension + // matches. + if (watchedDirectory.ExtensionFilter == null || filePath.EndsWith(watchedDirectory.ExtensionFilter, stringComparison)) + { + return true; + } + } + } + + return false; + } } /// @@ -72,4 +92,21 @@ internal interface IFileChangeContext : IDisposable internal interface IWatchedFile : IDisposable { } + + /// + /// When a FileChangeWatcher already has a watch on a directory, a request to watch a specific file is a no-op. In that case, we return this token, + /// which when disposed also does nothing. + /// + internal sealed class NoOpWatchedFile : IWatchedFile + { + public static readonly IWatchedFile Instance = new NoOpWatchedFile(); + + private NoOpWatchedFile() + { + } + + public void Dispose() + { + } + } } diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.BatchingDocumentCollection.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.BatchingDocumentCollection.cs index 916231184d82f..1da7f3141f254 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.BatchingDocumentCollection.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.BatchingDocumentCollection.cs @@ -92,7 +92,7 @@ public DocumentId AddFile(string fullPath, SourceCodeKind sourceCodeKind, Immuta } var documentId = DocumentId.CreateNewId(_project.Id, fullPath); - var textLoader = new WorkspaceFileTextLoader(_project._projectSystemProjectFactory.Workspace.Services.SolutionServices, fullPath, defaultEncoding: null); + var textLoader = _project._projectSystemProjectFactory.CreateFileTextLoader(fullPath); var documentInfo = DocumentInfo.Create( documentId, name: FileNameUtilities.GetFileName(fullPath), diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectFactory.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectFactory.cs index 3e21a91d3b76a..09529fc7eaf3c 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectFactory.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectFactory.cs @@ -75,6 +75,9 @@ public ProjectSystemProjectFactory(Workspace workspace, IFileChangeWatcher fileC _onProjectRemoved = onProjectRemoved; } + public FileTextLoader CreateFileTextLoader(string fullPath) + => new WorkspaceFileTextLoader(this.Workspace.Services.SolutionServices, fullPath, defaultEncoding: null); + public async Task CreateAndAddToWorkspaceAsync(string projectSystemName, string language, ProjectSystemProjectCreationInfo creationInfo, ProjectSystemHostInfo hostInfo) { var projectId = ProjectId.CreateNewId(projectSystemName); @@ -192,9 +195,9 @@ public void ApplyChangeToWorkspace(Action action) /// /// Applies a single operation to the workspace. should be a call to one of the protected Workspace.On* methods. /// - public async ValueTask ApplyChangeToWorkspaceAsync(Func action) + public async ValueTask ApplyChangeToWorkspaceAsync(Func action, CancellationToken cancellationToken = default) { - using (await _gate.DisposableWaitAsync().ConfigureAwait(false)) + using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false)) { await action(Workspace).ConfigureAwait(false); } diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectOptionsProcessor.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectOptionsProcessor.cs index 04fb5aabf157e..eff75f9389d16 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectOptionsProcessor.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectOptionsProcessor.cs @@ -178,8 +178,13 @@ private void UpdateProjectOptions_NoLock() if (effectiveRuleSetPath != null) { - _ruleSetFile = _workspaceServices.GetRequiredService().GetOrCreateRuleSet(effectiveRuleSetPath); - _ruleSetFile.Target.Value.UpdatedOnDisk += RuleSetFile_UpdatedOnDisk; + // Ruleset service is not required across all our platforms + _ruleSetFile = _workspaceServices.GetService()?.GetOrCreateRuleSet(effectiveRuleSetPath); + + if (_ruleSetFile != null) + { + _ruleSetFile.Target.Value.UpdatedOnDisk += RuleSetFile_UpdatedOnDisk; + } } } diff --git a/src/Workspaces/Remote/Core/BrokeredServiceDescriptors.cs b/src/Workspaces/Remote/Core/BrokeredServiceDescriptors.cs new file mode 100644 index 0000000000000..97b7a4341fb3b --- /dev/null +++ b/src/Workspaces/Remote/Core/BrokeredServiceDescriptors.cs @@ -0,0 +1,99 @@ +// 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 Microsoft.ServiceHub.Framework; +using StreamJsonRpc; +using static Microsoft.ServiceHub.Framework.ServiceJsonRpcDescriptor; + +namespace Microsoft.CodeAnalysis.BrokeredServices; + +/// +/// Descriptors of brokered services not used by Roslyn remoting infrastructure. +/// +internal static class BrokeredServiceDescriptors +{ + /// + /// Descriptors for client services written in TypeScript. + /// + private sealed class ClientServiceDescriptor : ServiceJsonRpcDescriptor + { + private const string AsyncSuffix = "Async"; + + private static readonly Func NameNormalize = + name => CommonMethodNameTransforms.CamelCase(name.EndsWith(AsyncSuffix, StringComparison.OrdinalIgnoreCase) ? name.Substring(0, name.Length - AsyncSuffix.Length) : name); + + public ClientServiceDescriptor(ServiceMoniker serviceMoniker, Type? clientInterface = null) + : base(serviceMoniker, clientInterface, Formatters.MessagePack, MessageDelimiters.BigEndianInt32LengthHeader) + { + } + + public ClientServiceDescriptor(ClientServiceDescriptor copyFrom) + : base(copyFrom) + { + } + + protected override ServiceRpcDescriptor Clone() + => new ClientServiceDescriptor(this); + + protected override JsonRpcConnection CreateConnection(JsonRpc jsonRpc) + { + // allow TypeScript to name async methods without "Async" suffix + + var connection = base.CreateConnection(jsonRpc); + connection.LocalRpcTargetOptions.MethodNameTransform = NameNormalize; + connection.LocalRpcTargetOptions.EventNameTransform = NameNormalize; + connection.LocalRpcProxyOptions.MethodNameTransform = NameNormalize; + connection.LocalRpcProxyOptions.EventNameTransform = NameNormalize; + return connection; + } + } + + internal const string LanguageServerComponentNamespace = "Microsoft.CodeAnalysis"; + internal const string VisualStudioComponentNamespace = "Microsoft.VisualStudio"; + + /// + /// Services proffered by Language Server process. + /// + internal const string LanguageServerComponentName = "LanguageServer"; + + /// + /// Services proffered by language client in the Extension Host process. + /// + internal const string LanguageClientComponentName = "LanguageClient"; + + /// + /// Services proffered by one of the Debugger processes. + /// + internal const string DebuggerComponentName = "Debugger"; + + public static readonly ServiceRpcDescriptor SolutionSnapshotProvider = CreateClientServiceDescriptor("SolutionSnapshotProvider", new Version(0, 1)); + public static readonly ServiceRpcDescriptor DebuggerManagedHotReloadService = CreateDebuggerServiceDescriptor("ManagedHotReloadService", new Version(0, 1)); + + public static ServiceMoniker CreateMoniker(string namespaceName, string componentName, string serviceName, Version? version) + => new(namespaceName + "." + componentName + "." + serviceName, version); + + /// + /// Descriptor for services proferred by the client extension (implemented in TypeScript). + /// + public static ServiceJsonRpcDescriptor CreateClientServiceDescriptor(string serviceName, Version? version) + => new ClientServiceDescriptor(CreateMoniker(LanguageServerComponentNamespace, LanguageClientComponentName, serviceName, version), clientInterface: null) + .WithExceptionStrategy(ExceptionProcessing.ISerializable); + + /// + /// Descriptor for services proferred by Roslyn server (implemented in C#). + /// + public static ServiceJsonRpcDescriptor CreateServerServiceDescriptor(string serviceName, Version? version) + => CreateDescriptor(CreateMoniker(LanguageServerComponentNamespace, LanguageServerComponentName, serviceName, version)); + + /// + /// Descriptor for services proferred by the debugger server (implemented in C#). + /// + public static ServiceJsonRpcDescriptor CreateDebuggerServiceDescriptor(string serviceName, Version? version) + => CreateDescriptor(CreateMoniker(VisualStudioComponentNamespace, DebuggerComponentName, serviceName, version)); + + private static ServiceJsonRpcDescriptor CreateDescriptor(ServiceMoniker moniker) + => new ServiceJsonRpcDescriptor(moniker, Formatters.MessagePack, MessageDelimiters.BigEndianInt32LengthHeader) + .WithExceptionStrategy(ExceptionProcessing.ISerializable); +} diff --git a/src/Workspaces/Remote/Core/EditAndContinue/ISolutionSnapshotProvider.cs b/src/Workspaces/Remote/Core/EditAndContinue/ISolutionSnapshotProvider.cs new file mode 100644 index 0000000000000..7653654c181b2 --- /dev/null +++ b/src/Workspaces/Remote/Core/EditAndContinue/ISolutionSnapshotProvider.cs @@ -0,0 +1,22 @@ +// 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.BrokeredServices; +using Microsoft.CodeAnalysis.Contracts.Client; +using Microsoft.ServiceHub.Framework; + +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal sealed class SolutionSnapshotProviderProxy : BrokeredServiceProxy, ISolutionSnapshotProvider +{ + public SolutionSnapshotProviderProxy(IServiceBroker serviceBroker) + : base(serviceBroker, BrokeredServiceDescriptors.SolutionSnapshotProvider) + { + } + + public ValueTask RegisterSolutionSnapshotAsync(CancellationToken cancellationToken) + => InvokeAsync((service, cancellationToken) => service.RegisterSolutionSnapshotAsync(cancellationToken), cancellationToken); +} diff --git a/src/Workspaces/Remote/Core/EditAndContinue/ManagedHotReloadLanguageService.cs b/src/Workspaces/Remote/Core/EditAndContinue/ManagedHotReloadLanguageService.cs new file mode 100644 index 0000000000000..01ec5b471d988 --- /dev/null +++ b/src/Workspaces/Remote/Core/EditAndContinue/ManagedHotReloadLanguageService.cs @@ -0,0 +1,309 @@ +// 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.ComponentModel.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Contracts.Client; +using Microsoft.CodeAnalysis.Contracts.EditAndContinue; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Text; +using Microsoft.ServiceHub.Framework; +using Microsoft.VisualStudio.Shell.ServiceBroker; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.EditAndContinue; + +[Export(typeof(IManagedHotReloadLanguageService))] +internal sealed partial class ManagedHotReloadLanguageService : IManagedHotReloadLanguageService +{ + private sealed class PdbMatchingSourceTextProvider : IPdbMatchingSourceTextProvider + { + public static readonly PdbMatchingSourceTextProvider Instance = new(); + + // Returning null will check the file on disk: + public ValueTask TryGetMatchingSourceTextAsync(string filePath, ImmutableArray requiredChecksum, SourceHashAlgorithm checksumAlgorithm, CancellationToken cancellationToken) + => ValueTaskFactory.FromResult(null); + } + + private static readonly ActiveStatementSpanProvider s_emptyActiveStatementProvider = + (_, _, _) => ValueTaskFactory.FromResult(ImmutableArray.Empty); + + private readonly IManagedHotReloadService _debuggerService; + private readonly ISolutionSnapshotProvider _solutionSnapshotProvider; + private readonly IEditAndContinueService _encService; + private readonly SolutionSnapshotRegistry _solutionSnapshotRegistry; + + private bool _disabled; + private DebuggingSessionId? _debuggingSession; + private Solution? _committedDesignTimeSolution; + private Solution? _pendingUpdatedDesignTimeSolution; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public ManagedHotReloadLanguageService( + [Import(typeof(SVsFullAccessServiceBroker))] IServiceBroker serviceBroker, + IEditAndContinueService encService, + SolutionSnapshotRegistry registry) + { + _debuggerService = new ManagedHotReloadServiceProxy(serviceBroker); + _solutionSnapshotProvider = new SolutionSnapshotProviderProxy(serviceBroker); + _encService = encService; + _solutionSnapshotRegistry = registry; + } + + private void Disable() + { + _disabled = true; + _debuggingSession = null; + _committedDesignTimeSolution = null; + _pendingUpdatedDesignTimeSolution = null; + _solutionSnapshotRegistry.Clear(); + } + + private async ValueTask GetCurrentDesignTimeSolutionAsync(CancellationToken cancellationToken) + { + // First, calls to the client to get the current snapshot id. + // The client service calls the LSP client, which sends message to the LSP server, which in turn calls back to RegisterSolutionSnapshot. + // Once complete the snapshot should be registered. + var id = await _solutionSnapshotProvider.RegisterSolutionSnapshotAsync(cancellationToken).ConfigureAwait(false); + + return _solutionSnapshotRegistry.GetRegisteredSolutionSnapshot(id); + } + + private static Solution GetCurrentCompileTimeSolution(Solution currentDesignTimeSolution) + => currentDesignTimeSolution.Services.GetRequiredService().GetCompileTimeSolution(currentDesignTimeSolution); + + public async ValueTask StartSessionAsync(CancellationToken cancellationToken) + { + if (_disabled) + { + return; + } + + try + { + var currentDesignTimeSolution = await GetCurrentDesignTimeSolutionAsync(cancellationToken).ConfigureAwait(false); + _committedDesignTimeSolution = currentDesignTimeSolution; + var compileTimeSolution = GetCurrentCompileTimeSolution(currentDesignTimeSolution); + + // TODO: use remote proxy once we transition to pull diagnostics + _debuggingSession = await _encService.StartDebuggingSessionAsync( + compileTimeSolution, + _debuggerService, + PdbMatchingSourceTextProvider.Instance, + captureMatchingDocuments: ImmutableArray.Empty, + captureAllMatchingDocuments: false, + reportDiagnostics: true, + cancellationToken).ConfigureAwait(false); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + // the service failed, error has been reported - disable further operations + Disable(); + } + } + + private ValueTask BreakStateOrCapabilitiesChangedAsync(bool? inBreakState, CancellationToken cancellationToken) + { + if (_disabled) + { + return ValueTaskFactory.CompletedTask; + } + + try + { + Contract.ThrowIfNull(_debuggingSession); + _encService.BreakStateOrCapabilitiesChanged(_debuggingSession.Value, inBreakState, out _); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + Disable(); + } + + return ValueTaskFactory.CompletedTask; + } + + public ValueTask EnterBreakStateAsync(CancellationToken cancellationToken) + => BreakStateOrCapabilitiesChangedAsync(inBreakState: true, cancellationToken); + + public ValueTask ExitBreakStateAsync(CancellationToken cancellationToken) + => BreakStateOrCapabilitiesChangedAsync(inBreakState: false, cancellationToken); + + public ValueTask OnCapabilitiesChangedAsync(CancellationToken cancellationToken) + => BreakStateOrCapabilitiesChangedAsync(inBreakState: null, cancellationToken); + + public ValueTask CommitUpdatesAsync(CancellationToken cancellationToken) + { + if (_disabled) + { + return ValueTaskFactory.CompletedTask; + } + + try + { + Contract.ThrowIfNull(_debuggingSession); + var committedDesignTimeSolution = Interlocked.Exchange(ref _pendingUpdatedDesignTimeSolution, null); + Contract.ThrowIfNull(committedDesignTimeSolution); + + _committedDesignTimeSolution = committedDesignTimeSolution; + + _encService.CommitSolutionUpdate(_debuggingSession.Value, out _); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + Disable(); + } + + return ValueTaskFactory.CompletedTask; + } + + public ValueTask DiscardUpdatesAsync(CancellationToken cancellationToken) + { + if (_disabled) + { + return ValueTaskFactory.CompletedTask; + } + + try + { + Contract.ThrowIfNull(_debuggingSession); + Contract.ThrowIfNull(Interlocked.Exchange(ref _pendingUpdatedDesignTimeSolution, null)); + + _encService.DiscardSolutionUpdate(_debuggingSession.Value); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + Disable(); + } + + return ValueTaskFactory.CompletedTask; + } + + public ValueTask EndSessionAsync(CancellationToken cancellationToken) + { + if (_disabled) + { + return ValueTaskFactory.CompletedTask; + } + + try + { + Contract.ThrowIfNull(_debuggingSession); + + _encService.EndDebuggingSession(_debuggingSession.Value, out _); + + _debuggingSession = null; + _committedDesignTimeSolution = null; + _pendingUpdatedDesignTimeSolution = null; + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + Disable(); + } + + return ValueTaskFactory.CompletedTask; + } + + /// + /// Returns true if any changes have been made to the source since the last changes had been applied. + /// For performance reasons it only implements a heuristic and may return both false positives and false negatives. + /// If the result is a false negative the debugger will not apply the changes unless the user explicitly triggers apply change command. + /// The background diagnostic analysis will still report rude edits for these ignored changes. It may also happen that these rude edits + /// will disappear once the debuggee is resumed - if they are caused by presence of active statements around the change. + /// If the result is a false positive the debugger attempts to apply the changes, which will result in a delay but will correctly end up + /// with no actual deltas to be applied. + /// + /// If is specified checks for changes only in a document of the given path. + /// This is not supported (returns false) for source-generated documents. + /// + public async ValueTask HasChangesAsync(string? sourceFilePath, CancellationToken cancellationToken) + { + try + { + var debuggingSession = _debuggingSession; + if (debuggingSession == null) + { + return false; + } + + Contract.ThrowIfNull(_committedDesignTimeSolution); + var oldSolution = _committedDesignTimeSolution; + + var newSolution = await GetCurrentDesignTimeSolutionAsync(cancellationToken).ConfigureAwait(false); + + return (sourceFilePath != null) + ? await EditSession.HasChangesAsync(oldSolution, newSolution, sourceFilePath, cancellationToken).ConfigureAwait(false) + : await EditSession.HasChangesAsync(oldSolution, newSolution, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + return true; + } + } + + public async ValueTask GetUpdatesAsync(CancellationToken cancellationToken) + { + if (_disabled) + { + return new ManagedHotReloadUpdates(ImmutableArray.Empty, ImmutableArray.Empty); + } + + try + { + Contract.ThrowIfNull(_debuggingSession); + + var designTimeSolution = await GetCurrentDesignTimeSolutionAsync(cancellationToken).ConfigureAwait(false); + var solution = GetCurrentCompileTimeSolution(designTimeSolution); + + ModuleUpdates moduleUpdates; + ImmutableArray diagnosticData; + ImmutableArray<(DocumentId DocumentId, ImmutableArray Diagnostics)> rudeEdits; + DiagnosticData? syntaxError; + + try + { + var results = await _encService.EmitSolutionUpdateAsync(_debuggingSession.Value, solution, s_emptyActiveStatementProvider, cancellationToken).ConfigureAwait(false); + + moduleUpdates = results.ModuleUpdates; + diagnosticData = results.Diagnostics.ToDiagnosticData(solution); + rudeEdits = results.RudeEdits; + syntaxError = results.GetSyntaxErrorData(solution); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + var descriptor = EditAndContinueDiagnosticDescriptors.GetDescriptor(RudeEditKind.InternalError); + + var diagnostic = Diagnostic.Create( + descriptor, + Location.None, + string.Format(descriptor.MessageFormat.ToString(), "", e.Message)); + + diagnosticData = ImmutableArray.Create(DiagnosticData.Create(designTimeSolution, diagnostic, project: null)); + rudeEdits = ImmutableArray<(DocumentId DocumentId, ImmutableArray Diagnostics)>.Empty; + moduleUpdates = new ModuleUpdates(ModuleUpdateStatus.RestartRequired, ImmutableArray.Empty); + syntaxError = null; + } + + // Only store the solution if we have any changes to apply, otherwise CommitUpdatesAsync/DiscardUpdatesAsync won't be called. + if (moduleUpdates.Status == ModuleUpdateStatus.Ready) + { + _pendingUpdatedDesignTimeSolution = designTimeSolution; + } + + var diagnostics = await EmitSolutionUpdateResults.GetHotReloadDiagnosticsAsync(solution, diagnosticData, rudeEdits, syntaxError, moduleUpdates.Status, cancellationToken).ConfigureAwait(false); + return new ManagedHotReloadUpdates(moduleUpdates.Updates, diagnostics); + } + catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) + { + Disable(); + return new ManagedHotReloadUpdates(ImmutableArray.Empty, ImmutableArray.Empty); + } + } +} diff --git a/src/Workspaces/Remote/Core/EditAndContinue/ManagedHotReloadServiceProxy.cs b/src/Workspaces/Remote/Core/EditAndContinue/ManagedHotReloadServiceProxy.cs new file mode 100644 index 0000000000000..5dbdede70efe9 --- /dev/null +++ b/src/Workspaces/Remote/Core/EditAndContinue/ManagedHotReloadServiceProxy.cs @@ -0,0 +1,33 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.BrokeredServices; +using Microsoft.CodeAnalysis.Contracts.EditAndContinue; +using Microsoft.ServiceHub.Framework; + +namespace Microsoft.CodeAnalysis.EditAndContinue; + +internal sealed class ManagedHotReloadServiceProxy : BrokeredServiceProxy, IManagedHotReloadService +{ + public ManagedHotReloadServiceProxy(IServiceBroker serviceBroker) + : base(serviceBroker, BrokeredServiceDescriptors.DebuggerManagedHotReloadService) + { + } + + public ValueTask> GetActiveStatementsAsync(CancellationToken cancellationToken) + => InvokeAsync((service, cancellationToken) => service.GetActiveStatementsAsync(cancellationToken), cancellationToken); + + public ValueTask GetAvailabilityAsync(Guid module, CancellationToken cancellationToken) + => InvokeAsync((service, module, cancellationToken) => service.GetAvailabilityAsync(module, cancellationToken), module, cancellationToken); + + public ValueTask> GetCapabilitiesAsync(CancellationToken cancellationToken) + => InvokeAsync((service, cancellationToken) => service.GetCapabilitiesAsync(cancellationToken), cancellationToken); + + public ValueTask PrepareModuleForUpdateAsync(Guid module, CancellationToken cancellationToken) + => InvokeAsync((service, module, cancellationToken) => service.PrepareModuleForUpdateAsync(module, cancellationToken), module, cancellationToken); +} diff --git a/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj b/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj index a792c6aee9825..fce054f51f6eb 100644 --- a/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj +++ b/src/Workspaces/Remote/Core/Microsoft.CodeAnalysis.Remote.Workspaces.csproj @@ -45,8 +45,8 @@ - - + + diff --git a/src/Workspaces/Remote/Core/Serialization/MessagePackFormatters.cs b/src/Workspaces/Remote/Core/Serialization/MessagePackFormatters.cs index 2c8b9f80fe6bc..58076b05fcaf5 100644 --- a/src/Workspaces/Remote/Core/Serialization/MessagePackFormatters.cs +++ b/src/Workspaces/Remote/Core/Serialization/MessagePackFormatters.cs @@ -146,12 +146,12 @@ internal sealed class EncodingFormatter : IMessagePackFormatter } var name = reader.ReadString(); - if (name != null) + if (name is null) { - return Encoding.GetEncoding(name); + return null; } - return null; + return Encoding.GetEncoding(name); } catch (Exception e) when (e is not MessagePackSerializationException) { diff --git a/src/Workspaces/Remote/Core/ServiceBrokerExtensions.cs b/src/Workspaces/Remote/Core/ServiceBrokerExtensions.cs new file mode 100644 index 0000000000000..c15e8c154203b --- /dev/null +++ b/src/Workspaces/Remote/Core/ServiceBrokerExtensions.cs @@ -0,0 +1,63 @@ +// 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.ServiceHub.Framework; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.BrokeredServices; + +internal abstract class BrokeredServiceProxy where TService : class +{ + private readonly IServiceBroker _serviceBroker; + private readonly ServiceRpcDescriptor _descriptor; + + public BrokeredServiceProxy(IServiceBroker serviceBroker, ServiceRpcDescriptor descriptor) + { + _serviceBroker = serviceBroker; + _descriptor = descriptor; + } + + protected async ValueTask InvokeAsync(Func operation, CancellationToken cancellationToken) + { + var service = await _serviceBroker.GetProxyAsync(_descriptor, cancellationToken).ConfigureAwait(false); + using ((IDisposable?)service) + { + Contract.ThrowIfNull(service); + await operation(service, cancellationToken).ConfigureAwait(false); + } + } + + protected async ValueTask InvokeAsync(Func> operation, CancellationToken cancellationToken) + { + var service = await _serviceBroker.GetProxyAsync(_descriptor, cancellationToken).ConfigureAwait(false); + using ((IDisposable?)service) + { + Contract.ThrowIfNull(service); + return await operation(service, cancellationToken).ConfigureAwait(false); + } + } + + protected async ValueTask InvokeAsync(Func> operation, TArgs args, CancellationToken cancellationToken) + { + var service = await _serviceBroker.GetProxyAsync(_descriptor, cancellationToken).ConfigureAwait(false); + using ((IDisposable?)service) + { + Contract.ThrowIfNull(service); + return await operation(service, args, cancellationToken).ConfigureAwait(false); + } + } + + protected async ValueTask InvokeAsync(Func operation, TArgs args, CancellationToken cancellationToken) + { + var service = await _serviceBroker.GetProxyAsync(_descriptor, cancellationToken).ConfigureAwait(false); + using ((IDisposable?)service) + { + Contract.ThrowIfNull(service); + await operation(service, args, cancellationToken).ConfigureAwait(false); + } + } +}