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.126.0.0-rtm.21518.12
- 17.6.26-preview
+ 17.7.4-preview17.7.8-preview-g8c33dc3a7617.6.3582916.10.0
@@ -78,6 +78,7 @@
$(RefOnlyMicrosoftBuildPackagesVersion)6.0.0-preview.0.1517.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 @@
falsefalsetrue
+
+
+ $(ArtifactsDir)\packages\$(Configuration)\NPM\
+
+ false
+ true
+
truetrue
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