diff --git a/.github/policies/resourceManagement.yml b/.github/policies/resourceManagement.yml index 7039575ec2659..5738ee47aae0c 100644 --- a/.github/policies/resourceManagement.yml +++ b/.github/policies/resourceManagement.yml @@ -277,34 +277,6 @@ configuration: then: - closeIssue - - description: Add "untriaged" label on new issues - triggerOnOwnActions: false - if: - - payloadType: Issues - - not: - hasLabel: - label: untriaged - - and: - - isAction: - action: Opened - then: - - addLabel: - label: untriaged - - - description: Remove "untriaged" label from issues when closed or added to a milestone - triggerOnOwnActions: false - if: - - payloadType: Issues - - or: - - isAction: - action: Closed - - isPartOfAnyMilestone - - hasLabel: - label: untriaged - then: - - removeLabel: - label: untriaged - - description: Add breaking change doc instructions to issue if: - payloadType: Issues diff --git a/NuGet.config b/NuGet.config index 0472eeb256e12..703703fb400a9 100644 --- a/NuGet.config +++ b/NuGet.config @@ -9,8 +9,6 @@ - - diff --git a/Roslyn.sln b/Roslyn.sln index 3e4776aca3d40..09b0cc37519be 100644 --- a/Roslyn.sln +++ b/Roslyn.sln @@ -727,6 +727,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Build.Tasks.CodeA EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.BuildClient.Package", "src\NuGet\Microsoft.CodeAnalysis.BuildClient.Package\Microsoft.CodeAnalysis.BuildClient.Package.csproj", "{5E4F7448-B00B-4F5B-859F-6ED0354253D5}" EndProject +Project("{9a19103f-16f7-4668-be54-9a1e7a4f7556}") = "Microsoft.CodeAnalysis.SemanticSearch.Extensions", "src\Tools\SemanticSearch\Extensions\Microsoft.CodeAnalysis.SemanticSearch.Extensions.csproj", "{66C8265C-2C79-F259-9807-3E97CA586F1E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1797,6 +1799,10 @@ Global {5E4F7448-B00B-4F5B-859F-6ED0354253D5}.Debug|Any CPU.Build.0 = Debug|Any CPU {5E4F7448-B00B-4F5B-859F-6ED0354253D5}.Release|Any CPU.ActiveCfg = Release|Any CPU {5E4F7448-B00B-4F5B-859F-6ED0354253D5}.Release|Any CPU.Build.0 = Release|Any CPU + {66C8265C-2C79-F259-9807-3E97CA586F1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66C8265C-2C79-F259-9807-3E97CA586F1E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66C8265C-2C79-F259-9807-3E97CA586F1E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {66C8265C-2C79-F259-9807-3E97CA586F1E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2137,6 +2143,7 @@ Global {91F9EAA4-ACA2-87EE-868E-6CC3B73D6A11} = {A41D1B99-F489-4C43-BBDF-96D61B19A6B9} {5399BBCC-417F-C710-46DE-EB0C0074C34D} = {A41D1B99-F489-4C43-BBDF-96D61B19A6B9} {5E4F7448-B00B-4F5B-859F-6ED0354253D5} = {C52D8057-43AF-40E6-A01B-6CDBB7301985} + {66C8265C-2C79-F259-9807-3E97CA586F1E} = {52ABB0E4-C3A1-4897-B51B-18EDA83F5D20} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {604E6B91-7BC0-4126-AE07-D4D2FEFC3D29} diff --git a/azure-pipelines-pr-validation.yml b/azure-pipelines-pr-validation.yml index 0afa702c1f38e..7e1eb38f8c8fd 100644 --- a/azure-pipelines-pr-validation.yml +++ b/azure-pipelines-pr-validation.yml @@ -153,12 +153,14 @@ extends: displayName: Disable Real-time Monitoring - task: Powershell@2 + name: SetOriginalBuildNumber displayName: Setting OriginalBuildNumber variable condition: succeeded() inputs: targetType: inline script: | $originalBuildNumber = "$(Build.BuildNumber)".Split(' - ')[0] + Write-Host "##vso[task.setvariable variable=OutputOriginalBuildNumber;isoutput=true;isreadonly=true]$originalBuildNumber" Write-Host "##vso[task.setvariable variable=OriginalBuildNumber;isreadonly=true]$originalBuildNumber" - powershell: Write-Host "##vso[task.setvariable variable=SourceBranchName;isreadonly=true]$('$(Build.SourceBranch)'.Substring('refs/heads/'.Length))" @@ -326,12 +328,20 @@ extends: - job: insert variables: FancyBuildNumber: $[stageDependencies.build.PRValidationBuild.outputs['FancyBuild.BuildNumber']] + OriginalBuildNumber: $[stageDependencies.build.PRValidationBuild.outputs['SetOriginalBuildNumber.OutputOriginalBuildNumber']] displayName: Insert to VS pool: name: VSEngSS-MicroBuild2022-1ES steps: - download: current artifact: VSSetup + + # RIT looks at the build number and cannot handle the fancy build version. + # While in normal scenarios this is already set to the original number, we reset it again here in case this is a re-run. + - powershell: Write-Host "##vso[build.updatebuildnumber]$(OriginalBuildNumber)" + displayName: Reset BuildNumber + condition: succeeded() + - powershell: | $branchName = "$(Build.SourceBranch)".Substring("refs/heads/".Length) Write-Host "##vso[task.setvariable variable=ComponentBranchName]$branchName" diff --git a/docs/features/incremental-generators.md b/docs/features/incremental-generators.md index 48dc1a9a3eac1..d25cdb969a2fb 100644 --- a/docs/features/incremental-generators.md +++ b/docs/features/incremental-generators.md @@ -1022,11 +1022,11 @@ The generator would run select1 on the first and second files, producing select for the third file, as the input has not changed. It can just use the previously cached value. -AdditionalText | Select1 | Select2 ------------------------------|----------------|----------- -**Text{ Path: "diff.txt" }** | **"diff.txt"** | -**Text{ Path: "def.txt" }** | **"def.txt"** | -Text{ Path: "ghi.txt" } | "ghi.txt" | +AdditionalText | Select1 | Select2 +-----------------------------|----------------------|----------- +**Text{ Path: "diff.txt" }** | **"diff.txt"** (new) | +**Text{ Path: "def.txt" }** | **"def.txt"** (new) | +Text{ Path: "ghi.txt" } | "ghi.txt" (reuse) | Next the driver would look to run Select2. It would operate on `"diff.txt"` producing `"prefix_diff.txt"`, but when it comes to `"def.txt"` it can observe @@ -1038,9 +1038,9 @@ it can just use the cached value from before. Similarly the cached state of AdditionalText | Select1 | Select2 -----------------------------|----------------|---------------------- -**Text{ Path: "diff.txt" }** | **"diff.txt"** | **"prefix_diff.txt"** -**Text{ Path: "def.txt" }** | **"def.txt"** | "prefix_def.txt" -Text{ Path: "ghi.txt" } | "ghi.txt" | "prefix_ghi.txt" +**Text{ Path: "diff.txt" }** | **"diff.txt"** | **"prefix_diff.txt"** (new) +**Text{ Path: "def.txt" }** | **"def.txt"** | "prefix_def.txt" (reuse) +Text{ Path: "ghi.txt" } | "ghi.txt" | "prefix_ghi.txt" (reuse) In this way, only changes that are consequential flow through the pipeline, and duplicate work is avoided. If a generator only relies on `AdditionalTexts` then diff --git a/eng/Directory.Packages.props b/eng/Directory.Packages.props index 8b0c5a473de71..e8dece7fb2225 100644 --- a/eng/Directory.Packages.props +++ b/eng/Directory.Packages.props @@ -2,7 +2,6 @@ 3.11.0-beta1.24081.1 - 8.0.0-preview.23468.1 1.1.3-beta1.24319.1 0.1.796-beta <_BasicReferenceAssembliesVersion>1.8.3 @@ -53,13 +52,13 @@ - + - + @@ -78,6 +77,7 @@ + @@ -117,7 +117,7 @@ - + @@ -252,7 +252,7 @@ - + diff --git a/eng/Signing.props b/eng/Signing.props index f1d94e766f317..e7a63462562b6 100644 --- a/eng/Signing.props +++ b/eng/Signing.props @@ -61,4 +61,13 @@ + + + + + + diff --git a/eng/Version.Details.props b/eng/Version.Details.props index aa1fc141196d4..5b5244a709636 100644 --- a/eng/Version.Details.props +++ b/eng/Version.Details.props @@ -1 +1,97 @@ - + + + + + + 3.11.0 + 4.10.0-1.24061.4 + + 2.0.0-rc.1.25406.102 + + 9.0.0 + 9.0.0 + 9.0.0 + 8.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 8.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + 9.0.0 + + 10.0.0-beta.25367.5 + 10.0.0-beta.25367.5 + 10.0.0-beta.25367.5 + + 2.0.0 + + 3.11.0 + 3.3.0 + 10.0.0-preview.25375.1 + + + + + $(MicrosoftCodeAnalysisPackageVersion) + $(MicrosoftNetCompilersToolsetPackageVersion) + + $(SystemCommandLinePackageVersion) + + $(MicrosoftBclAsyncInterfacesPackageVersion) + $(MicrosoftExtensionsConfigurationPackageVersion) + $(MicrosoftExtensionsDependencyInjectionPackageVersion) + $(MicrosoftExtensionsFileSystemGlobbingPackageVersion) + $(MicrosoftExtensionsLoggingPackageVersion) + $(MicrosoftExtensionsLoggingAbstractionsPackageVersion) + $(MicrosoftExtensionsLoggingConsolePackageVersion) + $(MicrosoftExtensionsOptionsPackageVersion) + $(MicrosoftExtensionsOptionsConfigurationExtensionPackageVersion) + $(MicrosoftExtensionsPrimitivesPackageVersion) + $(SystemCollectionsImmutablePackageVersion) + $(SystemComponentModelCompositionPackageVersion) + $(SystemCompositionPackageVersion) + $(SystemConfigurationConfigurationManagerPackageVersion) + $(SystemDiagnosticsDiagnosticSourcePackageVersion) + $(SystemDiagnosticsEventLogPackageVersion) + $(SystemIOHashingPackageVersion) + $(SystemIOPipelinesPackageVersion) + $(SystemReflectionMetadataPackageVersion) + $(SystemResourcesExtensionsPackageVersion) + $(SystemSecurityCryptographyProtectedDataPackageVersion) + $(SystemSecurityPermissionsPackageVersion) + $(SystemTextEncodingsWebPackageVersion) + $(SystemTextJsonPackageVersion) + $(SystemThreadingTasksDataflowPackageVersion) + $(SystemWindowsExtensionsPackageVersion) + + $(MicrosoftDotNetArcadeSdkPackageVersion) + $(MicrosoftDotNetHelixSdkPackageVersion) + $(MicrosoftDotNetXliffTasksPackageVersion) + + $(MicrosoftDiaSymReaderPackageVersion) + + $(MicrosoftCodeAnalysisAnalyzersPackageVersion) + $(MicrosoftCodeAnalysisAnalyzerUtilitiesPackageVersion) + $(MicrosoftCodeAnalysisNetAnalyzersPackageVersion) + + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 68107c2ef0d67..bef618c5ab9ca 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,15 +1,15 @@ - + https://github.com/dotnet/roslyn ae1fff344d46976624e68ae17164e0607ab68b10 - + https://github.com/dotnet/dotnet - c0e325f90fb79db0da6be5128dc292f2aabb264f + 30bc8f92be07c2c8c3a6addb946877260e042f63 @@ -24,6 +24,10 @@ https://github.com/dotnet/runtime 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3 + + https://github.com/dotnet/runtime + 5535e31a712343a63f5d7d796cd874e563e5ac14 + https://github.com/dotnet/runtime 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3 @@ -76,6 +80,7 @@ https://github.com/dotnet/runtime 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3 + https://github.com/dotnet/runtime 9d5a6a9aa463d6d10b0b0ba6d5982cc82f363dc3 @@ -130,9 +135,9 @@ https://github.com/dotnet/arcade d777c20040bdc2e52b372fa98dcb84141ed692d3 - + https://github.com/dotnet/roslyn-analyzers - 2c9a20b6706b8a9ad650b41bff30980cf5af67ed + dd67b33c8b4164fa2c2c1dd13b616aea3948f7da diff --git a/eng/Versions.props b/eng/Versions.props index 540580b3c22d7..7cd36718f8df0 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -1,6 +1,7 @@ + @@ -44,53 +45,17 @@ 4.6.0 4.5.0 - - 2.0.0-beta7.25373.104 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - - 9.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 5.0.0-1.25277.114 - 8.0.0-preview.23468.1 - 2.0.0 - 9.0.0 - 9.0.0 - 8.0.0 - 9.0.0 - 9.0.0 - 9.0.0 - 8.0.0 - 9.0.0 - 9.0.0 17.14.1043-preview2 3.1.7 - 4.61.3 1.0.865 6.34.0 - 13.0.3 - 6.34.0 4.61.3 + 13.0.3 6.34.0 - + @@ -24,7 +24,10 @@ + + + diff --git a/src/Features/ExternalAccess/Copilot/SemanticSearch/CopilotSemanticSearchUtilities.cs b/src/Features/ExternalAccess/Copilot/SemanticSearch/CopilotSemanticSearchUtilities.cs new file mode 100644 index 0000000000000..e9de02e2f8ad9 --- /dev/null +++ b/src/Features/ExternalAccess/Copilot/SemanticSearch/CopilotSemanticSearchUtilities.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.FindSymbols; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Shared.Utilities; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Copilot.SemanticSearch; + +internal static class CopilotSemanticSearchUtilities +{ + private static readonly FindReferencesSearchOptions s_options = new() + { + AssociatePropertyReferencesWithSpecificAccessor = false, + Cascade = false, + DisplayAllDefinitions = false, + Explicit = true, + UnidirectionalHierarchyCascade = false + }; + + public static SyntaxTree CreateSyntaxTree(SolutionServices services, string language, string? filePath, ParseOptions options, SourceText? text, Encoding? encoding, SourceHashAlgorithm checksumAlgorithm, SyntaxNode root) + => services.GetRequiredLanguageService(language).CreateSyntaxTree(filePath, options, text, encoding, checksumAlgorithm, root); + + public static SyntaxTree ParseSyntaxTree(SolutionServices services, string language, string? filePath, ParseOptions options, SourceText text, CancellationToken cancellationToken) + => services.GetRequiredLanguageService(language).ParseSyntaxTree(filePath, options, text, cancellationToken); + + public static PortableExecutableReference GetMetadataReference(SolutionServices services, string resolvedPath, MetadataReferenceProperties properties) + => services.GetRequiredService().GetReference(resolvedPath, properties); + + public static ImmutableArray ToTaggedText(this IEnumerable? displayParts) + => TaggedTextExtensions.ToTaggedText(displayParts); + + public static SyntaxNode FindNode(this Location location, bool findInsideTrivia, bool getInnermostNodeForTie, CancellationToken cancellationToken) + => location.SourceTree!.GetRoot(cancellationToken).FindNode(location.SourceSpan, findInsideTrivia, getInnermostNodeForTie); + + public static Task FindReferencesAsync(Solution solution, ISymbol symbol, Action callback, CancellationToken cancellationToken) + => SymbolFinder.FindReferencesAsync( + symbol, solution, new Progress(callback), documents: null, s_options, cancellationToken); + + private sealed class Progress(Action callback) : IStreamingFindReferencesProgress + { + public ValueTask OnStartedAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask; + public ValueTask OnCompletedAsync(CancellationToken cancellationToken) => ValueTask.CompletedTask; + public ValueTask OnDefinitionFoundAsync(SymbolGroup group, CancellationToken cancellationToken) => ValueTask.CompletedTask; + + public ValueTask OnReferencesFoundAsync(ImmutableArray<(SymbolGroup group, ISymbol symbol, ReferenceLocation location)> references, CancellationToken cancellationToken) + { + foreach (var (_, _, location) in references) + callback(location); + + return ValueTask.CompletedTask; + } + + public IStreamingProgressTracker ProgressTracker + => NoOpStreamingFindReferencesProgress.Instance.ProgressTracker; + } +} diff --git a/src/Features/ExternalAccess/Copilot/SemanticSearch/ICopilotSemanticSearchQueryService.cs b/src/Features/ExternalAccess/Copilot/SemanticSearch/ICopilotSemanticSearchQueryService.cs new file mode 100644 index 0000000000000..65bc4971b2a51 --- /dev/null +++ b/src/Features/ExternalAccess/Copilot/SemanticSearch/ICopilotSemanticSearchQueryService.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; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Copilot.SemanticSearch; + +internal interface ICopilotSemanticSearchQueryService +{ + CompileQueryResult CompileQuery( + SolutionServices services, + string query, + string? targetLanguage, + string referenceAssembliesDir, + TraceSource traceSource, + CancellationToken cancellationToken); + + Task ExecuteQueryAsync( + Solution solution, + CompiledQueryId queryId, + ICopilotSemanticSearchResultsObserver observer, + QueryExecutionOptions options, + TraceSource traceSource, + CancellationToken cancellationToken); + + void DiscardQuery(CompiledQueryId queryId); + + internal readonly record struct QueryExecutionOptions( + int? ResultCountLimit, + bool KeepCompiledQuery); + + internal readonly record struct ExecuteQueryResult( + string? ErrorMessage, + string[]? ErrorMessageArgs = null, + TimeSpan ExecutionTime = default); + + internal readonly record struct CompileQueryResult( + CompiledQueryId QueryId, + ImmutableArray CompilationErrors, + TimeSpan EmitTime = default); + + internal readonly record struct QueryCompilationError( + string Id, + string Message, + TextSpan Span); + + internal readonly record struct CompiledQueryId( + int Id); +} diff --git a/src/Features/ExternalAccess/Copilot/SemanticSearch/ICopilotSemanticSearchResultsObserver.cs b/src/Features/ExternalAccess/Copilot/SemanticSearch/ICopilotSemanticSearchResultsObserver.cs new file mode 100644 index 0000000000000..b06bfcbc5665f --- /dev/null +++ b/src/Features/ExternalAccess/Copilot/SemanticSearch/ICopilotSemanticSearchResultsObserver.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.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Copilot.SemanticSearch; + +internal interface ICopilotSemanticSearchResultsObserver +{ + ValueTask OnUserCodeExceptionAsync(UserCodeExceptionInfo exception, CancellationToken cancellationToken); + ValueTask OnSymbolFoundAsync(Solution solution, ISymbol symbol, CancellationToken cancellationToken); + ValueTask AddItemsAsync(int itemCount, CancellationToken cancellationToken); + ValueTask ItemsCompletedAsync(int itemCount, CancellationToken cancellationToken); + + internal readonly record struct UserCodeExceptionInfo( + string ProjectDisplayName, + string Message, + ImmutableArray TypeName, + ImmutableArray StackTrace, + TextSpan Span); +} diff --git a/src/Features/ExternalAccess/Copilot/SemanticSearch/ICopilotSemanticSearchSolutionService.cs b/src/Features/ExternalAccess/Copilot/SemanticSearch/ICopilotSemanticSearchSolutionService.cs new file mode 100644 index 0000000000000..4ccac508783ac --- /dev/null +++ b/src/Features/ExternalAccess/Copilot/SemanticSearch/ICopilotSemanticSearchSolutionService.cs @@ -0,0 +1,15 @@ +// 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.ExternalAccess.Copilot.SemanticSearch; + +internal interface ICopilotSemanticSearchSolutionService +{ + DocumentId GetQueryDocumentId(Solution solution); + string GetQueryDocumentFilePath(); + + (WorkspaceChangeKind changeKind, ProjectId? projectId, DocumentId? documentId) GetWorkspaceChangeKind(Solution oldSolution, Solution newSolution); + + Solution SetQueryText(Solution solution, string? query, string referenceAssembliesDir); +} diff --git a/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs b/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs index c3b7810df4307..b3132866960e5 100644 --- a/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs +++ b/src/Features/Test/EditAndContinue/EditAndContinueWorkspaceServiceTests.cs @@ -152,12 +152,12 @@ public async Task StartDebuggingSession_CapturingDocuments(bool captureAllDocume EnterBreakState(debuggingSession); - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); - Assert.Empty(updates.Updates); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); + Assert.Empty(results.ModuleUpdates.Updates); // TODO: warning reported https://github.com/dotnet/roslyn/issues/78125 - // AssertEx.Equal([$"P.csproj: (0,0)-(0,0): Warning ENC1005: {string.Format(FeaturesResources.DocumentIsOutOfSyncWithDebuggee, sourceFileB.Path)}"], InspectDiagnostics(emitDiagnostics)); + // AssertEx.Equal([$"P.csproj: (0,0)-(0,0): Warning ENC1005: {string.Format(FeaturesResources.DocumentIsOutOfSyncWithDebuggee, sourceFileB.Path)}"], InspectDiagnostics(results.Diagnostics)); EndDebuggingSession(debuggingSession); } @@ -182,13 +182,13 @@ public async Task ProjectNotBuilt() Assert.Empty(diagnostics); // changes in the project are ignored: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); - Assert.Empty(updates.Updates); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); + Assert.Empty(results.ModuleUpdates.Updates); AssertEx.Equal( [ - $"{document1.Project.FilePath}: (0,0)-(0,0): Warning ENC1008: {string.Format(FeaturesResources.Changing_source_file_0_in_a_stale_project_has_no_effect_until_the_project_is_rebuit, document1.FilePath)}" - ], InspectDiagnostics(emitDiagnostics)); + $"proj: {document1.FilePath}: (0,0)-(0,0): Warning ENC1008: {string.Format(FeaturesResources.Changing_source_file_0_in_a_stale_project_has_no_effect_until_the_project_is_rebuit, document1.FilePath)}" + ], InspectDiagnostics(results.Diagnostics)); EndDebuggingSession(debuggingSession); } @@ -219,8 +219,8 @@ public async Task DifferentDocumentWithSameContent() Assert.Empty(diagnostics2); // validate solution update status and emit - changes made during run mode are ignored: - var (updates, _) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); EndDebuggingSession(debuggingSession); @@ -253,10 +253,10 @@ public async Task ProjectThatDoesNotSupportEnC_Language(bool breakMode) solution = solution.WithDocumentText(document1.Id, CreateText("dummy2")); // validate solution update status and emit: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); - Assert.Empty(updates.Updates); - Assert.Empty(emitDiagnostics); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); + Assert.Empty(results.ModuleUpdates.Updates); + Assert.Empty(results.Diagnostics); var document2 = solution.GetDocument(document1.Id); diagnostics = await service.GetDocumentDiagnosticsAsync(document2, s_noActiveSpans, CancellationToken.None); @@ -296,7 +296,7 @@ public void F() {} } """)); - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: true); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); @@ -335,10 +335,10 @@ class C { int Y => 2; } var diagnostics1 = await service.GetDocumentDiagnosticsAsync(generatedDocument, s_noActiveSpans, CancellationToken.None); Assert.Empty(diagnostics1); - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); - Assert.Empty(updates.Updates); - Assert.Empty(emitDiagnostics); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); + Assert.Empty(results.ModuleUpdates.Updates); + Assert.Empty(results.Diagnostics); EndDebuggingSession(debuggingSession); } @@ -415,7 +415,7 @@ End Class var diagnostics = await service.GetDocumentDiagnosticsAsync(solution.GetRequiredDocument(documentId), s_noActiveSpans, CancellationToken.None); AssertEx.Empty(diagnostics); - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: true); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); @@ -500,7 +500,7 @@ End Class var diagnostics = await service.GetDocumentDiagnosticsAsync(solution.GetRequiredDocument(documentId), s_noActiveSpans, CancellationToken.None); AssertEx.Empty(diagnostics); - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: true); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(isWarning ? ModuleUpdateStatus.None : ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); @@ -576,7 +576,7 @@ End Class var diagnostics = await service.GetDocumentDiagnosticsAsync(solution.GetRequiredDocument(documentId), s_noActiveSpans, CancellationToken.None); AssertEx.Empty(diagnostics); - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: true); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); @@ -629,7 +629,7 @@ class A return null; }; - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: true); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); Assert.Empty(results.Diagnostics); @@ -680,7 +680,7 @@ internal async Task Project_MetadataReferences() var diagnostics = await service.GetDocumentDiagnosticsAsync(solution.GetRequiredDocument(documentId), s_noActiveSpans, CancellationToken.None); AssertEx.Empty(diagnostics); - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: true); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); @@ -722,7 +722,7 @@ internal async Task Project_MetadataReferences_RemoveAdd() // remove dependency: solution = project.RemoveMetadataReference(libV1).Solution; - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: true); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); Assert.Empty(results.Diagnostics); @@ -730,7 +730,7 @@ internal async Task Project_MetadataReferences_RemoveAdd() // add newer version: solution = project.AddMetadataReference(libV2).Solution; - results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: true); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); @@ -770,7 +770,7 @@ internal async Task Project_MetadataReferences_Add() // add dependency: solution = project.AddMetadataReference(libV1).Solution; - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: true); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); Assert.Empty(results.Diagnostics); @@ -805,7 +805,7 @@ internal async Task Project_MetadataReferences_MultipleVersions() // add version 3: solution = project.AddMetadataReference(libV3).Solution; - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: true); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); @@ -847,8 +847,8 @@ public async Task DesignTimeOnlyDocument() Assert.Empty(diagnostics); // validate solution update status and emit - changes made in design-time-only documents are ignored: - var (updates, _) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); EndDebuggingSession(debuggingSession); @@ -884,15 +884,15 @@ public async Task DesignTimeOnlyDocument_Dynamic() solution = solution.WithDocumentText(document1.Id, CreateText("class E {}")); // validate solution update status and emit: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); - Assert.Empty(updates.Updates); - Assert.Empty(emitDiagnostics); - - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); - Assert.Empty(updates.Updates); - Assert.Empty(emitDiagnostics); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); + Assert.Empty(results.ModuleUpdates.Updates); + Assert.Empty(results.Diagnostics); + + results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); + Assert.Empty(results.ModuleUpdates.Updates); + Assert.Empty(results.Diagnostics); } [Theory, CombinatorialData] @@ -978,18 +978,18 @@ public async Task DesignTimeOnlyDocument_Wpf([CombinatorialValues(LanguageNames. Assert.Empty(await service.GetDocumentDiagnosticsAsync(designTimeOnlyDocument2, s_noActiveSpans, CancellationToken.None)); // validate solution update status and emit: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); - Assert.Empty(emitDiagnostics); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); + Assert.Empty(results.Diagnostics); if (delayLoad) { LoadLibraryToDebuggee(moduleId); // validate solution update status and emit: - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); - Assert.Empty(emitDiagnostics); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); + Assert.Empty(results.Diagnostics); } EndDebuggingSession(debuggingSession); @@ -1036,19 +1036,21 @@ public async Task ErrorReadingModuleFile(bool breakMode) var docDiagnostics = await service.GetDocumentDiagnosticsAsync(document2, s_noActiveSpans, CancellationToken.None); Assert.Empty(docDiagnostics); - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); AssertEx.Equal( [$"proj: : Error ENC1001: {string.Format(FeaturesResources.ErrorReadingFile, moduleFile.Path, expectedErrorMessage)}"], InspectDiagnostics(results.Diagnostics)); + debuggingSession.DiscardSolutionUpdate(); + // correct the error: EmitLibrary(projectId, source2); - var (updates2, diagnostics2) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.Ready, updates2.Status); - Assert.Empty(diagnostics2); + var results2 = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.Ready, results2.ModuleUpdates.Status); + Assert.Empty(results2.Diagnostics); CommitSolutionUpdate(debuggingSession); @@ -1117,13 +1119,14 @@ public async Task ErrorReadingPdbFile() Assert.Empty(docDiagnostics); // an error occurred so we need to call update to determine whether we have changes to apply or not: - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); AssertEx.Equal( [$"proj: {document2.FilePath}: (0,0)-(0,0): Error ENC1006: {string.Format(FeaturesResources.UnableToReadSourceFileOrPdb, sourceFile.Path)}"], InspectDiagnostics(results.Diagnostics)); + debuggingSession.DiscardSolutionUpdate(); EndDebuggingSession(debuggingSession); AssertEx.Equal( @@ -1165,17 +1168,18 @@ public async Task ErrorReadingSourceFile() Assert.Empty(docDiagnostics); // an error occurred so we need to call update to determine whether we have changes to apply or not: - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); AssertEx.Equal( [$"test: {document1.FilePath}: (0,0)-(0,0): Error ENC1006: {string.Format(FeaturesResources.UnableToReadSourceFileOrPdb, sourceFile.Path)}"], InspectDiagnostics(results.Diagnostics)); + debuggingSession.DiscardSolutionUpdate(); fileLock.Dispose(); // try apply changes again: - results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.NotEmpty(results.ModuleUpdates.Updates); Assert.Empty(results.Diagnostics); @@ -1227,8 +1231,8 @@ public async Task Document_Add(bool breakMode) var diagnostics2 = await service.GetDocumentDiagnosticsAsync(documentB, s_noActiveSpans, CancellationToken.None); Assert.Empty(diagnostics2); - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); debuggingSession.DiscardSolutionUpdate(); if (breakMode) @@ -1279,9 +1283,9 @@ public async Task Document_Delete() // delete B: solution = solution.RemoveDocument(documentBId); - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); - Assert.NotEmpty(updates.Updates); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); + Assert.NotEmpty(results.ModuleUpdates.Updates); debuggingSession.DiscardSolutionUpdate(); @@ -1316,9 +1320,9 @@ public async Task Document_RenameAndUpdate() var diagnostics = await service.GetDocumentDiagnosticsAsync(solution.GetRequiredDocument(document2Id), s_noActiveSpans, CancellationToken.None); AssertEx.Empty(diagnostics); - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); - Assert.NotEmpty(updates.Updates); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); + Assert.NotEmpty(results.ModuleUpdates.Updates); debuggingSession.DiscardSolutionUpdate(); @@ -1381,10 +1385,10 @@ public async Task ModuleDisallowsEditAndContinue_NoChanges(bool breakMode) // workspace is updated to new version after build completed and the session started: solution = solution.WithDocumentText(document0.Id, CreateText(source1)); - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); - Assert.Empty(updates.Updates); - Assert.Empty(emitDiagnostics); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); + Assert.Empty(results.ModuleUpdates.Updates); + Assert.Empty(results.Diagnostics); if (breakMode) { @@ -1419,9 +1423,9 @@ public async Task ModuleDisallowsEditAndContinue_SourceGenerator_NoChanges() var document1 = solution.Projects.Single().Documents.Single(); solution = solution.WithDocumentText(document1.Id, CreateText(source2)); - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); - Assert.Empty(updates.Updates); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); + Assert.Empty(results.ModuleUpdates.Updates); EndDebuggingSession(debuggingSession); } @@ -1476,13 +1480,14 @@ void M() AssertEx.Empty(docDiagnostics); // validate solution update status and emit: - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); AssertEx.Equal( [$"proj: {document2.FilePath}: (5,0)-(5,32): Error ENC2016: {string.Format(FeaturesResources.EditAndContinueDisallowedByProject, document2.Project.Name, "*message*")}"], InspectDiagnostics(results.Diagnostics)); + debuggingSession.DiscardSolutionUpdate(); EndDebuggingSession(debuggingSession); AssertEx.SetEqual([moduleId], debuggingSession.GetTestAccessor().GetModulesPreparedForUpdate()); @@ -1575,11 +1580,13 @@ public async Task RudeEdits(bool breakMode) InspectDiagnostics(docDiagnostics)); // validate solution update status and emit: - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); AssertEx.SequenceEqual(["ENC0110"], InspectDiagnosticIds(results.Diagnostics)); + debuggingSession.DiscardSolutionUpdate(); + if (breakMode) { ExitBreakState(debuggingSession); @@ -1663,10 +1670,10 @@ public async Task DeferredApplyChangeWithActiveStatementRudeEdits() ExitBreakState(debuggingSession); // apply the change: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); - Assert.NotEmpty(updates.Updates); - Assert.Empty(emitDiagnostics); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); + Assert.NotEmpty(results.ModuleUpdates.Updates); + Assert.Empty(results.Diagnostics); CommitSolutionUpdate(debuggingSession); @@ -1717,15 +1724,17 @@ class C { int Y => 2; } [$"{generatedDocument.FilePath}: (0,17)-(0,18): Error ENC0110: {string.Format(FeaturesResources.Changing_the_signature_of_0_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime, FeaturesResources.method)}"], InspectDiagnostics(docDiagnostics)); - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); AssertEx.Equal(["ENC0110"], InspectDiagnosticIds(results.Diagnostics)); + debuggingSession.DiscardSolutionUpdate(); EndDebuggingSession(debuggingSession); } - [Theory, CombinatorialData] + [Theory(Skip = "https://github.com/dotnet/roslyn/issues/79589")] + [CombinatorialData] public async Task RudeEdits_DocumentOutOfSync(bool breakMode) { var source0 = "class C1 { void M() { System.Console.WriteLine(0); } }"; @@ -1766,68 +1775,53 @@ public async Task RudeEdits_DocumentOutOfSync(bool breakMode) Assert.Empty(docDiagnostics); // the document is out-of-sync, so no rude edits reported: - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); + // project is stale: + Assert.True(debuggingSession.LastCommittedSolution.StaleProjects.ContainsKey(projectId)); + // TODO: warning reported https://github.com/dotnet/roslyn/issues/78125 - // AssertEx.Equal([$"proj.csproj: (0,0)-(0,0): Warning ENC1005: {string.Format(FeaturesResources.DocumentIsOutOfSyncWithDebuggee, sourceFile.Path)}"], InspectDiagnostics(emitDiagnostics)); + // AssertEx.Equal([$"proj.csproj: (0,0)-(0,0): Warning ENC1005: {string.Format(FeaturesResources.DocumentIsOutOfSyncWithDebuggee, sourceFile.Path)}"], InspectDiagnostics(results.Diagnostics)); // We do not reload the content of out-of-sync file for analyzer query. // We don't check if the content on disk has been updated to match either. - // Document state can only be reset via UpdateBaselines. sourceFile.WriteAllText(source0, Encoding.UTF8); docDiagnostics = await service.GetDocumentDiagnosticsAsync(document2, s_noActiveSpans, CancellationToken.None); Assert.Empty(docDiagnostics); // rebuild triggers reload of out-of-sync file content: moduleId = EmitAndLoadLibraryToDebuggee(projectId, source0, sourceFilePath: sourceFile.Path); - debuggingSession.UpdateBaselines(solution.WithDocumentText(documentId, CreateText(source0)), [projectId]); - results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); - Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); - Assert.Empty(results.ModuleUpdates.Updates); - AssertEx.SequenceEqual(["ENC0110"], InspectDiagnosticIds(results.Diagnostics)); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); - // now we see the rude edit: - docDiagnostics = await service.GetDocumentDiagnosticsAsync(document2, s_noActiveSpans, CancellationToken.None); - AssertEx.Equal( - [$"{document2.FilePath}: (0,11)-(0,22): Error ENC0110: {string.Format(FeaturesResources.Changing_the_signature_of_0_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime, FeaturesResources.method)}"], - InspectDiagnostics(docDiagnostics)); + // project has been unstaled: + Assert.False(debuggingSession.LastCommittedSolution.StaleProjects.ContainsKey(projectId)); - results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); - Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); - AssertEx.SequenceEqual(["ENC0110"], InspectDiagnosticIds(results.Diagnostics)); + Assert.Empty(results.Diagnostics); if (breakMode) { ExitBreakState(debuggingSession); - EndDebuggingSession(debuggingSession); - } - else - { - EndDebuggingSession(debuggingSession); } - AssertEx.SetEqual([moduleId], debuggingSession.GetTestAccessor().GetModulesPreparedForUpdate()); + EndDebuggingSession(debuggingSession); if (breakMode) { AssertEx.SequenceEqual( [ - "Debugging_EncSession: SolutionSessionId={00000000-AAAA-AAAA-AAAA-000000000000}|SessionId=1|SessionCount=1|EmptySessionCount=1|HotReloadSessionCount=0|EmptyHotReloadSessionCount=2", - "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=True|HadValidChanges=False|HadValidInsignificantChanges=False|RudeEditsCount=1|EmitDeltaErrorIdCount=0|InBreakState=True|Capabilities=31|ProjectIdsWithAppliedChanges=|ProjectIdsWithUpdatedBaselines=", - "Debugging_EncSession_EditSession_RudeEdit: SessionId=1|EditSessionId=2|RudeEditKind=110|RudeEditSyntaxKind=8875|RudeEditBlocking=True|RudeEditProjectId={6A6F7270-0000-4000-8000-000000000000}" + "Debugging_EncSession: SolutionSessionId={00000000-AAAA-AAAA-AAAA-000000000000}|SessionId=1|SessionCount=0|EmptySessionCount=1|HotReloadSessionCount=0|EmptyHotReloadSessionCount=2" ], _telemetryLog); } else { AssertEx.SequenceEqual( [ - "Debugging_EncSession: SolutionSessionId={00000000-AAAA-AAAA-AAAA-000000000000}|SessionId=1|SessionCount=0|EmptySessionCount=0|HotReloadSessionCount=1|EmptyHotReloadSessionCount=1", - "Debugging_EncSession_EditSession: SessionId=1|EditSessionId=2|HadCompilationErrors=False|HadRudeEdits=True|HadValidChanges=False|HadValidInsignificantChanges=False|RudeEditsCount=1|EmitDeltaErrorIdCount=0|InBreakState=False|Capabilities=31|ProjectIdsWithAppliedChanges=|ProjectIdsWithUpdatedBaselines=", - "Debugging_EncSession_EditSession_RudeEdit: SessionId=1|EditSessionId=2|RudeEditKind=110|RudeEditSyntaxKind=8875|RudeEditBlocking=True|RudeEditProjectId={6A6F7270-0000-4000-8000-000000000000}" + "Debugging_EncSession: SolutionSessionId={00000000-AAAA-AAAA-AAAA-000000000000}|SessionId=1|SessionCount=0|EmptySessionCount=0|HotReloadSessionCount=0|EmptyHotReloadSessionCount=1" ], _telemetryLog); } } @@ -1867,11 +1861,12 @@ public async Task RudeEdits_DocumentWithoutSequencePoints() InspectDiagnostics(docDiagnostics)); // validate solution update status and emit: - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); AssertEx.SequenceEqual(["ENC0023"], InspectDiagnosticIds(results.Diagnostics)); + debuggingSession.DiscardSolutionUpdate(); EndDebuggingSession(debuggingSession); } @@ -1909,11 +1904,13 @@ public async Task RudeEdits_DelayLoadedModule() [$"{document2.FilePath}: (0,24)-(0,25): Error ENC0110: {string.Format(FeaturesResources.Changing_the_signature_of_0_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime, FeaturesResources.method)}"], InspectDiagnostics(docDiagnostics)); - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); AssertEx.SequenceEqual(["ENC0110"], InspectDiagnosticIds(results.Diagnostics)); + debuggingSession.DiscardSolutionUpdate(); + // load library to the debuggee: LoadLibraryToDebuggee(moduleId); @@ -1923,17 +1920,18 @@ public async Task RudeEdits_DelayLoadedModule() [$"{document2.FilePath}: (0,24)-(0,25): Error ENC0110: {string.Format(FeaturesResources.Changing_the_signature_of_0_requires_restarting_the_application_because_it_is_not_supported_by_the_runtime, FeaturesResources.method)}"], InspectDiagnostics(docDiagnostics)); - results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); AssertEx.SequenceEqual(["ENC0110"], InspectDiagnosticIds(results.Diagnostics)); + debuggingSession.DiscardSolutionUpdate(); EndDebuggingSession(debuggingSession); } [Theory] [CombinatorialData] - public async Task RudeEdits_UpdateBaseline(bool validChangeBeforeRudeEdit, bool allowPartialUpdates) + public async Task RudeEdits_UpdateBaseline(bool validChangeBeforeRudeEdit) { var source3 = "abstract class C { void F() {} public abstract void G(); }"; var projectDir = Temp.CreateDirectory(); @@ -1956,7 +1954,7 @@ public async Task RudeEdits_UpdateBaseline(bool validChangeBeforeRudeEdit, bool { solution = solution.WithDocumentText(documentId, CreateText("abstract class C { void F() {} }")); - results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdates); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ProjectsToRebuild); Assert.Empty(results.ProjectsToRestart); @@ -1985,29 +1983,21 @@ public async Task RudeEdits_UpdateBaseline(bool validChangeBeforeRudeEdit, bool InspectDiagnostics(docDiagnostics)); // validate solution update status and emit: - results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdates); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); AssertEx.SequenceEqual(["ENC0023"], InspectDiagnosticIds(results.GetAllDiagnostics())); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); Assert.Empty(results.ModuleUpdates.Updates); AssertEx.Equal([projectId], results.ProjectsToRebuild); AssertEx.Equal([projectId], results.ProjectsToRestart.Keys); - if (allowPartialUpdates) - { - // assuming user approved restart and rebuild: - CommitSolutionUpdate(debuggingSession); - } + // assuming user approved restart and rebuild: + CommitSolutionUpdate(debuggingSession); // rebuild and restart: _debuggerService.LoadedModules.Remove(moduleId); File.WriteAllText(sourceFilePath, source3, Encoding.UTF8); moduleId = EmitAndLoadLibraryToDebuggee(solution.GetRequiredDocument(documentId)); - if (!allowPartialUpdates) - { - debuggingSession.UpdateBaselines(solution, results.ProjectsToRebuild); - } - if (validChangeBeforeRudeEdit) { // baseline should be removed: @@ -2027,7 +2017,7 @@ public async Task RudeEdits_UpdateBaseline(bool validChangeBeforeRudeEdit, bool Assert.Empty(await service.GetDocumentDiagnosticsAsync(solution.GetRequiredDocument(documentId), s_noActiveSpans, CancellationToken.None)); // apply valid change: - results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdates); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); CommitSolutionUpdate(debuggingSession); @@ -2075,10 +2065,10 @@ public async Task SyntaxError() AssertEx.Empty(diagnostics1); // validate solution update status and emit: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.Blocked, updates.Status); - Assert.Empty(updates.Updates); - Assert.Empty(emitDiagnostics); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.Blocked, results.ModuleUpdates.Status); + Assert.Empty(results.ModuleUpdates.Updates); + Assert.Empty(results.Diagnostics); EndDebuggingSession(debuggingSession); @@ -2116,14 +2106,14 @@ public async Task SemanticError() // The EnC analyzer does not check for and block on all semantic errors as they are already reported by diagnostic analyzer. // Blocking update on semantic errors would be possible, but the status check is only an optimization to avoid emitting. - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.Blocked, updates.Status); - Assert.Empty(updates.Updates); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.Blocked, results.ModuleUpdates.Status); + Assert.Empty(results.ModuleUpdates.Updates); // TODO: https://github.com/dotnet/roslyn/issues/36061 // Semantic errors should not be reported in emit diagnostics. - AssertEx.Equal([$"{document2.FilePath}: (0,30)-(0,32): Error CS0266: {string.Format(CSharpResources.ERR_NoImplicitConvCast, "long", "int")}"], InspectDiagnostics(emitDiagnostics)); + AssertEx.Equal([$"proj: {document2.FilePath}: (0,30)-(0,32): Error CS0266: {string.Format(CSharpResources.ERR_NoImplicitConvCast, "long", "int")}"], InspectDiagnostics(results.Diagnostics)); EndDebuggingSession(debuggingSession); @@ -2186,8 +2176,8 @@ public async Task HasChanges() Assert.False(await EditSession.HasChangesAsync(oldSolution, solution, sourceFilePath: "NonexistentFile.cs", CancellationToken.None)); // All projects must have no errors. - var (updates, _) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.Blocked, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.Blocked, results.ModuleUpdates.Status); // add a project: @@ -2552,8 +2542,8 @@ public async Task Project_Add() .AddTestDocument(sourceB1, path: sourceFileB.Path, out var documentBId).Project.Solution .AddProjectReference(projectAId, new ProjectReference(projectBId)); - var runningProjects = ImmutableDictionary.Empty - .Add(projectAId, new RunningProjectInfo() { AllowPartialUpdate = true, RestartWhenChangesHaveNoEffect = false }); + var runningProjects = ImmutableDictionary.Empty + .Add(projectAId, new RunningProjectOptions() { RestartWhenChangesHaveNoEffect = false }); var results = await debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects, s_noActiveSpans, CancellationToken.None); @@ -2593,8 +2583,8 @@ public async Task ProjectReference_Add() // Add project reference A -> B solution = solution.AddProjectReference(projectAId, new ProjectReference(projectBId)); - var runningProjects = ImmutableDictionary.Empty - .Add(projectAId, new RunningProjectInfo() { AllowPartialUpdate = true, RestartWhenChangesHaveNoEffect = false }); + var runningProjects = ImmutableDictionary.Empty + .Add(projectAId, new RunningProjectOptions() { RestartWhenChangesHaveNoEffect = false }); var results = await debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects, s_noActiveSpans, CancellationToken.None); @@ -2687,11 +2677,11 @@ public async Task Project_Add_BinaryAlreadyLoaded() // update document with a valid change: solution = solution.WithDocumentText(documentB2.Id, CreateText("class B { int F() => 2; }")); - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); // TODO: https://github.com/dotnet/roslyn/issues/1204 // verify valid update - Assert.Equal(ModuleUpdateStatus.None, updates.Status); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); ExitBreakState(debuggingSession); @@ -2832,12 +2822,12 @@ int M() """)); // validate solution update status and emit - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); // check that no types have been updated. this used to throw - var delta = updates.Updates.Single(); + var delta = results.ModuleUpdates.Updates.Single(); Assert.Empty(delta.UpdatedTypes); debuggingSession.DiscardSolutionUpdate(); @@ -2871,12 +2861,13 @@ public async Task Capabilities_SynthesizedNewType() AssertEx.Empty(diagnostics); // They are reported as emit diagnostics - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - AssertEx.Equal([$"{project.FilePath}: (0,0)-(0,0): Error ENC1007: {FeaturesResources.ChangesRequiredSynthesizedType}"], InspectDiagnostics(emitDiagnostics)); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + AssertEx.Equal([$"proj: : Error ENC1007: {FeaturesResources.ChangesRequiredSynthesizedType}"], InspectDiagnostics(results.Diagnostics)); // no emitted delta: - Assert.Empty(updates.Updates); + Assert.Empty(results.ModuleUpdates.Updates); + debuggingSession.DiscardSolutionUpdate(); EndDebuggingSession(debuggingSession); } @@ -2903,11 +2894,11 @@ public async Task ValidSignificantChange_EmitError() AssertEx.Empty(diagnostics1); // validate solution update status and emit: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - AssertEx.Equal([$"{document2.FilePath}: (0,0)-(0,54): Error CS8055: {string.Format(CSharpResources.ERR_EncodinglessSyntaxTree)}"], InspectDiagnostics(emitDiagnostics)); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + AssertEx.Equal([$"proj: {document2.FilePath}: (0,0)-(0,54): Error CS8055: {string.Format(CSharpResources.ERR_EncodinglessSyntaxTree)}"], InspectDiagnostics(results.Diagnostics)); // no emitted delta: - Assert.Empty(updates.Updates); + Assert.Empty(results.ModuleUpdates.Updates); // no pending update: Assert.Null(debuggingSession.GetTestAccessor().GetPendingSolutionUpdate()); @@ -2919,9 +2910,9 @@ public async Task ValidSignificantChange_EmitError() Assert.Empty(debuggingSession.EditSession.NonRemappableRegions); // solution update status after discarding an update (still has update ready): - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.Blocked, updates.Status); - AssertEx.Equal([$"{document2.FilePath}: (0,0)-(0,54): Error CS8055: {string.Format(CSharpResources.ERR_EncodinglessSyntaxTree)}"], InspectDiagnostics(emitDiagnostics)); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.Blocked, results.ModuleUpdates.Status); + AssertEx.Equal([$"proj: {document2.FilePath}: (0,0)-(0,54): Error CS8055: {string.Format(CSharpResources.ERR_EncodinglessSyntaxTree)}"], InspectDiagnostics(results.Diagnostics)); EndDebuggingSession(debuggingSession); @@ -2996,10 +2987,10 @@ public async Task ValidSignificantChange_ApplyBeforeFileWatcherEvent(bool saveDo } // EnC service queries for a document, which triggers read of the source file from disk. - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); CommitSolutionUpdate(debuggingSession); ExitBreakState(debuggingSession); @@ -3010,16 +3001,16 @@ public async Task ValidSignificantChange_ApplyBeforeFileWatcherEvent(bool saveDo solution = solution.WithDocumentText(documentId, CreateTextFromFile(sourceFile.Path)); var document3 = solution.Projects.Single().Documents.Single(); - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); if (saveDocument) { - Assert.Equal(ModuleUpdateStatus.None, updates.Status); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); } else { - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); debuggingSession.DiscardSolutionUpdate(); } @@ -3070,11 +3061,11 @@ public async Task ValidSignificantChange_FileUpdateNotObservedBeforeDebuggingSes AssertEx.Empty(diagnostics); // since the document is out-of-sync we need to call update to determine whether we have changes to apply or not: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); // TODO: warning reported https://github.com/dotnet/roslyn/issues/78125 - // AssertEx.Equal([$"test.csproj: (0,0)-(0,0): Warning ENC1005: {string.Format(FeaturesResources.DocumentIsOutOfSyncWithDebuggee, sourceFile.Path)}"], InspectDiagnostics(emitDiagnostics)); + // AssertEx.Equal([$"test.csproj: (0,0)-(0,0): Warning ENC1005: {string.Format(FeaturesResources.DocumentIsOutOfSyncWithDebuggee, sourceFile.Path)}"], InspectDiagnostics(results.Diagnostics)); // undo: solution = solution.WithDocumentText(documentId, CreateText(source1)); @@ -3088,14 +3079,14 @@ public async Task ValidSignificantChange_FileUpdateNotObservedBeforeDebuggingSes Assert.Equal(CommittedSolution.DocumentState.OutOfSync, state); sourceFile.WriteAllText(source1, Encoding.UTF8); - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); AssertEx.Equal( [ - $"{project.FilePath}: (0,0)-(0,0): Warning ENC1008: {string.Format(FeaturesResources.Changing_source_file_0_in_a_stale_project_has_no_effect_until_the_project_is_rebuit, sourceFile.Path)}" - ], InspectDiagnostics(emitDiagnostics)); + $"test: {document3.FilePath}: (0,0)-(0,0): Warning ENC1008: {string.Format(FeaturesResources.Changing_source_file_0_in_a_stale_project_has_no_effect_until_the_project_is_rebuit, sourceFile.Path)}" + ], InspectDiagnostics(results.Diagnostics)); // the content actually hasn't changed: - Assert.Equal(ModuleUpdateStatus.None, updates.Status); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); EndDebuggingSession(debuggingSession); } @@ -3158,10 +3149,10 @@ public async Task ValidSignificantChange_AddedFileNotObservedBeforeDebuggingSess // AssertEx.Equal(new[] { $"({activeLineSpan1}, LeafFrame)" }, spans.Single().Select(s => s.ToString())); // No changes. - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); - AssertEx.Empty(emitDiagnostics); + AssertEx.Empty(results.Diagnostics); EndDebuggingSession(debuggingSession); } @@ -3197,10 +3188,10 @@ public async Task ValidSignificantChange_DocumentOutOfSync(bool delayLoad) EnterBreakState(debuggingSession); // no changes have been made to the project - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); - Assert.Empty(updates.Updates); - Assert.Empty(emitDiagnostics); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); + Assert.Empty(results.ModuleUpdates.Updates); + Assert.Empty(results.Diagnostics); // a file watcher observed a change and updated the document, so it now reflects the content on disk (the code that we compiled): solution = solution.WithDocumentText(document1.Id, CreateText(sourceOnDisk)); @@ -3210,9 +3201,9 @@ public async Task ValidSignificantChange_DocumentOutOfSync(bool delayLoad) Assert.Empty(diagnostics); // the content of the file is now exactly the same as the compiled document, so there is no change to be applied: - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); - Assert.Empty(emitDiagnostics); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); + Assert.Empty(results.Diagnostics); EndDebuggingSession(debuggingSession); @@ -3243,10 +3234,10 @@ public async Task ValidSignificantChange_EmitSuccessful(bool breakMode, bool com AssertEx.Empty(diagnostics1); // validate solution update status and emit: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); - ValidateDelta(updates.Updates.Single()); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); + ValidateDelta(results.ModuleUpdates.Updates.Single()); void ValidateDelta(ManagedHotReloadUpdate delta) { @@ -3265,7 +3256,7 @@ void ValidateDelta(ManagedHotReloadUpdate delta) // the update should be stored on the service: var pendingUpdate = debuggingSession.GetTestAccessor().GetPendingSolutionUpdate(); var newBaseline = pendingUpdate.ProjectBaselines.Single(); - AssertEx.Equal(updates.Updates, pendingUpdate.Deltas); + AssertEx.Equal(results.ModuleUpdates.Updates, pendingUpdate.Deltas); Assert.Equal(document2.Project.Id, newBaseline.ProjectId); Assert.Equal(moduleId, newBaseline.EmitBaseline.OriginalMetadata.GetModuleVersionId()); @@ -3293,9 +3284,9 @@ void ValidateDelta(ManagedHotReloadUpdate delta) Assert.Same(newBaseline.EmitBaseline, debuggingSession.GetTestAccessor().GetProjectBaselines(document2.Project.Id).Single().EmitBaseline); // solution update status after committing an update: - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); } else { @@ -3305,11 +3296,11 @@ void ValidateDelta(ManagedHotReloadUpdate delta) Assert.Null(debuggingSession.GetTestAccessor().GetPendingSolutionUpdate()); // solution update status after committing an update: - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); - ValidateDelta(updates.Updates.Single()); + ValidateDelta(results.ModuleUpdates.Updates.Single()); debuggingSession.DiscardSolutionUpdate(); } @@ -3381,12 +3372,12 @@ public async Task ValidSignificantChange_EmitSuccessful_UpdateDeferred(bool comm var document2 = solution.GetDocument(document1.Id); // validate solution update status and emit: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); - Assert.Empty(emitDiagnostics); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); + Assert.Empty(results.Diagnostics); // delta to apply: - var delta = updates.Updates.Single(); + var delta = results.ModuleUpdates.Updates.Single(); Assert.Empty(delta.ActiveStatements); Assert.NotEmpty(delta.ILDelta); Assert.NotEmpty(delta.MetadataDelta); @@ -3432,9 +3423,9 @@ public async Task ValidSignificantChange_EmitSuccessful_UpdateDeferred(bool comm var document3 = solution.GetDocument(document1.Id); solution = solution.WithDocumentText(document3.Id, CreateText("class C1 { void M1() { int a = 3; System.Console.WriteLine(a); } void M2() { System.Console.WriteLine(2); } }")); - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); - Assert.Empty(emitDiagnostics); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); + Assert.Empty(results.Diagnostics); debuggingSession.DiscardSolutionUpdate(); } else @@ -3500,12 +3491,12 @@ partial class C { int Y = 2; } """)); // validate solution update status and emit: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); // check emitted delta: - var delta = updates.Updates.Single(); + var delta = results.ModuleUpdates.Updates.Single(); Assert.Empty(delta.ActiveStatements); Assert.NotEmpty(delta.ILDelta); Assert.NotEmpty(delta.MetadataDelta); @@ -3517,10 +3508,9 @@ partial class C { int Y = 2; } EndDebuggingSession(debuggingSession); } - [Theory] - [CombinatorialData] + [Fact] [WorkItem("https://github.com/dotnet/roslyn/issues/78244")] - public async Task MultiProjectUpdates_ValidSignificantChange_RudeEdit(bool allowPartialUpdate) + public async Task MultiProjectUpdates_ValidSignificantChange_RudeEdit() { using var _ = CreateWorkspace(out var solution, out var service); @@ -3580,31 +3570,21 @@ void F() {} InspectDiagnostics(docDiagnostics)); // validate solution update status and emit: - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); AssertEx.SequenceEqual(["ENC0023"], InspectDiagnosticIds(results.Diagnostics)); - if (allowPartialUpdate) - { - AssertEx.SetEqual([documentBId.ProjectId], results.ProjectsToRebuild); - AssertEx.SetEqual([documentBId.ProjectId], results.ProjectsToRestart.Keys); - - var delta = results.ModuleUpdates.Updates.Single(); - Assert.NotEmpty(delta.ILDelta); - Assert.NotEmpty(delta.MetadataDelta); - Assert.NotEmpty(delta.PdbDelta); - Assert.Equal(1, delta.UpdatedMethods.Length); + AssertEx.SetEqual([documentBId.ProjectId], results.ProjectsToRebuild); + AssertEx.SetEqual([documentBId.ProjectId], results.ProjectsToRestart.Keys); - debuggingSession.DiscardSolutionUpdate(); - } - else - { - AssertEx.SetEqual([documentAId.ProjectId, documentBId.ProjectId], results.ProjectsToRebuild); - AssertEx.SetEqual([documentAId.ProjectId, documentBId.ProjectId], results.ProjectsToRestart.Keys); + var delta = results.ModuleUpdates.Updates.Single(); + Assert.NotEmpty(delta.ILDelta); + Assert.NotEmpty(delta.MetadataDelta); + Assert.NotEmpty(delta.PdbDelta); + Assert.Equal(1, delta.UpdatedMethods.Length); - Assert.Empty(results.ModuleUpdates.Updates); - } + debuggingSession.DiscardSolutionUpdate(); EndDebuggingSession(debuggingSession); } @@ -3672,21 +3652,19 @@ static B() // TODO: Set RestartWhenChangesHaveNoEffect=true and AllowPartialUpdate=true // https://github.com/dotnet/roslyn/issues/78244 - var runningProjects = ImmutableDictionary.Empty - .Add(projectAId, new RunningProjectInfo() { RestartWhenChangesHaveNoEffect = false, AllowPartialUpdate = false }) - .Add(projectBId, new RunningProjectInfo() { RestartWhenChangesHaveNoEffect = false, AllowPartialUpdate = false }); + var runningProjects = ImmutableDictionary.Empty + .Add(projectAId, new RunningProjectOptions() { RestartWhenChangesHaveNoEffect = false }) + .Add(projectBId, new RunningProjectOptions() { RestartWhenChangesHaveNoEffect = false }); // emit updates: - var result = await debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects, s_noActiveSpans, CancellationToken.None); - - AssertEx.SetEqual([], result.ProjectsToRestart.Select(p => p.Key.DebugName)); + var results = await debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects, s_noActiveSpans, CancellationToken.None); - var updates = result.ModuleUpdates; - AssertEx.SequenceEqual(["ENC0118"], InspectDiagnosticIds(result.GetAllDiagnostics())); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + AssertEx.SetEqual([], results.ProjectsToRestart.Select(p => p.Key.DebugName)); + AssertEx.SequenceEqual(["ENC0118"], InspectDiagnosticIds(results.GetAllDiagnostics())); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); // check emitted delta: - Assert.Equal(2, updates.Updates.Length); + Assert.Equal(2, results.ModuleUpdates.Updates.Length); // Process will be restarted, so discard all updates: debuggingSession.DiscardSolutionUpdate(); @@ -3737,12 +3715,12 @@ class C { int Y => 2; } """)); // validate solution update status and emit: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); // check emitted delta: - var delta = updates.Updates.Single(); + var delta = results.ModuleUpdates.Updates.Single(); Assert.Empty(delta.ActiveStatements); Assert.NotEmpty(delta.ILDelta); Assert.NotEmpty(delta.MetadataDelta); @@ -3792,7 +3770,7 @@ class C { int Y => 2; } """)); // validate solution update status and emit: - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); var generatedFilePath = Path.Combine( TempRoot.Root, @@ -3804,6 +3782,7 @@ class C { int Y => 2; } [$"proj: {generatedFilePath}: (0,0)-(0,56): Error ENC0021: {string.Format(FeaturesResources.Adding_0_requires_restarting_the_application, FeaturesResources.attribute)}"], InspectDiagnostics(results.Diagnostics)); + debuggingSession.DiscardSolutionUpdate(); EndDebuggingSession(debuggingSession); } @@ -3852,12 +3831,12 @@ int M() """)); // validate solution update status and emit: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); // check emitted delta: - var delta = updates.Updates.Single(); + var delta = results.ModuleUpdates.Updates.Single(); Assert.Empty(delta.ActiveStatements); var lineUpdate = delta.SequencePoints.Single(); @@ -3902,12 +3881,12 @@ partial class C { int X = 1; } """)); // validate solution update status and emit: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); // check emitted delta: - var delta = updates.Updates.Single(); + var delta = results.ModuleUpdates.Updates.Single(); Assert.Empty(delta.ActiveStatements); Assert.NotEmpty(delta.ILDelta); Assert.NotEmpty(delta.MetadataDelta); @@ -3954,12 +3933,12 @@ class C { int Y => 1; } """)); // validate solution update status and emit: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); // check emitted delta: - var delta = updates.Updates.Single(); + var delta = results.ModuleUpdates.Updates.Single(); Assert.Empty(delta.ActiveStatements); Assert.NotEmpty(delta.ILDelta); Assert.NotEmpty(delta.MetadataDelta); @@ -4000,12 +3979,12 @@ class C { int Y => 1; } solution = solution.WithAnalyzerConfigDocumentText(configDocument1.Id, GetAnalyzerConfigText(configV2)); // validate solution update status and emit: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); // check emitted delta: - var delta = updates.Updates.Single(); + var delta = results.ModuleUpdates.Updates.Single(); Assert.Empty(delta.ActiveStatements); Assert.NotEmpty(delta.ILDelta); Assert.NotEmpty(delta.MetadataDelta); @@ -4041,12 +4020,12 @@ public async Task ValidSignificantChange_SourceGenerators_DocumentRemove() solution = document1.Project.Solution.RemoveDocument(document1.Id); // validate solution update status and emit: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); // check emitted delta: - var delta = updates.Updates.Single(); + var delta = results.ModuleUpdates.Updates.Single(); Assert.Empty(delta.ActiveStatements); Assert.NotEmpty(delta.ILDelta); Assert.NotEmpty(delta.MetadataDelta); @@ -4079,9 +4058,9 @@ public async Task ValidInsignificantChange() AssertEx.Empty(diagnostics1); // validate solution update status and emit: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); // solution has been updated: var text = await debuggingSession.LastCommittedSolution.GetRequiredProject(document1.Project.Id).GetRequiredDocument(document1.Id).GetTextAsync(); @@ -4123,12 +4102,13 @@ public async Task RudeEdit() AssertEx.Empty(diagnostics); // They are reported as emit diagnostics - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - AssertEx.Equal([$"{project.FilePath}: (0,0)-(0,0): Error ENC1007: {FeaturesResources.ChangesRequiredSynthesizedType}"], InspectDiagnostics(emitDiagnostics)); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + AssertEx.Equal([$"proj: : Error ENC1007: {FeaturesResources.ChangesRequiredSynthesizedType}"], InspectDiagnostics(results.Diagnostics)); // no emitted delta: - Assert.Empty(updates.Updates); + Assert.Empty(results.ModuleUpdates.Updates); + debuggingSession.DiscardSolutionUpdate(); EndDebuggingSession(debuggingSession); } @@ -4186,13 +4166,13 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule() solution = solution.WithDocumentText(projectB.Documents.Single().Id, CreateText(source2)); // validate solution update status and emit: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); - Assert.Empty(emitDiagnostics); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); + Assert.Empty(results.Diagnostics); - var deltaA = updates.Updates.Single(d => d.Module == moduleIdA); - var deltaB = updates.Updates.Single(d => d.Module == moduleIdB); - Assert.Equal(2, updates.Updates.Length); + var deltaA = results.ModuleUpdates.Updates.Single(d => d.Module == moduleIdA); + var deltaB = results.ModuleUpdates.Updates.Single(d => d.Module == moduleIdB); + Assert.Equal(2, results.ModuleUpdates.Updates.Length); // the update should be stored on the service: var pendingUpdate = debuggingSession.GetTestAccessor().GetPendingSolutionUpdate(); @@ -4219,9 +4199,9 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule() Assert.Same(newBaselineA1, debuggingSession.GetTestAccessor().GetProjectBaselines(projectA.Id).Single().EmitBaseline); Assert.Same(newBaselineB1, debuggingSession.GetTestAccessor().GetProjectBaselines(projectB.Id).Single().EmitBaseline); - // solution update status after committing an update:(updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + // solution update status after committing an update:results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); ExitBreakState(debuggingSession); EnterBreakState(debuggingSession); @@ -4234,13 +4214,13 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule() solution = solution.WithDocumentText(projectB.Documents.Single().Id, CreateText(source3)); // validate solution update status and emit: - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); - Assert.Empty(emitDiagnostics); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); + Assert.Empty(results.Diagnostics); - deltaA = updates.Updates.Single(d => d.Module == moduleIdA); - deltaB = updates.Updates.Single(d => d.Module == moduleIdB); - Assert.Equal(2, updates.Updates.Length); + deltaA = results.ModuleUpdates.Updates.Single(d => d.Module == moduleIdA); + deltaB = results.ModuleUpdates.Updates.Single(d => d.Module == moduleIdB); + Assert.Equal(2, results.ModuleUpdates.Updates.Length); // the update should be stored on the service: pendingUpdate = debuggingSession.GetTestAccessor().GetPendingSolutionUpdate(); @@ -4273,9 +4253,9 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule() Assert.Same(newBaselineB2, debuggingSession.GetTestAccessor().GetProjectBaselines(projectB.Id).Single().EmitBaseline); // solution update status after committing an update: - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); ExitBreakState(debuggingSession); EndDebuggingSession(debuggingSession); @@ -4330,10 +4310,10 @@ public async Task MultiTargetedPartiallyBuiltProjects() solution = solution.WithDocumentText(documentA.Id, text2).WithDocumentText(documentB.Id, text2); // delta emitted only for up-to-date project - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); - Assert.Empty(emitDiagnostics); - AssertEx.SequenceEqual([mvidA], updates.Updates.Select(u => u.Module)); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); + Assert.Empty(results.Diagnostics); + AssertEx.SequenceEqual([mvidA], results.ModuleUpdates.Updates.Select(u => u.Module)); CommitSolutionUpdate(debuggingSession); @@ -4346,10 +4326,10 @@ public async Task MultiTargetedPartiallyBuiltProjects() solution = solution.WithDocumentText(documentA.Id, text0).WithDocumentText(documentB.Id, text0); // both projects are up-to-date now, but B hasn't changed w.r.t. baseline: - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); - Assert.Empty(emitDiagnostics); - AssertEx.SetEqual([mvidA], updates.Updates.Select(u => u.Module)); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); + Assert.Empty(results.Diagnostics); + AssertEx.SetEqual([mvidA], results.ModuleUpdates.Updates.Select(u => u.Module)); CommitSolutionUpdate(debuggingSession); @@ -4357,10 +4337,10 @@ public async Task MultiTargetedPartiallyBuiltProjects() solution = solution.WithDocumentText(documentA.Id, text2).WithDocumentText(documentB.Id, text2); // project B is considered stale until rebuilt (even though the document content matches now): - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); - Assert.Empty(emitDiagnostics); - AssertEx.SetEqual([mvidA], updates.Updates.Select(u => u.Module)); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); + Assert.Empty(results.Diagnostics); + AssertEx.SetEqual([mvidA], results.ModuleUpdates.Updates.Select(u => u.Module)); CommitSolutionUpdate(debuggingSession); @@ -4371,19 +4351,19 @@ public async Task MultiTargetedPartiallyBuiltProjects() // no changes have been made: - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); - Assert.Empty(emitDiagnostics); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); + Assert.Empty(results.Diagnostics); // update source file in the editor: var text3 = CreateText(source3); solution = solution.WithDocumentText(documentA.Id, text3).WithDocumentText(documentB.Id, text3); // both modules updated now: - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); - Assert.Empty(emitDiagnostics); - AssertEx.SetEqual([mvidA, mvidB2], updates.Updates.Select(u => u.Module)); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); + Assert.Empty(results.Diagnostics); + AssertEx.SetEqual([mvidA, mvidB2], results.ModuleUpdates.Updates.Select(u => u.Module)); CommitSolutionUpdate(debuggingSession); EndDebuggingSession(debuggingSession); @@ -4430,13 +4410,13 @@ public async Task MultiTargeted_AllTargetsStale() solution = solution.WithDocumentText(documentA.Id, text2).WithDocumentText(documentB.Id, text2); // no updates - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Equal(ModuleUpdateStatus.None, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Equal(ModuleUpdateStatus.None, results.ModuleUpdates.Status); AssertEx.Equal( [ - $"{documentA.Project.FilePath}: (0,0)-(0,0): Warning ENC1008: {string.Format(FeaturesResources.Changing_source_file_0_in_a_stale_project_has_no_effect_until_the_project_is_rebuit, sourcePath)}", - $"{documentB.Project.FilePath}: (0,0)-(0,0): Warning ENC1008: {string.Format(FeaturesResources.Changing_source_file_0_in_a_stale_project_has_no_effect_until_the_project_is_rebuit, sourcePath)}" - ], InspectDiagnostics(emitDiagnostics)); + $"A: {documentA.FilePath}: (0,0)-(0,0): Warning ENC1008: {string.Format(FeaturesResources.Changing_source_file_0_in_a_stale_project_has_no_effect_until_the_project_is_rebuit, sourcePath)}", + $"B: {documentB.FilePath}: (0,0)-(0,0): Warning ENC1008: {string.Format(FeaturesResources.Changing_source_file_0_in_a_stale_project_has_no_effect_until_the_project_is_rebuit, sourcePath)}" + ], InspectDiagnostics(results.Diagnostics)); EndDebuggingSession(debuggingSession); } @@ -4461,7 +4441,7 @@ public async Task ValidSignificantChange_BaselineCreationFailed_NoStream() // change the source (valid edit): solution = solution.WithDocumentText(document1.Id, CreateText("class C1 { void M() { System.Console.WriteLine(2); } }")); - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); AssertEx.Equal( [$"proj: : Error ENC1001: {string.Format(FeaturesResources.ErrorReadingFile, "test-pdb", new FileNotFoundException().Message)}"], InspectDiagnostics(results.Diagnostics)); @@ -4497,12 +4477,13 @@ public async Task ValidSignificantChange_BaselineCreationFailed_AssemblyReadErro var document1 = solution.Projects.Single().Documents.Single(); solution = solution.WithDocumentText(document1.Id, CreateText("class C1 { void M() { System.Console.WriteLine(2); } }")); - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); AssertEx.Equal( [$"proj: : Error ENC1001: {string.Format(FeaturesResources.ErrorReadingFile, "test-assembly", "*message*")}"], InspectDiagnostics(results.Diagnostics)); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); + debuggingSession.DiscardSolutionUpdate(); EndDebuggingSession(debuggingSession); @@ -4961,12 +4942,12 @@ void F() solution = solution.WithDocumentText(document1.Id, CreateText(source2)); // validate solution update status and emit: - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); // check emitted delta: - var delta = updates.Updates.Single(); + var delta = results.ModuleUpdates.Updates.Single(); Assert.Empty(delta.ActiveStatements); Assert.NotEmpty(delta.ILDelta); Assert.NotEmpty(delta.MetadataDelta); @@ -5051,10 +5032,12 @@ int F() [$"{document.FilePath}: (9,8)-(9,13): Error ENC0063: {string.Format(FeaturesResources.Updating_a_0_around_an_active_statement_requires_restarting_the_application, CSharpFeaturesResources.catch_clause)}"], InspectDiagnostics(docDiagnostics)); - var results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); AssertEx.SequenceEqual(["ENC0063"], InspectDiagnosticIds(results.Diagnostics)); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); + debuggingSession.DiscardSolutionUpdate(); + // undo the change solution = solution.WithDocumentText(document.Id, CreateText(source1)); document = solution.GetDocument(document.Id); @@ -5068,7 +5051,7 @@ int F() Assert.Empty(docDiagnostics); // validate solution update status and emit (Hot Reload change): - results = await EmitSolutionUpdateAsync(debuggingSession, solution, allowPartialUpdate: false); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); Assert.Empty(results.Diagnostics); Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); @@ -5134,11 +5117,11 @@ static void F() solution = solution.WithDocumentText(documentId, CreateText(SourceMarkers.Clear(markedSourceV2))); - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(0x06000003, updates.Updates.Single().UpdatedMethods.Single()); - Assert.Equal(0x02000002, updates.Updates.Single().UpdatedTypes.Single()); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(0x06000003, results.ModuleUpdates.Updates.Single().UpdatedMethods.Single()); + Assert.Equal(0x02000002, results.ModuleUpdates.Updates.Single().UpdatedTypes.Single()); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); CommitSolutionUpdate(debuggingSession); @@ -5154,11 +5137,11 @@ static void F() solution = solution.WithDocumentText(documentId, CreateText(SourceMarkers.Clear(markedSourceV3))); - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(0x06000003, updates.Updates.Single().UpdatedMethods.Single()); - Assert.Equal(0x02000002, updates.Updates.Single().UpdatedTypes.Single()); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(0x06000003, results.ModuleUpdates.Updates.Single().UpdatedMethods.Single()); + Assert.Equal(0x02000002, results.ModuleUpdates.Updates.Single().UpdatedTypes.Single()); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); CommitSolutionUpdate(debuggingSession); @@ -5190,11 +5173,11 @@ static void F() solution = solution.WithDocumentText(documentId, CreateText(SourceMarkers.Clear(markedSourceV4))); - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(0x06000003, updates.Updates.Single().UpdatedMethods.Single()); - Assert.Equal(0x02000002, updates.Updates.Single().UpdatedTypes.Single()); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(0x06000003, results.ModuleUpdates.Updates.Single().UpdatedMethods.Single()); + Assert.Equal(0x02000002, results.ModuleUpdates.Updates.Single().UpdatedTypes.Single()); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); CommitSolutionUpdate(debuggingSession); @@ -5309,11 +5292,11 @@ static void F() var diagnostics = await service.GetDocumentDiagnosticsAsync(document1, s_noActiveSpans, CancellationToken.None); Assert.Empty(diagnostics); - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(0x06000003, updates.Updates.Single().UpdatedMethods.Single()); - Assert.Equal(0x02000002, updates.Updates.Single().UpdatedTypes.Single()); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(0x06000003, results.ModuleUpdates.Updates.Single().UpdatedMethods.Single()); + Assert.Equal(0x02000002, results.ModuleUpdates.Updates.Single().UpdatedTypes.Single()); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); CommitSolutionUpdate(debuggingSession); @@ -5371,11 +5354,11 @@ static void F() } """))); - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(0x06000003, updates.Updates.Single().UpdatedMethods.Single()); - Assert.Equal(0x02000002, updates.Updates.Single().UpdatedTypes.Single()); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(0x06000003, results.ModuleUpdates.Updates.Single().UpdatedMethods.Single()); + Assert.Equal(0x02000002, results.ModuleUpdates.Updates.Single().UpdatedTypes.Single()); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); CommitSolutionUpdate(debuggingSession); @@ -5478,10 +5461,10 @@ static void H() Assert.Equal(1, oldProject.DocumentIds.Count); Assert.Equal(2, newProject.DocumentIds.Count); - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, modifiedSolution); - Assert.Empty(emitDiagnostics); - Assert.False(updates.Updates.IsEmpty); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, modifiedSolution); + Assert.Empty(results.Diagnostics); + Assert.False(results.ModuleUpdates.Updates.IsEmpty); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); CommitSolutionUpdate(debuggingSession); EndDebuggingSession(debuggingSession); @@ -5524,14 +5507,14 @@ public async Task MultiSession() var solution1 = solution.WithDocumentText(documentIdA, CreateText("class C { void M() { System.Console.WriteLine(" + i + "); } }")); - var result1 = await encService.EmitSolutionUpdateAsync(sessionId, solution1, runningProjects: ImmutableDictionary.Empty, s_noActiveSpans, CancellationToken.None); + var result1 = await encService.EmitSolutionUpdateAsync(sessionId, solution1, runningProjects: ImmutableDictionary.Empty, s_noActiveSpans, CancellationToken.None); Assert.Empty(result1.Diagnostics); Assert.Equal(1, result1.ModuleUpdates.Updates.Length); encService.DiscardSolutionUpdate(sessionId); var solution2 = solution1.WithDocumentText(documentIdA, CreateText(source3)); - var result2 = await encService.EmitSolutionUpdateAsync(sessionId, solution2, runningProjects: ImmutableDictionary.Empty, s_noActiveSpans, CancellationToken.None); + var result2 = await encService.EmitSolutionUpdateAsync(sessionId, solution2, runningProjects: ImmutableDictionary.Empty, s_noActiveSpans, CancellationToken.None); Assert.Equal("CS0103", result2.Diagnostics.Single().Diagnostics.Single().Id); Assert.Empty(result2.ModuleUpdates.Updates); @@ -5554,7 +5537,7 @@ public async Task Disposal() EndDebuggingSession(debuggingSession); // The folling methods shall not be called after the debugging session ended. - await Assert.ThrowsAsync(async () => await debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects: ImmutableDictionary.Empty, s_noActiveSpans, CancellationToken.None)); + await Assert.ThrowsAsync(async () => await debuggingSession.EmitSolutionUpdateAsync(solution, runningProjects: ImmutableDictionary.Empty, s_noActiveSpans, CancellationToken.None)); Assert.Throws(() => debuggingSession.BreakStateOrCapabilitiesChanged(inBreakState: true)); Assert.Throws(() => debuggingSession.DiscardSolutionUpdate()); Assert.Throws(() => debuggingSession.CommitSolutionUpdate()); @@ -5605,11 +5588,11 @@ public class C // lib source is updated: solution = solution.WithDocumentText(documentId, CreateText(libSource2)); - var (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + var results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); - var update = updates.Updates.Single(); + var update = results.ModuleUpdates.Updates.Single(); GetModuleIds(update.MetadataDelta, out var updateModuleId, out var baseId, out var genId, out var gen); Assert.Equal(libMvid1, updateModuleId); Assert.Equal(1, gen); @@ -5629,13 +5612,13 @@ public class C } """)); - (updates, emitDiagnostics) = await EmitSolutionUpdateAsync(debuggingSession, solution); - Assert.Empty(emitDiagnostics); - Assert.Equal(ModuleUpdateStatus.Ready, updates.Status); + results = await EmitSolutionUpdateAsync(debuggingSession, solution); + Assert.Empty(results.Diagnostics); + Assert.Equal(ModuleUpdateStatus.Ready, results.ModuleUpdates.Status); - Assert.Equal(2, updates.Updates.Length); - var libUpdate1 = updates.Updates.Single(u => u.Module == libMvid1); - var libUpdate2 = updates.Updates.Single(u => u.Module == libMvid2); + Assert.Equal(2, results.ModuleUpdates.Updates.Length); + var libUpdate1 = results.ModuleUpdates.Updates.Single(u => u.Module == libMvid1); + var libUpdate2 = results.ModuleUpdates.Updates.Single(u => u.Module == libMvid2); // update of the original library should chain to its previous delta: GetModuleIds(libUpdate1.MetadataDelta, out var updateModuleId1, out var baseId1, out var genId1, out var gen1); diff --git a/src/Features/Test/EditAndContinue/EmitSolutionUpdateResultsTests.cs b/src/Features/Test/EditAndContinue/EmitSolutionUpdateResultsTests.cs index 0e93680c4a32b..0b3120a89fa18 100644 --- a/src/Features/Test/EditAndContinue/EmitSolutionUpdateResultsTests.cs +++ b/src/Features/Test/EditAndContinue/EmitSolutionUpdateResultsTests.cs @@ -53,8 +53,8 @@ private static ImmutableArray CreateProjectRudeEdits(IEnumer .OrderBy(g => g.Key) .Select(g => new ProjectDiagnostics(g.Key, [.. g.Select(e => Diagnostic.Create(EditAndContinueDiagnosticDescriptors.GetDescriptor(e.kind), Location.None))]))]; - private static ImmutableDictionary CreateRunningProjects(IEnumerable<(ProjectId id, bool noEffectRestarts)> projectIds, bool allowPartialUpdate = true) - => projectIds.ToImmutableDictionary(keySelector: e => e.id, elementSelector: e => new RunningProjectInfo() { RestartWhenChangesHaveNoEffect = e.noEffectRestarts, AllowPartialUpdate = allowPartialUpdate }); + private static ImmutableDictionary CreateRunningProjects(IEnumerable<(ProjectId id, bool noEffectRestarts)> projectIds) + => projectIds.ToImmutableDictionary(keySelector: e => e.id, elementSelector: e => new RunningProjectOptions() { RestartWhenChangesHaveNoEffect = e.noEffectRestarts }); private static IEnumerable Inspect(ImmutableDictionary> projectsToRestart) => projectsToRestart @@ -382,9 +382,8 @@ public void RunningProjects_NoEffectEditAndRudeEdit_SameProject() AssertEx.SetEqual([a, b], projectsToRebuild); } - [Theory] - [CombinatorialData] - public void RunningProjects_NoEffectEditAndRudeEdit_DifferentProjects(bool allowPartialUpdate) + [Fact] + public void RunningProjects_NoEffectEditAndRudeEdit_DifferentProjects() { using var _ = CreateWorkspace(out var solution); @@ -402,7 +401,7 @@ public void RunningProjects_NoEffectEditAndRudeEdit_DifferentProjects(bool allow CreateValidUpdates(p0, q), CreateProjectRudeEdits(blocking: [p1, p2], noEffect: [q]), addedUnbuiltProjects: [], - CreateRunningProjects([(r0, noEffectRestarts: false), (r1, noEffectRestarts: false), (r2, noEffectRestarts: false)], allowPartialUpdate), + CreateRunningProjects([(r0, noEffectRestarts: false), (r1, noEffectRestarts: false), (r2, noEffectRestarts: false)]), out var projectsToRestart, out var projectsToRebuild); @@ -413,24 +412,12 @@ public void RunningProjects_NoEffectEditAndRudeEdit_DifferentProjects(bool allow // ==> R0 has to restart due to rude edits in P1 and P2 // Q has update // ==> R0 has to restart due to rude edits in P1 and P2 - if (allowPartialUpdate) - { - AssertEx.Equal( - [ - "R0: [P1,P2]", - "R1: [P1]", - "R2: [P2]", - ], Inspect(projectsToRestart)); - } - else - { - AssertEx.Equal( - [ - "R0: []", - "R1: [P1]", - "R2: [P2]", - ], Inspect(projectsToRestart)); - } + AssertEx.Equal( + [ + "R0: [P1,P2]", + "R1: [P1]", + "R2: [P2]", + ], Inspect(projectsToRestart)); AssertEx.SetEqual([r0, r1, r2], projectsToRebuild); } @@ -482,7 +469,7 @@ public void RunningProjects_RudeEditAndUpdate_DependentOnRebuiltProject() CreateValidUpdates(c), CreateProjectRudeEdits(blocking: [b], noEffect: []), addedUnbuiltProjects: [], - CreateRunningProjects([(a, noEffectRestarts: false)], allowPartialUpdate: true), + CreateRunningProjects([(a, noEffectRestarts: false)]), out var projectsToRestart, out var projectsToRebuild); @@ -513,7 +500,7 @@ public void RunningProjects_AddedProject_NotImpactingRunningProject() CreateValidUpdates(c), CreateProjectRudeEdits(blocking: [], noEffect: []), addedUnbuiltProjects: [b], - CreateRunningProjects([(a, noEffectRestarts: false)], allowPartialUpdate: true), + CreateRunningProjects([(a, noEffectRestarts: false)]), out var projectsToRestart, out var projectsToRebuild); @@ -540,7 +527,7 @@ public void RunningProjects_AddedProject_ImpactingRunningProject() CreateValidUpdates(c), CreateProjectRudeEdits(blocking: [], noEffect: []), addedUnbuiltProjects: [b], - CreateRunningProjects([(a, noEffectRestarts: false), (e, noEffectRestarts: false)], allowPartialUpdate: true), + CreateRunningProjects([(a, noEffectRestarts: false), (e, noEffectRestarts: false)]), out var projectsToRestart, out var projectsToRebuild); @@ -556,9 +543,8 @@ public void RunningProjects_AddedProject_ImpactingRunningProject() AssertEx.SetEqual([a, b, e], projectsToRebuild); } - [Theory] - [CombinatorialData] - public void RunningProjects_RudeEditAndUpdate_Independent(bool allowPartialUpdate) + [Fact] + public void RunningProjects_RudeEditAndUpdate_Independent() { using var _ = CreateWorkspace(out var solution); @@ -573,31 +559,17 @@ public void RunningProjects_RudeEditAndUpdate_Independent(bool allowPartialUpdat CreateValidUpdates(c), CreateProjectRudeEdits(blocking: [d], noEffect: []), addedUnbuiltProjects: [], - CreateRunningProjects([(a, noEffectRestarts: false), (b, noEffectRestarts: false)], allowPartialUpdate), + CreateRunningProjects([(a, noEffectRestarts: false), (b, noEffectRestarts: false)]), out var projectsToRestart, out var projectsToRebuild); - if (allowPartialUpdate) - { - // D has rude edit => B has to restart - AssertEx.Equal(["B: [D]"], Inspect(projectsToRestart)); - AssertEx.SetEqual([b], projectsToRebuild); - } - else - { - AssertEx.Equal( - [ - "A: []", - "B: [D]", - ], Inspect(projectsToRestart)); - - AssertEx.SetEqual([a, b], projectsToRebuild); - } + // D has rude edit => B has to restart + AssertEx.Equal(["B: [D]"], Inspect(projectsToRestart)); + AssertEx.SetEqual([b], projectsToRebuild); } - [Theory] - [CombinatorialData] - public void RunningProjects_NoEffectEditAndUpdate(bool allowPartialUpdate) + [Fact] + public void RunningProjects_NoEffectEditAndUpdate() { using var _ = CreateWorkspace(out var solution); @@ -612,7 +584,7 @@ public void RunningProjects_NoEffectEditAndUpdate(bool allowPartialUpdate) CreateValidUpdates(c, d), CreateProjectRudeEdits(blocking: [], noEffect: [d]), addedUnbuiltProjects: [], - CreateRunningProjects([(a, noEffectRestarts: false), (b, noEffectRestarts: true)], allowPartialUpdate), + CreateRunningProjects([(a, noEffectRestarts: false), (b, noEffectRestarts: true)]), out var projectsToRestart, out var projectsToRebuild); @@ -620,22 +592,11 @@ public void RunningProjects_NoEffectEditAndUpdate(bool allowPartialUpdate) // ==> B has to restart // C has update, A -> C, B -> C, B restarting // ==> A has to restart even though it does not restart on no-effect edits - if (allowPartialUpdate) - { - AssertEx.Equal( - [ - "A: [D]", - "B: [D]", - ], Inspect(projectsToRestart)); - } - else - { - AssertEx.Equal( - [ - "A: []", - "B: [D]", - ], Inspect(projectsToRestart)); - } + AssertEx.Equal( + [ + "A: [D]", + "B: [D]", + ], Inspect(projectsToRestart)); AssertEx.SetEqual([a, b], projectsToRebuild); } diff --git a/src/Features/Test/EditAndContinue/RemoteEditAndContinueServiceTests.cs b/src/Features/Test/EditAndContinue/RemoteEditAndContinueServiceTests.cs index 40e1521986f9d..0001339a8a846 100644 --- a/src/Features/Test/EditAndContinue/RemoteEditAndContinueServiceTests.cs +++ b/src/Features/Test/EditAndContinue/RemoteEditAndContinueServiceTests.cs @@ -176,9 +176,9 @@ await localWorkspace.ChangeSolutionAsync(localWorkspace.CurrentSolution var diagnosticDescriptor1 = EditAndContinueDiagnosticDescriptors.GetDescriptor(EditAndContinueErrorCode.ErrorReadingFile); - var runningProjects1 = new Dictionary + var runningProjects1 = new Dictionary { - { project.Id, new RunningProjectInfo() { RestartWhenChangesHaveNoEffect = true, AllowPartialUpdate = true} } + { project.Id, new RunningProjectOptions() { RestartWhenChangesHaveNoEffect = true } } }.ToImmutableDictionary(); mockEncService.EmitSolutionUpdateImpl = (solution, runningProjects, activeStatementSpanProvider) => diff --git a/src/Features/Test/EditAndContinue/WatchHotReloadServiceTests.cs b/src/Features/Test/EditAndContinue/WatchHotReloadServiceTests.cs index 028161f3b8df0..8b1c1e1c1c1ab 100644 --- a/src/Features/Test/EditAndContinue/WatchHotReloadServiceTests.cs +++ b/src/Features/Test/EditAndContinue/WatchHotReloadServiceTests.cs @@ -37,15 +37,9 @@ private static Task GetCommittedDocumentTextAsync(WatchHotReloadServ .GetRequiredDocument(documentId) .GetTextAsync(); - [Theory] - [CombinatorialData] - public async Task Test(bool requireCommit) + [Fact] + public async Task Test() { - // See https://github.com/dotnet/sdk/blob/main/src/BuiltInTools/dotnet-watch/HotReload/CompilationHandler.cs#L125 - - // Note that xUnit does not run test case of a theory in parallel, so we can set global state here: - WatchHotReloadService.RequireCommit = requireCommit; - var source1 = "class C { void M() { System.Console.WriteLine(1); } }"; var source2 = "class C { void M() { System.Console.WriteLine(2); /*2*/} }"; var source3 = "class C { void M() { System.Console.WriteLine(2); /*3*/} }"; @@ -89,10 +83,7 @@ public async Task Test(bool requireCommit) Assert.Equal(1, result.ProjectUpdates.Length); AssertEx.Equal([0x02000002], result.ProjectUpdates[0].UpdatedTypes); - if (requireCommit) - { - hotReload.CommitUpdate(); - } + hotReload.CommitUpdate(); var updatedText = await GetCommittedDocumentTextAsync(hotReload, documentIdA); Assert.Equal(source2, updatedText.ToString()); @@ -124,12 +115,9 @@ public async Task Test(bool requireCommit) AssertEx.SetEqual(["P"], result.ProjectsToRestart.Select(p => solution.GetRequiredProject(p.Key).Name)); AssertEx.SetEqual(["P"], result.ProjectsToRebuild.Select(p => solution.GetRequiredProject(p).Name)); - if (requireCommit) - { - // Emulate the user making choice to not restart. - // dotnet-watch then waits until Ctrl+R forces restart. - hotReload.DiscardUpdate(); - } + // Emulate the user making choice to not restart. + // dotnet-watch then waits until Ctrl+R forces restart. + hotReload.DiscardUpdate(); updatedText = await GetCommittedDocumentTextAsync(hotReload, documentIdA); Assert.Equal(source3, updatedText.ToString()); diff --git a/src/Features/TestUtilities/EditAndContinue/EditAndContinueWorkspaceTestBase.cs b/src/Features/TestUtilities/EditAndContinue/EditAndContinueWorkspaceTestBase.cs index 308fc11242d2c..fe27c3d83ce62 100644 --- a/src/Features/TestUtilities/EditAndContinue/EditAndContinueWorkspaceTestBase.cs +++ b/src/Features/TestUtilities/EditAndContinue/EditAndContinueWorkspaceTestBase.cs @@ -219,28 +219,14 @@ internal static void DiscardSolutionUpdate(DebuggingSession session) internal static void EndDebuggingSession(DebuggingSession session) => session.EndSession(out _); - internal static async Task<(ModuleUpdates updates, ImmutableArray diagnostics)> EmitSolutionUpdateAsync( - DebuggingSession session, - Solution solution, - ActiveStatementSpanProvider? activeStatementSpanProvider = null) - { - var runningProjects = solution.ProjectIds.ToImmutableDictionary( - keySelector: id => id, - elementSelector: id => new RunningProjectInfo() { AllowPartialUpdate = false, RestartWhenChangesHaveNoEffect = false }); - - var result = await session.EmitSolutionUpdateAsync(solution, runningProjects, activeStatementSpanProvider ?? s_noActiveSpans, CancellationToken.None); - return (result.ModuleUpdates, result.Diagnostics.OrderBy(d => d.ProjectId.DebugName).ToImmutableArray().ToDiagnosticData(solution)); - } - internal static async ValueTask EmitSolutionUpdateAsync( DebuggingSession session, Solution solution, - bool allowPartialUpdate, ActiveStatementSpanProvider? activeStatementSpanProvider = null) { var runningProjects = solution.ProjectIds.ToImmutableDictionary( keySelector: id => id, - elementSelector: id => new RunningProjectInfo() { AllowPartialUpdate = allowPartialUpdate, RestartWhenChangesHaveNoEffect = false }); + elementSelector: id => new RunningProjectOptions() { RestartWhenChangesHaveNoEffect = false }); var results = await session.EmitSolutionUpdateAsync(solution, runningProjects, activeStatementSpanProvider ?? s_noActiveSpans, CancellationToken.None); @@ -249,26 +235,14 @@ internal static async ValueTask EmitSolutionUpdateAsy Assert.Equal(hasTransientError, results.ProjectsToRestart.Any()); Assert.Equal(hasTransientError, results.ProjectsToRebuild.Any()); - if (!allowPartialUpdate) - { - // No updates should be produced if transient error is reported: - Assert.True(!hasTransientError || results.ModuleUpdates.Updates.IsEmpty); - } - return results; } - internal static IEnumerable InspectDiagnostics(ImmutableArray actual) - => actual.Select(InspectDiagnostic); - - internal static string InspectDiagnostic(DiagnosticData diagnostic) - => $"{(string.IsNullOrWhiteSpace(diagnostic.DataLocation.MappedFileSpan.Path) ? diagnostic.ProjectId.ToString() : diagnostic.DataLocation.MappedFileSpan.ToString())}: {diagnostic.Severity} {diagnostic.Id}: {diagnostic.Message}"; - internal static IEnumerable InspectDiagnostics(ImmutableArray actual) - => actual.SelectMany(pd => pd.Diagnostics.Select(d => $"{pd.ProjectId.DebugName}: {InspectDiagnostic(d)}")); + => actual.SelectMany(pd => pd.Diagnostics.Select(d => $"{pd.ProjectId.DebugName}: {InspectDiagnostic(d)}")).Order(); internal static IEnumerable InspectDiagnostics(ImmutableArray<(ProjectId project, ImmutableArray diagnostics)> diagnostics) - => diagnostics.SelectMany(pd => pd.diagnostics.Select(d => $"{pd.project.DebugName}: {InspectDiagnostic(d)}")); + => diagnostics.SelectMany(pd => pd.diagnostics.Select(d => $"{pd.project.DebugName}: {InspectDiagnostic(d)}")).Order(); internal static string InspectDiagnostic(Diagnostic actual) => $"{Inspect(actual.Location)}: {actual.Severity} {actual.Id}: {actual.GetMessage()}"; diff --git a/src/Features/TestUtilities/EditAndContinue/MockEditAndContinueService.cs b/src/Features/TestUtilities/EditAndContinue/MockEditAndContinueService.cs index 4a90b5ecf172f..0d384f2ac1eee 100644 --- a/src/Features/TestUtilities/EditAndContinue/MockEditAndContinueService.cs +++ b/src/Features/TestUtilities/EditAndContinue/MockEditAndContinueService.cs @@ -23,10 +23,9 @@ internal sealed class MockEditAndContinueService() : IEditAndContinueService public Func, bool, bool, DebuggingSessionId>? StartDebuggingSessionImpl; public Action? EndDebuggingSessionImpl; - public Func, ActiveStatementSpanProvider, EmitSolutionUpdateResults>? EmitSolutionUpdateImpl; + public Func, ActiveStatementSpanProvider, EmitSolutionUpdateResults>? EmitSolutionUpdateImpl; public Action? OnSourceFileUpdatedImpl; public Action? CommitSolutionUpdateImpl; - public Action>? UpdateBaselinesImpl; public Action? BreakStateOrCapabilitiesChangedImpl; public Action? DiscardSolutionUpdateImpl; public Func>? GetDocumentDiagnosticsImpl; @@ -40,10 +39,7 @@ public void CommitSolutionUpdate(DebuggingSessionId sessionId) public void DiscardSolutionUpdate(DebuggingSessionId sessionId) => DiscardSolutionUpdateImpl?.Invoke(); - public void UpdateBaselines(DebuggingSessionId sessionId, Solution solution, ImmutableArray rebuiltProjects) - => UpdateBaselinesImpl?.Invoke(solution, rebuiltProjects); - - public ValueTask EmitSolutionUpdateAsync(DebuggingSessionId sessionId, Solution solution, ImmutableDictionary runningProjects, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) + public ValueTask EmitSolutionUpdateAsync(DebuggingSessionId sessionId, Solution solution, ImmutableDictionary runningProjects, ActiveStatementSpanProvider activeStatementSpanProvider, CancellationToken cancellationToken) => new((EmitSolutionUpdateImpl ?? throw new NotImplementedException()).Invoke(solution, runningProjects, activeStatementSpanProvider)); public void EndDebuggingSession(DebuggingSessionId sessionId) diff --git a/src/Features/VisualBasic/Portable/DocumentationComments/VisualBasicDocumentationCommentFormattingService.vb b/src/Features/VisualBasic/Portable/DocumentationComments/VisualBasicDocumentationCommentFormattingService.vb index ec71234351301..7fdc4e03d2cbe 100644 --- a/src/Features/VisualBasic/Portable/DocumentationComments/VisualBasicDocumentationCommentFormattingService.vb +++ b/src/Features/VisualBasic/Portable/DocumentationComments/VisualBasicDocumentationCommentFormattingService.vb @@ -8,9 +8,8 @@ Imports Microsoft.CodeAnalysis.DocumentationComments Imports Microsoft.CodeAnalysis.Host.Mef Namespace Microsoft.CodeAnalysis.VisualBasic.DocumentationComments - - Friend Class VisualBasicDocumentationCommentFormattingService + Friend NotInheritable Class VisualBasicDocumentationCommentFormattingService Inherits AbstractDocumentationCommentFormattingService diff --git a/src/Features/VisualBasic/Portable/LanguageServices/VisualBasicSymbolDisplayService.SymbolDescriptionBuilder.vb b/src/Features/VisualBasic/Portable/LanguageServices/VisualBasicSymbolDisplayService.SymbolDescriptionBuilder.vb index 2f45f4a385428..95de2d1babd9a 100644 --- a/src/Features/VisualBasic/Portable/LanguageServices/VisualBasicSymbolDisplayService.SymbolDescriptionBuilder.vb +++ b/src/Features/VisualBasic/Portable/LanguageServices/VisualBasicSymbolDisplayService.SymbolDescriptionBuilder.vb @@ -11,7 +11,7 @@ Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.LanguageServices Partial Friend Class VisualBasicSymbolDisplayService - Protected Class SymbolDescriptionBuilder + Protected NotInheritable Class SymbolDescriptionBuilder Inherits AbstractSymbolDescriptionBuilder Private Shared ReadOnly s_minimallyQualifiedFormat As SymbolDisplayFormat = SymbolDisplayFormat.MinimallyQualifiedFormat _ @@ -172,6 +172,10 @@ Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.LanguageServices End If End Sub + Protected Overrides Function GetCommentText(trivia As SyntaxTrivia) As String + Return trivia.ToFullString().Substring(1) + End Function + Protected Overrides ReadOnly Property MinimallyQualifiedFormat As SymbolDisplayFormat = s_minimallyQualifiedFormat Protected Overrides ReadOnly Property MinimallyQualifiedFormatWithConstants As SymbolDisplayFormat = s_minimallyQualifiedFormatWithConstants diff --git a/src/Features/VisualBasic/Portable/QuickInfo/VisualBasicQuickInfoService.vb b/src/Features/VisualBasic/Portable/QuickInfo/VisualBasicQuickInfoService.vb index e245c1d97cf45..c827e6ba97b9c 100644 --- a/src/Features/VisualBasic/Portable/QuickInfo/VisualBasicQuickInfoService.vb +++ b/src/Features/VisualBasic/Portable/QuickInfo/VisualBasicQuickInfoService.vb @@ -9,7 +9,7 @@ Imports Microsoft.CodeAnalysis.QuickInfo Namespace Microsoft.CodeAnalysis.VisualBasic.QuickInfo - Friend Class VisualBasicQuickInfoServiceFactory + Friend NotInheritable Class VisualBasicQuickInfoServiceFactory Implements ILanguageServiceFactory @@ -21,13 +21,12 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.QuickInfo Return New VisualBasicQuickInfoService(languageServices.LanguageServices) End Function - End Class - - Friend Class VisualBasicQuickInfoService - Inherits QuickInfoServiceWithProviders + Private NotInheritable Class VisualBasicQuickInfoService + Inherits QuickInfoServiceWithProviders - Public Sub New(services As Host.LanguageServices) - MyBase.New(services) - End Sub + Public Sub New(services As LanguageServices) + MyBase.New(services) + End Sub + End Class End Class End Namespace diff --git a/src/Interactive/Host/Microsoft.CodeAnalysis.InteractiveHost.csproj b/src/Interactive/Host/Microsoft.CodeAnalysis.InteractiveHost.csproj index c486450b640e0..9098badd0229d 100644 --- a/src/Interactive/Host/Microsoft.CodeAnalysis.InteractiveHost.csproj +++ b/src/Interactive/Host/Microsoft.CodeAnalysis.InteractiveHost.csproj @@ -8,6 +8,9 @@ true true true + + + $(DefineConstants);MICROSOFT_CODEANALYSIS_THREADING_NO_CHANNELS true @@ -55,7 +58,6 @@ add an explicit reference to System.Threading.Tasks.Dataflow to ensure that the correct BindingRedirect is included in the app.config of InteractiveHost.exe --> - diff --git a/src/LanguageServer/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticSource.cs b/src/LanguageServer/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticSource.cs index 137716f79981a..1fe4074b6a56f 100644 --- a/src/LanguageServer/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticSource.cs +++ b/src/LanguageServer/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticSource.cs @@ -20,13 +20,12 @@ internal sealed class HotReloadDiagnosticSource(IHotReloadDiagnosticSource sourc public async Task> GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken) { var diagnostics = await source.GetDiagnosticsAsync(new HotReloadRequestContext(context), cancellationToken).ConfigureAwait(false); - var result = diagnostics.Select(diagnostic => DiagnosticData.Create(diagnostic, textDocument)).ToImmutableArray(); + var result = diagnostics.SelectAsArray(diagnostic => DiagnosticData.Create(diagnostic, textDocument)); return result; } public TextDocumentIdentifier? GetDocumentIdentifier() => new() { DocumentUri = textDocument.GetURI() }; public ProjectOrDocumentId GetId() => new(textDocument.Id); public Project GetProject() => textDocument.Project; - public bool IsLiveSource() => true; public string ToDisplayString() => $"{this.GetType().Name}: {textDocument.FilePath ?? textDocument.Name} in {textDocument.Project.Name}"; } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsWorkspaceTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsWorkspaceTests.cs new file mode 100644 index 0000000000000..fd85df0abbd49 --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsWorkspaceTests.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 Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; +using Microsoft.CodeAnalysis.LanguageServer.UnitTests.Miscellaneous; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.UnitTests; +using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.Composition; +using Roslyn.Test.Utilities; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; + +public sealed class FileBasedProgramsWorkspaceTests : AbstractLspMiscellaneousFilesWorkspaceTests, IDisposable +{ + private readonly ILoggerFactory _loggerFactory; + private readonly TestOutputLoggerProvider _loggerProvider; + private readonly TempRoot _tempRoot; + private readonly TempDirectory _mefCacheDirectory; + + public FileBasedProgramsWorkspaceTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + _loggerProvider = new TestOutputLoggerProvider(testOutputHelper); + _loggerFactory = new LoggerFactory([_loggerProvider]); + _tempRoot = new(); + _mefCacheDirectory = _tempRoot.CreateDirectory(); + } + + public void Dispose() + { + _tempRoot.Dispose(); + _loggerProvider.Dispose(); + } + + protected override async ValueTask CreateExportProviderAsync() + { + AsynchronousOperationListenerProvider.Enable(enable: true); + + var (exportProvider, _) = await LanguageServerTestComposition.CreateExportProviderAsync( + _loggerFactory, + includeDevKitComponents: false, + cacheDirectory: _mefCacheDirectory.Path, + extensionPaths: []); + + return exportProvider; + } + + private protected override async ValueTask AddDocumentAsync(TestLspServer testLspServer, string filePath, string content) + { + // For the file-based programs, we want to put them in the real workspace via the real host service + var workspaceFactory = testLspServer.TestWorkspace.ExportProvider.GetExportedValue(); + var project = await workspaceFactory.HostProjectFactory.CreateAndAddToWorkspaceAsync( + Guid.NewGuid().ToString(), + LanguageNames.CSharp, + new ProjectSystemProjectCreationInfo { AssemblyName = Guid.NewGuid().ToString() }, + workspaceFactory.ProjectSystemHostInfo); + + project.AddSourceFile(filePath); + + return workspaceFactory.HostWorkspace.CurrentSolution.GetRequiredProject(project.Id).Documents.Single(); + } + + private protected override Workspace GetHostWorkspace(TestLspServer testLspServer) + { + var workspaceFactory = testLspServer.TestWorkspace.ExportProvider.GetExportedValue(); + return workspaceFactory.HostWorkspace; + } +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Microsoft.CodeAnalysis.LanguageServer.UnitTests.csproj b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Microsoft.CodeAnalysis.LanguageServer.UnitTests.csproj index 0f61561851842..abc6163b26998 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Microsoft.CodeAnalysis.LanguageServer.UnitTests.csproj +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Microsoft.CodeAnalysis.LanguageServer.UnitTests.csproj @@ -15,14 +15,10 @@ - - - - - + diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs index bce854d90bd4c..d0fc5fe098665 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/LanguageServerTestComposition.cs @@ -31,7 +31,7 @@ internal sealed class LanguageServerTestComposition var extensionManager = ExtensionAssemblyManager.Create(serverConfiguration, loggerFactory); var assemblyLoader = new CustomExportAssemblyLoader(extensionManager, loggerFactory); - var exportProvider = await LanguageServerExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, devKitDependencyPath, cacheDirectory, loggerFactory, CancellationToken.None); + var exportProvider = await LanguageServerExportProviderBuilder.CreateExportProviderAsync(TestPaths.GetLanguageServerDirectory(), extensionManager, assemblyLoader, devKitDependencyPath, cacheDirectory, loggerFactory, CancellationToken.None); exportProvider.GetExportedValue().InitializeConfiguration(serverConfiguration); return (exportProvider, assemblyLoader); } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/TestPaths.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/TestPaths.cs index 5b392926b53c4..e925ef9184846 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/TestPaths.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/Utilities/TestPaths.cs @@ -21,6 +21,8 @@ public static string GetDevKitExtensionPath() private const string LanguageServerSubdirectory = "RoslynLSP"; private const string LanguageServerAssemblyFileName = "Microsoft.CodeAnalysis.LanguageServer.dll"; + public static string GetLanguageServerDirectory() + => Path.Combine(AppContext.BaseDirectory, LanguageServerSubdirectory); public static string GetLanguageServerPath() - => Path.Combine(AppContext.BaseDirectory, LanguageServerSubdirectory, LanguageServerAssemblyFileName); + => Path.Combine(GetLanguageServerDirectory(), LanguageServerAssemblyFileName); } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs index 01f6754bd709f..498874b318996 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsProjectSystem.cs @@ -6,7 +6,6 @@ using Microsoft.CodeAnalysis.Features.Workspaces; using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry; -using Microsoft.CodeAnalysis.MetadataAsSource; using Microsoft.CodeAnalysis.MSBuild; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.ProjectSystem; @@ -25,13 +24,11 @@ internal sealed class FileBasedProgramsProjectSystem : LanguageServerProjectLoad { private readonly ILspServices _lspServices; private readonly ILogger _logger; - private readonly IMetadataAsSourceFileService _metadataAsSourceFileService; private readonly VirtualProjectXmlProvider _projectXmlProvider; private readonly LanguageServerWorkspaceFactory _workspaceFactory; public FileBasedProgramsProjectSystem( ILspServices lspServices, - IMetadataAsSourceFileService metadataAsSourceFileService, VirtualProjectXmlProvider projectXmlProvider, LanguageServerWorkspaceFactory workspaceFactory, IFileChangeWatcher fileChangeWatcher, @@ -54,7 +51,6 @@ public FileBasedProgramsProjectSystem( { _lspServices = lspServices; _logger = loggerFactory.CreateLogger(); - _metadataAsSourceFileService = metadataAsSourceFileService; _projectXmlProvider = projectXmlProvider; _workspaceFactory = workspaceFactory; } @@ -74,14 +70,6 @@ public async ValueTask IsMiscellaneousFilesDocumentAsync(TextDocument docu { var documentFilePath = GetDocumentFilePath(uri); - // https://github.com/dotnet/roslyn/issues/78421: MetadataAsSource should be its own workspace - if (_metadataAsSourceFileService.TryAddDocumentToWorkspace(documentFilePath, documentText.Container, out var documentId)) - { - var metadataWorkspace = _metadataAsSourceFileService.TryGetWorkspace(); - Contract.ThrowIfNull(metadataWorkspace); - return metadataWorkspace.CurrentSolution.GetRequiredDocument(documentId); - } - var primordialDoc = AddPrimordialDocument(uri, documentText, languageId); Contract.ThrowIfNull(primordialDoc.FilePath); @@ -120,14 +108,9 @@ TextDocument AddPrimordialDocument(DocumentUri uri, SourceText documentText, str } } - public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri, bool removeFromMetadataWorkspace) + public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri) { var documentPath = GetDocumentFilePath(uri); - if (removeFromMetadataWorkspace && _metadataAsSourceFileService.TryRemoveDocumentFromWorkspace(documentPath)) - { - return; - } - await UnloadProjectAsync(documentPath); } @@ -158,7 +141,7 @@ public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri, bool var isFileBasedProgram = VirtualProjectXmlProvider.IsFileBasedProgram(documentPath, textAndVersion.Text); const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore; - var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, cancellationToken); + var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, virtualProjectPath, dotnetPath: null, cancellationToken); var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, virtualProjectContent, languageName: LanguageNames.CSharp, cancellationToken); return new RemoteProjectLoadResult( @@ -170,4 +153,9 @@ public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri, bool Preferred: buildHostKind, Actual: buildHostKind); } + + protected override async ValueTask OnProjectUnloadedAsync(string projectFilePath) + { + await _projectXmlProvider.UnloadCachedDiagnosticsAsync(projectFilePath); + } } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsWorkspaceProviderFactory.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsWorkspaceProviderFactory.cs index 3754d38086a01..56dc2a9a3b657 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsWorkspaceProviderFactory.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsWorkspaceProviderFactory.cs @@ -8,7 +8,6 @@ using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry; -using Microsoft.CodeAnalysis.MetadataAsSource; using Microsoft.CodeAnalysis.MSBuild; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.ProjectSystem; @@ -19,7 +18,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms; /// -/// Service to create instances. +/// Service to create instances. /// This is not exported as a as it requires /// special base language server dependencies such as the /// @@ -27,7 +26,6 @@ namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms; [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed class FileBasedProgramsWorkspaceProviderFactory( - IMetadataAsSourceFileService metadataAsSourceFileService, VirtualProjectXmlProvider projectXmlProvider, LanguageServerWorkspaceFactory workspaceFactory, IFileChangeWatcher fileChangeWatcher, @@ -42,7 +40,6 @@ public ILspMiscellaneousFilesWorkspaceProvider CreateLspMiscellaneousFilesWorksp { return new FileBasedProgramsProjectSystem( lspServices, - metadataAsSourceFileService, projectXmlProvider, workspaceFactory, fileChangeWatcher, diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/RunApiModels.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/RunApiModels.cs index 5d59892adcdbd..e1605dd25d082 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/RunApiModels.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/RunApiModels.cs @@ -43,7 +43,8 @@ public sealed class Project : RunApiOutput public required ImmutableArray Diagnostics { get; init; } } } - internal sealed class SimpleDiagnostic + + internal sealed record SimpleDiagnostic { public required Position Location { get; init; } public required string Message { get; init; } @@ -51,20 +52,37 @@ internal sealed class SimpleDiagnostic /// /// An adapter of that ensures we JSON-serialize only the necessary fields. /// - public readonly struct Position + public readonly record struct Position { public string Path { get; init; } - public LinePositionSpan Span { get; init; } + public LinePositionSpanInternal Span { get; init; } + } + } - public static implicit operator Position(FileLinePositionSpan fileLinePositionSpan) => new() - { - Path = fileLinePositionSpan.Path, - Span = fileLinePositionSpan.Span, - }; + internal record struct LinePositionInternal + { + public int Line { get; init; } + public int Character { get; init; } + } + + /// + /// Workaround for inability to deserialize directly to . + /// + internal record struct LinePositionSpanInternal + { + public LinePositionInternal Start { get; init; } + public LinePositionInternal End { get; init; } + + public LinePositionSpan ToLinePositionSpan() + { + return new LinePositionSpan( + start: new LinePosition(Start.Line, Start.Character), + end: new LinePosition(End.Line, End.Character)); } } [JsonSerializable(typeof(RunApiInput))] [JsonSerializable(typeof(RunApiOutput))] + [JsonSerializable(typeof(LinePositionSpanInternal))] internal partial class RunFileApiJsonSerializerContext : JsonSerializerContext; } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlDiagnosticSourceProvider.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..fad1ea38ae007 --- /dev/null +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlDiagnosticSourceProvider.cs @@ -0,0 +1,91 @@ +// 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.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; +using Microsoft.CodeAnalysis.Text; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms; + +[Export(typeof(IDiagnosticSourceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class VirtualProjectXmlDiagnosticSourceProvider(VirtualProjectXmlProvider virtualProjectXmlProvider) : IDiagnosticSourceProvider +{ + public const string FileBasedPrograms = nameof(FileBasedPrograms); + + public bool IsDocument => true; + public string Name => FileBasedPrograms; + + public bool IsEnabled(ClientCapabilities clientCapabilities) => true; + + public ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + ImmutableArray sources = context.Document is null + ? [] + : [new VirtualProjectXmlDiagnosticSource(context.Document, virtualProjectXmlProvider)]; + + return ValueTask.FromResult(sources); + } + + private sealed class VirtualProjectXmlDiagnosticSource(Document document, VirtualProjectXmlProvider virtualProjectXmlProvider) : IDiagnosticSource + { + public async Task> GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(document.FilePath)) + return []; + + var simpleDiagnostics = await virtualProjectXmlProvider.GetCachedDiagnosticsAsync(document.FilePath, cancellationToken); + if (simpleDiagnostics.IsDefaultOrEmpty) + return []; + + var diagnosticDatas = new FixedSizeArrayBuilder(simpleDiagnostics.Length); + foreach (var simpleDiagnostic in simpleDiagnostics) + { + var location = new FileLinePositionSpan(simpleDiagnostic.Location.Path, simpleDiagnostic.Location.Span.ToLinePositionSpan()); + var diagnosticData = new DiagnosticData( + id: FileBasedPrograms, + category: FileBasedPrograms, + message: simpleDiagnostic.Message, + severity: DiagnosticSeverity.Error, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + // Warning level 0 is used as a placeholder when the diagnostic has error severity + warningLevel: 0, + // Mark these diagnostics as build errors so they can be overridden by diagnostics from an explicit build. + customTags: [WellKnownDiagnosticTags.Build], + properties: ImmutableDictionary.Empty, + projectId: document.Project.Id, + location: new DiagnosticDataLocation(location, document.Id) + ); + diagnosticDatas.Add(diagnosticData); + } + return diagnosticDatas.MoveToImmutable(); + } + + public TextDocumentIdentifier? GetDocumentIdentifier() + { + return !string.IsNullOrEmpty(document.FilePath) + ? new VSTextDocumentIdentifier { ProjectContext = ProtocolConversions.ProjectToProjectContext(document.Project), DocumentUri = document.GetURI() } + : null; + } + + public ProjectOrDocumentId GetId() + { + return new ProjectOrDocumentId(document.Id); + } + + public Project GetProject() + { + return document.Project; + } + + public string ToDisplayString() => nameof(VirtualProjectXmlDiagnosticSource); + } +} diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs index a5143086c4046..a5fe8cd5606bf 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/VirtualProjectXmlProvider.cs @@ -11,6 +11,7 @@ using System.Text.Json; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.Logging; @@ -21,9 +22,63 @@ namespace Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms; [Export(typeof(VirtualProjectXmlProvider)), Shared] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] -internal class VirtualProjectXmlProvider(DotnetCliHelper dotnetCliHelper) +internal class VirtualProjectXmlProvider(IDiagnosticsRefresher diagnosticRefresher, DotnetCliHelper dotnetCliHelper) { + private readonly SemaphoreSlim _gate = new(initialCount: 1); + private readonly Dictionary> _diagnosticsByFilePath = []; + + internal async ValueTask> GetCachedDiagnosticsAsync(string path, CancellationToken cancellationToken) + { + using (await _gate.DisposableWaitAsync(cancellationToken)) + { + _diagnosticsByFilePath.TryGetValue(path, out var diagnostics); + return diagnostics; + } + } + + internal async ValueTask UnloadCachedDiagnosticsAsync(string path) + { + using (await _gate.DisposableWaitAsync(CancellationToken.None)) + { + _diagnosticsByFilePath.Remove(path); + } + } + internal async Task<(string VirtualProjectXml, ImmutableArray Diagnostics)?> GetVirtualProjectContentAsync(string documentFilePath, ILogger logger, CancellationToken cancellationToken) + { + var result = await GetVirtualProjectContentImplAsync(documentFilePath, logger, cancellationToken); + if (result is { } project) + { + using (await _gate.DisposableWaitAsync(cancellationToken)) + { + _diagnosticsByFilePath.TryGetValue(documentFilePath, out var previousCachedDiagnostics); + _diagnosticsByFilePath[documentFilePath] = project.Diagnostics; + + // check for difference, and signal to host to update if so. + if (previousCachedDiagnostics.IsDefault || !project.Diagnostics.SequenceEqual(previousCachedDiagnostics)) + diagnosticRefresher.RequestWorkspaceRefresh(); + } + } + else + { + using (await _gate.DisposableWaitAsync(CancellationToken.None)) + { + if (_diagnosticsByFilePath.TryGetValue(documentFilePath, out var diagnostics)) + { + _diagnosticsByFilePath.Remove(documentFilePath); + if (!diagnostics.IsDefaultOrEmpty) + { + // diagnostics have changed from "non-empty" to "unloaded". refresh. + diagnosticRefresher.RequestWorkspaceRefresh(); + } + } + } + } + + return result; + } + + private async Task<(string VirtualProjectXml, ImmutableArray Diagnostics)?> GetVirtualProjectContentImplAsync(string documentFilePath, ILogger logger, CancellationToken cancellationToken) { var workingDirectory = Path.GetDirectoryName(documentFilePath); var process = dotnetCliHelper.Run(["run-api"], workingDirectory, shouldLocalizeOutput: true, keepStandardInputOpen: true); @@ -70,7 +125,6 @@ internal class VirtualProjectXmlProvider(DotnetCliHelper dotnetCliHelper) if (response is RunApiOutput.Project project) { - return (project.Content, project.Diagnostics); } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs index 5dbc51ba3f3de..57d0d90555c93 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectLoader.cs @@ -181,6 +181,14 @@ protected sealed record RemoteProjectLoadResult(RemoteProjectFile ProjectFile, P protected abstract Task TryLoadProjectInMSBuildHostAsync( BuildHostProcessManager buildHostProcessManager, string projectPath, CancellationToken cancellationToken); + /// Called after a project is unloaded to allow the subtype to clean up any resources associated with the project. + /// + /// Note that this refers to unloading of the project on the project-system level. + /// So, for example, changing the target frameworks of a project, or transitioning between + /// "file-based program" and "true miscellaneous file", will not result in this being called. + /// + protected abstract ValueTask OnProjectUnloadedAsync(string projectFilePath); + /// True if the project needs a NuGet restore, false otherwise. private async Task ReloadProjectAsync(ProjectToLoad projectToLoad, ToastErrorReporter toastErrorReporter, BuildHostProcessManager buildHostProcessManager, CancellationToken cancellationToken) { @@ -445,5 +453,7 @@ protected async ValueTask UnloadProjectAsync(string projectPath) throw ExceptionUtilities.UnexpectedValue(loadState); } } + + await OnProjectUnloadedAsync(projectPath); } } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs index 7e86d7845f54d..9d52bb416242f 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs @@ -92,4 +92,10 @@ public async Task OpenProjectsAsync(ImmutableArray projectFilePaths) var loadedFile = await buildHost.LoadProjectFileAsync(projectPath, languageName, cancellationToken); return new RemoteProjectLoadResult(loadedFile, _hostProjectFactory, IsMiscellaneousFile: false, preferredBuildHostKind, actualBuildHostKind); } + + protected override ValueTask OnProjectUnloadedAsync(string projectFilePath) + { + // Nothing else to unload for ordinary projects. + return ValueTask.CompletedTask; + } } diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs index 01ec48364d582..7dcc9c22864cc 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LoadedProject.cs @@ -154,7 +154,7 @@ public void Dispose() DocumentFileInfoComparer.Instance, document => _projectSystemProject.AddSourceFile(document.FilePath, folders: document.Folders), document => _projectSystemProject.RemoveSourceFile(document.FilePath), - "Project {0} now has {1} source file(s)."); + "Project {0} now has {1} source file(s). ({2} added, {3} removed.)"); var relativePathResolver = new RelativePathResolver(commandLineArguments.ReferencePaths, commandLineArguments.BaseDirectory); var metadataReferences = commandLineArguments.MetadataReferences.Select(cr => @@ -167,7 +167,7 @@ public void Dispose() FileUtilities.ResolveRelativePath(cr.Reference, commandLineArguments.BaseDirectory); return absolutePath is not null ? new CommandLineReference(absolutePath, cr.Properties) : default; - }).Where(static cr => cr.Reference is not null).ToImmutableArray(); + }).WhereAsArray(static cr => cr.Reference is not null); UpdateProjectSystemProjectCollection( metadataReferences, @@ -175,7 +175,7 @@ public void Dispose() EqualityComparer.Default, // CommandLineReference already implements equality reference => _projectSystemProject.AddMetadataReference(reference.Reference, reference.Properties), reference => _projectSystemProject.RemoveMetadataReference(reference.Reference, reference.Properties), - "Project {0} now has {1} reference(s)."); + "Project {0} now has {1} reference(s). ({2} added, {3} removed.)"); // 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; @@ -185,7 +185,7 @@ public void Dispose() // Note that unlike regular references, we do not resolve these with the relative path resolver that searches reference paths var absolutePath = FileUtilities.ResolveRelativePath(cr.FilePath, commandLineArguments.BaseDirectory); return absolutePath is not null ? new CommandLineAnalyzerReference(absolutePath) : default; - }).Where(static cr => cr.FilePath is not null).ToImmutableArray(); + }).WhereAsArray(static cr => cr.FilePath is not null); UpdateProjectSystemProjectCollection( analyzerReferences, @@ -193,7 +193,7 @@ public void Dispose() EqualityComparer.Default, // CommandLineAnalyzerReference already implements equality reference => _projectSystemProject.AddAnalyzerReference(reference.FilePath), reference => _projectSystemProject.RemoveAnalyzerReference(reference.FilePath), - "Project {0} now has {1} analyzer reference(s)."); + "Project {0} now has {1} analyzer reference(s). ({2} added, {3} removed.)"); _mostRecentAnalyzerReferences = analyzerReferences; @@ -203,7 +203,7 @@ public void Dispose() DocumentFileInfoComparer.Instance, document => _projectSystemProject.AddAdditionalFile(document.FilePath, folders: document.Folders), document => _projectSystemProject.RemoveAdditionalFile(document.FilePath), - "Project {0} now has {1} additional file(s)."); + "Project {0} now has {1} additional file(s). ({2} added, {3} removed.)"); UpdateProjectSystemProjectCollection( newProjectInfo.AnalyzerConfigDocuments, @@ -211,7 +211,7 @@ public void Dispose() DocumentFileInfoComparer.Instance, document => _projectSystemProject.AddAnalyzerConfigFile(document.FilePath), document => _projectSystemProject.RemoveAnalyzerConfigFile(document.FilePath), - "Project {0} now has {1} analyzer config file(s)."); + "Project {0} now has {1} analyzer config file(s). ({2} added, {3} removed.)"); UpdateProjectSystemProjectCollection( newProjectInfo.AdditionalDocuments.Where(TreatAsIsDynamicFile), @@ -219,7 +219,7 @@ public void Dispose() DocumentFileInfoComparer.Instance, document => _projectSystemProject.AddDynamicSourceFile(document.FilePath, folders: []), document => _projectSystemProject.RemoveDynamicSourceFile(document.FilePath), - "Project {0} now has {1} dynamic file(s)."); + "Project {0} now has {1} dynamic file(s). ({2} added, {3} removed.)"); WatchProjectAssetsFile(newProjectInfo); @@ -243,33 +243,32 @@ public void Dispose() var telemetryInfo = new ProjectLoadTelemetryReporter.TelemetryInfo { OutputKind = outputKind, MetadataReferences = metadataReferences }; return (telemetryInfo, needsRestore); - // logMessage should be a string with two placeholders; the first is the project name, the second is the number of items. + // logMessage must have 4 placeholders: project name, number of items, added items count, and removed items count. void UpdateProjectSystemProjectCollection(IEnumerable loadedCollection, IEnumerable? oldLoadedCollection, IEqualityComparer comparer, Action addItem, Action removeItem, string logMessage) { var newItems = new HashSet(loadedCollection, comparer); - var oldItems = new HashSet(comparer); - var oldItemsCount = oldItems.Count; + var oldItems = new HashSet(oldLoadedCollection ?? [], comparer); - if (oldLoadedCollection != null) - { - foreach (var item in oldLoadedCollection) - oldItems.Add(item); - } + var addedCount = 0; foreach (var newItem in newItems) { // 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); + addedCount++; + } } + var removedCount = oldItems.Count; foreach (var oldItem in oldItems) { removeItem(oldItem); } - if (newItems.Count != oldItemsCount) - logger.LogTrace(logMessage, projectFullPathWithTargetFramework, newItems.Count); + if (addedCount != 0 || removedCount != 0) + logger.LogTrace(logMessage, projectFullPathWithTargetFramework, newItems.Count, addedCount, removedCount); } void WatchProjectAssetsFile(ProjectFileInfo currentProjectInfo) diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectTelemetry/ProjectLoadTelemetryReporter.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectTelemetry/ProjectLoadTelemetryReporter.cs index 086f2f35824e5..eb69562f72a75 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectTelemetry/ProjectLoadTelemetryReporter.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/ProjectTelemetry/ProjectLoadTelemetryReporter.cs @@ -103,8 +103,7 @@ private static ImmutableDictionary GetUniqueHashedFileExtensionsAnd var sourceFiles = projectFileInfo.Documents .Concat(projectFileInfo.AdditionalDocuments) .Concat(projectFileInfo.AnalyzerConfigDocuments) - .Where(d => !d.IsGenerated) - .SelectAsArray(d => d.FilePath); + .SelectAsArray(d => !d.IsGenerated, d => d.FilePath); var allFiles = contentFiles.Concat(sourceFiles); var fileCounts = new Dictionary(); foreach (var file in allFiles) diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs index 82eb3cf9d89d7..64623a1dc24b4 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServerExportProviderBuilder.cs @@ -31,6 +31,7 @@ private LanguageServerExportProviderBuilder( } public static async Task CreateExportProviderAsync( + string baseDirectory, ExtensionAssemblyManager extensionManager, IAssemblyLoader assemblyLoader, string? devKitDependencyPath, @@ -38,8 +39,6 @@ public static async Task CreateExportProviderAsync( ILoggerFactory loggerFactory, CancellationToken cancellationToken) { - var baseDirectory = AppContext.BaseDirectory; - // Load any Roslyn assemblies from the extension directory using var _ = ArrayBuilder.GetInstance(out var assemblyPathsBuilder); @@ -90,13 +89,13 @@ protected override Task CreateExportProviderAsync(CancellationTo return base.CreateExportProviderAsync(cancellationToken); } - protected override bool ContainsUnexpectedErrors(IEnumerable erroredParts, ImmutableList partDiscoveryExceptions) + protected override bool ContainsUnexpectedErrors(IEnumerable erroredParts) { // 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. var expectedErrorPartsSet = new HashSet(["CSharpMapCodeService", "PythiaSignatureHelpProvider", "CopilotSemanticSearchQueryExecutor"]); var hasUnexpectedErroredParts = erroredParts.Any(part => !expectedErrorPartsSet.Contains(part)); - return hasUnexpectedErroredParts || !partDiscoveryExceptions.IsEmpty; + return hasUnexpectedErroredParts; } protected override Task WriteCompositionCacheAsync(string compositionCacheFile, CompositionConfiguration config, CancellationToken cancellationToken) diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs index 435c54303a75f..f37d5caf2900e 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Program.cs @@ -103,7 +103,7 @@ static async Task RunAsync(ServerConfiguration serverConfiguration, Cancellation var cacheDirectory = Path.Combine(Path.GetDirectoryName(typeof(Program).Assembly.Location)!, "cache"); - using var exportProvider = await LanguageServerExportProviderBuilder.CreateExportProviderAsync(extensionManager, assemblyLoader, serverConfiguration.DevKitDependencyPath, cacheDirectory, loggerFactory, cancellationToken); + using var exportProvider = await LanguageServerExportProviderBuilder.CreateExportProviderAsync(AppContext.BaseDirectory, extensionManager, assemblyLoader, serverConfiguration.DevKitDependencyPath, cacheDirectory, loggerFactory, cancellationToken); // LSP server doesn't have the pieces yet to support 'balanced' mode for source-generators. Hardcode us to // 'automatic' for now. diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/LspExtractClassOptionsService.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/LspExtractClassOptionsService.cs index 5a297e8549fd2..d6e8d61270273 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/LspExtractClassOptionsService.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Services/LspExtractClassOptionsService.cs @@ -32,7 +32,7 @@ internal sealed class LspExtractClassOptionsService() : IExtractClassOptionsServ }) : selectedMembers; - var memberAnalysisResults = symbolsToUse.Select(m => new ExtractClassMemberAnalysisResult(m, makeAbstract: false)).ToImmutableArray(); + var memberAnalysisResults = symbolsToUse.SelectAsArray(m => new ExtractClassMemberAnalysisResult(m, makeAbstract: false)); const string name = "NewBaseType"; var extension = document.Project.Language == LanguageNames.CSharp ? ".cs" : ".vb"; return new( diff --git a/src/LanguageServer/ProtocolUnitTests/Miscellaneous/LspMiscellaneousFilesWorkspaceTests.cs b/src/LanguageServer/Protocol.TestUtilities/AbstractLspMiscellaneousFilesWorkspaceTests.cs similarity index 63% rename from src/LanguageServer/ProtocolUnitTests/Miscellaneous/LspMiscellaneousFilesWorkspaceTests.cs rename to src/LanguageServer/Protocol.TestUtilities/AbstractLspMiscellaneousFilesWorkspaceTests.cs index 7319ac6a09ab2..ab29e37ae4f5d 100644 --- a/src/LanguageServer/ProtocolUnitTests/Miscellaneous/LspMiscellaneousFilesWorkspaceTests.cs +++ b/src/LanguageServer/Protocol.TestUtilities/AbstractLspMiscellaneousFilesWorkspaceTests.cs @@ -2,9 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.Text; using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; using Xunit; @@ -13,19 +18,18 @@ namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.Miscellaneous; -public sealed class LspMiscellaneousFilesWorkspaceTests : AbstractLanguageServerProtocolTests +public abstract class AbstractLspMiscellaneousFilesWorkspaceTests : AbstractLanguageServerProtocolTests { - public LspMiscellaneousFilesWorkspaceTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + public AbstractLspMiscellaneousFilesWorkspaceTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { } [Theory, CombinatorialData] public async Task TestLooseFile_Opened(bool mutatingLspWorkspace) { - // Create a server that supports LSP misc files and verify no misc files present. await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); - Assert.Null(GetMiscellaneousDocument(testLspServer)); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); // Open an empty loose file and make a request to verify it gets added to the misc workspace. var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); @@ -45,14 +49,10 @@ void M() [Theory, CombinatorialData] public async Task TestLooseFile_Changed(bool mutatingLspWorkspace) { - // Create a server that supports LSP misc files and verify no misc files present. await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); - var miscWorkspace = testLspServer.GetManager().GetTestAccessor().GetLspMiscellaneousFilesWorkspace(); - testLspServer.TestWorkspace.GetService().Register(miscWorkspace); - - Assert.Null(GetMiscellaneousDocument(testLspServer)); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); @@ -61,7 +61,7 @@ public async Task TestLooseFile_Changed(bool mutatingLspWorkspace) await AssertFileInMiscWorkspaceAsync(testLspServer, looseFileUri).ConfigureAwait(false); // Assert that the misc workspace contains the initial document. - var miscWorkspaceText = await GetMiscellaneousDocument(testLspServer)!.GetTextAsync(CancellationToken.None); + var miscWorkspaceText = await (await GetMiscellaneousDocumentAsync(testLspServer))!.GetTextAsync(CancellationToken.None); Assert.Empty(miscWorkspaceText.ToString()); // Make a text change to the loose file and verify requests appropriately reflect the changes. @@ -79,17 +79,16 @@ void M() await AssertFileInMiscWorkspaceAsync(testLspServer, looseFileUri).ConfigureAwait(false); // Assert that the misc workspace contains the updated document. - miscWorkspaceText = await GetMiscellaneousDocument(testLspServer)!.GetTextAsync(CancellationToken.None); + miscWorkspaceText = await (await GetMiscellaneousDocumentAsync(testLspServer))!.GetTextAsync(CancellationToken.None); Assert.Contains("class A", miscWorkspaceText.ToString()); } [Theory, CombinatorialData] public async Task TestLooseFile_Closed(bool mutatingLspWorkspace) { - // Create a server that supports LSP misc files and verify no misc files present. await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); - Assert.Null(GetMiscellaneousDocument(testLspServer)); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); // Open an empty loose file and make a request to verify it gets added to the misc workspace. var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); @@ -105,7 +104,7 @@ void M() // Verify the loose file is removed from the misc workspace on close. await testLspServer.CloseDocumentAsync(looseFileUri).ConfigureAwait(false); - Assert.Null(GetMiscellaneousDocument(testLspServer)); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); } [Theory, CombinatorialData] @@ -122,13 +121,14 @@ void M() """; // Create a server that supports LSP misc files and verify no misc files present. - await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); - Assert.Null(GetMiscellaneousDocument(testLspServer)); + await using var testLspServer = await CreateTestLspServerAsync("", mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); // Open a file that is part of a registered workspace and verify it is not present in the misc workspace. - var fileInWorkspaceUri = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single().GetURI(); - await testLspServer.OpenDocumentAsync(fileInWorkspaceUri).ConfigureAwait(false); - Assert.Null(GetMiscellaneousDocument(testLspServer)); + var document = await AddDocumentAsync(testLspServer, "C:\\SomeFile.cs", markup).ConfigureAwait(false); + var fileInWorkspaceUri = document.GetURI(); + await testLspServer.OpenDocumentAsync(fileInWorkspaceUri, markup).ConfigureAwait(false); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); } [Theory, CombinatorialData] @@ -146,7 +146,7 @@ void M() // Create a server that supports LSP misc files and verify no misc files present. await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); - Assert.Null(GetMiscellaneousDocument(testLspServer)); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); // Open an empty loose file and make a request to verify it gets added to the misc workspace. // Include some Unicode characters to test URL handling. @@ -164,34 +164,24 @@ void M() var documentPath = ProtocolConversions.GetDocumentFilePathFromUri(looseFileUri.GetRequiredParsedUri()); // Update the workspace to contain the loose file. - var project = testLspServer.GetCurrentSolution().Projects.Single(); - var documentInfo = DocumentInfo.Create( - DocumentId.CreateNewId(project.Id), - documentPath, - sourceCodeKind: SourceCodeKind.Regular, - loader: new TestTextLoader(source), - filePath: documentPath); - testLspServer.TestWorkspace.OnDocumentAdded(documentInfo); - await WaitForWorkspaceOperationsAsync(testLspServer.TestWorkspace); - - Assert.Contains(documentPath, testLspServer.GetCurrentSolution().Projects.Single().Documents.Select(d => d.FilePath)); + await AddDocumentAsync(testLspServer, documentPath, source); + Assert.Contains(documentPath, GetHostWorkspace(testLspServer).CurrentSolution.Projects.Single().Documents.Select(d => d.FilePath)); // Verify that the manager returns the file that has been added to the main workspace. await AssertFileInMainWorkspaceAsync(testLspServer, looseFileUri).ConfigureAwait(false); // Make sure doc was removed from misc workspace. Assert.False(miscWorkspace.CurrentSolution.ContainsDocument(miscDocument.Id)); - Assert.Null(GetMiscellaneousDocument(testLspServer)); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); } [Theory, CombinatorialData] public async Task TestLooseFile_RazorFile(bool mutatingLspWorkspace) { - // Create a server that supports LSP misc files and verify no misc files present. await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); - Assert.Null(GetMiscellaneousDocument(testLspServer)); - Assert.Null(GetMiscellaneousAdditionalDocument(testLspServer)); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + Assert.Null(await GetMiscellaneousAdditionalDocumentAsync(testLspServer)); // Open an empty loose file and make a request to verify it gets added to the misc workspace. var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.razor"); @@ -199,25 +189,24 @@ public async Task TestLooseFile_RazorFile(bool mutatingLspWorkspace) // Trigger a request and assert we got a file in the misc workspace. await AssertFileInMiscWorkspaceAsync(testLspServer, looseFileUri).ConfigureAwait(false); - Assert.Null(GetMiscellaneousDocument(testLspServer)); - Assert.NotNull(GetMiscellaneousAdditionalDocument(testLspServer)); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + Assert.NotNull(await GetMiscellaneousAdditionalDocumentAsync(testLspServer)); // Trigger another request and assert we got a file in the misc workspace. await AssertFileInMiscWorkspaceAsync(testLspServer, looseFileUri).ConfigureAwait(false); - Assert.NotNull(GetMiscellaneousAdditionalDocument(testLspServer)); + Assert.NotNull(await GetMiscellaneousAdditionalDocumentAsync(testLspServer)); await testLspServer.CloseDocumentAsync(looseFileUri).ConfigureAwait(false); - Assert.Null(GetMiscellaneousDocument(testLspServer)); - Assert.Null(GetMiscellaneousAdditionalDocument(testLspServer)); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + Assert.Null(await GetMiscellaneousAdditionalDocumentAsync(testLspServer)); } [Theory, CombinatorialData] public async Task TestLooseFile_RequestedTwiceAndClosed(bool mutatingLspWorkspace) { - // Create a server that supports LSP misc files and verify no misc files present. await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); - Assert.Null(GetMiscellaneousDocument(testLspServer)); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); // Open an empty loose file and make a request to verify it gets added to the misc workspace. var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\SomeFile.cs"); @@ -237,16 +226,15 @@ void M() await AssertFileInMiscWorkspaceAsync(testLspServer, looseFileUri).ConfigureAwait(false); await testLspServer.CloseDocumentAsync(looseFileUri).ConfigureAwait(false); - Assert.Null(GetMiscellaneousDocument(testLspServer)); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); } [Theory, CombinatorialData] public async Task TestLooseFile_OpenedWithLanguageId(bool mutatingLspWorkspace) { - // Create a server that supports LSP misc files and verify no misc files present. await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); - Assert.Null(GetMiscellaneousDocument(testLspServer)); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); // Open an empty loose file that hasn't been saved with a name. @@ -263,7 +251,7 @@ void M() // Verify file is added to the misc file workspace. await AssertFileInMiscWorkspaceAsync(testLspServer, looseFileUri).ConfigureAwait(false); - var miscDoc = GetMiscellaneousDocument(testLspServer); + var miscDoc = await GetMiscellaneousDocumentAsync(testLspServer); AssertEx.NotNull(miscDoc); Assert.Equal(LanguageNames.CSharp, miscDoc.Project.Language); } @@ -271,10 +259,9 @@ void M() [Theory, CombinatorialData] public async Task TestLooseFile_OpenedWithLanguageIdWithSubsequentRequest(bool mutatingLspWorkspace) { - // Create a server that supports LSP misc files and verify no misc files present. await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); - Assert.Null(GetMiscellaneousDocument(testLspServer)); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); // Open an empty loose file that hasn't been saved with a name. @@ -303,7 +290,7 @@ void M() // Verify file was added to the misc file workspace. await AssertFileInMiscWorkspaceAsync(testLspServer, looseFileUri).ConfigureAwait(false); - var miscDoc = GetMiscellaneousDocument(testLspServer); + var miscDoc = await GetMiscellaneousDocumentAsync(testLspServer); AssertEx.NotNull(miscDoc); Assert.Equal(LanguageNames.CSharp, miscDoc.Project.Language); @@ -315,26 +302,92 @@ void M() Assert.Equal(7, result.Single().Range.End.Character); } - private static async Task AssertFileInMiscWorkspaceAsync(TestLspServer testLspServer, DocumentUri fileUri) + [Theory, CombinatorialData] + public async Task TestLspTransfersFromMiscellaneousFilesToHostWorkspaceAsync(bool mutatingLspWorkspace, bool waitForWorkspace, bool fileBasedProgramContent) { - var (lspWorkspace, _, _) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = fileUri }, CancellationToken.None); - Assert.Equal(testLspServer.GetManagerAccessor().GetLspMiscellaneousFilesWorkspace(), lspWorkspace); + var markup = fileBasedProgramContent ? "Console.WriteLine();" : "class C { }"; + + // Create a server that includes the LSP misc files workspace so we can test transfers to and from it. + await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + + // Include some Unicode characters to test URL handling. + using var tempRoot = new TempRoot(); + var newDocumentFilePath = Path.Combine(tempRoot.CreateDirectory().Path, "ue25b\ud86d\udeac.cs"); + + // If this is file based, we're going to be inspecting the actual content on disk as a part of a dotnet run-api invocation + if (fileBasedProgramContent) + File.WriteAllText(newDocumentFilePath, markup); + var newDocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(newDocumentFilePath); + + // Open the document via LSP before the workspace sees it. + await testLspServer.OpenDocumentAsync(newDocumentUri, "LSP text"); + + // Verify it is a miscellaneous document of some kind + var (_, miscDocument) = await GetLspWorkspaceAndDocumentAsync(newDocumentUri, testLspServer).ConfigureAwait(false); + Assert.NotNull(miscDocument); + Assert.True(await testLspServer.GetManagerAccessor().IsMiscellaneousFilesDocumentAsync(miscDocument)); + Assert.Equal("LSP text", (await miscDocument.GetTextAsync(CancellationToken.None)).ToString()); + + if (waitForWorkspace) + { + // Optionally wait for the workspace so we can test what happens if we're seeing if it's a file-based program; otherwise we can test what + // happens if that analysis is still happening while we're loading real solutions. + await testLspServer.TestWorkspace.GetService().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync(); + } + + // Make a change and verify the misc document is updated. + await testLspServer.InsertTextAsync(newDocumentUri, (0, 0, "More LSP text")); + (_, miscDocument) = await GetLspWorkspaceAndDocumentAsync(newDocumentUri, testLspServer).ConfigureAwait(false); + AssertEx.NotNull(miscDocument); + var miscText = await miscDocument.GetTextAsync(CancellationToken.None); + Assert.Equal("More LSP textLSP text", miscText.ToString()); + + // Update the registered workspace with the new document. + var newDocumentId = (await AddDocumentAsync(testLspServer, newDocumentFilePath, "New Doc")).Id; + + // Verify that the newly added document in the registered workspace is returned. + var (documentWorkspace, document) = await GetLspWorkspaceAndDocumentAsync(newDocumentUri, testLspServer).ConfigureAwait(false); + AssertEx.NotNull(document); + Assert.Equal(GetHostWorkspace(testLspServer), documentWorkspace); + Assert.False(await testLspServer.GetManagerAccessor().IsMiscellaneousFilesDocumentAsync(document)); + Assert.Equal(newDocumentId, document.Id); + // Verify we still are using the tracked LSP text for the document. + var documentText = await document.GetTextAsync(CancellationToken.None); + Assert.Equal("More LSP textLSP text", documentText.ToString()); } - private static async Task AssertFileInMainWorkspaceAsync(TestLspServer testLspServer, DocumentUri fileUri) + private protected abstract ValueTask AddDocumentAsync(TestLspServer testLspServer, string filePath, string content); + private protected abstract Workspace GetHostWorkspace(TestLspServer testLspServer); + + private static async Task<(Workspace? workspace, Document? document)> GetLspWorkspaceAndDocumentAsync(DocumentUri uri, TestLspServer testLspServer) { - var (lspWorkspace, _, _) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = fileUri }, CancellationToken.None).ConfigureAwait(false); - Assert.Equal(testLspServer.TestWorkspace, lspWorkspace); + var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(CreateTextDocumentIdentifier(uri), CancellationToken.None).ConfigureAwait(false); + return (workspace, document as Document); + } + + private static async ValueTask GetMiscellaneousDocumentAsync(TestLspServer testLspServer) + { + var documents = await testLspServer.GetManagerAccessor().GetMiscellaneousDocumentsAsync(static p => p.Documents).ToImmutableArrayAsync(CancellationToken.None); + return documents.SingleOrDefault(); + } + + private static async ValueTask GetMiscellaneousAdditionalDocumentAsync(TestLspServer testLspServer) + { + var documents = await testLspServer.GetManagerAccessor().GetMiscellaneousDocumentsAsync(static p => p.AdditionalDocuments).ToImmutableArrayAsync(CancellationToken.None); + return documents.SingleOrDefault(); } - private static Document? GetMiscellaneousDocument(TestLspServer testLspServer) + private static async Task AssertFileInMiscWorkspaceAsync(TestLspServer testLspServer, DocumentUri fileUri) { - return testLspServer.GetManagerAccessor().GetLspMiscellaneousFilesWorkspace()!.CurrentSolution.Projects.SingleOrDefault()?.Documents.SingleOrDefault(); + var (_, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = fileUri }, CancellationToken.None); + Assert.NotNull(document); + Assert.True(await testLspServer.GetManagerAccessor().IsMiscellaneousFilesDocumentAsync(document)); } - private static TextDocument? GetMiscellaneousAdditionalDocument(TestLspServer testLspServer) + private async Task AssertFileInMainWorkspaceAsync(TestLspServer testLspServer, DocumentUri fileUri) { - return testLspServer.GetManagerAccessor().GetLspMiscellaneousFilesWorkspace()!.CurrentSolution.Projects.SingleOrDefault()?.AdditionalDocuments.SingleOrDefault(); + var (lspWorkspace, _, _) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = fileUri }, CancellationToken.None).ConfigureAwait(false); + Assert.Equal(GetHostWorkspace(testLspServer), lspWorkspace); } private static async Task RunGetHoverAsync(TestLspServer testLspServer, LSP.Location caret) diff --git a/src/LanguageServer/Protocol.TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs b/src/LanguageServer/Protocol.TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs index 9de1171a566ac..9cad08424bb7c 100644 --- a/src/LanguageServer/Protocol.TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs +++ b/src/LanguageServer/Protocol.TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs @@ -21,12 +21,14 @@ using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CodeAnalysis.LanguageServer.Handler.CodeActions; using Microsoft.CodeAnalysis.LanguageServer.Handler.Completion; +using Microsoft.CodeAnalysis.MetadataAsSource; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; using Microsoft.CommonLanguageServerProtocol.Framework; +using Microsoft.VisualStudio.Composition; using Nerdbank.Streams; using Roslyn.LanguageServer.Protocol; using Roslyn.Utilities; @@ -101,6 +103,7 @@ private protected sealed class OrderLocations : Comparer public override int Compare(LSP.Location? x, LSP.Location? y) => CompareLocations(x, y); } + protected virtual ValueTask CreateExportProviderAsync() => ValueTask.FromResult(Composition.ExportProviderFactory.CreateExportProvider()); protected virtual TestComposition Composition => FeaturesLspComposition; private protected virtual TestAnalyzerReferenceByLanguage CreateTestAnalyzersReference() @@ -306,12 +309,12 @@ private protected Task CreateTestLspServerAsync([StringSyntax(Pre private protected Task CreateVisualBasicTestLspServerAsync(string markup, bool mutatingLspWorkspace, InitializationOptions? initializationOptions = null, TestComposition? composition = null) => CreateTestLspServerAsync([markup], LanguageNames.VisualBasic, mutatingLspWorkspace, initializationOptions, composition); - private protected Task CreateTestLspServerAsync( + private protected async Task CreateTestLspServerAsync( string[] markups, string languageName, bool mutatingLspWorkspace, InitializationOptions? initializationOptions, TestComposition? composition = null, bool commonReferences = true) { var lspOptions = initializationOptions ?? new InitializationOptions(); - var workspace = CreateWorkspace(lspOptions, workspaceKind: null, mutatingLspWorkspace, composition); + var workspace = await CreateWorkspaceAsync(lspOptions, workspaceKind: null, mutatingLspWorkspace, composition); workspace.InitializeDocuments( LspTestWorkspace.CreateWorkspaceElement( @@ -323,10 +326,10 @@ private protected Task CreateTestLspServerAsync( commonReferences: commonReferences), openDocuments: false); - return CreateTestLspServerAsync(workspace, lspOptions, languageName); + return await CreateTestLspServerAsync(workspace, lspOptions, languageName); } - private async Task CreateTestLspServerAsync(LspTestWorkspace workspace, InitializationOptions initializationOptions, string languageName) + private protected async Task CreateTestLspServerAsync(LspTestWorkspace workspace, InitializationOptions initializationOptions, string languageName) { var solution = workspace.CurrentSolution; @@ -348,7 +351,7 @@ private protected async Task CreateXmlTestLspServerAsync( { var lspOptions = initializationOptions ?? new InitializationOptions(); - var workspace = CreateWorkspace(lspOptions, workspaceKind, mutatingLspWorkspace); + var workspace = await CreateWorkspaceAsync(lspOptions, workspaceKind, mutatingLspWorkspace); workspace.InitializeDocuments(XElement.Parse(xmlContent), openDocuments: false); workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences([CreateTestAnalyzersReference()])); @@ -356,14 +359,24 @@ private protected async Task CreateXmlTestLspServerAsync( return await TestLspServer.CreateAsync(workspace, lspOptions, TestOutputLspLogger); } - internal LspTestWorkspace CreateWorkspace( + internal async Task CreateWorkspaceAsync( InitializationOptions? options, string? workspaceKind, bool mutatingLspWorkspace, TestComposition? composition = null) { var workspace = new LspTestWorkspace( - composition ?? Composition, workspaceKind, configurationOptions: new WorkspaceConfigurationOptions(ValidateCompilationTrackerStates: true), supportsLspMutation: mutatingLspWorkspace); + composition?.ExportProviderFactory.CreateExportProvider() ?? await CreateExportProviderAsync(), + workspaceKind, + supportsLspMutation: mutatingLspWorkspace); options?.OptionUpdater?.Invoke(workspace.GetService()); - workspace.GetService().Register(workspace); + // By default in most MEF containers, workspace event listeners are disabled in tests. Explicitly enable the LSP workspace registration event listener + // to ensure that the lsp workspace registration service sees all workspaces. If we're running tests against our full LSP server + // composition, we don't expect the mock to exist and the real thing is running. + var listenerProvider = workspace.ExportProvider.GetExportedValues().SingleOrDefault(); + if (listenerProvider is not null) + { + var lspWorkspaceRegistrationListener = (LspWorkspaceRegistrationEventListener)workspace.ExportProvider.GetExports().Single(e => e.Value is LspWorkspaceRegistrationEventListener).Value; + listenerProvider.EventListeners = [lspWorkspaceRegistrationListener]; + } return workspace; } @@ -606,10 +619,6 @@ internal async Task InitializeAsync() // Initialize the language server _ = _languageServer.Value; - // Workspace listener events do not run in tests, so we manually register the lsp misc workspace. - // This must be done after the language server is created in order to access the misc workspace off of the LSP workspace manager. - TestWorkspace.GetService().Register(GetManagerAccessor().GetLspMiscellaneousFilesWorkspace()); - if (_initializationOptions.CallInitialize) { _initializeResult = await this.ExecuteRequestAsync(LSP.Methods.InitializeName, new LSP.InitializeParams @@ -850,9 +859,6 @@ internal async ValueTask RunCodeAnalysisAsync(ProjectId? projectId) public async ValueTask DisposeAsync() { - TestWorkspace.GetService().Deregister(TestWorkspace); - TestWorkspace.GetService().Deregister(GetManagerAccessor().GetLspMiscellaneousFilesWorkspace()); - // Some tests will manually call shutdown and exit, so attempting to call this during dispose // will fail as the server's jsonrpc instance will be disposed of. if (!_languageServer.Value.GetTestAccessor().HasShutdownStarted()) diff --git a/src/LanguageServer/Protocol.TestUtilities/Microsoft.CodeAnalysis.LanguageServer.Protocol.Test.Utilities.csproj b/src/LanguageServer/Protocol.TestUtilities/Microsoft.CodeAnalysis.LanguageServer.Protocol.Test.Utilities.csproj index 91693179de616..aa9aef09e9e2f 100644 --- a/src/LanguageServer/Protocol.TestUtilities/Microsoft.CodeAnalysis.LanguageServer.Protocol.Test.Utilities.csproj +++ b/src/LanguageServer/Protocol.TestUtilities/Microsoft.CodeAnalysis.LanguageServer.Protocol.Test.Utilities.csproj @@ -16,6 +16,7 @@ + diff --git a/src/LanguageServer/Protocol.TestUtilities/Workspaces/LspTestWorkspace.cs b/src/LanguageServer/Protocol.TestUtilities/Workspaces/LspTestWorkspace.cs index 1623033092eb7..025e30f45eba0 100644 --- a/src/LanguageServer/Protocol.TestUtilities/Workspaces/LspTestWorkspace.cs +++ b/src/LanguageServer/Protocol.TestUtilities/Workspaces/LspTestWorkspace.cs @@ -8,7 +8,7 @@ using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageServer; using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; +using Microsoft.VisualStudio.Composition; namespace Microsoft.CodeAnalysis.Test.Utilities; @@ -17,14 +17,14 @@ public sealed partial class LspTestWorkspace : TestWorkspace, ILspWorkspace private readonly bool _supportsLspMutation; internal LspTestWorkspace( - TestComposition? composition = null, + ExportProvider exportProvider, string? workspaceKind = WorkspaceKind.Host, Guid solutionTelemetryId = default, bool disablePartialSolutions = true, bool ignoreUnchangeableDocumentsWhenApplyingChanges = true, WorkspaceConfigurationOptions? configurationOptions = null, bool supportsLspMutation = false) - : base(composition, + : base(exportProvider, workspaceKind, solutionTelemetryId, disablePartialSolutions, diff --git a/src/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs b/src/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs index a00b1f647ebfe..fd93f3c3ff9df 100644 --- a/src/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs +++ b/src/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs @@ -135,6 +135,15 @@ public ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) // Using VS server capabilities because we have our own custom client. capabilities.OnAutoInsertProvider = new VSInternalDocumentOnAutoInsertOptions { TriggerCharacters = ["'", "/", "\n"] }; + var diagnosticDynamicRegistationCapabilities = clientCapabilities.TextDocument?.Diagnostic?.DynamicRegistration; + if (diagnosticDynamicRegistationCapabilities is false) + { + capabilities.DiagnosticOptions = new DiagnosticOptions() + { + InterFileDependencies = true + }; + } + return capabilities; } diff --git a/src/LanguageServer/Protocol/Extensions/Extensions.cs b/src/LanguageServer/Protocol/Extensions/Extensions.cs index 75621b0a39a85..b5c55b447bed2 100644 --- a/src/LanguageServer/Protocol/Extensions/Extensions.cs +++ b/src/LanguageServer/Protocol/Extensions/Extensions.cs @@ -114,20 +114,26 @@ public static ImmutableArray GetDocumentIds(this Solution solution, /// Finds the TextDocument for a TextDocumentIdentifier, potentially returning a source-generated file. /// public static async ValueTask GetTextDocumentAsync(this Solution solution, TextDocumentIdentifier documentIdentifier, CancellationToken cancellationToken) + { + var documents = await solution.GetTextDocumentsAsync(documentIdentifier.DocumentUri, cancellationToken).ConfigureAwait(false); + return documents.Length == 0 + ? null + : documents.FindDocumentInProjectContext(documentIdentifier, (sln, id) => sln.GetRequiredTextDocument(id)); + } + + public static async ValueTask> GetTextDocumentsAsync(this Solution solution, DocumentUri documentUri, CancellationToken cancellationToken) { // If it's the URI scheme for source generated files, delegate to our other helper, otherwise we can handle anything else here. - if (documentIdentifier.DocumentUri.ParsedUri?.Scheme == SourceGeneratedDocumentUri.Scheme) + if (documentUri.ParsedUri?.Scheme == SourceGeneratedDocumentUri.Scheme) { // In the case of a URI scheme for source generated files, we generate a different URI for each project, thus this URI cannot be linked into multiple projects; // this means we can safely call .SingleOrDefault() and not worry about calling FindDocumentInProjectContext. - var documentId = solution.GetDocumentIds(documentIdentifier.DocumentUri).SingleOrDefault(); - return await solution.GetDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); + var documentId = solution.GetDocumentIds(documentUri).SingleOrDefault(); + var document = await solution.GetDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); + return document is not null ? [document] : []; } - var documents = solution.GetTextDocuments(documentIdentifier.DocumentUri); - return documents.Length == 0 - ? null - : documents.FindDocumentInProjectContext(documentIdentifier, (sln, id) => sln.GetRequiredTextDocument(id)); + return solution.GetTextDocuments(documentUri); } private static T FindItemInProjectContext( @@ -183,7 +189,7 @@ public static T FindDocumentInProjectContext(this ImmutableArray documents return null; } - var projects = solution.Projects.Where(project => project.FilePath == projectIdentifier.DocumentUri.ParsedUri.LocalPath).ToImmutableArray(); + var projects = solution.Projects.WhereAsArray(project => project.FilePath == projectIdentifier.DocumentUri.ParsedUri.LocalPath); return !projects.Any() ? null : FindItemInProjectContext(projects, projectIdentifier, projectIdGetter: (item) => item.Id, defaultGetter: () => projects[0]); diff --git a/src/LanguageServer/Protocol/Extensions/ProtocolConversions.Diagnostics.cs b/src/LanguageServer/Protocol/Extensions/ProtocolConversions.Diagnostics.cs index 0bf58091d3323..4d7216e45b5b0 100644 --- a/src/LanguageServer/Protocol/Extensions/ProtocolConversions.Diagnostics.cs +++ b/src/LanguageServer/Protocol/Extensions/ProtocolConversions.Diagnostics.cs @@ -17,23 +17,33 @@ namespace Microsoft.CodeAnalysis.LanguageServer; internal static partial class ProtocolConversions { + internal static ImmutableArray AddBuildTagIfNotPresent(ImmutableArray diagnostics) + { + return diagnostics.SelectAsArray(static d => + { + if (d.CustomTags.Contains(WellKnownDiagnosticTags.Build)) + return d; + + return d.WithCustomTags(d.CustomTags.Add(WellKnownDiagnosticTags.Build)); + }); + } + /// /// Converts from to /// /// The diagnostic to convert /// Whether the client is Visual Studio /// The project the diagnostic is relevant to - /// Whether the diagnostic is considered "live" and should supersede others /// Whether the diagnostic is potentially a duplicate to a build diagnostic /// The global options service - public static ImmutableArray ConvertDiagnostic(DiagnosticData diagnosticData, bool supportsVisualStudioExtensions, Project project, bool isLiveSource, bool potentialDuplicate, IGlobalOptionService globalOptionService) + public static ImmutableArray ConvertDiagnostic(DiagnosticData diagnosticData, bool supportsVisualStudioExtensions, Project project, bool potentialDuplicate, IGlobalOptionService globalOptionService) { if (!ShouldIncludeHiddenDiagnostic(diagnosticData, supportsVisualStudioExtensions)) { return []; } - var diagnostic = CreateLspDiagnostic(diagnosticData, project, isLiveSource, potentialDuplicate, supportsVisualStudioExtensions); + var diagnostic = CreateLspDiagnostic(diagnosticData, project, potentialDuplicate, supportsVisualStudioExtensions); // Check if we need to handle the unnecessary tag (fading). if (!diagnosticData.CustomTags.Contains(WellKnownDiagnosticTags.Unnecessary)) @@ -65,7 +75,7 @@ internal static partial class ProtocolConversions diagnosticsBuilder.Add(diagnostic); foreach (var location in unnecessaryLocations) { - var additionalDiagnostic = CreateLspDiagnostic(diagnosticData, project, isLiveSource, potentialDuplicate, supportsVisualStudioExtensions); + var additionalDiagnostic = CreateLspDiagnostic(diagnosticData, project, potentialDuplicate, supportsVisualStudioExtensions); additionalDiagnostic.Severity = LSP.DiagnosticSeverity.Hint; additionalDiagnostic.Range = GetRange(location); additionalDiagnostic.Tags = [DiagnosticTag.Unnecessary, VSDiagnosticTags.HiddenInEditor, VSDiagnosticTags.HiddenInErrorList, VSDiagnosticTags.SuppressEditorToolTip]; @@ -94,7 +104,6 @@ internal static partial class ProtocolConversions private static LSP.VSDiagnostic CreateLspDiagnostic( DiagnosticData diagnosticData, Project project, - bool isLiveSource, bool potentialDuplicate, bool supportsVisualStudioExtensions) { @@ -108,7 +117,7 @@ private static LSP.VSDiagnostic CreateLspDiagnostic( CodeDescription = ProtocolConversions.HelpLinkToCodeDescription(diagnosticData.GetValidHelpLinkUri()), Message = diagnosticData.Message, Severity = ConvertDiagnosticSeverity(diagnosticData.Severity), - Tags = ConvertTags(diagnosticData, isLiveSource, potentialDuplicate), + Tags = ConvertTags(diagnosticData, potentialDuplicate), DiagnosticRank = ConvertRank(diagnosticData), Range = GetRange(diagnosticData.DataLocation) }; @@ -223,7 +232,7 @@ private static LSP.DiagnosticSeverity ConvertDiagnosticSeverity(DiagnosticSeveri /// If you make change in this method, please also update the corresponding file in /// src\VisualStudio\Xaml\Impl\Implementation\LanguageServer\Handler\Diagnostics\AbstractPullDiagnosticHandler.cs /// - private static DiagnosticTag[] ConvertTags(DiagnosticData diagnosticData, bool isLiveSource, bool potentialDuplicate) + private static DiagnosticTag[] ConvertTags(DiagnosticData diagnosticData, bool potentialDuplicate) { using var _ = ArrayBuilder.GetInstance(out var result); @@ -246,11 +255,8 @@ private static DiagnosticTag[] ConvertTags(DiagnosticData diagnosticData, bool i if (potentialDuplicate) result.Add(VSDiagnosticTags.PotentialDuplicate); - // Mark this also as a build error. That way an explicitly kicked off build from a source like CPS can + // If tagged as build, mark this also as a build error. That way an explicitly kicked off build from a source like CPS can // override it. - if (!isLiveSource) - result.Add(VSDiagnosticTags.BuildError); - result.Add(diagnosticData.CustomTags.Contains(WellKnownDiagnosticTags.Build) ? VSDiagnosticTags.BuildError : VSDiagnosticTags.IntellisenseError); diff --git a/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs b/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs index 9c4c254d8ee7a..2c8495ffc5537 100644 --- a/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs +++ b/src/LanguageServer/Protocol/Extensions/ProtocolConversions.cs @@ -417,7 +417,7 @@ public static LSP.Range TextSpanToRange(TextSpan textSpan, SourceText text) } // Map all the text changes' spans for this document. - var mappedResults = await GetMappedSpanResultAsync(oldDocument, [.. textChanges.Select(tc => tc.Span)], cancellationToken).ConfigureAwait(false); + var mappedResults = await SpanMappingHelper.TryGetMappedSpanResultAsync(oldDocument, [.. textChanges.Select(tc => tc.Span)], cancellationToken).ConfigureAwait(false); if (mappedResults == null) { // There's no span mapping available, just create text edits from the original text changes. @@ -472,7 +472,9 @@ public static LSP.Range TextSpanToRange(TextSpan textSpan, SourceText text) { Debug.Assert(document.FilePath != null); - var result = await GetMappedSpanResultAsync(document, [textSpan], cancellationToken).ConfigureAwait(false); + var result = document is Document d + ? await SpanMappingHelper.TryGetMappedSpanResultAsync(d, [textSpan], cancellationToken).ConfigureAwait(false) + : null; if (result == null) return await ConvertTextSpanToLocationAsync(document, textSpan, isStale, cancellationToken).ConfigureAwait(false); @@ -1008,25 +1010,6 @@ static string GetStyledText(TaggedText taggedText, bool isInCodeBlock) } } - private static async Task?> GetMappedSpanResultAsync(TextDocument textDocument, ImmutableArray textSpans, CancellationToken cancellationToken) - { - if (textDocument is not Document document) - { - return null; - } - - var spanMappingService = document.DocumentServiceProvider.GetService(); - if (spanMappingService == null) - { - return null; - } - - var mappedSpanResult = await spanMappingService.MapSpansAsync(document, textSpans, cancellationToken).ConfigureAwait(false); - Contract.ThrowIfFalse(textSpans.Length == mappedSpanResult.Length, - $"The number of input spans {textSpans.Length} should match the number of mapped spans returned {mappedSpanResult.Length}"); - return mappedSpanResult; - } - private static LSP.Range MappedSpanResultToRange(MappedSpanResult mappedSpanResult) { return new LSP.Range diff --git a/src/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_OpenDocument.cs b/src/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_OpenDocument.cs index c4b2b704f4e96..db886e9f5f633 100644 --- a/src/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_OpenDocument.cs +++ b/src/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_OpenDocument.cs @@ -18,9 +18,6 @@ internal static partial class EditAndContinueDiagnosticSource { private sealed class OpenDocumentSource(Document document) : AbstractDocumentDiagnosticSource(document) { - public override bool IsLiveSource() - => true; - public override async Task> GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken) { var designTimeDocument = Document; diff --git a/src/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_Workspace.cs b/src/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_Workspace.cs index 4188a319bc83e..9069b39d0e3fc 100644 --- a/src/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_Workspace.cs +++ b/src/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_Workspace.cs @@ -19,8 +19,6 @@ internal static partial class EditAndContinueDiagnosticSource { private sealed class ProjectSource(Project project, ImmutableArray diagnostics) : AbstractProjectDiagnosticSource(project) { - public override bool IsLiveSource() - => true; public override Task> GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken) => Task.FromResult(diagnostics); @@ -28,8 +26,6 @@ public override Task> GetDiagnosticsAsync(Request private sealed class ClosedDocumentSource(TextDocument document, ImmutableArray diagnostics) : AbstractWorkspaceDocumentDiagnosticSource(document) { - public override bool IsLiveSource() - => true; public override Task> GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken) => Task.FromResult(diagnostics); diff --git a/src/LanguageServer/Protocol/Features/FindUsages/SimpleFindUsagesContext.cs b/src/LanguageServer/Protocol/Features/FindUsages/SimpleFindUsagesContext.cs index ba54d187372e1..1b192d32ff53a 100644 --- a/src/LanguageServer/Protocol/Features/FindUsages/SimpleFindUsagesContext.cs +++ b/src/LanguageServer/Protocol/Features/FindUsages/SimpleFindUsagesContext.cs @@ -68,7 +68,7 @@ public override ValueTask OnDefinitionFoundAsync(DefinitionItem definition, Canc public override async ValueTask OnReferencesFoundAsync(IAsyncEnumerable references, CancellationToken cancellationToken) { - await foreach (var reference in references) + await foreach (var reference in references.ConfigureAwait(false)) { lock (_gate) { diff --git a/src/LanguageServer/Protocol/Features/UnifiedSuggestions/UnifiedSuggestedActionsSource.cs b/src/LanguageServer/Protocol/Features/UnifiedSuggestions/UnifiedSuggestedActionsSource.cs index 147461645c514..77219716714e5 100644 --- a/src/LanguageServer/Protocol/Features/UnifiedSuggestions/UnifiedSuggestedActionsSource.cs +++ b/src/LanguageServer/Protocol/Features/UnifiedSuggestions/UnifiedSuggestedActionsSource.cs @@ -290,11 +290,11 @@ private static ImmutableArray PrioritizeFixGroups( { var actions = map[groupKey]; - var nonSuppressionActions = actions.Where(a => !IsTopLevelSuppressionAction(a.OriginalCodeAction)).ToImmutableArray(); + var nonSuppressionActions = actions.WhereAsArray(a => !IsTopLevelSuppressionAction(a.OriginalCodeAction)); AddUnifiedSuggestedActionsSet(originalSolution, text, nonSuppressionActions, groupKey, nonSuppressionSets); - var suppressionActions = actions.Where(a => IsTopLevelSuppressionAction(a.OriginalCodeAction) && - !IsBulkConfigurationAction(a.OriginalCodeAction)).ToImmutableArray(); + var suppressionActions = actions.WhereAsArray(a => IsTopLevelSuppressionAction(a.OriginalCodeAction) && + !IsBulkConfigurationAction(a.OriginalCodeAction)); AddUnifiedSuggestedActionsSet(originalSolution, text, suppressionActions, groupKey, suppressionSets); bulkConfigurationActions.AddRange(actions.Where(a => IsBulkConfigurationAction(a.OriginalCodeAction))); diff --git a/src/LanguageServer/Protocol/Handler/CodeLens/CodeLensHandler.cs b/src/LanguageServer/Protocol/Handler/CodeLens/CodeLensHandler.cs index 490e9e35c41ff..028b16114706c 100644 --- a/src/LanguageServer/Protocol/Handler/CodeLens/CodeLensHandler.cs +++ b/src/LanguageServer/Protocol/Handler/CodeLens/CodeLensHandler.cs @@ -42,11 +42,13 @@ public CodeLensHandler(IGlobalOptionService globalOptionService) public LSP.TextDocumentIdentifier GetTextDocumentIdentifier(LSP.CodeLensParams request) => request.TextDocument; - public async Task HandleRequestAsync(LSP.CodeLensParams request, RequestContext context, CancellationToken cancellationToken) + public Task HandleRequestAsync(LSP.CodeLensParams request, RequestContext context, CancellationToken cancellationToken) + => GetCodeLensAsync(request.TextDocument, context.GetRequiredDocument(), _globalOptionService, cancellationToken); + + internal static async Task GetCodeLensAsync(LSP.TextDocumentIdentifier textDocumentIdentifier, Document document, IGlobalOptionService globalOptionService, CancellationToken cancellationToken) { - var document = context.GetRequiredDocument(); - var referencesCodeLensEnabled = _globalOptionService.GetOption(LspOptionsStorage.LspEnableReferencesCodeLens, document.Project.Language); - var testsCodeLensEnabled = _globalOptionService.GetOption(LspOptionsStorage.LspEnableTestsCodeLens, document.Project.Language); + var referencesCodeLensEnabled = globalOptionService.GetOption(LspOptionsStorage.LspEnableReferencesCodeLens, document.Project.Language); + var testsCodeLensEnabled = globalOptionService.GetOption(LspOptionsStorage.LspEnableTestsCodeLens, document.Project.Language); if (!referencesCodeLensEnabled && !testsCodeLensEnabled) { @@ -67,13 +69,13 @@ public LSP.TextDocumentIdentifier GetTextDocumentIdentifier(LSP.CodeLensParams r if (referencesCodeLensEnabled) { - await AddReferencesCodeLensAsync(codeLenses, members, document, text, request.TextDocument, cancellationToken).ConfigureAwait(false); + await AddReferencesCodeLensAsync(codeLenses, members, document, text, textDocumentIdentifier, cancellationToken).ConfigureAwait(false); } - if (!_globalOptionService.GetOption(LspOptionsStorage.LspUsingDevkitFeatures) && testsCodeLensEnabled) + if (!globalOptionService.GetOption(LspOptionsStorage.LspUsingDevkitFeatures) && testsCodeLensEnabled) { // Only return test codelenses if we're not using devkit. - AddTestCodeLens(codeLenses, members, document, text, request.TextDocument); + AddTestCodeLens(codeLenses, members, document, text, textDocumentIdentifier); } return codeLenses.ToArray(); diff --git a/src/LanguageServer/Protocol/Handler/CodeLens/CodeLensResolveHandler.cs b/src/LanguageServer/Protocol/Handler/CodeLens/CodeLensResolveHandler.cs index 9ccffd4c5a595..fdbdd7eb15080 100644 --- a/src/LanguageServer/Protocol/Handler/CodeLens/CodeLensResolveHandler.cs +++ b/src/LanguageServer/Protocol/Handler/CodeLens/CodeLensResolveHandler.cs @@ -33,9 +33,14 @@ internal sealed class CodeLensResolveHandler() : ILspServiceDocumentRequestHandl public LSP.TextDocumentIdentifier GetTextDocumentIdentifier(LSP.CodeLens request) => GetCodeLensResolveData(request).TextDocument; - public async Task HandleRequestAsync(LSP.CodeLens request, RequestContext context, CancellationToken cancellationToken) + public Task HandleRequestAsync(LSP.CodeLens request, RequestContext context, CancellationToken cancellationToken) { var document = context.GetRequiredDocument(); + return ResolveCodeLensAsync(request, document, cancellationToken); + } + + internal static async Task ResolveCodeLensAsync(LSP.CodeLens request, Document document, CancellationToken cancellationToken) + { var currentDocumentSyntaxVersion = await document.GetSyntaxVersionAsync(cancellationToken).ConfigureAwait(false); var resolveData = GetCodeLensResolveData(request); diff --git a/src/LanguageServer/Protocol/Handler/Completion/CompletionHandler.cs b/src/LanguageServer/Protocol/Handler/Completion/CompletionHandler.cs index 9634aea2cb4ae..4952c56ab0c60 100644 --- a/src/LanguageServer/Protocol/Handler/Completion/CompletionHandler.cs +++ b/src/LanguageServer/Protocol/Handler/Completion/CompletionHandler.cs @@ -236,8 +236,7 @@ private static (CompletionList CompletionList, bool IsIncomplete, bool isHardSel var filteredList = matchResultsBuilder .Take(completionListMaxSize) .Concat(matchResultsBuilder.Skip(completionListMaxSize).Where(match => match.CompletionItem.Rules.MatchPriority == MatchPriority.Preselect)) - .Select(matchResult => matchResult.CompletionItem) - .ToImmutableArray(); + .SelectAsArray(matchResult => matchResult.CompletionItem); var newCompletionList = completionList.WithItemsList(filteredList); // Per the LSP spec, the completion list should be marked with isIncomplete = false when further insertions will diff --git a/src/LanguageServer/Protocol/Handler/Diagnostics/AbstractPullDiagnosticHandler.cs b/src/LanguageServer/Protocol/Handler/Diagnostics/AbstractPullDiagnosticHandler.cs index 33ce7c29a33fc..b1490635f95ef 100644 --- a/src/LanguageServer/Protocol/Handler/Diagnostics/AbstractPullDiagnosticHandler.cs +++ b/src/LanguageServer/Protocol/Handler/Diagnostics/AbstractPullDiagnosticHandler.cs @@ -313,7 +313,6 @@ private void HandleRemovedDocuments(RequestContext context, HashSet GetDocumentSourceProviderNames(ClientCapabilities clientCapabilities) - => _nameToDocumentProviderMap.Where(kvp => kvp.Value.IsEnabled(clientCapabilities)).SelectAsArray(kvp => kvp.Key); + => _nameToDocumentProviderMap.SelectAsArray( + predicate: kvp => kvp.Value.IsEnabled(clientCapabilities), + selector: kvp => kvp.Key); public ImmutableArray GetWorkspaceSourceProviderNames(ClientCapabilities clientCapabilities) - => _nameToWorkspaceProviderMap.Where(kvp => kvp.Value.IsEnabled(clientCapabilities)).SelectAsArray(kvp => kvp.Key); + => _nameToWorkspaceProviderMap.SelectAsArray( + predicate: kvp => kvp.Value.IsEnabled(clientCapabilities), + selector: kvp => kvp.Key); public ValueTask> CreateDocumentDiagnosticSourcesAsync(RequestContext context, string? providerName, CancellationToken cancellationToken) => CreateDiagnosticSourcesAsync(context, providerName, _nameToDocumentProviderMap, isDocument: true, cancellationToken); @@ -78,7 +81,7 @@ private static async ValueTask> CreateDiagnost } else { - // VS Code (and legacy VS ?) pass null sourceName when requesting all sources. + // Some clients (legacy VS/VSCode, Razor) do not support multiple sources - a null source indicates that diagnostics from all sources should be returned. using var _ = ArrayBuilder.GetInstance(out var sourcesBuilder); foreach (var (name, provider) in nameToProviderMap) { @@ -106,7 +109,6 @@ public static ImmutableArray AggregateSourcesIfNeeded(Immutab if (isDocument) { // Group all document sources into a single source. - Debug.Assert(sources.All(s => s.IsLiveSource()), "All document sources should be live"); sources = [new AggregatedDocumentDiagnosticSource(sources)]; } else @@ -115,7 +117,7 @@ public static ImmutableArray AggregateSourcesIfNeeded(Immutab // will have same value for GetDocumentIdentifier and GetProject(). Thus can be // aggregated in a single source which will return same values. See // AggregatedDocumentDiagnosticSource implementation for more details. - sources = [.. sources.GroupBy(s => (s.GetId(), s.IsLiveSource()), s => s).SelectMany(g => AggregatedDocumentDiagnosticSource.AggregateIfNeeded(g))]; + sources = [.. sources.GroupBy(s => s.GetId(), s => s).SelectMany(g => AggregatedDocumentDiagnosticSource.AggregateIfNeeded(g))]; } return sources; @@ -141,7 +143,6 @@ public static ImmutableArray AggregateIfNeeded(IEnumerable true; public Project GetProject() => sources[0].GetProject(); public ProjectOrDocumentId GetId() => sources[0].GetId(); public TextDocumentIdentifier? GetDocumentIdentifier() => sources[0].GetDocumentIdentifier(); diff --git a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractDocumentDiagnosticSource.cs b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractDocumentDiagnosticSource.cs index c92822a6b9cd6..c77cfb567f96e 100644 --- a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractDocumentDiagnosticSource.cs +++ b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractDocumentDiagnosticSource.cs @@ -16,8 +16,6 @@ internal abstract class AbstractDocumentDiagnosticSource(TDocument do public TDocument Document { get; } = document; public Solution Solution => this.Document.Project.Solution; - public abstract bool IsLiveSource(); - public abstract Task> GetDiagnosticsAsync( RequestContext context, CancellationToken cancellationToken); diff --git a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractProjectDiagnosticSource.cs b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractProjectDiagnosticSource.cs index a2e58d7ac8a3d..04e70a6e08763 100644 --- a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractProjectDiagnosticSource.cs +++ b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractProjectDiagnosticSource.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServer; using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; @@ -23,7 +24,6 @@ public static AbstractProjectDiagnosticSource CreateForFullSolutionAnalysisDiagn public static AbstractProjectDiagnosticSource CreateForCodeAnalysisDiagnostics(Project project, ICodeAnalysisDiagnosticAnalyzerService codeAnalysisService) => new CodeAnalysisDiagnosticSource(project, codeAnalysisService); - public abstract bool IsLiveSource(); public abstract Task> GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken); public ProjectOrDocumentId GetId() => new(Project.Id); @@ -37,12 +37,6 @@ public static AbstractProjectDiagnosticSource CreateForCodeAnalysisDiagnostics(P private sealed class FullSolutionAnalysisDiagnosticSource(Project project, Func? shouldIncludeAnalyzer) : AbstractProjectDiagnosticSource(project) { - /// - /// This is a normal project source that represents live/fresh diagnostics that should supersede everything else. - /// - public override bool IsLiveSource() - => true; - public override async Task> GetDiagnosticsAsync( RequestContext context, CancellationToken cancellationToken) @@ -64,19 +58,17 @@ public override async Task> GetDiagnosticsAsync( private sealed class CodeAnalysisDiagnosticSource(Project project, ICodeAnalysisDiagnosticAnalyzerService codeAnalysisService) : AbstractProjectDiagnosticSource(project) { - /// - /// This source provides the results of the *last* explicitly kicked off "run code analysis" command from the - /// user. As such, it is definitely not "live" data, and it should be overridden by any subsequent fresh data - /// that has been produced. - /// - public override bool IsLiveSource() - => false; - public override Task> GetDiagnosticsAsync( RequestContext context, CancellationToken cancellationToken) { - return Task.FromResult(codeAnalysisService.GetLastComputedProjectDiagnostics(Project.Id)); + var diagnostics = codeAnalysisService.GetLastComputedProjectDiagnostics(Project.Id); + + // This source provides the results of the *last* explicitly kicked off "run code analysis" command from the + // user. As such, it is definitely not "live" data, and it should be overridden by any subsequent fresh data + // that has been produced. + diagnostics = ProtocolConversions.AddBuildTagIfNotPresent(diagnostics); + return Task.FromResult(diagnostics); } } } diff --git a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractWorkspaceDocumentDiagnosticSource.cs b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractWorkspaceDocumentDiagnosticSource.cs index 9e0a50253403e..e243c04ef9404 100644 --- a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractWorkspaceDocumentDiagnosticSource.cs +++ b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractWorkspaceDocumentDiagnosticSource.cs @@ -32,12 +32,6 @@ private sealed class FullSolutionAnalysisDiagnosticSource( /// private static readonly ConditionalWeakTable>> s_projectToDiagnostics = new(); - /// - /// This is a normal document source that represents live/fresh diagnostics that should supersede everything else. - /// - public override bool IsLiveSource() - => true; - public override async Task> GetDiagnosticsAsync( RequestContext context, CancellationToken cancellationToken) @@ -92,19 +86,17 @@ AsyncLazy> GetLazyDiagnostics() private sealed class CodeAnalysisDiagnosticSource(TextDocument document, ICodeAnalysisDiagnosticAnalyzerService codeAnalysisService) : AbstractWorkspaceDocumentDiagnosticSource(document) { - /// - /// This source provides the results of the *last* explicitly kicked off "run code analysis" command from the - /// user. As such, it is definitely not "live" data, and it should be overridden by any subsequent fresh data - /// that has been produced. - /// - public override bool IsLiveSource() - => false; - public override Task> GetDiagnosticsAsync( RequestContext context, CancellationToken cancellationToken) { - return Task.FromResult(codeAnalysisService.GetLastComputedDocumentDiagnostics(Document.Id)); + var diagnostics = codeAnalysisService.GetLastComputedDocumentDiagnostics(Document.Id); + + // This source provides the results of the *last* explicitly kicked off "run code analysis" command from the + // user. As such, it is definitely not "live" data, and it should be overridden by any subsequent fresh data + // that has been produced. + diagnostics = ProtocolConversions.AddBuildTagIfNotPresent(diagnostics); + return Task.FromResult(diagnostics); } } } diff --git a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/DocumentDiagnosticSource.cs b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/DocumentDiagnosticSource.cs index 9960d4dc5a7e5..448a9e5b9497e 100644 --- a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/DocumentDiagnosticSource.cs +++ b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/DocumentDiagnosticSource.cs @@ -15,12 +15,6 @@ internal sealed class DocumentDiagnosticSource(DiagnosticKind diagnosticKind, Te { public DiagnosticKind DiagnosticKind { get; } = diagnosticKind; - /// - /// This is a normal document source that represents live/fresh diagnostics that should supersede everything else. - /// - public override bool IsLiveSource() - => true; - public override async Task> GetDiagnosticsAsync( RequestContext context, CancellationToken cancellationToken) { diff --git a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/IDiagnosticSource.cs b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/IDiagnosticSource.cs index e8cb70bc1de4c..030cdd7d924b0 100644 --- a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/IDiagnosticSource.cs +++ b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/IDiagnosticSource.cs @@ -20,17 +20,6 @@ internal interface IDiagnosticSource ProjectOrDocumentId GetId(); TextDocumentIdentifier? GetDocumentIdentifier(); string ToDisplayString(); - - /// - /// True if this source produces diagnostics that are considered 'live' or not. Live errors represent up to date - /// information that should supersede other sources. Non 'live' errors (aka "build errors") are recognized to - /// potentially represent stale results from a point in the past when the computation occurred. The only time - /// Roslyn produces non-live errors through an explicit user gesture to "run code analysis". Because these represent - /// errors from the past, we do want them to be superseded by a more recent live run, or a more recent build from - /// another source. - /// - bool IsLiveSource(); - Task> GetDiagnosticsAsync( RequestContext context, CancellationToken cancellationToken); diff --git a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/NonLocalDocumentDiagnosticSource.cs b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/NonLocalDocumentDiagnosticSource.cs index 2551c6592a82b..4b6df55b2bf57 100644 --- a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/NonLocalDocumentDiagnosticSource.cs +++ b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/NonLocalDocumentDiagnosticSource.cs @@ -16,9 +16,6 @@ internal sealed class NonLocalDocumentDiagnosticSource( { private readonly Func? _shouldIncludeAnalyzer = shouldIncludeAnalyzer; - public override bool IsLiveSource() - => true; - public override async Task> GetDiagnosticsAsync( RequestContext context, CancellationToken cancellationToken) diff --git a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/TaskListDiagnosticSource.cs b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/TaskListDiagnosticSource.cs index fe6ebc182c6f9..f3c0adfe559fc 100644 --- a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/TaskListDiagnosticSource.cs +++ b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/TaskListDiagnosticSource.cs @@ -29,9 +29,6 @@ internal sealed class TaskListDiagnosticSource(Document document, IGlobalOptionS private readonly IGlobalOptionService _globalOptions = globalOptions; - public override bool IsLiveSource() - => true; - public override async Task> GetDiagnosticsAsync( RequestContext context, CancellationToken cancellationToken) { diff --git a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticsPullCache.cs b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticsPullCache.cs index c8c824eca7a04..80387153f7113 100644 --- a/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticsPullCache.cs +++ b/src/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticsPullCache.cs @@ -29,21 +29,16 @@ internal abstract partial class AbstractPullDiagnosticHandler private sealed class DiagnosticsPullCache(IGlobalOptionService globalOptions, string uniqueKey) - : VersionedPullCache<(int globalStateVersion, VersionStamp? dependentVersion), (int globalStateVersion, Checksum dependentChecksum), DiagnosticsRequestState, ImmutableArray>(uniqueKey) + : VersionedPullCache<(int globalStateVersion, Checksum dependentChecksum), DiagnosticsRequestState, ImmutableArray>(uniqueKey) { private readonly IGlobalOptionService _globalOptions = globalOptions; - public override async Task<(int globalStateVersion, VersionStamp? dependentVersion)> ComputeCheapVersionAsync(DiagnosticsRequestState state, CancellationToken cancellationToken) - { - return (state.GlobalStateVersion, await state.Project.GetDependentVersionAsync(cancellationToken).ConfigureAwait(false)); - } - - public override async Task<(int globalStateVersion, Checksum dependentChecksum)> ComputeExpensiveVersionAsync(DiagnosticsRequestState state, CancellationToken cancellationToken) + public override async Task<(int globalStateVersion, Checksum dependentChecksum)> ComputeVersionAsync(DiagnosticsRequestState state, CancellationToken cancellationToken) { return (state.GlobalStateVersion, await state.Project.GetDiagnosticChecksumAsync(cancellationToken).ConfigureAwait(false)); } - /// + /// public override async Task> ComputeDataAsync(DiagnosticsRequestState state, CancellationToken cancellationToken) { var diagnostics = await state.DiagnosticSource.GetDiagnosticsAsync(state.Context, cancellationToken).ConfigureAwait(false); diff --git a/src/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler.cs b/src/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler.cs index 06bcae4f8a9bd..39acee6fc7315 100644 --- a/src/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler.cs +++ b/src/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler.cs @@ -85,14 +85,14 @@ protected override bool TryCreateUnchangedReport(TextDocumentIdentifier identifi protected override ImmutableArray? GetPreviousResults(WorkspaceDiagnosticParams diagnosticsParams) { - return diagnosticsParams.PreviousResultId.Select(id => new PreviousPullResult + return diagnosticsParams.PreviousResultId.SelectAsArray(id => new PreviousPullResult { PreviousResultId = id.Value, TextDocument = new TextDocumentIdentifier { DocumentUri = id.Uri } - }).ToImmutableArray(); + }); } internal override TestAccessor GetTestAccessor() => new(this); diff --git a/src/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandler.cs b/src/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandler.cs index 9710bd9a508ed..c6cc04b029bb2 100644 --- a/src/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandler.cs +++ b/src/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandler.cs @@ -55,7 +55,7 @@ protected override bool TryCreateUnchangedReport(TextDocumentIdentifier identifi } protected override ImmutableArray? GetPreviousResults(VSInternalWorkspaceDiagnosticsParams diagnosticsParams) - => diagnosticsParams.PreviousResults?.Where(d => d.PreviousResultId != null).Select(d => new PreviousPullResult(d.PreviousResultId!, d.TextDocument!)).ToImmutableArray(); + => diagnosticsParams.PreviousResults?.Where(d => d.PreviousResultId != null).SelectAsArray(d => new PreviousPullResult(d.PreviousResultId!, d.TextDocument!)); protected override VSInternalWorkspaceDiagnosticReport[]? CreateReturn(BufferedProgress progress) { diff --git a/src/LanguageServer/Protocol/Handler/FoldingRanges/FoldingRangesHandler.cs b/src/LanguageServer/Protocol/Handler/FoldingRanges/FoldingRangesHandler.cs index b35fb881c029e..d58ddd991b575 100644 --- a/src/LanguageServer/Protocol/Handler/FoldingRanges/FoldingRangesHandler.cs +++ b/src/LanguageServer/Protocol/Handler/FoldingRanges/FoldingRangesHandler.cs @@ -111,7 +111,7 @@ private static FoldingRange[] GetFoldingRanges(BlockStructure blockStructure, So BlockTypes.Imports => FoldingRangeKind.Imports, BlockTypes.PreprocessorRegion => FoldingRangeKind.Region, BlockTypes.Member => VSFoldingRangeKind.Implementation, - _ => null, + _ => span.AutoCollapse ? VSFoldingRangeKind.Implementation : null, }; foldingRanges.Add(new FoldingRange() diff --git a/src/LanguageServer/Protocol/Handler/InlineCompletions/XmlSnippetParser.CodeSnippet.cs b/src/LanguageServer/Protocol/Handler/InlineCompletions/XmlSnippetParser.CodeSnippet.cs index e9855779a1fac..95a90ec975478 100644 --- a/src/LanguageServer/Protocol/Handler/InlineCompletions/XmlSnippetParser.CodeSnippet.cs +++ b/src/LanguageServer/Protocol/Handler/InlineCompletions/XmlSnippetParser.CodeSnippet.cs @@ -78,7 +78,7 @@ public static CodeSnippet ReadSnippetFromFile(string filePath, string snippetTit if (codeSnippetsElement.Name.LocalName.Equals("CodeSnippets", StringComparison.OrdinalIgnoreCase)) { - return codeSnippetsElement.Elements().Where(e => e.Name.LocalName.Equals("CodeSnippet", StringComparison.OrdinalIgnoreCase)).ToImmutableArray(); + return codeSnippetsElement.Elements().WhereAsArray(e => e.Name.LocalName.Equals("CodeSnippet", StringComparison.OrdinalIgnoreCase)); } else if (codeSnippetsElement.Name.LocalName.Equals("CodeSnippet", StringComparison.OrdinalIgnoreCase)) { diff --git a/src/LanguageServer/Protocol/Handler/OnAutoInsert/OnAutoInsertHandler.cs b/src/LanguageServer/Protocol/Handler/OnAutoInsert/OnAutoInsertHandler.cs index 516899c0e30c7..cd6c950489a5a 100644 --- a/src/LanguageServer/Protocol/Handler/OnAutoInsert/OnAutoInsertHandler.cs +++ b/src/LanguageServer/Protocol/Handler/OnAutoInsert/OnAutoInsertHandler.cs @@ -54,7 +54,7 @@ internal sealed class OnAutoInsertHandler( if (!onAutoInsertEnabled) return SpecializedTasks.Null(); - var servicesForDocument = _braceCompletionServices.Where(s => s.Metadata.Language == document.Project.Language).SelectAsArray(s => s.Value); + var servicesForDocument = _braceCompletionServices.SelectAsArray(s => s.Metadata.Language == document.Project.Language, s => s.Value); var isRazorRequest = context.ServerKind == WellKnownLspServerKinds.RazorLspServer; var position = ProtocolConversions.PositionToLinePosition(request.Position); return GetOnAutoInsertResponseAsync(_globalOptions, servicesForDocument, document, position, request.Character, request.Options, isRazorRequest, cancellationToken); diff --git a/src/LanguageServer/Protocol/Handler/PullHandlers/VersionedPullCache.CacheItem.cs b/src/LanguageServer/Protocol/Handler/PullHandlers/VersionedPullCache.CacheItem.cs index 1ecbf8fcc33e8..c78d9a6493264 100644 --- a/src/LanguageServer/Protocol/Handler/PullHandlers/VersionedPullCache.CacheItem.cs +++ b/src/LanguageServer/Protocol/Handler/PullHandlers/VersionedPullCache.CacheItem.cs @@ -9,10 +9,10 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler; -internal abstract partial class VersionedPullCache +internal abstract partial class VersionedPullCache { /// - /// Internal cache item that updates state for a particular and in + /// Internal cache item that updates state for a particular and in /// This type ensures that the state for a particular key is never updated concurrently for the same key (but different key states can be concurrent). /// private sealed class CacheItem(string uniqueKey) @@ -44,7 +44,7 @@ private sealed class CacheItem(string uniqueKey) /// /// /// - private (string resultId, TCheapVersion cheapVersion, TExpensiveVersion expensiveVersion, Checksum dataChecksum)? _lastResult; + private (string resultId, TVersion version, Checksum dataChecksum)? _lastResult; /// /// Updates the values for this cache entry. Guarded by @@ -52,7 +52,7 @@ private sealed class CacheItem(string uniqueKey) /// Returns if the previousPullResult can be re-used, otherwise returns a new resultId and the new data associated with it. /// public async Task<(string, TComputedData)?> UpdateCacheItemAsync( - VersionedPullCache cache, + VersionedPullCache cache, PreviousPullResult? previousPullResult, bool isFullyLoaded, TState state, @@ -63,38 +63,27 @@ private sealed class CacheItem(string uniqueKey) // This means that the computation of new data for this item only occurs sequentially. using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false)) { - TCheapVersion cheapVersion; - TExpensiveVersion expensiveVersion; + TVersion version; // Check if the version we have in the cache matches the request version. If so we can re-use the resultId. if (isFullyLoaded && _lastResult is not null && _lastResult.Value.resultId == previousPullResult?.PreviousResultId) { - cheapVersion = await cache.ComputeCheapVersionAsync(state, cancellationToken).ConfigureAwait(false); - if (cheapVersion != null && cheapVersion.Equals(_lastResult.Value.cheapVersion)) - { - // The client's resultId matches our cached resultId and the cheap version is an - // exact match for our current cheap version. We return early here to avoid calculating - // expensive versions as we know nothing is changed. - return null; - } - // The current cheap version does not match the last reported. This may be because we've forked // or reloaded a project, so fall back to calculating the full expensive version to determine if // anything is actually changed. - expensiveVersion = await cache.ComputeExpensiveVersionAsync(state, cancellationToken).ConfigureAwait(false); - if (expensiveVersion != null && expensiveVersion.Equals(_lastResult.Value.expensiveVersion)) + version = await cache.ComputeVersionAsync(state, cancellationToken).ConfigureAwait(false); + if (version != null && version.Equals(_lastResult.Value.version)) { return null; } } else { - // The versions we have in our cache (if any) do not match the ones provided by the client (if any). + // The version we have in our cache does not match the one provided by the client (if any). // We need to calculate new results. - cheapVersion = await cache.ComputeCheapVersionAsync(state, cancellationToken).ConfigureAwait(false); - expensiveVersion = await cache.ComputeExpensiveVersionAsync(state, cancellationToken).ConfigureAwait(false); + version = await cache.ComputeVersionAsync(state, cancellationToken).ConfigureAwait(false); } // Compute the new result for the request. @@ -111,7 +100,7 @@ _lastResult is not null && // subsequent requests will always fail the version comparison check (the resultId is still associated with the older version even // though we reused it here for a newer version) and will trigger re-computation. // By storing the updated version with the resultId we can short circuit earlier in the version checks. - _lastResult = (_lastResult.Value.resultId, cheapVersion, expensiveVersion, dataChecksum); + _lastResult = (_lastResult.Value.resultId, version, dataChecksum); return null; } else @@ -127,7 +116,7 @@ _lastResult is not null && // Note that we can safely update the map before computation as any cancellation or exception // during computation means that the client will never recieve this resultId and so cannot ask us for it. newResultId = $"{uniqueKey}:{cache.GetNextResultId()}"; - _lastResult = (newResultId, cheapVersion, expensiveVersion, dataChecksum); + _lastResult = (newResultId, version, dataChecksum); return (newResultId, data); } } diff --git a/src/LanguageServer/Protocol/Handler/PullHandlers/VersionedPullCache.cs b/src/LanguageServer/Protocol/Handler/PullHandlers/VersionedPullCache.cs index f378b4b092ecf..28b123f876e5d 100644 --- a/src/LanguageServer/Protocol/Handler/PullHandlers/VersionedPullCache.cs +++ b/src/LanguageServer/Protocol/Handler/PullHandlers/VersionedPullCache.cs @@ -17,7 +17,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler; /// that existing results can be reused, or if new results need to be computed. Multiple keys can be used, /// with different computation costs to determine if the previous cached data is still valid. /// -internal abstract partial class VersionedPullCache(string uniqueKey) +internal abstract partial class VersionedPullCache(string uniqueKey) { /// /// Map of workspace and diagnostic source to the data used to make the last pull report. @@ -37,19 +37,12 @@ internal abstract partial class VersionedPullCache - /// Computes a cheap version of the current state. This is compared to the cached version we calculated for the client's previous resultId. + /// Computes the version of the current state. We compare the version of the current state against the + /// version we have cached for the client's previous resultId. /// /// Note - this will run under the semaphore in . /// - public abstract Task ComputeCheapVersionAsync(TState state, CancellationToken cancellationToken); - - /// - /// Computes a more expensive version of the current state. If the cheap versions are mismatched, we then compare the expensive version of the current state against the - /// expensive version we have cached for the client's previous resultId. - /// - /// Note - this will run under the semaphore in . - /// - public abstract Task ComputeExpensiveVersionAsync(TState state, CancellationToken cancellationToken); + public abstract Task ComputeVersionAsync(TState state, CancellationToken cancellationToken); /// /// Computes new data for this request. This data must be hashable as it we store the hash with the requestId to determine if diff --git a/src/LanguageServer/Protocol/Handler/References/FindUsagesLSPContext.cs b/src/LanguageServer/Protocol/Handler/References/FindUsagesLSPContext.cs index 7998035ba6d07..69ee3b7d73fc7 100644 --- a/src/LanguageServer/Protocol/Handler/References/FindUsagesLSPContext.cs +++ b/src/LanguageServer/Protocol/Handler/References/FindUsagesLSPContext.cs @@ -136,7 +136,7 @@ public override async ValueTask OnDefinitionFoundAsync(DefinitionItem definition public override async ValueTask OnReferencesFoundAsync(IAsyncEnumerable references, CancellationToken cancellationToken) { - await foreach (var reference in references) + await foreach (var reference in references.ConfigureAwait(false)) { using (await _semaphore.DisposableWaitAsync(cancellationToken).ConfigureAwait(false)) { diff --git a/src/LanguageServer/Protocol/Handler/Rename/PrepareRenameHandler.cs b/src/LanguageServer/Protocol/Handler/Rename/PrepareRenameHandler.cs index 0e3eb9b0cc8d5..829c4dba5ba40 100644 --- a/src/LanguageServer/Protocol/Handler/Rename/PrepareRenameHandler.cs +++ b/src/LanguageServer/Protocol/Handler/Rename/PrepareRenameHandler.cs @@ -33,7 +33,7 @@ public LSP.TextDocumentIdentifier GetTextDocumentIdentifier(LSP.PrepareRenamePar var position = await document.GetPositionFromLinePositionAsync(linePosition, cancellationToken).ConfigureAwait(false); var symbolicRenameInfo = await SymbolicRenameInfo.GetRenameInfoAsync( - document, position, includeSourceGenerated: false, cancellationToken).ConfigureAwait(false); + document, position, cancellationToken).ConfigureAwait(false); if (symbolicRenameInfo.IsError) return null; diff --git a/src/LanguageServer/Protocol/Handler/Rename/RenameHandler.cs b/src/LanguageServer/Protocol/Handler/Rename/RenameHandler.cs index 186c0218cfda4..3ab023d511b5b 100644 --- a/src/LanguageServer/Protocol/Handler/Rename/RenameHandler.cs +++ b/src/LanguageServer/Protocol/Handler/Rename/RenameHandler.cs @@ -28,24 +28,23 @@ internal sealed class RenameHandler() : ILspServiceDocumentRequestHandler request.TextDocument; public Task HandleRequestAsync(RenameParams request, RequestContext context, CancellationToken cancellationToken) - => GetRenameEditAsync(context.GetRequiredDocument(), ProtocolConversions.PositionToLinePosition(request.Position), request.NewName, includeSourceGenerated: false, cancellationToken); + => GetRenameEditAsync(context.GetRequiredDocument(), ProtocolConversions.PositionToLinePosition(request.Position), request.NewName, cancellationToken); - internal static async Task GetRenameEditAsync(Document document, LinePosition linePosition, string newName, bool includeSourceGenerated, CancellationToken cancellationToken) + internal static async Task GetRenameEditAsync(Document document, LinePosition linePosition, string newName, CancellationToken cancellationToken) { var oldSolution = document.Project.Solution; var position = await document.GetPositionFromLinePositionAsync(linePosition, cancellationToken).ConfigureAwait(false); var symbolicRenameInfo = await SymbolicRenameInfo.GetRenameInfoAsync( - document, position, includeSourceGenerated, cancellationToken).ConfigureAwait(false); + document, position, cancellationToken).ConfigureAwait(false); if (symbolicRenameInfo.IsError) return null; var options = new SymbolRenameOptions( - renameOverloads: false, - renameInStrings: false, - renameInComments: false, - renameFile: false, - renameInSourceGeneratedDocuments: includeSourceGenerated); + RenameOverloads: false, + RenameInStrings: false, + RenameInComments: false, + RenameFile: false); var renameLocationSet = await Renamer.FindRenameLocationsAsync( oldSolution, @@ -70,8 +69,6 @@ internal sealed class RenameHandler() : ILspServiceDocumentRequestHandler p.GetChangedDocuments(onlyGetDocumentsWithTextChanges: true)) diff --git a/src/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs b/src/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs index d81dbe025b6b2..31fe98d5180f7 100644 --- a/src/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs +++ b/src/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensHelpers.cs @@ -12,7 +12,6 @@ using Microsoft.CodeAnalysis.Collections; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using LSP = Roslyn.LanguageServer.Protocol; @@ -21,8 +20,6 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens; internal static class SemanticTokensHelpers { - private static readonly ObjectPool> s_tokenListPool = new(() => new List(capacity: 1000)); - /// The ranges to get semantic tokens for. If null then the entire document will be /// processed. internal static async Task HandleRequestHelperAsync( @@ -241,12 +238,7 @@ private static int[] ComputeTokens( var lastStartCharacter = 0; var tokenTypeMap = SemanticTokensSchema.GetSchema(supportsVisualStudioExtensions).TokenTypeMap; - - using var pooledData = s_tokenListPool.GetPooledObject(); - var data = pooledData.Object; - - // Items in the pool may not have been cleared - data.Clear(); + var data = AllocateTokenArray(classifiedSpans); for (var currentClassifiedSpanIndex = 0; currentClassifiedSpanIndex < classifiedSpans.Count; currentClassifiedSpanIndex++) { @@ -263,7 +255,31 @@ private static int[] ComputeTokens( data.Add(tokenModifiers); } - return [.. data]; + return data.MoveToArray(); + } + + // This method allocates an array of integers to hold the semantic tokens data. + // NOTE: The number of items in the array is based on the number of unique classified spans + // in the provided list and is closely tied with how ComputeNextToken's loop works + private static FixedSizeArrayBuilder AllocateTokenArray(SegmentedList classifiedSpans) + { + if (classifiedSpans.Count == 0) + return new FixedSizeArrayBuilder(0); + + var uniqueSpanCount = 1; + var lastSpan = classifiedSpans[0].TextSpan; + + for (var index = 1; index < classifiedSpans.Count; index++) + { + var currentSpan = classifiedSpans[index].TextSpan; + if (currentSpan != lastSpan) + { + uniqueSpanCount++; + lastSpan = currentSpan; + } + } + + return new FixedSizeArrayBuilder(5 * uniqueSpanCount); } private static int ComputeNextToken( @@ -315,6 +331,8 @@ private static int ComputeNextToken( var tokenTypeIndex = 0; // Classified spans with the same text span should be combined into one token. + // NOTE: The update of currentClassifiedSpanIndex is closely tied to the allocation + // of the data array in AllocateTokenArray. while (classifiedSpans[currentClassifiedSpanIndex].TextSpan == originalTextSpan) { var classificationType = classifiedSpans[currentClassifiedSpanIndex].ClassificationType; diff --git a/src/LanguageServer/Protocol/Handler/ServerLifetime/LspServiceLifeCycleManager.cs b/src/LanguageServer/Protocol/Handler/ServerLifetime/LspServiceLifeCycleManager.cs index 94ca5ca32f332..e8bec0292d0fe 100644 --- a/src/LanguageServer/Protocol/Handler/ServerLifetime/LspServiceLifeCycleManager.cs +++ b/src/LanguageServer/Protocol/Handler/ServerLifetime/LspServiceLifeCycleManager.cs @@ -43,7 +43,10 @@ public async Task ShutdownAsync(string message = "Shutting down") // Shutting down is not cancellable. var cancellationToken = CancellationToken.None; - var hostWorkspace = _lspWorkspaceRegistrationService.GetAllRegistrations().SingleOrDefault(w => w.Kind == WorkspaceKind.Host); + // HACK: we're doing FirstOrDefault rather than SingleOrDefault because right now in unit tests we might have more than one. Tests that derive from + // AbstractLanguageServerProtocolTests create a TestLspWorkspace, even if the ExportProvider already has some other workspace registered. + // Since we're only using this as a proxy to fetch a workspace service that won't differ between the workspaces, we can pick any of them. + var hostWorkspace = _lspWorkspaceRegistrationService.GetAllRegistrations().FirstOrDefault(w => w.Kind == WorkspaceKind.Host); if (hostWorkspace is not null) { var service = hostWorkspace.Services.GetRequiredService(); diff --git a/src/LanguageServer/Protocol/Handler/SourceGenerators/SourceGeneratedDocumentCache.cs b/src/LanguageServer/Protocol/Handler/SourceGenerators/SourceGeneratedDocumentCache.cs index 31ebb7e377f02..2802028a06405 100644 --- a/src/LanguageServer/Protocol/Handler/SourceGenerators/SourceGeneratedDocumentCache.cs +++ b/src/LanguageServer/Protocol/Handler/SourceGenerators/SourceGeneratedDocumentCache.cs @@ -14,9 +14,9 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.SourceGenerators; internal record struct SourceGeneratedDocumentGetTextState(Document Document); -internal sealed class SourceGeneratedDocumentCache(string uniqueKey) : VersionedPullCache<(SourceGeneratorExecutionVersion, VersionStamp), object?, SourceGeneratedDocumentGetTextState, SourceText?>(uniqueKey), ILspService +internal sealed class SourceGeneratedDocumentCache(string uniqueKey) : VersionedPullCache<(SourceGeneratorExecutionVersion, VersionStamp), SourceGeneratedDocumentGetTextState, SourceText?>(uniqueKey), ILspService { - public override async Task<(SourceGeneratorExecutionVersion, VersionStamp)> ComputeCheapVersionAsync(SourceGeneratedDocumentGetTextState state, CancellationToken cancellationToken) + public override async Task<(SourceGeneratorExecutionVersion, VersionStamp)> ComputeVersionAsync(SourceGeneratedDocumentGetTextState state, CancellationToken cancellationToken) { // The execution version and the dependent version must be considered as one version cached together - // it is not correct to say that if the execution version is the same then we can re-use results (as in automatic mode the execution version never changes). @@ -25,11 +25,6 @@ internal sealed class SourceGeneratedDocumentCache(string uniqueKey) : Versioned return (executionVersion, dependentVersion); } - public override Task ComputeExpensiveVersionAsync(SourceGeneratedDocumentGetTextState state, CancellationToken cancellationToken) - { - return SpecializedTasks.Null(); - } - public override Checksum ComputeChecksum(SourceText? data, string language) { return data is null ? Checksum.Null : Checksum.From(data.GetChecksum()); diff --git a/src/LanguageServer/Protocol/Handler/SpellCheck/SpellCheckPullCache.cs b/src/LanguageServer/Protocol/Handler/SpellCheck/SpellCheckPullCache.cs index b334349d74e92..93e0f79783800 100644 --- a/src/LanguageServer/Protocol/Handler/SpellCheck/SpellCheckPullCache.cs +++ b/src/LanguageServer/Protocol/Handler/SpellCheck/SpellCheckPullCache.cs @@ -13,12 +13,12 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.SpellCheck; internal record struct SpellCheckState(ISpellCheckSpanService Service, Document Document); /// -/// Simplified version of that only uses a +/// Simplified version of that only uses a /// single cheap key to check results against. /// -internal sealed class SpellCheckPullCache(string uniqueKey) : VersionedPullCache<(Checksum parseOptionsChecksum, Checksum textChecksum)?, object?, SpellCheckState, ImmutableArray>(uniqueKey) +internal sealed class SpellCheckPullCache(string uniqueKey) : VersionedPullCache<(Checksum parseOptionsChecksum, Checksum textChecksum), SpellCheckState, ImmutableArray>(uniqueKey) { - public override async Task<(Checksum parseOptionsChecksum, Checksum textChecksum)?> ComputeCheapVersionAsync(SpellCheckState state, CancellationToken cancellationToken) + public override async Task<(Checksum parseOptionsChecksum, Checksum textChecksum)> ComputeVersionAsync(SpellCheckState state, CancellationToken cancellationToken) { var project = state.Document.Project; var parseOptionsChecksum = project.State.GetParseOptionsChecksum(); @@ -41,12 +41,6 @@ public override async Task> ComputeDataAsync(Spel return spans; } - public override Task ComputeExpensiveVersionAsync(SpellCheckState state, CancellationToken cancellationToken) - { - // Spell check does not need an expensive version check - we return null to effectively skip this check. - return SpecializedTasks.Null(); - } - private void SerializeSpellCheckSpan(SpellCheckSpan span, ObjectWriter writer) { writer.WriteInt32(span.TextSpan.Start); diff --git a/src/LanguageServer/Protocol/Handler/SpellCheck/WorkspaceSpellCheckHandler.cs b/src/LanguageServer/Protocol/Handler/SpellCheck/WorkspaceSpellCheckHandler.cs index a3233c7bcac9d..3430c03e95374 100644 --- a/src/LanguageServer/Protocol/Handler/SpellCheck/WorkspaceSpellCheckHandler.cs +++ b/src/LanguageServer/Protocol/Handler/SpellCheck/WorkspaceSpellCheckHandler.cs @@ -26,7 +26,7 @@ protected override VSInternalWorkspaceSpellCheckableReport CreateReport(TextDocu public override TextDocumentIdentifier? GetTextDocumentIdentifier(VSInternalWorkspaceSpellCheckableParams requestParams) => null; protected override ImmutableArray? GetPreviousResults(VSInternalWorkspaceSpellCheckableParams requestParams) - => requestParams.PreviousResults?.Where(d => d.PreviousResultId != null).Select(d => new PreviousPullResult(d.PreviousResultId!, d.TextDocument!)).ToImmutableArray(); + => requestParams.PreviousResults?.Where(d => d.PreviousResultId != null).SelectAsArray(d => new PreviousPullResult(d.PreviousResultId!, d.TextDocument!)); protected override ImmutableArray GetOrderedDocuments(RequestContext context, CancellationToken cancellationToken) { diff --git a/src/LanguageServer/Protocol/Workspaces/ILspMiscellaneousFilesWorkspaceProvider.cs b/src/LanguageServer/Protocol/Workspaces/ILspMiscellaneousFilesWorkspaceProvider.cs index e31acc207ddf3..a6c1e34dc504c 100644 --- a/src/LanguageServer/Protocol/Workspaces/ILspMiscellaneousFilesWorkspaceProvider.cs +++ b/src/LanguageServer/Protocol/Workspaces/ILspMiscellaneousFilesWorkspaceProvider.cs @@ -36,5 +36,5 @@ internal interface ILspMiscellaneousFilesWorkspaceProvider : ILspService /// Note that the implementation of this method should not depend on anything expensive such as RPC calls. /// async is used here to allow taking locks asynchronously and "relatively fast" stuff like that. /// - ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri, bool removeFromMetadataWorkspace); + ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri); } diff --git a/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProvider.cs b/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProvider.cs index 2c48b294a8c42..1146d56854d60 100644 --- a/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProvider.cs +++ b/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProvider.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Features.Workspaces; using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.MetadataAsSource; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Microsoft.CommonLanguageServerProtocol.Framework; @@ -26,7 +25,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer; /// Future work for this workspace includes supporting basic metadata references (mscorlib, System dlls, etc), /// but that is dependent on having a x-plat mechanism for retrieving those references from the framework / sdk. /// -internal sealed class LspMiscellaneousFilesWorkspaceProvider(ILspServices lspServices, IMetadataAsSourceFileService metadataAsSourceFileService, HostServices hostServices) +internal sealed class LspMiscellaneousFilesWorkspaceProvider(ILspServices lspServices, HostServices hostServices) : Workspace(hostServices, WorkspaceKind.MiscellaneousFiles), ILspMiscellaneousFilesWorkspaceProvider, ILspWorkspace { public bool SupportsMutation => true; @@ -40,7 +39,7 @@ public ValueTask IsMiscellaneousFilesDocumentAsync(TextDocument document, /// /// Takes in a file URI and text and creates a misc project and document for the file. /// - /// Calls to this method and are made + /// Calls to this method and are made /// from LSP text sync request handling which do not run concurrently. /// public ValueTask AddMiscellaneousDocumentAsync(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger) @@ -54,15 +53,6 @@ public ValueTask IsMiscellaneousFilesDocumentAsync(TextDocument document, documentFilePath = ProtocolConversions.GetDocumentFilePathFromUri(uri.ParsedUri); } - var container = new StaticSourceTextContainer(documentText); - if (metadataAsSourceFileService.TryAddDocumentToWorkspace(documentFilePath, container, out var documentId)) - { - var metadataWorkspace = metadataAsSourceFileService.TryGetWorkspace(); - Contract.ThrowIfNull(metadataWorkspace); - var document = metadataWorkspace.CurrentSolution.GetRequiredDocument(documentId); - return document; - } - var languageInfoProvider = lspServices.GetRequiredService(); if (!languageInfoProvider.TryGetLanguageInformation(uri, languageId, out var languageInformation)) { @@ -93,13 +83,8 @@ public ValueTask IsMiscellaneousFilesDocumentAsync(TextDocument document, /// Calls to this method and are made /// from LSP text sync request handling which do not run concurrently. /// - public ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri, bool removeFromMetadataWorkspace) + public ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri) { - if (removeFromMetadataWorkspace && uri.ParsedUri is not null && metadataAsSourceFileService.TryRemoveDocumentFromWorkspace(ProtocolConversions.GetDocumentFilePathFromUri(uri.ParsedUri))) - { - return ValueTask.CompletedTask; - } - // We'll only ever have a single document matching this URI in the misc solution. var matchingDocument = CurrentSolution.GetDocumentIds(uri).SingleOrDefault(); if (matchingDocument != null) diff --git a/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProviderFactory.cs b/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProviderFactory.cs index 568903cd80a81..7a5c6fe44966d 100644 --- a/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProviderFactory.cs +++ b/src/LanguageServer/Protocol/Workspaces/LspMiscellaneousFilesWorkspaceProviderFactory.cs @@ -20,10 +20,10 @@ namespace Microsoft.CodeAnalysis.LanguageServer; [ExportCSharpVisualBasicStatelessLspService(typeof(ILspMiscellaneousFilesWorkspaceProviderFactory)), Shared] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] -internal sealed class LspMiscellaneousFilesWorkspaceProviderFactory(IMetadataAsSourceFileService metadataAsSourceFileService) : ILspMiscellaneousFilesWorkspaceProviderFactory +internal sealed class LspMiscellaneousFilesWorkspaceProviderFactory() : ILspMiscellaneousFilesWorkspaceProviderFactory { public ILspMiscellaneousFilesWorkspaceProvider CreateLspMiscellaneousFilesWorkspaceProvider(ILspServices lspServices, HostServices hostServices) { - return new LspMiscellaneousFilesWorkspaceProvider(lspServices, metadataAsSourceFileService, hostServices); + return new LspMiscellaneousFilesWorkspaceProvider(lspServices, hostServices); } } diff --git a/src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs b/src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs index a23b3e4bb0693..832fb857a1508 100644 --- a/src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs +++ b/src/LanguageServer/Protocol/Workspaces/LspWorkspaceManager.cs @@ -13,7 +13,6 @@ using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CodeAnalysis.LanguageServer.Handler.DocumentChanges; using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Microsoft.CommonLanguageServerProtocol.Framework; @@ -53,7 +52,7 @@ internal sealed class LspWorkspaceManager : IDocumentChangeTracker, ILspService /// workspace). /// Access to this is guaranteed to be serial by the /// - private readonly Dictionary _cachedLspSolutions = []; + private readonly Dictionary _cachedLspSolutions = []; /// /// Stores the current source text for each URI that is being tracked by LSP. Each time an LSP text sync @@ -153,12 +152,12 @@ public async ValueTask StopTrackingAsync(DocumentUri uri, CancellationToken canc // If LSP changed, we need to compare against the workspace again to get the updated solution. _cachedLspSolutions.Clear(); - // Also remove it from our loose files or metadata workspace if it is still there. + // Also remove it from our loose files if it is still there. if (_lspMiscellaneousFilesWorkspaceProvider is not null) { try { - await _lspMiscellaneousFilesWorkspaceProvider.TryRemoveMiscellaneousDocumentAsync(uri, removeFromMetadataWorkspace: true).ConfigureAwait(false); + await _lspMiscellaneousFilesWorkspaceProvider.TryRemoveMiscellaneousDocumentAsync(uri).ConfigureAwait(false); } catch (Exception ex) when (FatalError.ReportAndCatch(ex)) { @@ -251,30 +250,43 @@ public void UpdateTrackedDocument(DocumentUri uri, SourceText newSourceText) // Find the matching document from the LSP solutions. foreach (var (workspace, lspSolution, isForked) in lspSolutions) { - var document = await lspSolution.GetTextDocumentAsync(textDocumentIdentifier, cancellationToken).ConfigureAwait(false); - if (document != null) + var documents = await lspSolution.GetTextDocumentsAsync(textDocumentIdentifier.DocumentUri, cancellationToken).ConfigureAwait(false); + if (documents.Length > 0) { - // Record metadata on how we got this document. - var workspaceKind = document.Project.Solution.WorkspaceKind; - _requestTelemetryLogger.UpdateFindDocumentTelemetryData(success: true, workspaceKind); - _requestTelemetryLogger.UpdateUsedForkedSolutionCounter(isForked); - _logger.LogDebug($"{document.FilePath} found in workspace {workspaceKind}"); + // We have at least one document, so find the one in the right project context + var document = documents.FindDocumentInProjectContext(textDocumentIdentifier, (sln, id) => sln.GetRequiredTextDocument(id)); - // If we found the document in a non-misc workspace, also attempt to remove it from the misc workspace - // if it happens to be in there as well. - if (_lspMiscellaneousFilesWorkspaceProvider is not null && !await _lspMiscellaneousFilesWorkspaceProvider.IsMiscellaneousFilesDocumentAsync(document, cancellationToken).ConfigureAwait(false)) + if (_lspMiscellaneousFilesWorkspaceProvider is not null) { - try + // If we started with multiple documents and didn't have specific context information, it's possible we picked a miscellaneous files document when + // we could have picked a real one. + if (documents.Length > 1 && await _lspMiscellaneousFilesWorkspaceProvider.IsMiscellaneousFilesDocumentAsync(document, cancellationToken).ConfigureAwait(false)) { - // Do not attempt to remove the file from the metadata workspace (the document is still open). - await _lspMiscellaneousFilesWorkspaceProvider.TryRemoveMiscellaneousDocumentAsync(uri, removeFromMetadataWorkspace: false).ConfigureAwait(false); + // Pick a different one; our choice here is arbitrary, since if we had a specified context in the first place we would have picked the right one. + document = documents.First(d => d != document); } - catch (Exception ex) when (FatalError.ReportAndCatch(ex)) + + // If we found the document in a non-misc workspace (either immediately or by the correction above), also attempt to remove it from the misc workspace + // if it happens to be in there as well. + if (_lspMiscellaneousFilesWorkspaceProvider is not null && !await _lspMiscellaneousFilesWorkspaceProvider.IsMiscellaneousFilesDocumentAsync(document, cancellationToken).ConfigureAwait(false)) { - _logger.LogException(ex); + try + { + await _lspMiscellaneousFilesWorkspaceProvider.TryRemoveMiscellaneousDocumentAsync(uri).ConfigureAwait(false); + } + catch (Exception ex) when (FatalError.ReportAndCatch(ex)) + { + _logger.LogException(ex); + } } } + // Record metadata on how we got this document. + var workspaceKind = document.Project.Solution.WorkspaceKind; + _requestTelemetryLogger.UpdateFindDocumentTelemetryData(success: true, workspaceKind); + _requestTelemetryLogger.UpdateUsedForkedSolutionCounter(isForked); + _logger.LogDebug($"{document.FilePath} found in workspace {workspaceKind}"); + return (workspace, document.Project.Solution, document); } } @@ -380,8 +392,9 @@ .. registeredWorkspaces.Where(workspace => workspace.Kind == WorkspaceKind.Misce _trackedDocuments.Keys.Where(static trackedDocument => trackedDocument.ParsedUri?.Scheme == SourceGeneratedDocumentUri.Scheme) // We know we have a non null URI with a source generated scheme. .Select(uri => (identity: SourceGeneratedDocumentUri.DeserializeIdentity(workspaceCurrentSolution, uri.ParsedUri!), _trackedDocuments[uri].Text)) - .Where(tuple => tuple.identity.HasValue) - .SelectAsArray(tuple => (tuple.identity!.Value, DateTime.Now, tuple.Text)); + .SelectAsArray( + predicate: tuple => tuple.identity.HasValue, + selector: tuple => (tuple.identity!.Value, DateTime.Now, tuple.Text)); // First we check if normal document text matches the workspace solution. // This does not look at source generated documents. @@ -394,13 +407,20 @@ .. registeredWorkspaces.Where(workspace => workspace.Kind == WorkspaceKind.Misce if (doesAllTextMatch && doesAllSourceGeneratedTextMatch) { // Remember that the current LSP text matches the text in this workspace solution. - _cachedLspSolutions[workspace] = (forkedFromVersion: null, workspaceCurrentSolution); + _cachedLspSolutions[workspace] = (forkedFromVersion: null, sourceGeneratorChecksum: null, workspaceCurrentSolution); return (workspaceCurrentSolution, IsForked: false); } + var forkedFromVersion = workspaceCurrentSolution.SolutionStateContentVersion; + var sourceGeneratorChecksum = workspaceCurrentSolution.CompilationState.SourceGeneratorExecutionVersionMap.GetChecksum(); + // Step 4: See if we can reuse a previously forked solution. - if (cachedSolution != default && cachedSolution.forkedFromVersion == workspaceCurrentSolution.WorkspaceVersion) + if (cachedSolution != default && + cachedSolution.forkedFromVersion == forkedFromVersion && + cachedSolution.sourceGeneratorChecksum == sourceGeneratorChecksum) + { return (cachedSolution.solution, IsForked: true); + } // Step 5: Fork a new solution from the workspace with the LSP text applied. var lspSolution = workspaceCurrentSolution; @@ -418,7 +438,7 @@ .. registeredWorkspaces.Where(workspace => workspace.Kind == WorkspaceKind.Misce } // Remember this forked solution and the workspace version it was forked from. - _cachedLspSolutions[workspace] = (workspaceCurrentSolution.WorkspaceVersion, lspSolution); + _cachedLspSolutions[workspace] = (forkedFromVersion, sourceGeneratorChecksum, lspSolution); return (lspSolution, IsForked: true); } @@ -564,11 +584,21 @@ internal readonly struct TestAccessor public TestAccessor(LspWorkspaceManager manager) => _manager = manager; - public Workspace? GetLspMiscellaneousFilesWorkspace() + public ValueTask IsMiscellaneousFilesDocumentAsync(TextDocument document) { - // For purposes of testing, we test against the implementation that is also a Workspace. - // TODO: once we also test the FileBasedPrograms implementation, we need to do something else here. - return _manager._lspMiscellaneousFilesWorkspaceProvider as Workspace; + return _manager._lspMiscellaneousFilesWorkspaceProvider!.IsMiscellaneousFilesDocumentAsync(document, CancellationToken.None); + } + + public async IAsyncEnumerable GetMiscellaneousDocumentsAsync(Func> documentSelector) where T : TextDocument + { + foreach (var workspace in _manager._lspWorkspaceRegistrationService.GetAllRegistrations()) + { + foreach (var document in workspace.CurrentSolution.Projects.SelectMany(documentSelector)) + { + if (await IsMiscellaneousFilesDocumentAsync(document).ConfigureAwait(false)) + yield return document; + } + } } public bool IsWorkspaceRegistered(Workspace workspace) diff --git a/src/LanguageServer/Protocol/Workspaces/LspWorkspaceRegistrationService.cs b/src/LanguageServer/Protocol/Workspaces/LspWorkspaceRegistrationService.cs index 1b7a230ddb6b6..35f3406c1bbf1 100644 --- a/src/LanguageServer/Protocol/Workspaces/LspWorkspaceRegistrationService.cs +++ b/src/LanguageServer/Protocol/Workspaces/LspWorkspaceRegistrationService.cs @@ -39,9 +39,7 @@ public virtual void Register(Workspace? workspace) m["WorkspacePartialSemanticsEnabled"] = workspace.PartialSemanticsEnabled; }, workspace)); - // Forward workspace change events for all registered LSP workspaces. Requires main thread as it - // fires LspSolutionChanged which hasn't been guaranteed to be thread safe. - var workspaceChangedDisposer = workspace.RegisterWorkspaceChangedHandler(OnLspWorkspaceChanged, WorkspaceEventOptions.RequiresMainThreadOptions); + var workspaceChangedDisposer = workspace.RegisterWorkspaceChangedHandler(OnLspWorkspaceChanged); lock (_gate) { @@ -94,9 +92,7 @@ public void Dispose() } /// - /// Indicates whether the LSP solution has changed in a non-tracked document context. - /// - /// IMPORTANT: Implementations of this event handler should do as little synchronous work as possible since this will block. + /// Indicates whether the LSP solution has changed in a non-tracked document context. May be raised on any thread. /// public EventHandler? LspSolutionChanged; } diff --git a/src/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs b/src/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs index af1aaba629419..e393c941f80aa 100644 --- a/src/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs @@ -711,7 +711,7 @@ void M() Assert.NotNull(completionResult.ItemDefaults.Data); Assert.NotNull(completionResult.ItemDefaults.CommitCharacters); - var myClassItems = completionResult.Items.Where(i => i.Label == "MyClass").ToImmutableArray(); + var myClassItems = completionResult.Items.WhereAsArray(i => i.Label == "MyClass"); var itemFromNS1 = myClassItems.Single(i => i.LabelDetails?.Description == "Namespace1"); var itemFromNS2 = myClassItems.Single(i => i.LabelDetails?.Description == "Namespace2"); diff --git a/src/LanguageServer/ProtocolUnitTests/Definitions/GoToDefinitionTests.cs b/src/LanguageServer/ProtocolUnitTests/Definitions/GoToDefinitionTests.cs index b9d5731e0dd29..417c89abb919d 100644 --- a/src/LanguageServer/ProtocolUnitTests/Definitions/GoToDefinitionTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Definitions/GoToDefinitionTests.cs @@ -4,10 +4,15 @@ #nullable disable +using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; using Roslyn.Test.Utilities.TestGenerators; using Xunit; @@ -22,6 +27,8 @@ public GoToDefinitionTests(ITestOutputHelper testOutputHelper) : base(testOutput { } + protected override TestComposition Composition => base.Composition.AddParts(typeof(TestSourceGeneratedDocumentSpanMappingService)); + [Theory, CombinatorialData] public async Task TestGotoDefinitionAsync(bool mutatingLspWorkspace) { @@ -331,6 +338,42 @@ void M() Assert.True(results.Single().DocumentUri.GetRequiredParsedUri().OriginalString.EndsWith("String.cs")); } + [Theory, CombinatorialData] + public async Task TestGotoDefinitionAsync_WithRazorSourceGeneratedFile(bool mutatingLspWorkspace) + { + var generatedMarkup = """ + public class B + { + public void {|definition:M|}() + { + } + } + """; + await using var testLspServer = await CreateTestLspServerAsync(""" + public class A + { + public void M() + { + new B().{|caret:M|}(); + } + } + """, mutatingLspWorkspace); + + TestFileMarkupParser.GetSpans(generatedMarkup, out var generatedCode, out ImmutableDictionary> spans); + var generatedSourceText = SourceText.From(generatedCode); + + var razorGenerator = new Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator((c) => c.AddSource("generated_file.cs", generatedCode)); + var workspace = testLspServer.TestWorkspace; + var project = workspace.CurrentSolution.Projects.First().AddAnalyzerReference(new TestGeneratorReference(razorGenerator)); + workspace.TryApplyChanges(project.Solution); + + var results = await RunGotoDefinitionAsync(testLspServer, testLspServer.GetLocations("caret").Single()); + Assert.True(results.Single().DocumentUri.GetRequiredParsedUri().LocalPath.EndsWith("generated_file.cs")); + + var service = Assert.IsType(workspace.Services.GetService()); + Assert.True(service.DidMapSpans); + } + private static async Task RunGotoDefinitionAsync(TestLspServer testLspServer, LSP.Location caret) { return await testLspServer.ExecuteRequestAsync(LSP.Methods.TextDocumentDefinitionName, diff --git a/src/LanguageServer/ProtocolUnitTests/Diagnostics/AdditionalFileDiagnosticsTests.cs b/src/LanguageServer/ProtocolUnitTests/Diagnostics/AdditionalFileDiagnosticsTests.cs index a40f429582e35..037f2a333e3ca 100644 --- a/src/LanguageServer/ProtocolUnitTests/Diagnostics/AdditionalFileDiagnosticsTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Diagnostics/AdditionalFileDiagnosticsTests.cs @@ -217,8 +217,6 @@ public Task> GetDiagnosticsAsync(RequestContext c public Project GetProject() => textDocument.Project; - public bool IsLiveSource() => true; - public string ToDisplayString() => textDocument.ToString()!; } } diff --git a/src/LanguageServer/ProtocolUnitTests/Diagnostics/DiagnosticRegistrationTests.cs b/src/LanguageServer/ProtocolUnitTests/Diagnostics/DiagnosticRegistrationTests.cs index c6630cc56b93f..89a986b06713a 100644 --- a/src/LanguageServer/ProtocolUnitTests/Diagnostics/DiagnosticRegistrationTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Diagnostics/DiagnosticRegistrationTests.cs @@ -8,8 +8,10 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using ICSharpCode.Decompiler.CSharp.Syntax; using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; +using Microsoft.CodeAnalysis.UnitTests; using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; using StreamJsonRpc; @@ -25,7 +27,7 @@ public DiagnosticRegistrationTests(ITestOutputHelper? testOutputHelper) : base(t } [Theory, CombinatorialData] - public async Task TestPublicDiagnosticSourcesAreRegisteredWhenSupported(bool mutatingLspWorkspace) + public async Task TestPublicDiagnosticSourcesAreRegisteredWhenSupported(bool mutatingLspWorkspace, bool dynamicRegistration) { var clientCapabilities = new ClientCapabilities { @@ -33,7 +35,7 @@ public async Task TestPublicDiagnosticSourcesAreRegisteredWhenSupported(bool mut { Diagnostic = new DiagnosticSetting { - DynamicRegistration = true, + DynamicRegistration = dynamicRegistration, } } }; @@ -49,42 +51,54 @@ public async Task TestPublicDiagnosticSourcesAreRegisteredWhenSupported(bool mut await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, initializationOptions); var registrations = clientCallbackTarget.GetRegistrations(); + var serverCapabilities = testLspServer.GetServerCapabilities(); // Get all registrations for diagnostics (note that workspace registrations are registered against document method name). var diagnosticRegistrations = registrations .Where(r => r.Method == Methods.TextDocumentDiagnosticName) .Select(r => JsonSerializer.Deserialize((JsonElement)r.RegisterOptions!, ProtocolConversions.LspJsonSerializerOptions)!); - Assert.NotEmpty(diagnosticRegistrations); - - string[] documentSources = [ - PullDiagnosticCategories.DocumentCompilerSyntax, - PullDiagnosticCategories.DocumentCompilerSemantic, - PullDiagnosticCategories.DocumentAnalyzerSyntax, - PullDiagnosticCategories.DocumentAnalyzerSemantic, - PublicDocumentNonLocalDiagnosticSourceProvider.NonLocal - ]; - - string[] documentAndWorkspaceSources = [ - PullDiagnosticCategories.EditAndContinue, - PullDiagnosticCategories.WorkspaceDocumentsAndProject - ]; - - // Verify document only sources are present (and do not set the workspace diagnostic option). - foreach (var documentSource in documentSources) + if (dynamicRegistration) { - var options = Assert.Single(diagnosticRegistrations, (r) => r.Identifier == documentSource); - Assert.False(options.WorkspaceDiagnostics); - Assert.True(options.InterFileDependencies); - } + Assert.NotEmpty(diagnosticRegistrations); + + string[] documentSources = [ + PullDiagnosticCategories.DocumentCompilerSyntax, + PullDiagnosticCategories.DocumentCompilerSemantic, + PullDiagnosticCategories.DocumentAnalyzerSyntax, + PullDiagnosticCategories.DocumentAnalyzerSemantic, + PublicDocumentNonLocalDiagnosticSourceProvider.NonLocal + ]; + + string[] documentAndWorkspaceSources = [ + PullDiagnosticCategories.EditAndContinue, + PullDiagnosticCategories.WorkspaceDocumentsAndProject + ]; + + // Verify document only sources are present (and do not set the workspace diagnostic option). + foreach (var documentSource in documentSources) + { + var options = Assert.Single(diagnosticRegistrations, (r) => r.Identifier == documentSource); + Assert.False(options.WorkspaceDiagnostics); + Assert.True(options.InterFileDependencies); + } - // Verify workspace sources are present (and do set the workspace diagnostic option). - foreach (var workspaceSource in documentAndWorkspaceSources) + // Verify workspace sources are present (and do set the workspace diagnostic option). + foreach (var workspaceSource in documentAndWorkspaceSources) + { + var options = Assert.Single(diagnosticRegistrations, (r) => r.Identifier == workspaceSource); + Assert.True(options.WorkspaceDiagnostics); + Assert.True(options.InterFileDependencies); + Assert.True(options.WorkDoneProgress); + } + } + else { - var options = Assert.Single(diagnosticRegistrations, (r) => r.Identifier == workspaceSource); - Assert.True(options.WorkspaceDiagnostics); - Assert.True(options.InterFileDependencies); - Assert.True(options.WorkDoneProgress); + var diagnosticOptions = (DiagnosticOptions?)serverCapabilities.DiagnosticOptions; + + Assert.Empty(diagnosticRegistrations); + Assert.NotNull(diagnosticOptions); + Assert.True(diagnosticOptions.InterFileDependencies); } // Verify task diagnostics are not present. diff --git a/src/LanguageServer/ProtocolUnitTests/Diagnostics/DiagnosticsPullCacheTests.cs b/src/LanguageServer/ProtocolUnitTests/Diagnostics/DiagnosticsPullCacheTests.cs index 1017a75137049..ab67bd1e23670 100644 --- a/src/LanguageServer/ProtocolUnitTests/Diagnostics/DiagnosticsPullCacheTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Diagnostics/DiagnosticsPullCacheTests.cs @@ -125,11 +125,6 @@ public override Task> GetDiagnosticsAsync(Request isEnabledByDefault: true, warningLevel: 0, [], ImmutableDictionary.Empty,context.Document!.Project.Id, new DiagnosticDataLocation(new FileLinePositionSpan(context.Document!.FilePath!, new Text.LinePosition(0, 0), new Text.LinePosition(0, 0))))]); } - - public override bool IsLiveSource() - { - return true; - } } [Export(typeof(IDiagnosticSourceProvider)), Shared, PartNotDiscoverable] diff --git a/src/LanguageServer/ProtocolUnitTests/Diagnostics/PullDiagnosticTests.cs b/src/LanguageServer/ProtocolUnitTests/Diagnostics/PullDiagnosticTests.cs index d8d0c218a7c00..3e60dcc6da199 100644 --- a/src/LanguageServer/ProtocolUnitTests/Diagnostics/PullDiagnosticTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Diagnostics/PullDiagnosticTests.cs @@ -680,6 +680,7 @@ public partial class C // First diagnostic request should report a diagnostic since the generator does not produce any source (text does not match). var results = await RunGetDocumentPullDiagnosticsAsync(testLspServer, document.GetURI(), useVSDiagnostics); + var firstResultId = results.Single().ResultId; var diagnostic = AssertEx.Single(results.Single().Diagnostics); Assert.Equal("CS0103", diagnostic.Code); @@ -703,7 +704,8 @@ public partial class C } await testLspServer.WaitForSourceGeneratorsAsync(); - results = await RunGetDocumentPullDiagnosticsAsync(testLspServer, document.GetURI(), useVSDiagnostics); + results = await RunGetDocumentPullDiagnosticsAsync(testLspServer, document.GetURI(), useVSDiagnostics, previousResultId: firstResultId); + var secondResultId = results.Single().ResultId; if (executionPreference == SourceGeneratorExecutionPreference.Automatic) { @@ -712,15 +714,18 @@ public partial class C } else { - // In balanced mode, the diagnostic should remain until there is a manual source generator run that updates the sg text. - diagnostic = AssertEx.Single(results.Single().Diagnostics); - Assert.Equal("CS0103", diagnostic.Code); + // In balanced mode, the diagnostic should be unchanged until there is a manual source generator run that updates the sg text. + Assert.Null(results.Single().Diagnostics); + Assert.Equal(firstResultId, secondResultId); testLspServer.TestWorkspace.EnqueueUpdateSourceGeneratorVersion(document.Project.Id, forceRegeneration: false); await testLspServer.WaitForSourceGeneratorsAsync(); - results = await RunGetDocumentPullDiagnosticsAsync(testLspServer, document.GetURI(), useVSDiagnostics); + results = await RunGetDocumentPullDiagnosticsAsync(testLspServer, document.GetURI(), useVSDiagnostics, previousResultId: secondResultId); + var thirdResultId = results.Single().ResultId; + AssertEx.NotNull(results.Single().Diagnostics); Assert.Empty(results.Single().Diagnostics!); + Assert.NotEqual(firstResultId, thirdResultId); } } @@ -2014,7 +2019,7 @@ class A : B { } // Get updated workspace diagnostics for the change. var previousResults = CreateDiagnosticParamsFromPreviousReports(results); - var previousResultIds = previousResults.Select(param => param.resultId).ToImmutableArray(); + var previousResultIds = previousResults.SelectAsArray(param => param.resultId); results = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics, previousResults: previousResults); // Verify that since no actual changes have been made we report unchanged diagnostics. diff --git a/src/LanguageServer/ProtocolUnitTests/FoldingRanges/FoldingRangesTests.cs b/src/LanguageServer/ProtocolUnitTests/FoldingRanges/FoldingRangesTests.cs index 4fb10c9fe770c..5b0de3bd09d1a 100644 --- a/src/LanguageServer/ProtocolUnitTests/FoldingRanges/FoldingRangesTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/FoldingRanges/FoldingRangesTests.cs @@ -53,12 +53,24 @@ public void M(){|implementation: }|} """); + [Theory, CombinatorialData] + public Task TestGetFoldingRangeAsync_AutoCollapse(bool mutatingLspWorkspace) + => AssertFoldingRanges(mutatingLspWorkspace, """ + class C{|foldingRange: + { + private Action Foo(){|implementation: => i =>{|foldingRange: + { + };|}|} + }|} + """); + private async Task AssertFoldingRanges(bool mutatingLspWorkspace, string markup, string collapsedText = null) { var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace); var expected = testLspServer.GetLocations() .SelectMany(kvp => kvp.Value.Select(location => CreateFoldingRange(kvp.Key, location.Range, collapsedText ?? "..."))) .OrderByDescending(range => range.StartLine) + .ThenByDescending(range => range.StartCharacter) .ToArray(); var results = await RunGetFoldingRangeAsync(testLspServer); diff --git a/src/LanguageServer/ProtocolUnitTests/Initialize/LocaleTests.cs b/src/LanguageServer/ProtocolUnitTests/Initialize/LocaleTests.cs index 681db0ed4b8e8..c96b532168aa4 100644 --- a/src/LanguageServer/ProtocolUnitTests/Initialize/LocaleTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Initialize/LocaleTests.cs @@ -41,10 +41,10 @@ public async Task TestUsesLspLocalePerServer(bool mutatingLspWorkspace) Locale = "ja" }); - await using var testLspServerTwo = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions + await using var testLspServerTwo = await CreateTestLspServerAsync(testLspServerOne.TestWorkspace, new InitializationOptions { Locale = "zh" - }); + }, LanguageNames.CSharp); var resultOne = await testLspServerOne.ExecuteRequestAsync(LocaleTestHandler.MethodName, new Request(), CancellationToken.None); var resultTwo = await testLspServerTwo.ExecuteRequestAsync(LocaleTestHandler.MethodName, new Request(), CancellationToken.None); diff --git a/src/LanguageServer/ProtocolUnitTests/LanguageServerTargetTests.cs b/src/LanguageServer/ProtocolUnitTests/LanguageServerTargetTests.cs index d6bcb287fa3be..9f12a828f806a 100644 --- a/src/LanguageServer/ProtocolUnitTests/LanguageServerTargetTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/LanguageServerTargetTests.cs @@ -66,7 +66,7 @@ public async Task LanguageServerCleansUpOnUnexpectedJsonRpcDisconnectAsync(bool public async Task LanguageServerHasSeparateServiceInstances(bool mutatingLspWorkspace) { await using var serverOne = await CreateTestLspServerAsync("", mutatingLspWorkspace); - await using var serverTwo = await CreateTestLspServerAsync("", mutatingLspWorkspace); + await using var serverTwo = await CreateTestLspServerAsync(serverOne.TestWorkspace, initializationOptions: default, LanguageNames.CSharp); // Get an LSP service and verify each server has its own instance per server. Assert.NotSame(serverOne.GetRequiredLspService(), serverTwo.GetRequiredLspService()); diff --git a/src/LanguageServer/ProtocolUnitTests/LspServicesTests.cs b/src/LanguageServer/ProtocolUnitTests/LspServicesTests.cs index e3d31c12adab9..ec70cac816d6e 100644 --- a/src/LanguageServer/ProtocolUnitTests/LspServicesTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/LspServicesTests.cs @@ -78,7 +78,7 @@ public async Task ReturnsLspServiceForMatchingServer(bool mutatingLspWorkspace) var lspService = server.GetRequiredLspService(); Assert.True(lspService is CSharpLspService); - await using var server2 = await CreateTestLspServerAsync("", mutatingLspWorkspace, initializationOptions: new() { ServerKind = WellKnownLspServerKinds.AlwaysActiveVSLspServer }, composition); + await using var server2 = await CreateTestLspServerAsync(server.TestWorkspace, initializationOptions: new() { ServerKind = WellKnownLspServerKinds.AlwaysActiveVSLspServer }, LanguageNames.CSharp); var lspService2 = server2.GetRequiredLspService(); Assert.True(lspService2 is AlwaysActiveCSharpLspService); diff --git a/src/LanguageServer/ProtocolUnitTests/Metadata/LspMetadataAsSourceWorkspaceTests.cs b/src/LanguageServer/ProtocolUnitTests/Metadata/LspMetadataAsSourceWorkspaceTests.cs index 31001ce1d526c..01bed10bb2fbb 100644 --- a/src/LanguageServer/ProtocolUnitTests/Metadata/LspMetadataAsSourceWorkspaceTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Metadata/LspMetadataAsSourceWorkspaceTests.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.MetadataAsSource; using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; using Xunit; @@ -48,12 +47,13 @@ void M() await testLspServer.OpenDocumentAsync(definition.Single().DocumentUri, text: string.Empty).ConfigureAwait(false); Assert.Equal(WorkspaceKind.MetadataAsSource, (await GetWorkspaceForDocument(testLspServer, definition.Single().DocumentUri)).Kind); - AssertMiscFileWorkspaceEmpty(testLspServer); + await AssertMiscFileWorkspaceEmpty(testLspServer); - // Close the metadata file and verify it gets removed from the metadata workspace. + // Close the metadata file - the file will still be present in MAS. await testLspServer.CloseDocumentAsync(definition.Single().DocumentUri).ConfigureAwait(false); - AssertMetadataFileWorkspaceEmpty(testLspServer); + Assert.Equal(WorkspaceKind.MetadataAsSource, (await GetWorkspaceForDocument(testLspServer, definition.Single().DocumentUri)).Kind); + await AssertMiscFileWorkspaceEmpty(testLspServer); } [Theory, CombinatorialData] @@ -93,7 +93,7 @@ public static void WriteLine(string value) {} """).ConfigureAwait(false); var workspaceForDocument = await GetWorkspaceForDocument(testLspServer, definition.Single().DocumentUri); Assert.Equal(WorkspaceKind.MetadataAsSource, workspaceForDocument.Kind); - AssertMiscFileWorkspaceEmpty(testLspServer); + await AssertMiscFileWorkspaceEmpty(testLspServer); // Manually register the workspace for followup requests - the workspace event listener that // normally registers it on creation is not running in test code. @@ -123,16 +123,9 @@ private static async Task GetWorkspaceForDocument(TestLspServer testL return lspWorkspace!; } - private static void AssertMiscFileWorkspaceEmpty(TestLspServer testLspServer) - { - var doc = testLspServer.GetManagerAccessor().GetLspMiscellaneousFilesWorkspace()!.CurrentSolution.Projects.SingleOrDefault()?.Documents.SingleOrDefault(); - Assert.Null(doc); - } - - private static void AssertMetadataFileWorkspaceEmpty(TestLspServer testLspServer) + private static async Task AssertMiscFileWorkspaceEmpty(TestLspServer testLspServer) { - var provider = testLspServer.TestWorkspace.ExportProvider.GetExportedValue(); - var metadataDocument = provider.TryGetWorkspace()?.CurrentSolution.Projects.SingleOrDefault()?.Documents.SingleOrDefault(); - Assert.Null(metadataDocument); + var docs = await testLspServer.GetManagerAccessor().GetMiscellaneousDocumentsAsync(static p => p.Documents).ToImmutableArrayAsync(CancellationToken.None); + Assert.Empty(docs); } } diff --git a/src/LanguageServer/ProtocolUnitTests/MiscellaneousFiles/LspMiscellaneousFilesWorkspaceTests.cs b/src/LanguageServer/ProtocolUnitTests/MiscellaneousFiles/LspMiscellaneousFilesWorkspaceTests.cs new file mode 100644 index 0000000000000..6d492bd7664c3 --- /dev/null +++ b/src/LanguageServer/ProtocolUnitTests/MiscellaneousFiles/LspMiscellaneousFilesWorkspaceTests.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.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.LanguageServer.UnitTests.Miscellaneous; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Test.Utilities; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.MiscellaneousFiles; + +/// +/// This class runs all the tests in against the base implementation. +/// +public sealed class LspMiscellaneousFilesWorkspaceTests : AbstractLspMiscellaneousFilesWorkspaceTests +{ + public LspMiscellaneousFilesWorkspaceTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + } + + private protected override async ValueTask AddDocumentAsync(TestLspServer testLspServer, string filePath, string content) + { + var projectId = testLspServer.TestWorkspace.CurrentSolution.ProjectIds.Single(); + var documentId = DocumentId.CreateNewId(projectId, filePath); + await testLspServer.TestWorkspace.AddDocumentAsync( + DocumentInfo.Create( + documentId, + name: filePath, + filePath: filePath, + loader: new TestTextLoader(content))); + + return testLspServer.TestWorkspace.CurrentSolution.GetRequiredDocument(documentId); + } + + private protected override Workspace GetHostWorkspace(TestLspServer testLspServer) + { + return testLspServer.TestWorkspace; + } +} diff --git a/src/LanguageServer/ProtocolUnitTests/Options/SolutionAnalyzerConfigOptionsUpdaterTests.cs b/src/LanguageServer/ProtocolUnitTests/Options/SolutionAnalyzerConfigOptionsUpdaterTests.cs index f44f51312ecc7..7807f7f209a6c 100644 --- a/src/LanguageServer/ProtocolUnitTests/Options/SolutionAnalyzerConfigOptionsUpdaterTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Options/SolutionAnalyzerConfigOptionsUpdaterTests.cs @@ -24,7 +24,9 @@ public sealed class SolutionAnalyzerConfigOptionsUpdaterTests private static TestWorkspace CreateWorkspace() { var workspace = new LspTestWorkspace(LspTestCompositions.LanguageServerProtocol - .RemoveParts(typeof(MockFallbackAnalyzerConfigOptionsProvider))); + .RemoveParts(typeof(MockFallbackAnalyzerConfigOptionsProvider)) + .ExportProviderFactory + .CreateExportProvider()); var updater = (SolutionAnalyzerConfigOptionsUpdater)workspace.ExportProvider.GetExports().Single(e => e.Value is SolutionAnalyzerConfigOptionsUpdater).Value; var listenerProvider = workspace.GetService(); diff --git a/src/LanguageServer/ProtocolUnitTests/References/FindAllReferencesHandlerTests.cs b/src/LanguageServer/ProtocolUnitTests/References/FindAllReferencesHandlerTests.cs index ac634aebb0e1c..b36ed1edb78c2 100644 --- a/src/LanguageServer/ProtocolUnitTests/References/FindAllReferencesHandlerTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/References/FindAllReferencesHandlerTests.cs @@ -6,12 +6,17 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.LanguageServer.Handler; using Microsoft.CodeAnalysis.ReferenceHighlighting; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; using Roslyn.Text.Adornments; using Roslyn.Utilities; @@ -23,6 +28,8 @@ namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.References; public sealed class FindAllReferencesHandlerTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerProtocolTests(testOutputHelper) { + protected override TestComposition Composition => base.Composition.AddParts(typeof(TestSourceGeneratedDocumentSpanMappingService)); + [Theory, CombinatorialData] public async Task TestFindAllReferencesAsync(bool mutatingLspWorkspace) { @@ -341,6 +348,43 @@ void M() AssertLocationsEqual(testLspServer.GetLocations("reference"), results.Skip(1).Select(r => r.Location)); } + [Theory, CombinatorialData] + public async Task TestFindReferencesAsync_WithRazorSourceGeneratedFile(bool mutatingLspWorkspace) + { + var generatedMarkup = """ + public class B + { + public void {|reference:M|}() + { + } + } + """; + await using var testLspServer = await CreateTestLspServerAsync(""" + public class A + { + public void M() + { + new B().{|caret:M|}(); + } + } + """, mutatingLspWorkspace, CapabilitiesWithVSExtensions); + + TestFileMarkupParser.GetSpans(generatedMarkup, out var generatedCode, out ImmutableDictionary> spans); + var generatedSourceText = SourceText.From(generatedCode); + + var razorGenerator = new Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator((c) => c.AddSource("generated_file.cs", generatedCode)); + var workspace = testLspServer.TestWorkspace; + var project = workspace.CurrentSolution.Projects.First().AddAnalyzerReference(new TestGeneratorReference(razorGenerator)); + workspace.TryApplyChanges(project.Solution); + + var results = await RunFindAllReferencesAsync(testLspServer, testLspServer.GetLocations("caret").First()); + Assert.Equal(2, results.Length); + Assert.True(results[1].Location.DocumentUri.GetRequiredParsedUri().LocalPath.EndsWith("generated_file.cs")); + + var service = Assert.IsType(workspace.Services.GetService()); + Assert.True(service.DidMapSpans); + } + private static LSP.ReferenceParams CreateReferenceParams(LSP.Location caret, IProgress progress) => new LSP.ReferenceParams() { diff --git a/src/LanguageServer/ProtocolUnitTests/Rename/RenameTests.cs b/src/LanguageServer/ProtocolUnitTests/Rename/RenameTests.cs index 2e344bb92fbc6..bd79c74a4839e 100644 --- a/src/LanguageServer/ProtocolUnitTests/Rename/RenameTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Rename/RenameTests.cs @@ -4,11 +4,17 @@ #nullable disable +using System; using System.Collections.Immutable; +using System.Composition; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; using Xunit; @@ -19,6 +25,8 @@ namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.Rename; public sealed class RenameTests(ITestOutputHelper testOutputHelper) : AbstractLanguageServerProtocolTests(testOutputHelper) { + protected override TestComposition Composition => base.Composition.AddParts(typeof(TestSourceGeneratedDocumentSpanMappingService)); + [Theory, CombinatorialData] public async Task TestRenameAsync(bool mutatingLspWorkspace) { @@ -189,10 +197,10 @@ class B { void M() { - new A().{|renamed:M|}(); + new A().M(); var a = new A(); - a.{|renamed:M|}(); + a.M(); } } """; @@ -215,15 +223,62 @@ void M2() }); var renameLocation = testLspServer.GetLocations("caret").First(); - var renamePosition = ProtocolConversions.PositionToLinePosition(renameLocation.Range.Start); - var document = await testLspServer.GetDocumentAsync(renameLocation.DocumentUri); var renameValue = "RENAME"; var expectedEdits = testLspServer.GetLocations("renamed").Select(location => new LSP.TextEdit() { NewText = renameValue, Range = location.Range }); - var results = await RenameHandler.GetRenameEditAsync(document, renamePosition, renameValue, includeSourceGenerated: true, CancellationToken.None); + var results = await RunRenameAsync(testLspServer, CreateRenameParams(renameLocation, renameValue)); AssertJsonEquals(expectedEdits, ((TextDocumentEdit[])results.DocumentChanges).SelectMany(e => e.Edits)); } + [Theory, CombinatorialData] + public async Task TestRename_WithRazorSourceGeneratedFile(bool mutatingLspWorkspace) + { + var generatedMarkup = """ + class B + { + void M() + { + new A().{|renamed:M|}(); + + var a = new A(); + a.{|renamed:M|}(); + } + } + """; + await using var testLspServer = await CreateTestLspServerAsync(""" + public class A + { + public void {|caret:|}{|renamed:M|}() + { + } + + void M2() + { + {|renamed:M|}() + } + } + """, mutatingLspWorkspace); + + TestFileMarkupParser.GetSpans(generatedMarkup, out var generatedCode, out ImmutableDictionary> spans); + var generatedSourceText = SourceText.From(generatedCode); + + var razorGenerator = new Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator((c) => c.AddSource("generated_file.cs", generatedCode)); + var workspace = testLspServer.TestWorkspace; + var project = workspace.CurrentSolution.Projects.First().AddAnalyzerReference(new TestGeneratorReference(razorGenerator)); + workspace.TryApplyChanges(project.Solution); + + var renameLocation = testLspServer.GetLocations("caret").First(); + var renameValue = "RENAME"; + var expectedEdits = testLspServer.GetLocations("renamed").Select(location => new LSP.TextEdit() { NewText = renameValue, Range = location.Range }); + var expectedGeneratedEdits = spans["renamed"].Select(span => new LSP.TextEdit() { NewText = renameValue, Range = ProtocolConversions.TextSpanToRange(span, generatedSourceText) }); + + var results = await RunRenameAsync(testLspServer, CreateRenameParams(renameLocation, renameValue)); + AssertJsonEquals(expectedEdits.Concat(expectedGeneratedEdits), ((TextDocumentEdit[])results.DocumentChanges).SelectMany(e => e.Edits)); + + var service = Assert.IsType(workspace.Services.GetService()); + Assert.True(service.DidMapSpans); + } + [Theory, CombinatorialData] public async Task TestRename_OriginateInSourceGeneratedFile(bool mutatingLspWorkspace) { @@ -251,20 +306,27 @@ void M2() {|renamed:M|}() } } - """, mutatingLspWorkspace, - new InitializationOptions() - { - SourceGeneratedMarkups = [generatedMarkup] - }); + """, mutatingLspWorkspace); - var renameLocation = testLspServer.GetLocations("caret").First(); - var renamePosition = ProtocolConversions.PositionToLinePosition(renameLocation.Range.Start); - var document = await testLspServer.GetDocumentAsync(renameLocation.DocumentUri); + TestFileMarkupParser.GetSpans(generatedMarkup, out var generatedCode, out ImmutableDictionary> spans); + var generatedSourceText = SourceText.From(generatedCode); + + var razorGenerator = new Microsoft.NET.Sdk.Razor.SourceGenerators.RazorSourceGenerator((c) => c.AddSource("generated_file.cs", generatedCode)); + var workspace = testLspServer.TestWorkspace; + var project = workspace.CurrentSolution.Projects.First().AddAnalyzerReference(new TestGeneratorReference(razorGenerator)); + workspace.TryApplyChanges(project.Solution); + var generatedDocument = (await project.GetSourceGeneratedDocumentsAsync()).First(); + + var renameLocation = await ProtocolConversions.TextSpanToLocationAsync(generatedDocument, spans["caret"].First(), isStale: false, CancellationToken.None); var renameValue = "RENAME"; var expectedEdits = testLspServer.GetLocations("renamed").Select(location => new LSP.TextEdit() { NewText = renameValue, Range = location.Range }); + var expectedGeneratedEdits = spans["renamed"].Select(span => new LSP.TextEdit() { NewText = renameValue, Range = ProtocolConversions.TextSpanToRange(span, generatedSourceText) }); - var results = await RenameHandler.GetRenameEditAsync(document, renamePosition, renameValue, includeSourceGenerated: true, CancellationToken.None); - AssertJsonEquals(expectedEdits, ((TextDocumentEdit[])results.DocumentChanges).SelectMany(e => e.Edits)); + var results = await RunRenameAsync(testLspServer, CreateRenameParams(renameLocation, renameValue)); + AssertJsonEquals(expectedEdits.Concat(expectedGeneratedEdits), ((TextDocumentEdit[])results.DocumentChanges).SelectMany(e => e.Edits)); + + var service = Assert.IsType(workspace.Services.GetService()); + Assert.True(service.DidMapSpans); } private static LSP.RenameParams CreateRenameParams(LSP.Location location, string newName) diff --git a/src/LanguageServer/ProtocolUnitTests/TestSourceGeneratedDocumentSpanMappingService.cs b/src/LanguageServer/ProtocolUnitTests/TestSourceGeneratedDocumentSpanMappingService.cs new file mode 100644 index 0000000000000..d7962b84b14ec --- /dev/null +++ b/src/LanguageServer/ProtocolUnitTests/TestSourceGeneratedDocumentSpanMappingService.cs @@ -0,0 +1,45 @@ +// 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. + +#nullable disable + +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests; + +[ExportWorkspaceService(typeof(ISourceGeneratedDocumentSpanMappingService))] +[Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class TestSourceGeneratedDocumentSpanMappingService() : ISourceGeneratedDocumentSpanMappingService +{ + public bool DidMapSpans { get; private set; } + + public bool CanMapSpans(SourceGeneratedDocument sourceGeneratedDocument) + { + throw new NotImplementedException(); + } + + public Task> GetMappedTextChangesAsync(SourceGeneratedDocument oldDocument, SourceGeneratedDocument newDocument, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task> MapSpansAsync(SourceGeneratedDocument document, ImmutableArray spans, CancellationToken cancellationToken) + { + if (document.IsRazorSourceGeneratedDocument()) + { + DidMapSpans = true; + } + + return Task.FromResult(ImmutableArray.Empty); + } +} diff --git a/src/LanguageServer/ProtocolUnitTests/UriTests.cs b/src/LanguageServer/ProtocolUnitTests/UriTests.cs index 03030c16a44fa..020f9db691c8e 100644 --- a/src/LanguageServer/ProtocolUnitTests/UriTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/UriTests.cs @@ -50,9 +50,9 @@ void M() """, languageId: "csharp").ConfigureAwait(false); // Verify file is added to the misc file workspace. - var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = looseFileUri }, CancellationToken.None); - Assert.Equal(testLspServer.GetManager().GetTestAccessor().GetLspMiscellaneousFilesWorkspace(), workspace); - AssertEx.NotNull(document); + var (_, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = looseFileUri }, CancellationToken.None); + Assert.NotNull(document); + Assert.True(await testLspServer.GetManager().GetTestAccessor().IsMiscellaneousFilesDocumentAsync(document)); Assert.Equal(looseFileUri, document.GetURI()); Assert.Equal(filePath, document.FilePath); } @@ -76,9 +76,9 @@ void M() """, languageId: "csharp").ConfigureAwait(false); // Verify file is added to the misc file workspace. - var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = looseFileUri }, CancellationToken.None); - Assert.Equal(testLspServer.GetManager().GetTestAccessor().GetLspMiscellaneousFilesWorkspace(), workspace); - AssertEx.NotNull(document); + var (_, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = looseFileUri }, CancellationToken.None); + Assert.NotNull(document); + Assert.True(await testLspServer.GetManager().GetTestAccessor().IsMiscellaneousFilesDocumentAsync(document)); Assert.Equal(looseFileUri, document.GetURI()); Assert.Equal(looseFileUri.UriString, document.FilePath); } @@ -99,7 +99,7 @@ public class A """; - await using var testLspServer = await CreateXmlTestLspServerAsync(markup, mutatingLspWorkspace); + await using var testLspServer = await CreateXmlTestLspServerAsync(markup, mutatingLspWorkspace, initializationOptions: new() { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); var workspaceDocument = testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.Single(); var expectedDocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(documentFilePath); @@ -109,8 +109,8 @@ public class A // Verify file is not added to the misc file workspace. { var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = expectedDocumentUri }, CancellationToken.None); - Assert.NotEqual(testLspServer.GetManager().GetTestAccessor().GetLspMiscellaneousFilesWorkspace(), workspace); - AssertEx.NotNull(document); + Assert.NotNull(document); + Assert.False(await testLspServer.GetManager().GetTestAccessor().IsMiscellaneousFilesDocumentAsync(document)); Assert.Equal(expectedDocumentUri, document.GetURI()); Assert.Equal(documentFilePath, document.FilePath); } @@ -119,9 +119,9 @@ public class A { var lowercaseUri = ProtocolConversions.CreateAbsoluteDocumentUri(documentFilePath.ToLowerInvariant()); Assert.NotEqual(expectedDocumentUri.GetRequiredParsedUri().AbsolutePath, lowercaseUri.GetRequiredParsedUri().AbsolutePath); - var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = lowercaseUri }, CancellationToken.None); - Assert.NotEqual(testLspServer.GetManager().GetTestAccessor().GetLspMiscellaneousFilesWorkspace(), workspace); - AssertEx.NotNull(document); + var (_, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = lowercaseUri }, CancellationToken.None); + Assert.NotNull(document); + Assert.False(await testLspServer.GetManager().GetTestAccessor().IsMiscellaneousFilesDocumentAsync(document)); Assert.Equal(expectedDocumentUri, document.GetURI()); Assert.Equal(documentFilePath, document.FilePath); } @@ -301,8 +301,8 @@ public async Task TestDoesNotCrashIfUnableToDetermineLanguageInfo(bool mutatingL // Verify file is added to the misc file workspace. var (workspace, _, document) = await testLspServer.GetManager().GetLspDocumentInfoAsync(new LSP.TextDocumentIdentifier { DocumentUri = looseFileUri }, CancellationToken.None); - Assert.Equal(testLspServer.GetManager().GetTestAccessor().GetLspMiscellaneousFilesWorkspace(), workspace); - AssertEx.NotNull(document); + Assert.NotNull(document); + Assert.True(await testLspServer.GetManager().GetTestAccessor().IsMiscellaneousFilesDocumentAsync(document)); Assert.Equal(looseFileUri, document.GetURI()); Assert.Equal(looseFileUri.UriString, document.FilePath); diff --git a/src/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs b/src/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs index e661f144a2c14..ceddfdad77ebb 100644 --- a/src/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Workspaces/LspWorkspaceManagerTests.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -15,17 +14,15 @@ using Roslyn.LanguageServer.Protocol; using Roslyn.Test.Utilities; using Roslyn.Test.Utilities.TestGenerators; +using Roslyn.Utilities; using Xunit; using Xunit.Abstractions; namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.Workspaces; -public sealed class LspWorkspaceManagerTests : AbstractLanguageServerProtocolTests +public sealed class LspWorkspaceManagerTests(ITestOutputHelper testOutputHelper) + : AbstractLanguageServerProtocolTests(testOutputHelper) { - public LspWorkspaceManagerTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) - { - } - [Theory, CombinatorialData] public async Task TestUsesLspTextOnOpenCloseAsync(bool mutatingLspWorkspace) { @@ -232,51 +229,6 @@ public async Task TestLspFindsNewDocumentAsync(bool mutatingLspWorkspace) Assert.Equal(testLspServer.TestWorkspace.CurrentSolution, lspDocument.Project.Solution); } - [Theory, CombinatorialData] - public async Task TestLspTransfersDocumentToNewWorkspaceAsync(bool mutatingLspWorkspace) - { - var markup = "One"; - - // Create a server that includes the LSP misc files workspace so we can test transfers to and from it. - await using var testLspServer = await CreateTestLspServerAsync(markup, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); - - // Create a new document, but do not update the workspace solution yet. - var newDocumentId = DocumentId.CreateNewId(testLspServer.TestWorkspace.CurrentSolution.ProjectIds[0]); - - // Include some Unicode characters to test URL handling. - var newDocumentFilePath = "C:\\NewDoc\\\ue25b\ud86d\udeac.cs"; - var newDocumentInfo = DocumentInfo.Create(newDocumentId, "NewDoc.cs", filePath: newDocumentFilePath, loader: new TestTextLoader("New Doc")); - var newDocumentUri = ProtocolConversions.CreateAbsoluteDocumentUri(newDocumentFilePath); - - // Open the document via LSP before the workspace sees it. - await testLspServer.OpenDocumentAsync(newDocumentUri, "LSP text"); - - // Verify it is in the lsp misc workspace. - var (miscWorkspace, miscDocument) = await GetLspWorkspaceAndDocumentAsync(newDocumentUri, testLspServer).ConfigureAwait(false); - AssertEx.NotNull(miscDocument); - Assert.Equal(testLspServer.GetManagerAccessor().GetLspMiscellaneousFilesWorkspace(), miscWorkspace); - Assert.Equal("LSP text", (await miscDocument.GetTextAsync(CancellationToken.None)).ToString()); - - // Make a change and verify the misc document is updated. - await testLspServer.InsertTextAsync(newDocumentUri, (0, 0, "More LSP text")); - (_, miscDocument) = await GetLspWorkspaceAndDocumentAsync(newDocumentUri, testLspServer).ConfigureAwait(false); - AssertEx.NotNull(miscDocument); - var miscText = await miscDocument.GetTextAsync(CancellationToken.None); - Assert.Equal("More LSP textLSP text", miscText.ToString()); - - // Update the registered workspace with the new document. - await testLspServer.TestWorkspace.AddDocumentAsync(newDocumentInfo); - - // Verify that the newly added document in the registered workspace is returned. - var (documentWorkspace, document) = await GetLspWorkspaceAndDocumentAsync(newDocumentUri, testLspServer).ConfigureAwait(false); - AssertEx.NotNull(document); - Assert.Equal(testLspServer.TestWorkspace, documentWorkspace); - Assert.Equal(newDocumentId, document.Id); - // Verify we still are using the tracked LSP text for the document. - var documentText = await document.GetTextAsync(CancellationToken.None); - Assert.Equal("More LSP textLSP text", documentText.ToString()); - } - [Theory, CombinatorialData] public async Task TestUsesRegisteredHostWorkspace(bool mutatingLspWorkspace) { @@ -302,10 +254,10 @@ public async Task TestUsesRegisteredHostWorkspace(bool mutatingLspWorkspace) // Verify 1 workspace registered to start with. Assert.True(IsWorkspaceRegistered(testLspServer.TestWorkspace, testLspServer)); - using var testWorkspaceTwo = LspTestWorkspace.Create( + using var testWorkspaceTwo = TestWorkspace.Create( XElement.Parse(secondWorkspaceXml), - workspaceKind: "OtherWorkspaceKind", - composition: testLspServer.TestWorkspace.Composition); + exportProvider: testLspServer.TestWorkspace.ExportProvider, + workspaceKind: "OtherWorkspaceKind"); // Wait for workspace creation operations for the second workspace to complete. await WaitForWorkspaceOperationsAsync(testWorkspaceTwo); @@ -362,7 +314,7 @@ public async Task TestLspUpdatesCorrectWorkspaceWithMultipleWorkspacesAsync(bool """; await using var testLspServer = await CreateXmlTestLspServerAsync(firstWorkspaceXml, mutatingLspWorkspace); - using var testWorkspaceTwo = CreateWorkspace(options: null, WorkspaceKind.MSBuild, mutatingLspWorkspace); + using var testWorkspaceTwo = await CreateWorkspaceAsync(options: null, WorkspaceKind.MSBuild, mutatingLspWorkspace); testWorkspaceTwo.InitializeDocuments(XElement.Parse($""" @@ -421,7 +373,7 @@ public async Task TestWorkspaceEventUpdatesCorrectWorkspaceWithMultipleWorkspace """; await using var testLspServer = await CreateXmlTestLspServerAsync(firstWorkspaceXml, mutatingLspWorkspace); - using var testWorkspaceTwo = CreateWorkspace(options: null, workspaceKind: WorkspaceKind.MSBuild, mutatingLspWorkspace); + using var testWorkspaceTwo = await CreateWorkspaceAsync(options: null, workspaceKind: WorkspaceKind.MSBuild, mutatingLspWorkspace); testWorkspaceTwo.InitializeDocuments(XElement.Parse($""" @@ -466,7 +418,7 @@ public async Task TestWorkspaceEventUpdatesCorrectWorkspaceWithMultipleWorkspace [Theory, CombinatorialData] public async Task TestSeparateWorkspaceManagerPerServerAsync(bool mutatingLspWorkspace) { - using var testWorkspace = CreateWorkspace(options: null, workspaceKind: null, mutatingLspWorkspace); + using var testWorkspace = await CreateWorkspaceAsync(options: null, workspaceKind: null, mutatingLspWorkspace); testWorkspace.InitializeDocuments(XElement.Parse($""" @@ -583,7 +535,7 @@ await testLspServer.TestWorkspace.ChangeSolutionAsync( (await testLspServer.TestWorkspace.CurrentSolution.Projects.Single().Documents.Single().GetTextAsync()).ToString()); // The file should not be in the misc workspace. - Assert.Empty(testLspServer.GetManagerAccessor().GetLspMiscellaneousFilesWorkspace()!.CurrentSolution.Projects); + Assert.Empty(await testLspServer.GetManagerAccessor().GetMiscellaneousDocumentsAsync(static p => p.Documents).ToImmutableArrayAsync(CancellationToken.None)); // Now, if the project system removes the file, we will still see the lsp version of, but back to the misc workspace. await testLspServer.TestWorkspace.ChangeSolutionAsync( @@ -673,8 +625,8 @@ await testLspServer.ReplaceTextAsync(documentUri, // incremental parsing may be. For example, sometimes it will conservatively not reuse a node because that node // touches a node that is getting recreated. var syntaxFacts = originalDocument.GetRequiredLanguageService(); - var oldClassDeclarations = originalRoot.DescendantNodes().Where(n => syntaxFacts.IsClassDeclaration(n)).ToImmutableArray(); - var newClassDeclarations = newRoot.DescendantNodes().Where(n => syntaxFacts.IsClassDeclaration(n)).ToImmutableArray(); + var oldClassDeclarations = originalRoot.DescendantNodes().WhereAsArray(n => syntaxFacts.IsClassDeclaration(n)); + var newClassDeclarations = newRoot.DescendantNodes().WhereAsArray(n => syntaxFacts.IsClassDeclaration(n)); Assert.Equal(oldClassDeclarations.Length, newClassDeclarations.Length); Assert.Equal(3, oldClassDeclarations.Length); @@ -683,8 +635,8 @@ await testLspServer.ReplaceTextAsync(documentUri, Assert.False(oldClassDeclarations[2].IsIncrementallyIdenticalTo(newClassDeclarations[2])); // All the methods will get reused. - var oldMethodDeclarations = originalRoot.DescendantNodes().Where(n => syntaxFacts.IsMethodLevelMember(n)).ToImmutableArray(); - var newMethodDeclarations = newRoot.DescendantNodes().Where(n => syntaxFacts.IsMethodLevelMember(n)).ToImmutableArray(); + var oldMethodDeclarations = originalRoot.DescendantNodes().WhereAsArray(n => syntaxFacts.IsMethodLevelMember(n)); + var newMethodDeclarations = newRoot.DescendantNodes().WhereAsArray(n => syntaxFacts.IsMethodLevelMember(n)); Assert.Equal(oldMethodDeclarations.Length, newMethodDeclarations.Length); Assert.Equal(3, oldMethodDeclarations.Length); diff --git a/src/LanguageServer/ProtocolUnitTests/Workspaces/SourceGeneratedDocumentTests.cs b/src/LanguageServer/ProtocolUnitTests/Workspaces/SourceGeneratedDocumentTests.cs index fb298c5fbcc1e..99af09f681261 100644 --- a/src/LanguageServer/ProtocolUnitTests/Workspaces/SourceGeneratedDocumentTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/Workspaces/SourceGeneratedDocumentTests.cs @@ -2,6 +2,7 @@ // 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.Linq; using System.Threading; using System.Threading.Tasks; @@ -11,7 +12,6 @@ using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; using Roslyn.Test.Utilities.TestGenerators; -using Roslyn.Utilities; using Xunit; using Xunit.Abstractions; using LSP = Roslyn.LanguageServer.Protocol; @@ -223,6 +223,103 @@ internal async Task TestReturnsGeneratedSourceWhenManuallyRefreshed(bool mutatin Assert.Equal("// callCount: 1", secondRequest.Text); } + [Theory, CombinatorialData] + internal async Task TestCanRunSourceGeneratorAndApplyChangesConcurrently( + bool mutatingLspWorkspace, + bool majorVersionUpdate, + SourceGeneratorExecutionPreference sourceGeneratorExecution) + { + await using var testLspServer = await CreateTestLspServerAsync(""" + class C + { + } + """, mutatingLspWorkspace); + + var configService = testLspServer.TestWorkspace.ExportProvider.GetExportedValue(); + configService.Options = new WorkspaceConfigurationOptions(SourceGeneratorExecution: sourceGeneratorExecution); + + var callCount = 0; + var generatorReference = await AddGeneratorAsync(new CallbackGenerator(() => ("hintName.cs", "// callCount: " + callCount++)), testLspServer.TestWorkspace); + + var sourceGeneratedDocuments = await testLspServer.GetCurrentSolution().Projects.Single().GetSourceGeneratedDocumentsAsync(); + var sourceGeneratedDocumentIdentity = sourceGeneratedDocuments.Single().Identity; + var sourceGeneratorDocumentUri = SourceGeneratedDocumentUri.Create(sourceGeneratedDocumentIdentity); + + var text = await testLspServer.ExecuteRequestAsync(SourceGeneratedDocumentGetTextHandler.MethodName, + new SourceGeneratorGetTextParams(new LSP.TextDocumentIdentifier { DocumentUri = sourceGeneratorDocumentUri }, ResultId: null), CancellationToken.None); + + AssertEx.NotNull(text); + Assert.Equal("// callCount: 0", text.Text); + + var initialSolution = testLspServer.GetCurrentSolution(); + var initialExecutionMap = initialSolution.CompilationState.SourceGeneratorExecutionVersionMap.Map; + + // Updating the execution version should trigger source generators to run in both automatic and balanced mode. + var forceRegeneration = majorVersionUpdate; + testLspServer.TestWorkspace.EnqueueUpdateSourceGeneratorVersion(projectId: null, forceRegeneration); + await testLspServer.WaitForSourceGeneratorsAsync(); + + var solutionWithChangedExecutionVersion = testLspServer.GetCurrentSolution(); + + var secondRequest = await testLspServer.ExecuteRequestAsync(SourceGeneratedDocumentGetTextHandler.MethodName, + new SourceGeneratorGetTextParams(new LSP.TextDocumentIdentifier { DocumentUri = sourceGeneratorDocumentUri }, ResultId: text.ResultId), CancellationToken.None); + AssertEx.NotNull(secondRequest); + + if (forceRegeneration) + { + Assert.NotEqual(text.ResultId, secondRequest.ResultId); + Assert.Equal("// callCount: 1", secondRequest.Text); + } + else + { + Assert.Equal(text.ResultId, secondRequest.ResultId); + Assert.Null(secondRequest.Text); + } + + var projectId1 = initialSolution.ProjectIds.Single(); + var solutionWithDocumentChanged = initialSolution.WithDocumentText( + initialSolution.Projects.Single().Documents.Single().Id, + SourceText.From("class D { }")); + + var expectVersionChange = sourceGeneratorExecution is SourceGeneratorExecutionPreference.Balanced || forceRegeneration; + + // The content forked solution should have an SG execution version *less than* the one we just changed. + // Note: this will be patched up once we call TryApplyChanges. + if (expectVersionChange) + { + Assert.True( + solutionWithChangedExecutionVersion.CompilationState.SourceGeneratorExecutionVersionMap[projectId1] + > solutionWithDocumentChanged.CompilationState.SourceGeneratorExecutionVersionMap[projectId1]); + } + else + { + Assert.Equal( + solutionWithChangedExecutionVersion.CompilationState.SourceGeneratorExecutionVersionMap[projectId1], + solutionWithDocumentChanged.CompilationState.SourceGeneratorExecutionVersionMap[projectId1]); + } + + Assert.True(testLspServer.TestWorkspace.TryApplyChanges(solutionWithDocumentChanged)); + + var finalSolution = testLspServer.GetCurrentSolution(); + + if (expectVersionChange) + { + // In balanced (or if we forced regen) mode, the execution version should have been updated to the new value. + Assert.NotEqual(initialExecutionMap[projectId1], solutionWithChangedExecutionVersion.CompilationState.SourceGeneratorExecutionVersionMap[projectId1]); + Assert.NotEqual(initialExecutionMap[projectId1], finalSolution.CompilationState.SourceGeneratorExecutionVersionMap[projectId1]); + } + else + { + // In automatic mode, nothing should change wrt to execution versions (unless we specified force-regenerate). + Assert.Equal(initialExecutionMap[projectId1], solutionWithChangedExecutionVersion.CompilationState.SourceGeneratorExecutionVersionMap[projectId1]); + Assert.Equal(initialExecutionMap[projectId1], finalSolution.CompilationState.SourceGeneratorExecutionVersionMap[projectId1]); + } + + // The final execution version for the project should match the changed execution version, no matter what. + // Proving that the content change happened, but didn't drop the execution version change. + Assert.Equal(solutionWithChangedExecutionVersion.CompilationState.SourceGeneratorExecutionVersionMap[projectId1], finalSolution.CompilationState.SourceGeneratorExecutionVersionMap[projectId1]); + } + [Theory, CombinatorialData] public async Task TestReturnsNullForRemovedClosedGeneratedFile(bool mutatingLspWorkspace) { @@ -279,7 +376,9 @@ public async Task TestReturnsNullForRemovedOpenedGeneratedFile(bool mutatingLspW Assert.Null(secondRequest.Text); } - private async Task CreateTestLspServerWithGeneratorAsync(bool mutatingLspWorkspace, string generatedDocumentText) + private async Task CreateTestLspServerWithGeneratorAsync( + bool mutatingLspWorkspace, + [StringSyntax(PredefinedEmbeddedLanguageNames.CSharpTest)] string generatedDocumentText) { var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace); await AddGeneratorAsync(new SingleFileTestGenerator(generatedDocumentText), testLspServer.TestWorkspace); diff --git a/src/NuGet/VS.ExternalAPIs.Roslyn.Package/VS.ExternalAPIs.Roslyn.Package.csproj b/src/NuGet/VS.ExternalAPIs.Roslyn.Package/VS.ExternalAPIs.Roslyn.Package.csproj index b2875a2986cca..438601629916c 100644 --- a/src/NuGet/VS.ExternalAPIs.Roslyn.Package/VS.ExternalAPIs.Roslyn.Package.csproj +++ b/src/NuGet/VS.ExternalAPIs.Roslyn.Package/VS.ExternalAPIs.Roslyn.Package.csproj @@ -42,7 +42,6 @@ - @@ -83,7 +82,6 @@ <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.EditorFeatures\$(Configuration)\net472\Microsoft.CodeAnalysis.EditorFeatures.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.ExternalAccess.Apex\$(Configuration)\net472\Microsoft.CodeAnalysis.ExternalAccess.Apex.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.ExternalAccess.AspNetCore\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.ExternalAccess.AspNetCore.dll" TargetDir="" /> - <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.ExternalAccess.Copilot\$(Configuration)\net472\Microsoft.CodeAnalysis.ExternalAccess.Copilot.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.ExternalAccess.Debugger\$(Configuration)\net472\Microsoft.CodeAnalysis.ExternalAccess.Debugger.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.ExternalAccess.EditorConfigGenerator\$(Configuration)\net472\Microsoft.CodeAnalysis.ExternalAccess.EditorConfigGenerator.dll" TargetDir="" /> <_File Include="$(ArtifactsBinDir)Microsoft.CodeAnalysis.ExternalAccess.Extensions\$(Configuration)\netstandard2.0\Microsoft.CodeAnalysis.ExternalAccess.Extensions.dll" TargetDir="" /> diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/Core/MetaAnalyzers/DiagnosticDescriptorCreationAnalyzer.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/Core/MetaAnalyzers/DiagnosticDescriptorCreationAnalyzer.cs index 3a0e9f12b3246..ba7b738e4e38d 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/Core/MetaAnalyzers/DiagnosticDescriptorCreationAnalyzer.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/Core/MetaAnalyzers/DiagnosticDescriptorCreationAnalyzer.cs @@ -1084,7 +1084,7 @@ arrayCreation.DimensionSizes[0].ConstantValue.Value is int size && else if (arrayCreation.Initializer is IArrayInitializerOperation arrayInitializer && arrayInitializer.ElementValues.All(element => element.ConstantValue.HasValue && element.ConstantValue.Value is string)) { - customTags = arrayInitializer.ElementValues.Select(element => (string)element.ConstantValue.Value!).ToImmutableArray(); + customTags = arrayInitializer.ElementValues.SelectAsArray(element => (string)element.ConstantValue.Value!); } } finally diff --git a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/Core/MetaAnalyzers/Fixers/AnalyzerReleaseTrackingFix.FixAllProvider.cs b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/Core/MetaAnalyzers/Fixers/AnalyzerReleaseTrackingFix.FixAllProvider.cs index 835d2ca9cbd08..2c90de3c46d05 100644 --- a/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/Core/MetaAnalyzers/Fixers/AnalyzerReleaseTrackingFix.FixAllProvider.cs +++ b/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/Core/MetaAnalyzers/Fixers/AnalyzerReleaseTrackingFix.FixAllProvider.cs @@ -13,6 +13,7 @@ using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.ReleaseTracking; using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Analyzers.MetaAnalyzers.Fixers { @@ -63,7 +64,7 @@ public ReleaseTrackingFixAllProvider() { } if (fixAllContext.CodeActionEquivalenceKey == CodeAnalysisDiagnosticsResources.EnableAnalyzerReleaseTrackingRuleTitle) { - var projectIds = diagnosticsToFix.Select(d => d.Key.Id).ToImmutableArray(); + var projectIds = diagnosticsToFix.SelectAsArray(d => d.Key.Id); return new FixAllAddAdditionalDocumentsAction(projectIds, fixAllContext.Solution); } diff --git a/src/RoslynAnalyzers/PerformanceSensitiveAnalyzers/CSharp/Analyzers/EnumeratorAllocationAnalyzer.cs b/src/RoslynAnalyzers/PerformanceSensitiveAnalyzers/CSharp/Analyzers/EnumeratorAllocationAnalyzer.cs index b17213b7ae247..33f2d1abd45ad 100644 --- a/src/RoslynAnalyzers/PerformanceSensitiveAnalyzers/CSharp/Analyzers/EnumeratorAllocationAnalyzer.cs +++ b/src/RoslynAnalyzers/PerformanceSensitiveAnalyzers/CSharp/Analyzers/EnumeratorAllocationAnalyzer.cs @@ -64,7 +64,7 @@ protected override void AnalyzeNode(SyntaxNodeAnalysisContext context, in Perfor if ((enumerator == null || enumerator.IsEmpty) && typeInfo.Type.Interfaces != null) { // 2nd fallback, now we try and find the IEnumerable Interface explicitly - var iEnumerable = typeInfo.Type.Interfaces.Where(i => i.Name == "IEnumerable").ToImmutableArray(); + var iEnumerable = typeInfo.Type.Interfaces.WhereAsArray(i => i.Name == "IEnumerable"); if (iEnumerable != null && !iEnumerable.IsEmpty) { enumerator = iEnumerable[0].GetMembers("GetEnumerator"); diff --git a/src/RoslynAnalyzers/Text.Analyzers/Core/IdentifiersShouldBeSpelledCorrectly.cs b/src/RoslynAnalyzers/Text.Analyzers/Core/IdentifiersShouldBeSpelledCorrectly.cs index 32fd7a6e4f58c..426b6bc3bcce2 100644 --- a/src/RoslynAnalyzers/Text.Analyzers/Core/IdentifiersShouldBeSpelledCorrectly.cs +++ b/src/RoslynAnalyzers/Text.Analyzers/Core/IdentifiersShouldBeSpelledCorrectly.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; namespace Text.Analyzers { @@ -263,8 +264,7 @@ ImmutableArray ReadDictionaries() var fileProvider = AdditionalFileProvider.FromOptions(context.Options); return fileProvider.GetMatchingFiles(@"(?:dictionary|custom).*?\.(?:xml|dic)$") .Select(GetOrCreateDictionaryFromAdditionalText) - .Where(x => x != null) - .ToImmutableArray(); + .WhereAsArray(x => x != null); } CodeAnalysisDictionary GetOrCreateDictionaryFromAdditionalText(AdditionalText additionalText) diff --git a/src/RoslynAnalyzers/Utilities/FlowAnalysis/FlowAnalysis/Analysis/TaintedDataAnalysis/TaintedDataSymbolMapExtensions.cs b/src/RoslynAnalyzers/Utilities/FlowAnalysis/FlowAnalysis/Analysis/TaintedDataAnalysis/TaintedDataSymbolMapExtensions.cs index 460aedb98d80a..56947ae85e0b4 100644 --- a/src/RoslynAnalyzers/Utilities/FlowAnalysis/FlowAnalysis/Analysis/TaintedDataAnalysis/TaintedDataSymbolMapExtensions.cs +++ b/src/RoslynAnalyzers/Utilities/FlowAnalysis/FlowAnalysis/Analysis/TaintedDataAnalysis/TaintedDataSymbolMapExtensions.cs @@ -66,8 +66,8 @@ public static bool IsSourceMethod( IEnumerable<(PointsToCheck, string target)> positivePointsToTaintedTargets = pointsToTaintedTargets.Where(s => s.pointsToCheck( - arguments.Select(o => - pointsToAnalysisResult[o.Kind, o.Syntax]).ToImmutableArray())); + arguments.SelectAsArray(o => + pointsToAnalysisResult[o.Kind, o.Syntax]))); if (positivePointsToTaintedTargets.Any()) { allTaintedTargets ??= PooledHashSet.GetInstance(); @@ -90,8 +90,8 @@ public static bool IsSourceMethod( IEnumerable<(ValueContentCheck, string target)> positiveValueContentTaintedTargets = valueContentTaintedTargets.Where(s => s.valueContentCheck( - arguments.Select(o => pointsToAnalysisResult[o.Kind, o.Syntax]).ToImmutableArray(), - arguments.Select(o => valueContentAnalysisResult[o.Kind, o.Syntax]).ToImmutableArray())); + arguments.SelectAsArray(o => pointsToAnalysisResult[o.Kind, o.Syntax]), + arguments.SelectAsArray(o => valueContentAnalysisResult[o.Kind, o.Syntax]))); if (positiveValueContentTaintedTargets.Any()) { allTaintedTargets ??= PooledHashSet.GetInstance(); diff --git a/src/RoslynAnalyzers/Utilities/FlowAnalysis/FlowAnalysis/Framework/DataFlow/AnalysisEntityFactory.cs b/src/RoslynAnalyzers/Utilities/FlowAnalysis/FlowAnalysis/Framework/DataFlow/AnalysisEntityFactory.cs index 6be317bb84fca..f29bec91e0d63 100644 --- a/src/RoslynAnalyzers/Utilities/FlowAnalysis/FlowAnalysis/Framework/DataFlow/AnalysisEntityFactory.cs +++ b/src/RoslynAnalyzers/Utilities/FlowAnalysis/FlowAnalysis/Framework/DataFlow/AnalysisEntityFactory.cs @@ -300,7 +300,7 @@ private static void GetSymbolAndIndicesForMemberReference(IMemberReferenceOperat { symbol = propertyReference.Property; indices = !propertyReference.Arguments.IsEmpty ? - CreateAbstractIndices(propertyReference.Arguments.Select(a => a.Value).ToImmutableArray()) : + CreateAbstractIndices(propertyReference.Arguments.SelectAsArray(a => a.Value)) : ImmutableArray.Empty; } diff --git a/src/Setup/DevDivInsertionFiles/DevDivInsertionFiles.csproj b/src/Setup/DevDivInsertionFiles/DevDivInsertionFiles.csproj index 7269a9d22a04a..61bfdc1c685f5 100644 --- a/src/Setup/DevDivInsertionFiles/DevDivInsertionFiles.csproj +++ b/src/Setup/DevDivInsertionFiles/DevDivInsertionFiles.csproj @@ -100,6 +100,7 @@ <_Dependency Remove="@(_Dependency)" Condition="$([MSBuild]::ValueOrDefault('%(Identity)', '').StartsWith('Microsoft.ServiceHub.'))"/> <_Dependency Remove="@(_Dependency)" Condition="$([MSBuild]::ValueOrDefault('%(Identity)', '').StartsWith('System.Composition.'))"/> <_Dependency Remove="@(_Dependency)" Condition="$([MSBuild]::ValueOrDefault('%(Identity)', '').StartsWith('Microsoft.Internal.VisualStudio.'))"/> + <_Dependency Remove="Azure.Core"/> <_Dependency Remove="EnvDTE"/> <_Dependency Remove="EnvDTE80"/> <_Dependency Remove="EnvDTE90"/> @@ -128,6 +129,7 @@ <_Dependency Remove="stdole"/> <_Dependency Remove="StreamJsonRpc"/> <_Dependency Remove="System.Buffers" /> + <_Dependency Remove="System.ClientModel"/> <_Dependency Remove="System.Collections.Immutable"/> <_Dependency Remove="System.Configuration.ConfigurationManager"/> <_Dependency Remove="System.Diagnostics.DiagnosticSource"/> @@ -136,6 +138,7 @@ <_Dependency Remove="System.IO.Packaging"/> <_Dependency Remove="System.IO.Pipelines"/> <_Dependency Remove="System.Memory"/> + <_Dependency Remove="System.Memory.Data"/> <_Dependency Remove="System.Numerics.Vectors"/> <_Dependency Remove="System.Reflection.Metadata"/> <_Dependency Remove="System.Reflection.MetadataLoadContext"/> diff --git a/src/Tools/AnalyzerRunner/CodeRefactoringRunner.cs b/src/Tools/AnalyzerRunner/CodeRefactoringRunner.cs index 8997850368363..de3e30d3e5d36 100644 --- a/src/Tools/AnalyzerRunner/CodeRefactoringRunner.cs +++ b/src/Tools/AnalyzerRunner/CodeRefactoringRunner.cs @@ -18,6 +18,7 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Composition; +using Roslyn.Utilities; using static AnalyzerRunner.Program; namespace AnalyzerRunner @@ -231,7 +232,7 @@ private static ImmutableDictionary refactoring.Metadata.Languages).Distinct(); return languages.ToImmutableDictionary( language => language, - language => refactorings.Where(refactoring => refactoring.Metadata.Languages.Contains(language)).ToImmutableArray()); + language => refactorings.WhereAsArray(refactoring => refactoring.Metadata.Languages.Contains(language))); } private class CodeRefactoringProviderMetadata diff --git a/src/Tools/BuildValidator/Program.cs b/src/Tools/BuildValidator/Program.cs index 3a3c437a1ae7d..a83e074b0217a 100644 --- a/src/Tools/BuildValidator/Program.cs +++ b/src/Tools/BuildValidator/Program.cs @@ -14,6 +14,7 @@ using Microsoft.CodeAnalysis.Rebuild; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using Roslyn.Utilities; namespace BuildValidator { @@ -373,7 +374,7 @@ private static ImmutableArray ResolveSourceLinks(CompilationOpt var documents = JsonConvert.DeserializeAnonymousType(Encoding.UTF8.GetString(sourceLinkUtf8), new { documents = (Dictionary?)null })?.documents ?? throw new InvalidOperationException("Failed to deserialize source links."); - var sourceLinks = documents.Select(makeSourceLink).ToImmutableArray(); + var sourceLinks = documents.SelectAsArray(makeSourceLink); if (sourceLinks.IsDefault) { diff --git a/src/Tools/ExternalAccess/Razor/Features/Cohost/AbstractRazorCohostLifecycleService.cs b/src/Tools/ExternalAccess/Razor/Features/Cohost/AbstractRazorCohostLifecycleService.cs new file mode 100644 index 0000000000000..7e1720526fa0a --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Features/Cohost/AbstractRazorCohostLifecycleService.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; + +internal abstract class AbstractRazorCohostLifecycleService : IDisposable +{ + public abstract Task LspServerIntializedAsync(CancellationToken cancellationToken); + public abstract Task RazorActivatedAsync(ClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext, CancellationToken cancellationToken); + public abstract void Dispose(); +} diff --git a/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/CodeLens.cs b/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/CodeLens.cs new file mode 100644 index 0000000000000..da21ca75c5c53 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/CodeLens.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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.LanguageServer.Handler.CodeLens; +using Microsoft.CodeAnalysis.Options; +using LSP = Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers; + +internal static class CodeLens +{ + public static Task GetCodeLensAsync(LSP.TextDocumentIdentifier textDocumentIdentifier, Document document, CancellationToken cancellationToken) + { + var globalOptions = document.Project.Solution.Services.ExportProvider.GetService(); + + return CodeLensHandler.GetCodeLensAsync(textDocumentIdentifier, document, globalOptions, cancellationToken); + } + + public static Task ResolveCodeLensAsync(LSP.CodeLens codeLens, Document document, CancellationToken cancellationToken) + { + return CodeLensResolveHandler.ResolveCodeLensAsync(codeLens, document, cancellationToken); + } +} diff --git a/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/Completion.cs b/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/Completion.cs index 6e1ee9308e5a3..4368b83fe220c 100644 --- a/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/Completion.cs +++ b/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/Completion.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageServer; @@ -18,12 +19,15 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers; internal static class Completion { + [Obsolete("Use GetCompletionListAsync with CompletionListCacheWrapper instead.")] private static CompletionListCache? s_completionListCache; + [Obsolete("Use GetCompletionListAsync with CompletionListCacheWrapper instead.")] private static CompletionListCache GetCache() => s_completionListCache ??= InterlockedOperations.Initialize(ref s_completionListCache, () => new()); - public static async Task GetCompletionListAsync( + [Obsolete("Use GetCompletionListAsync with CompletionListCacheWrapper instead.")] + public static Task GetCompletionListAsync( Document document, LinePosition linePosition, LSP.CompletionContext? completionContext, @@ -33,6 +37,44 @@ private static CompletionListCache GetCache() { var cache = GetCache(); + return GetCompletionListAsync( + document, + linePosition, + completionContext, + supportsVSExtensions, + completionCapabilities, + GetCache(), + cancellationToken); + } + + public static Task GetCompletionListAsync( + Document document, + LinePosition linePosition, + LSP.CompletionContext? completionContext, + bool supportsVSExtensions, + LSP.CompletionSetting completionCapabilities, + CompletionListCacheWrapper cacheWrapper, + CancellationToken cancellationToken) + { + return GetCompletionListAsync( + document, + linePosition, + completionContext, + supportsVSExtensions, + completionCapabilities, + cacheWrapper.GetCache(), + cancellationToken); + } + + private static async Task GetCompletionListAsync( + Document document, + LinePosition linePosition, + LSP.CompletionContext? completionContext, + bool supportsVSExtensions, + LSP.CompletionSetting completionCapabilities, + CompletionListCache cache, + CancellationToken cancellationToken) + { var position = await document .GetPositionFromLinePositionAsync(linePosition, cancellationToken) .ConfigureAwait(false); @@ -50,6 +92,7 @@ private static CompletionListCache GetCache() cancellationToken).ConfigureAwait(false); } + [Obsolete("Use GetCompletionListAsync with CompletionListCacheWrapper instead.")] public static Task ResolveCompletionItemAsync( LSP.CompletionItem completionItem, Document document, @@ -57,8 +100,30 @@ private static CompletionListCache GetCache() LSP.CompletionSetting completionCapabilities, CancellationToken cancellationToken) { - var cache = GetCache(); + return ResolveCompletionItemAsync( + completionItem, document, supportsVSExtensions, completionCapabilities, GetCache(), cancellationToken); + } + public static Task ResolveCompletionItemAsync( + LSP.CompletionItem completionItem, + Document document, + bool supportsVSExtensions, + LSP.CompletionSetting completionCapabilities, + CompletionListCacheWrapper cacheWrapper, + CancellationToken cancellationToken) + { + return ResolveCompletionItemAsync( + completionItem, document, supportsVSExtensions, completionCapabilities, cacheWrapper.GetCache(), cancellationToken); + } + + private static Task ResolveCompletionItemAsync( + LSP.CompletionItem completionItem, + Document document, + bool supportsVSExtensions, + LSP.CompletionSetting completionCapabilities, + CompletionListCache cache, + CancellationToken cancellationToken) + { var globalOptions = document.Project.Solution.Services.ExportProvider.GetService(); var capabilityHelper = new CompletionCapabilityHelper(supportsVSExtensions, completionCapabilities); diff --git a/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/CompletionListCacheWrapper.cs b/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/CompletionListCacheWrapper.cs new file mode 100644 index 0000000000000..575c1ea618aae --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/CompletionListCacheWrapper.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 Microsoft.CodeAnalysis.LanguageServer.Handler.Completion; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers; + +/// +/// Provides a wrapper around the so that Razor can control the lifecycle. +/// +internal sealed class CompletionListCacheWrapper +{ + private readonly CompletionListCache _cache = new(); + + public CompletionListCache GetCache() => _cache; +} diff --git a/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/Diagnostics.cs b/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/Diagnostics.cs index 6e39c496e756b..ddb3c019a66bd 100644 --- a/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/Diagnostics.cs +++ b/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/Diagnostics.cs @@ -25,8 +25,6 @@ internal static class Diagnostics document, range: null, DiagnosticKind.All, cancellationToken).ConfigureAwait(false); var project = document.Project; - // isLiveSource means build might override a diagnostics, but this method is only used by tooling, so builds aren't relevant - const bool IsLiveSource = false; // Potential duplicate is only set for workspace diagnostics const bool PotentialDuplicate = false; @@ -34,7 +32,7 @@ internal static class Diagnostics foreach (var diagnostic in diagnostics) { if (!diagnostic.IsSuppressed) - result.AddRange(ProtocolConversions.ConvertDiagnostic(diagnostic, supportsVisualStudioExtensions, project, IsLiveSource, PotentialDuplicate, globalOptionsService)); + result.AddRange(ProtocolConversions.ConvertDiagnostic(diagnostic, supportsVisualStudioExtensions, project, PotentialDuplicate, globalOptionsService)); } return result.ToImmutableAndFree(); diff --git a/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/InlayHintCacheWrapper.cs b/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/InlayHintCacheWrapper.cs new file mode 100644 index 0000000000000..5d28d9f98d44e --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/InlayHintCacheWrapper.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 Microsoft.CodeAnalysis.LanguageServer.Handler.InlayHint; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers; + +/// +/// Provides a wrapper around the so that Razor can control the lifecycle. +/// +internal sealed class InlayHintCacheWrapper +{ + private readonly InlayHintCache _cache = new(); + + public InlayHintCache GetCache() => _cache; +} diff --git a/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/InlayHints.cs b/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/InlayHints.cs index 90b90fa6cd63e..8b81992d87d4c 100644 --- a/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/InlayHints.cs +++ b/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/InlayHints.cs @@ -2,11 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.InlineHints; using Microsoft.CodeAnalysis.LanguageServer.Handler.InlayHint; -using Roslyn.LanguageServer.Protocol; +using LSP = Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers { @@ -15,27 +16,49 @@ internal static class InlayHints // In the Roslyn LSP server this cache has the same lifetime as the LSP server. For Razor, running OOP, we don't have // that same lifetime anywhere, everything is just static. This is likely not ideal, but the inlay hint cache has a // max size of 3 items, so it's not a huge deal. + [Obsolete("Use GetInlayHintsAsync with InlayHintCacheWrapper instead.")] private static InlayHintCache? s_resolveCache; - public static Task GetInlayHintsAsync(Document document, TextDocumentIdentifier textDocumentIdentifier, Range range, bool displayAllOverride, CancellationToken cancellationToken) + [Obsolete("Use GetInlayHintsAsync with InlayHintCacheWrapper instead.")] + public static Task GetInlayHintsAsync(Document document, LSP.TextDocumentIdentifier textDocumentIdentifier, LSP.Range range, bool displayAllOverride, CancellationToken cancellationToken) { s_resolveCache ??= new(); + return GetInlayHintsAsync(document, textDocumentIdentifier, range, displayAllOverride, s_resolveCache, cancellationToken); + } + + public static Task GetInlayHintsAsync(Document document, LSP.TextDocumentIdentifier textDocumentIdentifier, LSP.Range range, bool displayAllOverride, InlayHintCacheWrapper cacheWrapper, CancellationToken cancellationToken) + { + return GetInlayHintsAsync(document, textDocumentIdentifier, range, displayAllOverride, cacheWrapper.GetCache(), cancellationToken); + } + private static Task GetInlayHintsAsync(Document document, LSP.TextDocumentIdentifier textDocumentIdentifier, LSP.Range range, bool displayAllOverride, InlayHintCache cache, CancellationToken cancellationToken) + { // Currently Roslyn options don't sync to OOP so trying to get the real options out of IGlobalOptionsService will // always just result in the defaults, which for inline hints are to not show anything. However, the editor has a // setting for LSP inlay hints, so we can assume that if we get a request from the client, the user wants hints. // When overriding however, Roslyn does a nicer job if type hints are off. var options = GetOptions(displayAllOverride); - return InlayHintHandler.GetInlayHintsAsync(document, textDocumentIdentifier, range, options, displayAllOverride, s_resolveCache, cancellationToken); + return InlayHintHandler.GetInlayHintsAsync(document, textDocumentIdentifier, range, options, displayAllOverride, cache, cancellationToken); } - public static Task ResolveInlayHintAsync(Document document, InlayHint request, CancellationToken cancellationToken) + [Obsolete("Use GetInlayHintsAsync with InlayHintCacheWrapper instead.")] + public static Task ResolveInlayHintAsync(Document document, LSP.InlayHint request, CancellationToken cancellationToken) { Contract.ThrowIfNull(s_resolveCache, "Cache should never be null for resolve, since it should have been created by the original request"); + return ResolveInlayHintAsync(document, request, s_resolveCache, cancellationToken); + } + + public static Task ResolveInlayHintAsync(Document document, LSP.InlayHint request, InlayHintCacheWrapper cacheWrapper, CancellationToken cancellationToken) + { + return ResolveInlayHintAsync(document, request, cacheWrapper.GetCache(), cancellationToken); + } + + private static Task ResolveInlayHintAsync(Document document, LSP.InlayHint request, InlayHintCache cache, CancellationToken cancellationToken) + { var data = InlayHintResolveHandler.GetInlayHintResolveData(request); var options = GetOptions(data.DisplayAllOverride); - return InlayHintResolveHandler.ResolveInlayHintAsync(document, request, s_resolveCache, data, options, cancellationToken); + return InlayHintResolveHandler.ResolveInlayHintAsync(document, request, cache, data, options, cancellationToken); } private static InlineHintsOptions GetOptions(bool displayAllOverride) diff --git a/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/OnAutoInsert.cs b/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/OnAutoInsert.cs index c4388d526027d..e688a3759f38c 100644 --- a/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/OnAutoInsert.cs +++ b/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/OnAutoInsert.cs @@ -20,7 +20,11 @@ internal static class OnAutoInsert public static Task GetOnAutoInsertResponseAsync(Document document, LinePosition linePosition, string character, FormattingOptions formattingOptions, CancellationToken cancellationToken) { var globalOptions = document.Project.Solution.Services.ExportProvider.GetService(); - var services = document.Project.Solution.Services.ExportProvider.GetExports().Where(s => s.Metadata.Language == LanguageNames.CSharp).SelectAsArray(s => s.Value); + var services = document.Project.Solution.Services.ExportProvider + .GetExports() + .SelectAsArray( + predicate: s => s.Metadata.Language == LanguageNames.CSharp, + selector: s => s.Value); return OnAutoInsertHandler.GetOnAutoInsertResponseAsync(globalOptions, services, document, linePosition, character, formattingOptions, isRazorRequest: true, cancellationToken); } diff --git a/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/Rename.cs b/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/Rename.cs index 2565e735d6406..c6feae0bab8e9 100644 --- a/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/Rename.cs +++ b/src/Tools/ExternalAccess/Razor/Features/Cohost/Handlers/Rename.cs @@ -16,5 +16,5 @@ internal static class Rename => PrepareRenameHandler.GetRenameRangeAsync(document, linePosition, cancellationToken); public static Task GetRenameEditAsync(Document document, LinePosition linePosition, string newName, CancellationToken cancellationToken) - => RenameHandler.GetRenameEditAsync(document, linePosition, newName, includeSourceGenerated: true, cancellationToken); + => RenameHandler.GetRenameEditAsync(document, linePosition, newName, cancellationToken); } diff --git a/src/Tools/ExternalAccess/Razor/Features/Cohost/ICohostStartupService.cs b/src/Tools/ExternalAccess/Razor/Features/Cohost/ICohostStartupService.cs index d8e01a448a05c..87b350649cd91 100644 --- a/src/Tools/ExternalAccess/Razor/Features/Cohost/ICohostStartupService.cs +++ b/src/Tools/ExternalAccess/Razor/Features/Cohost/ICohostStartupService.cs @@ -2,11 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Threading; using System.Threading.Tasks; namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +[Obsolete("Please move to AbstractRazorCohostLifecycleService. This will be removed in a future release.")] internal interface ICohostStartupService { Task StartupAsync(string serializedClientCapabilities, RazorCohostRequestContext requestContext, CancellationToken cancellationToken); diff --git a/src/Tools/ExternalAccess/Razor/Features/Cohost/RazorStartupServiceFactory.cs b/src/Tools/ExternalAccess/Razor/Features/Cohost/RazorStartupServiceFactory.cs index 5209e33afb7e6..a582960931a92 100644 --- a/src/Tools/ExternalAccess/Razor/Features/Cohost/RazorStartupServiceFactory.cs +++ b/src/Tools/ExternalAccess/Razor/Features/Cohost/RazorStartupServiceFactory.cs @@ -22,38 +22,51 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed class RazorStartupServiceFactory( [Import(AllowDefault = true)] IUIContextActivationService? uIContextActivationService, - [Import(AllowDefault = true)] Lazy? cohostStartupService) : ILspServiceFactory + [Import(AllowDefault = true)] Lazy? cohostStartupService, + [Import(AllowDefault = true)] AbstractRazorCohostLifecycleService? razorCohostLifecycleService) : ILspServiceFactory { public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) { - return new RazorStartupService(uIContextActivationService, cohostStartupService); + return new RazorStartupService(uIContextActivationService, cohostStartupService, razorCohostLifecycleService); } private class RazorStartupService( IUIContextActivationService? uIContextActivationService, - Lazy? cohostStartupService) : ILspService, IOnInitialized, IDisposable +#pragma warning disable CS0618 // Type or member is obsolete + Lazy? cohostStartupService, +#pragma warning restore CS0618 // Type or member is obsolete + AbstractRazorCohostLifecycleService? razorCohostLifecycleService) : ILspService, IOnInitialized, IDisposable { private readonly CancellationTokenSource _disposalTokenSource = new(); private IDisposable? _activation; public void Dispose() { + razorCohostLifecycleService?.Dispose(); + _activation?.Dispose(); _activation = null; _disposalTokenSource.Cancel(); } - public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) + public async Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) { if (context.ServerKind is not (WellKnownLspServerKinds.AlwaysActiveVSLspServer or WellKnownLspServerKinds.CSharpVisualBasicLspServer)) { // We have to register this class for Any server, but only want to run in the C# server in VS or VS Code - return Task.CompletedTask; + return; + } + + if (cohostStartupService is null && razorCohostLifecycleService is null) + { + return; } - if (cohostStartupService is null) + if (razorCohostLifecycleService is not null) { - return Task.CompletedTask; + // If we have a cohost lifecycle service, fire pre-initialization, which happens when the LSP server starts up, but before + // the UIContext is activated. + await razorCohostLifecycleService.LspServerIntializedAsync(cancellationToken).ConfigureAwait(false); } if (uIContextActivationService is null) @@ -66,7 +79,7 @@ public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestCon _activation = uIContextActivationService.ExecuteWhenActivated(Constants.RazorCohostingUIContext, InitializeRazor); } - return Task.CompletedTask; + return; void InitializeRazor() { @@ -85,13 +98,18 @@ private async Task InitializeRazorAsync(ClientCapabilities clientCapabilities, R using var languageScope = context.Logger.CreateLanguageContext(Constants.RazorLanguageName); - // We use a string to pass capabilities to/from Razor to avoid version issues with the Protocol DLL - var serializedClientCapabilities = JsonSerializer.Serialize(clientCapabilities, ProtocolConversions.LspJsonSerializerOptions); - var requestContext = new RazorCohostRequestContext(context); + if (razorCohostLifecycleService is not null) + { + // If we have a cohost lifecycle service, fire post-initialization, which happens when the UIContext is activated. + await razorCohostLifecycleService.RazorActivatedAsync(clientCapabilities, requestContext, cancellationToken).ConfigureAwait(false); + } + if (cohostStartupService is not null) { + // We use a string to pass capabilities to/from Razor to avoid version issues with the Protocol DLL + var serializedClientCapabilities = JsonSerializer.Serialize(clientCapabilities, ProtocolConversions.LspJsonSerializerOptions); await cohostStartupService.Value.StartupAsync(serializedClientCapabilities, requestContext, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Tools/ExternalAccess/Razor/Features/IRazorSourceGeneratedDocumentSpanMappingService.cs b/src/Tools/ExternalAccess/Razor/Features/IRazorSourceGeneratedDocumentSpanMappingService.cs new file mode 100644 index 0000000000000..51b91a0ad1694 --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Features/IRazorSourceGeneratedDocumentSpanMappingService.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor +{ + internal interface IRazorSourceGeneratedDocumentSpanMappingService + { + Task> GetMappedTextChangesAsync(SourceGeneratedDocument oldDocument, SourceGeneratedDocument newDocument, CancellationToken cancellationToken); + Task> MapSpansAsync(SourceGeneratedDocument document, ImmutableArray spans, CancellationToken cancellationToken); + } +} diff --git a/src/Tools/ExternalAccess/Razor/Features/RazorGeneratedDocumentIdentity.cs b/src/Tools/ExternalAccess/Razor/Features/RazorGeneratedDocumentIdentity.cs index 7c2290c9154ea..4e50c9137a69e 100644 --- a/src/Tools/ExternalAccess/Razor/Features/RazorGeneratedDocumentIdentity.cs +++ b/src/Tools/ExternalAccess/Razor/Features/RazorGeneratedDocumentIdentity.cs @@ -9,4 +9,17 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Razor; /// /// Wrapper for and /// -internal record struct RazorGeneratedDocumentIdentity(DocumentId DocumentId, string HintName, string FilePath, string GeneratorAssemblyName, string? GeneratorAssemblyPath, Version GeneratorAssemblyVersion, string GeneratorTypeName); +internal record struct RazorGeneratedDocumentIdentity(DocumentId DocumentId, string HintName, string FilePath, string GeneratorAssemblyName, string? GeneratorAssemblyPath, Version GeneratorAssemblyVersion, string GeneratorTypeName) +{ + internal static RazorGeneratedDocumentIdentity Create(SourceGeneratedDocument document) + => Create(document.Identity); + + internal static RazorGeneratedDocumentIdentity Create(SourceGeneratedDocumentIdentity identity) + => new(identity.DocumentId, + identity.HintName, + identity.FilePath, + identity.Generator.AssemblyName, + identity.Generator.AssemblyPath, + identity.Generator.AssemblyVersion, + identity.Generator.TypeName); +} diff --git a/src/Tools/ExternalAccess/Razor/Features/RazorMappedSpanResult.cs b/src/Tools/ExternalAccess/Razor/Features/RazorMappedSpanResult.cs index 96adfdc8fb60b..4d4e375b6b847 100644 --- a/src/Tools/ExternalAccess/Razor/Features/RazorMappedSpanResult.cs +++ b/src/Tools/ExternalAccess/Razor/Features/RazorMappedSpanResult.cs @@ -3,16 +3,21 @@ // See the LICENSE file in the project root for more information. using System; +using System.Runtime.Serialization; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.ExternalAccess.Razor; +[DataContract] internal readonly struct RazorMappedSpanResult { + [DataMember(Order = 0)] public readonly string FilePath; + [DataMember(Order = 1)] public readonly LinePositionSpan LinePositionSpan; + [DataMember(Order = 2)] public readonly TextSpan Span; public RazorMappedSpanResult(string filePath, LinePositionSpan linePositionSpan, TextSpan span) @@ -30,7 +35,10 @@ public RazorMappedSpanResult(string filePath, LinePositionSpan linePositionSpan, public bool IsDefault => FilePath == null; } -internal readonly record struct RazorMappedEditResult(string FilePath, TextChange[] TextChanges) +[DataContract] +internal readonly record struct RazorMappedEditResult( + [property: DataMember(Order = 0)] string FilePath, + [property: DataMember(Order = 1)] TextChange[] TextChanges) { public bool IsDefault => FilePath == null || TextChanges == null; } diff --git a/src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentSpanMappingServiceWrapper.cs b/src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentSpanMappingServiceWrapper.cs new file mode 100644 index 0000000000000..1b02fc896051c --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/Features/RazorSourceGeneratedDocumentSpanMappingServiceWrapper.cs @@ -0,0 +1,87 @@ + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor; + +[ExportWorkspaceService(typeof(ISourceGeneratedDocumentSpanMappingService)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class RazorSourceGeneratedDocumentSpanMappingServiceWrapper( + [Import(AllowDefault = true)] IRazorSourceGeneratedDocumentSpanMappingService? implementation) : ISourceGeneratedDocumentSpanMappingService +{ + private readonly IRazorSourceGeneratedDocumentSpanMappingService? _implementation = implementation; + + public bool CanMapSpans(SourceGeneratedDocument document) + { + return _implementation is not null && document.IsRazorSourceGeneratedDocument(); + } + + public async Task> GetMappedTextChangesAsync(SourceGeneratedDocument oldDocument, SourceGeneratedDocument newDocument, CancellationToken cancellationToken) + { + if (_implementation is null || + !oldDocument.IsRazorSourceGeneratedDocument() || + !newDocument.IsRazorSourceGeneratedDocument()) + { + return []; + } + + var mappedChanges = await _implementation.GetMappedTextChangesAsync(oldDocument, newDocument, cancellationToken).ConfigureAwait(false); + if (mappedChanges.IsDefaultOrEmpty) + { + return []; + } + + using var _ = ArrayBuilder.GetInstance(out var changesBuilder); + foreach (var change in mappedChanges) + { + if (change.IsDefault) + { + continue; + } + + foreach (var textChange in change.TextChanges) + { + changesBuilder.Add(new MappedTextChange(change.FilePath, textChange)); + } + } + + return changesBuilder.ToImmutableAndClear(); + } + + public async Task> MapSpansAsync(SourceGeneratedDocument document, ImmutableArray spans, CancellationToken cancellationToken) + { + if (_implementation is null || + !document.IsRazorSourceGeneratedDocument()) + { + return []; + } + + var mappedSpans = await _implementation.MapSpansAsync(document, spans, cancellationToken).ConfigureAwait(false); + if (mappedSpans.Length != spans.Length) + { + return []; + } + + using var _ = ArrayBuilder.GetInstance(out var spansBuilder); + foreach (var span in mappedSpans) + { + spansBuilder.Add(span.IsDefault + ? default + : new MappedSpanResult(span.FilePath, span.LinePositionSpan, span.Span)); + } + + return spansBuilder.ToImmutableAndClear(); + } +} diff --git a/src/Tools/ExternalAccess/Razor/Features/RazorUri.cs b/src/Tools/ExternalAccess/Razor/Features/RazorUri.cs index 8eb2e6972e94c..6841ab7a9a4bc 100644 --- a/src/Tools/ExternalAccess/Razor/Features/RazorUri.cs +++ b/src/Tools/ExternalAccess/Razor/Features/RazorUri.cs @@ -35,13 +35,6 @@ public static RazorGeneratedDocumentIdentity GetIdentityOfGeneratedDocument(Solu // Razor only cares about documents from its own generator, but it's better to just send them back the info they // need to check on their side, so we can avoid dual insertions if anything changes. - return new RazorGeneratedDocumentIdentity( - identity.DocumentId, - identity.HintName, - identity.FilePath, - identity.Generator.AssemblyName, - identity.Generator.AssemblyPath, - identity.Generator.AssemblyVersion, - identity.Generator.TypeName); + return RazorGeneratedDocumentIdentity.Create(identity); } } diff --git a/src/Tools/ExternalAccess/Razor/Features/SolutionExtensions.cs b/src/Tools/ExternalAccess/Razor/Features/SolutionExtensions.cs index 4cfa3dbc0d745..55c5de663ba90 100644 --- a/src/Tools/ExternalAccess/Razor/Features/SolutionExtensions.cs +++ b/src/Tools/ExternalAccess/Razor/Features/SolutionExtensions.cs @@ -16,5 +16,5 @@ public static ImmutableArray GetDocumentIds(this Solution solution, => LanguageServer.Extensions.GetDocumentIds(solution, new(documentUri)); public static int GetWorkspaceVersion(this Solution solution) - => solution.WorkspaceVersion; + => solution.SolutionStateContentVersion; } diff --git a/src/Tools/ExternalAccess/Xaml/Internal/XamlDiagnosticSource.cs b/src/Tools/ExternalAccess/Xaml/Internal/XamlDiagnosticSource.cs index 2da252a3ab76a..029d70823dd36 100644 --- a/src/Tools/ExternalAccess/Xaml/Internal/XamlDiagnosticSource.cs +++ b/src/Tools/ExternalAccess/Xaml/Internal/XamlDiagnosticSource.cs @@ -16,7 +16,6 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Xaml; internal sealed class XamlDiagnosticSource(IXamlDiagnosticSource xamlDiagnosticSource, TextDocument document) : IDiagnosticSource { - bool IDiagnosticSource.IsLiveSource() => true; Project IDiagnosticSource.GetProject() => document.Project; ProjectOrDocumentId IDiagnosticSource.GetId() => new(document.Id); TextDocumentIdentifier? IDiagnosticSource.GetDocumentIdentifier() => new() { DocumentUri = document.GetURI() }; @@ -26,7 +25,7 @@ async Task> IDiagnosticSource.GetDiagnosticsAsync { var xamlRequestContext = XamlRequestContext.FromRequestContext(context); var diagnostics = await xamlDiagnosticSource.GetDiagnosticsAsync(xamlRequestContext, cancellationToken).ConfigureAwait(false); - var result = diagnostics.Select(e => DiagnosticData.Create(e, document)).ToImmutableArray(); + var result = diagnostics.SelectAsArray(e => DiagnosticData.Create(e, document)); return result; } } diff --git a/src/Tools/Replay/Replay.cs b/src/Tools/Replay/Replay.cs index 436ce4c1188cb..c4775718fa51f 100644 --- a/src/Tools/Replay/Replay.cs +++ b/src/Tools/Replay/Replay.cs @@ -105,7 +105,7 @@ static async Task RunAsync(ReplayOptions options) var stopwatch = new Stopwatch(); stopwatch.Start(); - await foreach (var buildData in BuildAllAsync(options, compilerCalls, compilerServerLogger, CancellationToken.None)) + await foreach (var buildData in BuildAllAsync(options, compilerCalls, compilerServerLogger, CancellationToken.None).ConfigureAwait(false)) { Console.WriteLine($"{buildData.CompilerCall.GetDiagnosticName()} ... {buildData.BuildResponse.Type}"); } diff --git a/src/Tools/SemanticSearch/Extensions/Microsoft.CodeAnalysis.SemanticSearch.Extensions.csproj b/src/Tools/SemanticSearch/Extensions/Microsoft.CodeAnalysis.SemanticSearch.Extensions.csproj new file mode 100644 index 0000000000000..01db54595ee6b --- /dev/null +++ b/src/Tools/SemanticSearch/Extensions/Microsoft.CodeAnalysis.SemanticSearch.Extensions.csproj @@ -0,0 +1,15 @@ + + + + + Library + $(NetVSShared) + + + + + + + + + diff --git a/src/Tools/SemanticSearch/Extensions/ProjectModel.cs b/src/Tools/SemanticSearch/Extensions/ProjectModel.cs new file mode 100644 index 0000000000000..1b94e2e3ccd35 --- /dev/null +++ b/src/Tools/SemanticSearch/Extensions/ProjectModel.cs @@ -0,0 +1,147 @@ +// 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. + +#if NET + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Xml.Linq; +using System.Xml; +using System.Text; + +namespace Microsoft.CodeAnalysis.SemanticSearch.Extensions; + +/// +/// Models project information not tracked by Compilation. +/// +public sealed class ProjectModel +{ + private readonly Lazy> _lazyResxFiles; + + public string FilePath { get; } + + internal ProjectModel(string filePath) + { + FilePath = filePath; + _lazyResxFiles = new(LoadResxFiles, isThreadSafe: true); + } + + internal ProjectModel(string filePath, ImmutableDictionary resxFiles) + { + FilePath = filePath; + _lazyResxFiles = new(() => resxFiles, isThreadSafe: true); + } + + public ImmutableDictionary ResxFiles + => _lazyResxFiles.Value; + + public ProjectModel ReplaceResxFile(ResxFile file) + => new(FilePath, ResxFiles.SetItem(file.FilePath, file)); + + internal ImmutableDictionary LoadResxFiles() + { + var resxFiles = ImmutableDictionary.CreateBuilder(); + var projectDirectory = Path.GetDirectoryName(FilePath)!; + + // TODO: get EmbeddedResources items from msbuild instead + foreach (var filePath in Directory.EnumerateFileSystemEntries(projectDirectory, "*.resx", SearchOption.AllDirectories)) + { + resxFiles.Add(filePath, ResxFile.ReadFromFile(filePath)); + } + + return resxFiles.ToImmutable(); + } + + internal static IEnumerable<(string filePath, string? newContent)> GetChanges(ProjectModel oldModel, ProjectModel newModel) + { + if (!oldModel._lazyResxFiles.IsValueCreated && !newModel._lazyResxFiles.IsValueCreated) + { + yield break; + } + + foreach (var (filePath, newResx) in newModel.ResxFiles) + { + var newContent = newResx.GetContent(); + + if (oldModel.ResxFiles.TryGetValue(filePath, out var oldResx) && newContent == oldResx.GetContent()) + { + continue; + } + + // new or updated resx file: + yield return (filePath, newContent); + } + + foreach (var (filePath, _) in oldModel.ResxFiles) + { + if (!newModel.ResxFiles.ContainsKey(filePath)) + { + // deleted resx file: + yield return (filePath, null); + } + } + } +} + +public sealed class ResxFile +{ + public string FilePath { get; } + + private readonly ImmutableDictionary _changes; + + internal ResxFile(string filePath, ImmutableDictionary changes) + { + FilePath = filePath; + _changes = changes; + } + + internal static ResxFile ReadFromFile(string filePath) + { + return new ResxFile(filePath, changes: ImmutableDictionary.Empty); + } + + public ResxFile AddString(string name, string value) + => new(FilePath, _changes.SetItem(name, value)); + + internal string GetContent() + { + if (_changes.Count == 0) + { + return File.ReadAllText(FilePath, Encoding.UTF8); + } + + var newDocument = XDocument.Load(FilePath, LoadOptions.None); + + foreach (var (name, value) in _changes) + { + newDocument.Root!.Add(new XElement("data", + new XAttribute("name", name), + new XAttribute(XNamespace.Xml + "space", "preserve"), + new XElement("value", value) + )); + } + + using var stream = new MemoryStream(); + + using var xmlWriter = XmlWriter.Create(stream, new() + { + Indent = true, + Encoding = Encoding.UTF8, + IndentChars = "\t", + NewLineChars = "\r\n", + NewLineOnAttributes = false, + }); + + newDocument.Save(xmlWriter); + xmlWriter.Close(); + + stream.Position = 0; + using var reader = new StreamReader(stream, Encoding.UTF8); + return reader.ReadToEnd(); + } +} +#endif diff --git a/src/Tools/SemanticSearch/ReferenceAssemblies/SemanticSearch.ReferenceAssemblies.csproj b/src/Tools/SemanticSearch/ReferenceAssemblies/SemanticSearch.ReferenceAssemblies.csproj index 111b378b6692b..aaaef777c9c04 100644 --- a/src/Tools/SemanticSearch/ReferenceAssemblies/SemanticSearch.ReferenceAssemblies.csproj +++ b/src/Tools/SemanticSearch/ReferenceAssemblies/SemanticSearch.ReferenceAssemblies.csproj @@ -19,6 +19,7 @@ + @@ -39,12 +40,13 @@ - <_InputReference Include="@(ReferencePath)" + <_InputReference Include="@(ReferencePath)" Condition="'%(ReferencePath.FrameworkReferenceName)' == 'Microsoft.NETCore.App' or '%(ReferencePath.FileName)' == 'System.Collections.Immutable' or '%(ReferencePath.FileName)' == 'Microsoft.CodeAnalysis' or '%(ReferencePath.FileName)' == 'Microsoft.CodeAnalysis.CSharp' or - '%(ReferencePath.FileName)' == 'Microsoft.CodeAnalysis.VisualBasic'" /> + '%(ReferencePath.FileName)' == 'Microsoft.CodeAnalysis.VisualBasic' or + '%(ReferencePath.FileName)' == 'Microsoft.CodeAnalysis.SemanticSearch.Extensions'" /> @@ -52,7 +54,9 @@ <_InputFile Include="@(ApiSet)" /> <_InputFile Include="@(_InputReference)" /> - <_OutputFile Include="@(ApiSet->'$(_OutputDir)%(FileName).dll')" /> + <_OutputRefAssembly Include="@(ApiSet->'$(_OutputDir)%(FileName).dll')" /> + + <_OutputFile Include="@(_OutputRefAssembly)" /> <_OutputFile Include="@(Apis)" /> @@ -77,5 +81,5 @@ - + diff --git a/src/Tools/TestDiscoveryWorker/Program.cs b/src/Tools/TestDiscoveryWorker/Program.cs index c22744f8c250f..96b8d7591d066 100644 --- a/src/Tools/TestDiscoveryWorker/Program.cs +++ b/src/Tools/TestDiscoveryWorker/Program.cs @@ -9,6 +9,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Channels; +using System.Threading.Tasks; using Mono.Options; using Xunit; using Xunit.Abstractions; @@ -77,7 +78,7 @@ discoveryOptions: TestFrameworkOptions.ForDiscovery(configuration)); var testsToWrite = new HashSet(); - await foreach (var fullyQualifiedName in sink.GetTestCaseNamesAsync()) + await foreach (var fullyQualifiedName in sink.GetTestCaseNamesAsync().ConfigureAwait(false)) { testsToWrite.Add(fullyQualifiedName); } diff --git a/src/VisualStudio/CSharp/Impl/SemanticSearch/SemanticSearchDocumentNavigationService.cs b/src/VisualStudio/CSharp/Impl/SemanticSearch/SemanticSearchDocumentNavigationService.cs index fb0cfa542c6fb..44adda02940ba 100644 --- a/src/VisualStudio/CSharp/Impl/SemanticSearch/SemanticSearchDocumentNavigationService.cs +++ b/src/VisualStudio/CSharp/Impl/SemanticSearch/SemanticSearchDocumentNavigationService.cs @@ -28,7 +28,7 @@ public override Task CanNavigateToSpanAsync(Workspace workspace, DocumentI public override Task GetLocationForSpanAsync(Workspace workspace, DocumentId documentId, TextSpan textSpan, bool allowInvalidSpan, CancellationToken cancellationToken) { Debug.Assert(workspace is SemanticSearchWorkspace); - Debug.Assert(documentId == SemanticSearchUtilities.GetQueryDocumentId(workspace.CurrentSolution)); + Debug.Assert(documentId == window.SemanticSearchService.GetQueryDocumentId(workspace.CurrentSolution)); return Task.FromResult(window.GetNavigableLocation(textSpan)); } diff --git a/src/VisualStudio/CSharp/Impl/SemanticSearch/SemanticSearchPresenterController.cs b/src/VisualStudio/CSharp/Impl/SemanticSearch/SemanticSearchPresenterController.cs index bba469dc2de29..a2093478938ba 100644 --- a/src/VisualStudio/CSharp/Impl/SemanticSearch/SemanticSearchPresenterController.cs +++ b/src/VisualStudio/CSharp/Impl/SemanticSearch/SemanticSearchPresenterController.cs @@ -7,9 +7,11 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editor.Host; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.SemanticSearch; +using Microsoft.VisualStudio.Threading; namespace Microsoft.VisualStudio.LanguageServices.CSharp; @@ -22,14 +24,21 @@ namespace Microsoft.VisualStudio.LanguageServices.CSharp; internal sealed class SemanticSearchPresenterController( IStreamingFindUsagesPresenter resultsPresenter, VisualStudioWorkspace workspace, - IGlobalOptionService globalOptions) : ISemanticSearchPresenterController + IGlobalOptionService globalOptions, + IThreadingContext threadingContext) : ISemanticSearchPresenterController { public async Task ExecuteQueryAsync(string query, CancellationToken cancellationToken) { + await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + var (presenterContext, presenterCancellationToken) = resultsPresenter.StartSearch(ServicesVSResources.Semantic_search_results, StreamingFindUsagesPresenterOptions.Default); + + await TaskScheduler.Default; + using var queryCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(presenterCancellationToken, cancellationToken); - var executor = new SemanticSearchQueryExecutor(presenterContext, globalOptions); + // TODO: logger + var executor = new SemanticSearchQueryExecutor(presenterContext, logMessage: static _ => { }, globalOptions); await executor.ExecuteAsync(query, queryDocument: null, workspace.CurrentSolution, queryCancellationSource.Token).ConfigureAwait(false); } } diff --git a/src/VisualStudio/CSharp/Impl/SemanticSearch/SemanticSearchQueryExecutor.cs b/src/VisualStudio/CSharp/Impl/SemanticSearch/SemanticSearchQueryExecutor.cs index cf452757d6c10..3a26ad85af2c7 100644 --- a/src/VisualStudio/CSharp/Impl/SemanticSearch/SemanticSearchQueryExecutor.cs +++ b/src/VisualStudio/CSharp/Impl/SemanticSearch/SemanticSearchQueryExecutor.cs @@ -3,26 +3,35 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Concurrent; +using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.ErrorReporting; -using Microsoft.CodeAnalysis.FindSymbols; using Microsoft.CodeAnalysis.FindUsages; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Notification; using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.OrganizeImports; using Microsoft.CodeAnalysis.SemanticSearch; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; namespace Microsoft.VisualStudio.LanguageServices.CSharp; internal sealed class SemanticSearchQueryExecutor( FindUsagesContext presenterContext, + Action logMessage, IOptionsReader options) { - private sealed class ResultsObserver(IFindUsagesContext presenterContext, IOptionsReader options, Document? queryDocument) : ISemanticSearchResultsDefinitionObserver + private sealed class ResultsObserver(IFindUsagesContext presenterContext, IOptionsReader options, Action logMessage, Document? queryDocument) : ISemanticSearchResultsDefinitionObserver { + private readonly Lazy changes)>> _lazyDocumentUpdates = new(); + private readonly Lazy> _lazyTextFileUpdates = new(); + public ValueTask GetClassificationOptionsAsync(Microsoft.CodeAnalysis.Host.LanguageServices language, CancellationToken cancellationToken) => new(options.GetClassificationOptions(language.Language)); @@ -38,9 +47,61 @@ public ValueTask ItemsCompletedAsync(int itemCount, CancellationToken cancellati public ValueTask OnUserCodeExceptionAsync(UserCodeExceptionInfo exception, CancellationToken cancellationToken) => presenterContext.OnDefinitionFoundAsync( new SearchExceptionDefinitionItem(exception.Message, exception.TypeName, exception.StackTrace, (queryDocument != null) ? new DocumentSpan(queryDocument, exception.Span) : default), cancellationToken); + + public ValueTask OnLogMessageAsync(string message, CancellationToken cancellationToken) + { + logMessage(message); + return ValueTask.CompletedTask; + } + + public ValueTask OnDocumentUpdatedAsync(DocumentId documentId, ImmutableArray changes, CancellationToken cancellationToken) + { + _lazyDocumentUpdates.Value.Push((documentId, changes)); + return ValueTask.CompletedTask; + } + + private ImmutableArray<(DocumentId documentId, ImmutableArray changes)> GetDocumentUpdates() + => _lazyDocumentUpdates.IsValueCreated ? [.. _lazyDocumentUpdates.Value] : []; + + public async ValueTask GetUpdatedSolutionAsync(Solution oldSolution, CancellationToken cancellationToken) + { + var newSolution = oldSolution; + + foreach (var (documentId, changes) in GetDocumentUpdates()) + { + var oldText = await newSolution.GetRequiredDocument(documentId).GetTextAsync(cancellationToken).ConfigureAwait(false); + if (changes.IsEmpty) + { + newSolution = newSolution.RemoveDocument(documentId); + } + else + { + // TODO: auto-format/clean up changed spans + newSolution = newSolution.WithDocumentText(documentId, oldText.WithChanges(changes)); + + var newDocument = newSolution.GetRequiredDocument(documentId); + var organizeImportsService = newDocument.GetRequiredLanguageService(); + var options = await newDocument.GetOrganizeImportsOptionsAsync(cancellationToken).ConfigureAwait(false); + newDocument = await organizeImportsService.OrganizeImportsAsync(newDocument, options, cancellationToken).ConfigureAwait(false); + var updatedText = await newDocument.GetTextAsync(cancellationToken).ConfigureAwait(false); + newSolution = newSolution.WithDocumentText(newDocument.Id, updatedText); + } + } + + return newSolution; + } + + public ImmutableArray<(string filePath, string? newContent)> GetFileUpdates() + => _lazyTextFileUpdates.IsValueCreated ? _lazyTextFileUpdates.Value.SelectAsArray(static entry => (entry.Key, entry.Value)) : []; + + public ValueTask OnTextFileUpdatedAsync(string filePath, string? newContent, CancellationToken cancellationToken) + { + _lazyTextFileUpdates.Value.TryAdd(filePath, newContent); + return ValueTask.CompletedTask; + } } - public async Task ExecuteAsync(string? query, Document? queryDocument, Solution solution, CancellationToken cancellationToken) + public async Task<(Solution solution, ImmutableArray<(string filePath, string? newContent)> fileUpdates)> ExecuteAsync(string? query, Document? queryDocument, Solution solution, CancellationToken cancellationToken) { Contract.ThrowIfFalse(query is null ^ queryDocument is null); @@ -56,10 +117,10 @@ public async Task ExecuteAsync(string? query, Document? queryDocument, Solution await presenterContext.OnCompletedAsync(CancellationToken.None).ConfigureAwait(false); } - return; + return (solution, []); } - var resultsObserver = new ResultsObserver(presenterContext, options, queryDocument); + var resultsObserver = new ResultsObserver(presenterContext, options, logMessage, queryDocument); query ??= (await queryDocument!.GetTextAsync(cancellationToken).ConfigureAwait(false)).ToString(); ExecuteQueryResult result = default; @@ -71,14 +132,13 @@ public async Task ExecuteAsync(string? query, Document? queryDocument, Solution var compileResult = await RemoteSemanticSearchServiceProxy.CompileQueryAsync( solution.Services, query, - language: LanguageNames.CSharp, - SemanticSearchUtilities.ReferenceAssembliesDirectory, + targetLanguage: null, cancellationToken).ConfigureAwait(false); if (compileResult == null) { result = new ExecuteQueryResult(FeaturesResources.Semantic_search_only_supported_on_net_core); - return; + return (solution, []); } emitTime = compileResult.Value.EmitTime; @@ -90,14 +150,20 @@ public async Task ExecuteAsync(string? query, Document? queryDocument, Solution await presenterContext.OnDefinitionFoundAsync(new SearchCompilationFailureDefinitionItem(error, queryDocument), cancellationToken).ConfigureAwait(false); } - return; + return (solution, []); } result = await RemoteSemanticSearchServiceProxy.ExecuteQueryAsync( solution, compileResult.Value.QueryId, resultsObserver, + new QueryExecutionOptions(), cancellationToken).ConfigureAwait(false); + + // apply document changes: + var newSolution = await resultsObserver.GetUpdatedSolutionAsync(solution, cancellationToken).ConfigureAwait(false); + + return (newSolution, resultsObserver.GetFileUpdates()); } catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical)) { @@ -131,6 +197,8 @@ await presenterContext.ReportMessageAsync( ReportTelemetry(query, result, emitTime, canceled); } + + return (solution, []); } private static void ReportTelemetry(string queryString, ExecuteQueryResult result, TimeSpan emitTime, bool canceled) diff --git a/src/VisualStudio/CSharp/Impl/SemanticSearch/SemanticSearchToolWindowImpl.cs b/src/VisualStudio/CSharp/Impl/SemanticSearch/SemanticSearchToolWindowImpl.cs index 60824cc3e6055..0be2cd65ea7fe 100644 --- a/src/VisualStudio/CSharp/Impl/SemanticSearch/SemanticSearchToolWindowImpl.cs +++ b/src/VisualStudio/CSharp/Impl/SemanticSearch/SemanticSearchToolWindowImpl.cs @@ -3,7 +3,9 @@ // See the LICENSE file in the project root for more information. using System; +using System.IO; using System.Composition; +using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; @@ -13,6 +15,7 @@ using System.Windows.Markup; using System.Windows.Media; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Editor; using Microsoft.CodeAnalysis.Editor.Host; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.ErrorReporting; @@ -21,7 +24,9 @@ using Microsoft.CodeAnalysis.Navigation; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.SemanticSearch; +using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CodeAnalysis.Threading; using Microsoft.VisualStudio.Editor; using Microsoft.VisualStudio.Extensibility.VSSdkCompatibility; using Microsoft.VisualStudio.Imaging; @@ -54,14 +59,19 @@ internal sealed partial class SemanticSearchToolWindowImpl( IVsEditorAdaptersFactoryService vsEditorAdaptersFactoryService, IAsynchronousOperationListenerProvider listenerProvider, IGlobalOptionService globalOptions, + Lazy semanticSearchService, VisualStudioWorkspace workspace, IStreamingFindUsagesPresenter resultsPresenter, ITextUndoHistoryRegistry undoHistoryRegistry, - IVsService vsUIShellProvider) : IDisposable + IVsService vsUIShellProvider, + IPreviewFactoryService previewFactory) : IDisposable { private const int ToolBarHeight = 26; private const int ToolBarButtonSize = 20; + private static readonly Guid s_logOutputPainGuid = new("{4C4F1810-C865-493E-98A7-8E1120A9FDE4}"); + private const string LogOutputPaneName = "Semantic Search Log"; + private static readonly Lazy s_buttonTemplate = new(CreateButtonTemplate); private readonly IContentType _contentType = contentTypeRegistry.GetContentType(CSharpSemanticSearchContentType.Name); @@ -70,13 +80,16 @@ internal sealed partial class SemanticSearchToolWindowImpl( private readonly Lazy _semanticSearchWorkspace = new(() => new SemanticSearchEditorWorkspace( hostWorkspaceProvider.Workspace.Services.HostServices, - CSharpSemanticSearchUtilities.Configuration, + semanticSearchService.Value, threadingContext, listenerProvider)); // access interlocked: private volatile CancellationTokenSource? _pendingExecutionCancellationSource; + // Create on UI thread only, access on any thread: + private AsyncBatchingWorkQueue? _lazyLogQueue; + // Access on UI thread only: private Button? _executeButton; private Button? _cancelButton; @@ -84,12 +97,16 @@ private readonly Lazy _semanticSearchWorkspace private ITextBuffer? _textBuffer; private IRemoteUserControl? _lazyContent; + private IVsOutputWindowPane? _lazyLogOutputPane; public void Dispose() { _lazyContent?.Dispose(); } + public ISemanticSearchSolutionService SemanticSearchService + => semanticSearchService.Value; + public async Task InitializeAsync(CancellationToken cancellationToken) { var content = _lazyContent; @@ -123,7 +140,7 @@ private async Task CreateContentAsync(CancellationToken cancel // enable LSP: Contract.ThrowIfFalse(textDocumentFactory.TryGetTextDocument(_textBuffer, out var textDocument)); - textDocument.Rename(SemanticSearchUtilities.GetDocumentFilePath(LanguageNames.CSharp)); + textDocument.Rename(SemanticSearchService.GetQueryDocumentFilePath()); var toolWindowGrid = new Grid(); toolWindowGrid.ColumnDefinitions.Add(new ColumnDefinition()); @@ -381,11 +398,27 @@ public void RunQuery() UpdateUIState(); + _lazyLogQueue ??= new( + delay: TimeSpan.Zero, + async (messages, cancellationToken) => + { + await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(CancellationToken.None); + + var pane = GetOrCreateLogOutputPane(); + + foreach (var message in messages) + { + pane.OutputStringThreadSafe(message + Environment.NewLine); + } + }, + _asyncListener, + cancellationToken: CancellationToken.None); + var (presenterContext, presenterCancellationToken) = resultsPresenter.StartSearch(ServicesVSResources.Semantic_search_results, StreamingFindUsagesPresenterOptions.Default); presenterCancellationToken.Register(() => cancellationSource?.Cancel()); var querySolution = _semanticSearchWorkspace.Value.CurrentSolution; - var queryDocument = SemanticSearchUtilities.GetQueryDocument(querySolution); + var queryDocument = querySolution.GetRequiredDocument(SemanticSearchService.GetQueryDocumentId(querySolution)); var completionToken = _asyncListener.BeginAsyncOperation(nameof(SemanticSearchToolWindow) + ".Execute"); _ = ExecuteAsync(cancellationSource.Token).ReportNonFatalErrorAsync().CompletesAsyncOperation(completionToken); @@ -396,8 +429,57 @@ async Task ExecuteAsync(CancellationToken cancellationToken) try { - var executor = new SemanticSearchQueryExecutor(presenterContext, globalOptions); - await executor.ExecuteAsync(query: null, queryDocument, workspace.CurrentSolution, cancellationToken).ConfigureAwait(false); + var executor = new SemanticSearchQueryExecutor(presenterContext, message => _lazyLogQueue.AddWork(message), globalOptions); + var oldSolution = workspace.CurrentSolution; + var (newSolution, fileUpdates) = await executor.ExecuteAsync(query: null, queryDocument, oldSolution, cancellationToken).ConfigureAwait(false); + + var success = true; + if (newSolution != oldSolution) + { + var changedSolution = newSolution; + + var previewDialogService = workspace.Services.GetService(); + if (previewDialogService != null && + previewFactory.GetSolutionPreviews(oldSolution, newSolution, cancellationToken)?.ChangeSummary is { } changeSummary) + { + await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(CancellationToken.None); + + changedSolution = previewDialogService.PreviewChanges( + EditorFeaturesResources.Preview_Changes, + "vs.codefix.previewchanges", + "Updates", + EditorFeaturesResources.Changes, + Glyph.OpenFolder, + changeSummary.NewSolution, + changeSummary.OldSolution, + showCheckBoxes: false); + + // TODO: report error + success = changedSolution != null && workspace.TryApplyChanges(changedSolution); + } + else + { + await threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(CancellationToken.None); + + success = workspace.TryApplyChanges(changedSolution); + } + } + + if (success) + { + // TODO: parallelize, exceptions + foreach (var (filePath, newContent) in fileUpdates) + { + if (newContent == null) + { + File.Delete(filePath); + } + else + { + File.WriteAllText(filePath, newContent, Encoding.UTF8); + } + } + } } finally { @@ -440,6 +522,26 @@ public NavigableLocation GetNavigableLocation(TextSpan textSpan) return true; }); + private IVsOutputWindowPane GetOrCreateLogOutputPane() + { + if (_lazyLogOutputPane != null) + return _lazyLogOutputPane; + + ThreadHelper.ThrowIfNotOnUIThread(); + + var outputWindow = ServiceProvider.GlobalProvider.GetServiceOnMainThread(); + + // Try to get the pane, create if it doesn't exist + var guid = s_logOutputPainGuid; + if (outputWindow.GetPane(ref guid, out _lazyLogOutputPane) != VSConstants.S_OK || _lazyLogOutputPane == null) + { + outputWindow.CreatePane(ref guid, LogOutputPaneName, fInitVisible: 1, fClearWithSolution: 1); + outputWindow.GetPane(ref guid, out _lazyLogOutputPane); + } + + return _lazyLogOutputPane; + } + private sealed class CommandFilter : IOleCommandTarget { private readonly SemanticSearchToolWindowImpl _window; diff --git a/src/VisualStudio/CSharp/Test/CallHierarchy/CSharpCallHierarchyTests.cs b/src/VisualStudio/CSharp/Test/CallHierarchy/CSharpCallHierarchyTests.cs index d96ce859173e6..86926f690d611 100644 --- a/src/VisualStudio/CSharp/Test/CallHierarchy/CSharpCallHierarchyTests.cs +++ b/src/VisualStudio/CSharp/Test/CallHierarchy/CSharpCallHierarchyTests.cs @@ -551,4 +551,73 @@ public void M() testState.VerifyRoot(root, "Class1.Class1(string)", [string.Format(EditorFeaturesResources.Calls_To_0, ".ctor")]); testState.VerifyResult(root, string.Format(EditorFeaturesResources.Calls_To_0, ".ctor"), ["D.M()"]); } + + [WpfFact] + [WorkItem("https://github.com/dotnet/roslyn/issues/71068")] + public async Task Method_ExcludeNameofReferencesWithoutMemberAccess() + { + var text = """ + namespace N + { + class G + { + void B$$oo() + { + } + + void Main() + { + var g = new G(); + g.Boo(); // This should appear in call hierarchy + } + + void TestNameof() + { + var methodName = nameof(Boo); // This should NOT appear + } + } + } + """; + using var testState = CallHierarchyTestState.Create(text); + var root = await testState.GetRootAsync(); + testState.VerifyRoot(root, "N.G.Boo()", [string.Format(EditorFeaturesResources.Calls_To_0, "Boo")]); + // Only the actual method call should appear, not the nameof reference + testState.VerifyResult(root, string.Format(EditorFeaturesResources.Calls_To_0, "Boo"), ["N.G.Main()"]); + } + + [WpfFact] + [WorkItem("https://github.com/dotnet/roslyn/issues/71068")] + public async Task Method_ExcludeNameofReferences() + { + var text = """ + namespace N + { + class C + { + void G$$oo() + { + } + } + + class G + { + void Main() + { + var c = new C(); + c.Goo(); // This should appear in call hierarchy + } + + void TestNameof() + { + var methodName = nameof(C.Goo); // This should NOT appear + } + } + } + """; + using var testState = CallHierarchyTestState.Create(text); + var root = await testState.GetRootAsync(); + testState.VerifyRoot(root, "N.C.Goo()", [string.Format(EditorFeaturesResources.Calls_To_0, "Goo")]); + // Only the actual method call should appear, not the nameof reference + testState.VerifyResult(root, string.Format(EditorFeaturesResources.Calls_To_0, "Goo"), ["N.G.Main()"]); + } } diff --git a/src/VisualStudio/Core/Def/CodeLens/RemoteCodeLensReferencesService.cs b/src/VisualStudio/Core/Def/CodeLens/RemoteCodeLensReferencesService.cs index 222a13c2d8a2b..144857ca03451 100644 --- a/src/VisualStudio/Core/Def/CodeLens/RemoteCodeLensReferencesService.cs +++ b/src/VisualStudio/Core/Def/CodeLens/RemoteCodeLensReferencesService.cs @@ -5,6 +5,7 @@ using System.Collections.Immutable; using System.Composition; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -149,22 +150,22 @@ private async Task> FixUpDescriptors continue; } - var spanMapper = document.DocumentServiceProvider.GetService(); - if (spanMapper == null) + var span = new TextSpan(descriptor.SpanStart, descriptor.SpanLength); + var results = await SpanMappingHelper.TryGetMappedSpanResultAsync(document, [span], cancellationToken).ConfigureAwait(false); + if (results is null) { // for normal document, just add one as they are list.Add(descriptor); continue; } - var span = new TextSpan(descriptor.SpanStart, descriptor.SpanLength); - var results = await spanMapper.MapSpansAsync(document, [span], cancellationToken).ConfigureAwait(false); + var mappedSpans = results.GetValueOrDefault(); // external component violated contracts. the mapper should preserve input order/count. // since we gave in 1 span, it should return 1 span back - Contract.ThrowIfTrue(results.IsDefaultOrEmpty); + Contract.ThrowIfTrue(mappedSpans.IsDefaultOrEmpty); - var result = results[0]; + var result = mappedSpans[0]; if (result.IsDefault) { // it is allowed for mapper to return default @@ -175,6 +176,30 @@ private async Task> FixUpDescriptors var excerpter = document.DocumentServiceProvider.GetService(); if (excerpter == null) { + if (document.IsRazorSourceGeneratedDocument()) + { + // HACK: Razor doesn't have has a workspace level excerpt service, but if we just return a simple descriptor here, + // the user at least sees something, can navigate, and Razor can improve this later if necessary. Until + // https://github.com/dotnet/roslyn/issues/79699 is fixed this won't get hit anyway. + list.Add(new ReferenceLocationDescriptor( + descriptor.LongDescription, + descriptor.Language, + descriptor.Glyph, + result.Span.Start, + result.Span.Length, + result.LinePositionSpan.Start.Line, + result.LinePositionSpan.Start.Character, + descriptor.ProjectGuid, + descriptor.DocumentGuid, + result.FilePath, + descriptor.ReferenceLineText, + descriptor.ReferenceStart, + descriptor.ReferenceLength, + "", + "", + "", + "")); + } continue; } diff --git a/src/VisualStudio/Core/Def/Commands.vsct b/src/VisualStudio/Core/Def/Commands.vsct index 252b01bc7f9ec..0ecb421ea8d60 100644 --- a/src/VisualStudio/Core/Def/Commands.vsct +++ b/src/VisualStudio/Core/Def/Commands.vsct @@ -478,7 +478,6 @@