diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f85f5cc650b82..73dd695a2e492 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -67,6 +67,19 @@ "problemMatcher": "$msCompile", "group": "build" }, + { + "label": "build Roslyn.sln", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + "-p:RunAnalyzersDuringBuild=false", + "-p:GenerateFullPaths=true", + "Roslyn.sln" + ], + "problemMatcher": "$msCompile", + "group": "build" + }, { "label": "build current project", "type": "shell", @@ -123,6 +136,30 @@ }, "problemMatcher": "$msCompile", "group": "build" + }, + { + "label": "build language server", + "command": "dotnet", + "type": "shell", + "args": [ + "build", + "-c", + "Debug", + "src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Microsoft.CodeAnalysis.LanguageServer.csproj" + ], + "problemMatcher": "$msCompile", + "group": "build" + }, + { + "label": "launch vscode with language server", + "command": "${execPath}", + "type": "process", + "options": { + "env": { + "DOTNET_ROSLYN_SERVER_PATH": "${workspaceRoot}/artifacts/bin/Microsoft.CodeAnalysis.LanguageServer/Debug/net8.0/Microsoft.CodeAnalysis.LanguageServer.dll" + } + }, + "dependsOn": [ "build language server" ] } ] } diff --git a/Roslyn.sln b/Roslyn.sln index b54926c007207..848adc0f76206 100644 --- a/Roslyn.sln +++ b/Roslyn.sln @@ -547,8 +547,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Replay", "src\Tools\Replay\ EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.CommonLanguageServerProtocol.Framework.Shared", "src\Features\LanguageServer\Microsoft.CommonLanguageServerProtocol.Framework\Microsoft.CommonLanguageServerProtocol.Framework.Shared.shproj", "{64EADED3-4B5D-4431-BBE5-A4ABA1C38C00}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CommonLanguageServerProtocol.Framework.Binary", "src\Features\LanguageServer\Microsoft.CommonLanguageServerProtocol.Framework.Binary\Microsoft.CommonLanguageServerProtocol.Framework.Binary.csproj", "{730CADBA-701F-4722-9B6F-1FCC0DF2C95D}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.CSharp.Emit3.UnitTests", "src\Compilers\CSharp\Test\Emit3\Microsoft.CodeAnalysis.CSharp.Emit3.UnitTests.csproj", "{4E273CBC-BB1D-4AC1-91DB-C62FC83E0350}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SemanticSearch", "SemanticSearch", "{52ABB0E4-C3A1-4897-B51B-18EDA83F5D20}" @@ -1365,10 +1363,6 @@ Global {DB96C25F-39A9-4A6A-92BC-D1E42717308F}.Debug|Any CPU.Build.0 = Debug|Any CPU {DB96C25F-39A9-4A6A-92BC-D1E42717308F}.Release|Any CPU.ActiveCfg = Release|Any CPU {DB96C25F-39A9-4A6A-92BC-D1E42717308F}.Release|Any CPU.Build.0 = Release|Any CPU - {730CADBA-701F-4722-9B6F-1FCC0DF2C95D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {730CADBA-701F-4722-9B6F-1FCC0DF2C95D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {730CADBA-701F-4722-9B6F-1FCC0DF2C95D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {730CADBA-701F-4722-9B6F-1FCC0DF2C95D}.Release|Any CPU.Build.0 = Release|Any CPU {4E273CBC-BB1D-4AC1-91DB-C62FC83E0350}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4E273CBC-BB1D-4AC1-91DB-C62FC83E0350}.Debug|Any CPU.Build.0 = Debug|Any CPU {4E273CBC-BB1D-4AC1-91DB-C62FC83E0350}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1636,7 +1630,6 @@ Global {6D819E80-BA2F-4317-8368-37F8F4434D3A} = {8977A560-45C2-4EC2-A849-97335B382C74} {DB96C25F-39A9-4A6A-92BC-D1E42717308F} = {FD0FAF5F-1DED-485C-99FA-84B97F3A8EEC} {64EADED3-4B5D-4431-BBE5-A4ABA1C38C00} = {D449D505-CC6A-4E0B-AF1B-976E2D0AE67A} - {730CADBA-701F-4722-9B6F-1FCC0DF2C95D} = {D449D505-CC6A-4E0B-AF1B-976E2D0AE67A} {4E273CBC-BB1D-4AC1-91DB-C62FC83E0350} = {32A48625-F0AD-419D-828B-A50BDABA38EA} {52ABB0E4-C3A1-4897-B51B-18EDA83F5D20} = {FD0FAF5F-1DED-485C-99FA-84B97F3A8EEC} {FCE88BBD-9BBD-4871-B9B0-DE176D73A6B0} = {52ABB0E4-C3A1-4897-B51B-18EDA83F5D20} @@ -1686,7 +1679,6 @@ Global src\Features\LanguageServer\Microsoft.CommonLanguageServerProtocol.Framework\Microsoft.CommonLanguageServerProtocol.Framework.Shared.projitems*{64eaded3-4b5d-4431-bbe5-a4aba1c38c00}*SharedItemsImports = 13 src\Features\LanguageServer\Microsoft.CommonLanguageServerProtocol.Framework\Microsoft.CommonLanguageServerProtocol.Framework.Shared.projitems*{686bf57e-a6ff-467b-aab3-44de916a9772}*SharedItemsImports = 5 src\Workspaces\SharedUtilitiesAndExtensions\Compiler\CSharp\CSharpCompilerExtensions.projitems*{699fea05-aea7-403d-827e-53cf4e826955}*SharedItemsImports = 13 - src\Features\LanguageServer\Microsoft.CommonLanguageServerProtocol.Framework\Microsoft.CommonLanguageServerProtocol.Framework.Shared.projitems*{730cadba-701f-4722-9b6f-1fcc0df2c95d}*SharedItemsImports = 5 src\ExpressionEvaluator\VisualBasic\Source\ResultProvider\BasicResultProvider.projitems*{76242a2d-2600-49dd-8c15-fea07ecb1843}*SharedItemsImports = 5 src\Analyzers\Core\Analyzers\Analyzers.projitems*{76e96966-4780-4040-8197-bde2879516f4}*SharedItemsImports = 13 src\Analyzers\VisualBasic\Tests\VisualBasicAnalyzers.UnitTests.projitems*{7b7f4153-ae93-4908-b8f0-430871589f83}*SharedItemsImports = 13 diff --git a/azure-pipelines-official.yml b/azure-pipelines-official.yml index 1a86360a0fcb0..72fb4cfb5b958 100644 --- a/azure-pipelines-official.yml +++ b/azure-pipelines-official.yml @@ -85,7 +85,7 @@ extends: sdl: sourceAnalysisPool: name: NetCore1ESPool-Svc-Internal - image: 1es-windows-2022-pt + image: 1es-windows-2022 os: windows sbom: enabled: false @@ -149,6 +149,7 @@ extends: dropFolder: 'artifacts\VSSetup\$(BuildConfiguration)\Insertion' dropName: $(VisualStudio.DropName) accessToken: $(_DevDivDropAccessToken) + dropRetentionDays: 90 # Publish insertion packages to CoreXT store. - output: nuget diff --git a/docs/Language Feature Status.md b/docs/Language Feature Status.md index 3ba66cf3fe790..47fb6533a7218 100644 --- a/docs/Language Feature Status.md +++ b/docs/Language Feature Status.md @@ -10,16 +10,17 @@ efforts behind them. | Feature | Branch | State | Developer | Reviewer | IDE Buddy | LDM Champ | | ------- | ------ | ----- | --------- | -------- | --------- | --------- | -| Ref/unsafe in iterators/async | [RefInAsync](https://github.com/dotnet/roslyn/tree/features/RefInAsync) | [In Progress](https://github.com/dotnet/roslyn/issues/72662) | [jjonescz](https://github.com/jjonescz) | [AlekseyTs](https://github.com/AlekseyTs), [cston](https://github.com/cston) | [ToddGrun](https://github.com/ToddGrun) | | +| [Partial properties](https://github.com/dotnet/csharplang/issues/6420) | [partial-properties](https://github.com/dotnet/roslyn/tree/features/partial-properties) | [In Progress](https://github.com/dotnet/roslyn/issues/73090) | [RikkiGibson](https://github.com/RikkiGibson) | [jcouv](https://github.com/jcouv), [333fred](https://github.com/333fred) | [Cosifne](https://github.com/Cosifne) | [333fred](https://github.com/333fred), [RikkiGibson](https://github.com/RikkiGibson) | +| Ref/unsafe in iterators/async | [RefInAsync](https://github.com/dotnet/roslyn/tree/features/RefInAsync) | [In Progress](https://github.com/dotnet/roslyn/issues/72662) | [jjonescz](https://github.com/jjonescz) | [AlekseyTs](https://github.com/AlekseyTs), [cston](https://github.com/cston) | (no IDE impact) | | | [Ref Struct Interfaces](https://github.com/dotnet/csharplang/issues/7608) | [RefStructInterfaces](https://github.com/dotnet/roslyn/tree/features/RefStructInterfaces) | [In Progress](https://github.com/dotnet/roslyn/issues/72124) | [AlekseyTs](https://github.com/AlekseyTs) | [cston](https://github.com/cston), [jjonescz](https://github.com/jjonescz) | [ToddGrun](https://github.com/ToddGrun) | [agocke](https://github.com/agocke), [jaredpar](https://github.com/jaredpar) | -| [Semi-auto-properties](https://github.com/dotnet/csharplang/issues/140) | [semi-auto-props](https://github.com/dotnet/roslyn/tree/features/semi-auto-props) | [In Progress](https://github.com/dotnet/roslyn/issues/57012) | [Youssef1313](https://github.com/Youssef1313) | [333fred](https://github.com/333fred), [RikkiGibson](https://github.com/RikkiGibson) | | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | +| [Semi-auto-properties](https://github.com/dotnet/csharplang/issues/140) | [semi-auto-props](https://github.com/dotnet/roslyn/tree/features/semi-auto-props) | [In Progress](https://github.com/dotnet/roslyn/issues/57012) | [Youssef1313](https://github.com/Youssef1313) | [333fred](https://github.com/333fred), [RikkiGibson](https://github.com/RikkiGibson) | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | | [Default in deconstruction](https://github.com/dotnet/roslyn/pull/25562) | [decon-default](https://github.com/dotnet/roslyn/tree/features/decon-default) | [In Progress](https://github.com/dotnet/roslyn/issues/25559) | [jcouv](https://github.com/jcouv) | [gafter](https://github.com/gafter) | | [jcouv](https://github.com/jcouv) | | [Roles/Extensions](https://github.com/dotnet/csharplang/issues/5497) | [roles](https://github.com/dotnet/roslyn/tree/features/roles) | [In Progress](https://github.com/dotnet/roslyn/issues/66722) | [jcouv](https://github.com/jcouv) | [AlekseyTs](https://github.com/AlekseyTs), [jjonescz](https://github.com/jjonescz) | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | [MadsTorgersen](https://github.com/MadsTorgersen) | -| [Escape character](https://github.com/dotnet/csharplang/issues/7400) | N/A | [Merged into 17.9p1](https://github.com/dotnet/roslyn/pull/70497) | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | [jcouv](https://github.com/jcouv), [RikkiGibson](https://github.com/RikkiGibson) | | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | -| [Method group natural type improvements](https://github.com/dotnet/csharplang/blob/main/proposals/method-group-natural-type-improvements.md) | main | [Merged into 17.9p2](https://github.com/dotnet/roslyn/issues/69432) | [jcouv](https://github.com/jcouv) | [AlekseyTs](https://github.com/AlekseyTs), [cston](https://github.com/cston) | | [jcouv](https://github.com/jcouv) | -| [`Lock` object](https://github.com/dotnet/csharplang/issues/7104) | [LockObject](https://github.com/dotnet/roslyn/tree/features/LockObject) | [Merged into 17.10p2](https://github.com/dotnet/roslyn/issues/71888) | [jjonescz](https://github.com/jjonescz) | [cston](https://github.com/cston), [RikkiGibson](https://github.com/RikkiGibson) | | [stephentoub](https://github.com/stephentoub) | -| Implicit indexer access in object initializers | main | [Merged into 17.9p3](https://github.com/dotnet/roslyn/pull/70649) | [jcouv](https://github.com/jcouv) | [AlekseyTs](https://github.com/AlekseyTs), [cston](https://github.com/cston) | | | -| [Params-collections](https://github.com/dotnet/csharplang/issues/7700) | main | [Merged to 17.10p3](https://github.com/dotnet/roslyn/issues/71137) | [AlekseyTs](https://github.com/AlekseyTs) | [RikkiGibson](https://github.com/RikkiGibson), [333fred](https://github.com/333fred) | [akhera99](https://github.com/akhera99) | [MadsTorgersen](https://github.com/MadsTorgersen), [AlekseyTs](https://github.com/AlekseyTs) | +| [Escape character](https://github.com/dotnet/csharplang/issues/7400) | N/A | [Merged into 17.9p1](https://github.com/dotnet/roslyn/pull/70497) | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | [jcouv](https://github.com/jcouv), [RikkiGibson](https://github.com/RikkiGibson) | (no IDE impact) | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) | +| [Method group natural type improvements](https://github.com/dotnet/csharplang/blob/main/proposals/method-group-natural-type-improvements.md) | main | [Merged into 17.9p2](https://github.com/dotnet/roslyn/issues/69432) | [jcouv](https://github.com/jcouv) | [AlekseyTs](https://github.com/AlekseyTs), [cston](https://github.com/cston) | (no IDE impact) | [jcouv](https://github.com/jcouv) | +| [`Lock` object](https://github.com/dotnet/csharplang/issues/7104) | [LockObject](https://github.com/dotnet/roslyn/tree/features/LockObject) | [Merged into 17.10p2](https://github.com/dotnet/roslyn/issues/71888) | [jjonescz](https://github.com/jjonescz) | [cston](https://github.com/cston), [RikkiGibson](https://github.com/RikkiGibson) | [CyrusNajmabadi](https://github.com/CyrusNajmabadi) (needs IDE fixer) | [stephentoub](https://github.com/stephentoub) | +| Implicit indexer access in object initializers | main | [Merged into 17.9p3](https://github.com/dotnet/roslyn/pull/70649) | [jcouv](https://github.com/jcouv) | [AlekseyTs](https://github.com/AlekseyTs), [cston](https://github.com/cston) | (no IDE impact) | | +| [Params-collections](https://github.com/dotnet/csharplang/issues/7700) | main | [Merged to 17.10p3](https://github.com/dotnet/roslyn/issues/71137) | [AlekseyTs](https://github.com/AlekseyTs) | [RikkiGibson](https://github.com/RikkiGibson), [333fred](https://github.com/333fred) | [akhera99](https://github.com/akhera99) (needs IDE fixer) | [MadsTorgersen](https://github.com/MadsTorgersen), [AlekseyTs](https://github.com/AlekseyTs) | # C# 12.0 diff --git a/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 8.md b/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 8.md index 76b8741267d6e..351e636ac80db 100644 --- a/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 8.md +++ b/docs/compilers/CSharp/Compiler Breaking Changes - DotNet 8.md @@ -28,6 +28,34 @@ public class C } ``` +## Collection expression for type implementing `IEnumerable` must have elements implicitly convertible to `object` + +***Introduced in Visual Studio 2022 version 17.10*** + +*Conversion* of a collection expression to a `struct` or `class` that implements `System.Collections.IEnumerable` and *does not* have a strongly-typed `GetEnumerator()` +requires the elements in the collection expression are implicitly convertible to the `object`. +Previously, the elements of a collection expression targeting an `IEnumerable` implementation were assumed to be convertible to `object`, and converted only when binding to the applicable `Add` method. + +This additional requirement means that collection expression conversions to `IEnumerable` implementations are treated consistently with other target types where the elements in the collection expression must be implicitly convertible to the *iteration type* of the target type. + +This change affects collection expressions targeting `IEnumerable` implementations where the elements rely on target-typing to a strongly-typed `Add` method parameter type. +In the example below, an error is reported that `_ => { }` cannot be implicitly converted to `object`. +```csharp +class Actions : IEnumerable +{ + public void Add(Action action); + // ... +} + +Actions a = [_ => { }]; // error CS8917: The delegate type could not be inferred. +``` + +To resolve the error, the element expression could be explicitly typed. +```csharp +a = [(int _) => { }]; // ok +a = [(Action)(_ => { })]; // ok +``` + ## Collection expression target type must have constructor and `Add` method ***Introduced in Visual Studio 2022 version 17.10*** diff --git a/docs/contributing/Bootstrap Builds.md b/docs/contributing/Bootstrap Builds.md index 52e1a638cc877..128a4d59fe740 100644 --- a/docs/contributing/Bootstrap Builds.md +++ b/docs/contributing/Bootstrap Builds.md @@ -104,5 +104,5 @@ https://github.com/dotnet/roslyn/blob/d73d31cbccb9aa850f3582afb464b709fef88fd7/s Next just run the bootstrap build locally, wait for the `Debug.Assert` to trigger which pops up a dialog. From there you can attach to the VBCSCompiler process and debug through the problem ```cmd -> Build.cmd -bootstrap -bootstrapConfiguration Debug +> Build.cmd -bootstrap ``` diff --git a/docs/contributing/Building, Debugging, and Testing on Unix.md b/docs/contributing/Building, Debugging, and Testing on Unix.md index d71853057f74e..0690730630dd6 100644 --- a/docs/contributing/Building, Debugging, and Testing on Unix.md +++ b/docs/contributing/Building, Debugging, and Testing on Unix.md @@ -15,6 +15,7 @@ Particularly for developers who aren't experienced with .NET Core development on 1. Install the [.NET 8.0 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) which matches the `sdk.version` property in [global.json](../../global.json#L3) 3. You can build from VS Code by running the *Run Build Task* command, then selecting an appropriate task such as *build* or *build current project* (the latter builds the containing project for the current file you're viewing in the editor). 4. You can run tests from VS Code by opening a test class in the editor, then using the *Run Tests in Context* and *Debug Tests in Context* editor commands. You may want to bind these commands to keyboard shortcuts that match their Visual Studio equivalents (**Ctrl+R, T** for *Run Tests in Context* and **Ctrl+R, Ctrl+T** for *Debug Tests in Context*). +5. You can launch a new VS Code instance with the language server from your current code by running the "launch vscode with language server" task. ## Running Tests The unit tests can be executed by running `./build.sh --test`. diff --git a/docs/features/incremental-generators.cookbook.md b/docs/features/incremental-generators.cookbook.md index 9bbb0b9134c9d..3cf0870f6bf57 100644 --- a/docs/features/incremental-generators.cookbook.md +++ b/docs/features/incremental-generators.cookbook.md @@ -191,7 +191,7 @@ public class FileTransformGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { var pipeline = context.AdditionalTextsProvider - .Where(static (text, cancellationToken) => text.Path.EndsWith(".xml")) + .Where(static (text) => text.Path.EndsWith(".xml")) .Select(static (text, cancellationToken) => { var name = Path.GetFileName(text.Path); diff --git a/eng/Directory.Packages.props b/eng/Directory.Packages.props index 97ff84ec7abe2..8adaa8740c2ac 100644 --- a/eng/Directory.Packages.props +++ b/eng/Directory.Packages.props @@ -103,14 +103,14 @@ - + - + @@ -293,13 +293,13 @@ - - - - + + + + - - + + - - Library - netstandard2.0 - Microsoft.CommonLanguageServerProtocol.Framework - - - true - Microsoft.CommonLanguageServerProtocol.Framework.Binary - - A legacy binary implementation of Microsoft.CommonLanguageServerProtocol.Framework. - - - - BINARY_COMPAT - - - - - - - diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/ExampleLanguageServer.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/ExampleLanguageServer.cs index eb1381272b95e..4d6774ecd3ab3 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/ExampleLanguageServer.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/ExampleLanguageServer.cs @@ -27,7 +27,7 @@ protected override ILspServices ConstructLspServices() var serviceCollection = new ServiceCollection(); var _ = AddHandlers(serviceCollection) - .AddSingleton(_logger) + .AddSingleton(Logger) .AddSingleton, ExampleRequestContextFactory>() .AddSingleton(s => HandlerProvider) .AddSingleton, CapabilitiesManager>() diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/HandlerProviderTests.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/HandlerProviderTests.cs index 18c12b0d365be..9bc86ff2fcfa9 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/HandlerProviderTests.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.UnitTests/HandlerProviderTests.cs @@ -11,8 +11,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework.UnitTests; public class HandlerProviderTests { - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public void GetMethodHandler(bool supportsGetRegisteredServices) { var handlerProvider = GetHandlerProvider(supportsGetRegisteredServices); @@ -21,8 +20,7 @@ public void GetMethodHandler(bool supportsGetRegisteredServices) Assert.Same(TestMethodHandler.Instance, methodHandler); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public void GetMethodHandler_Parameterless(bool supportsGetRegisteredServices) { var handlerProvider = GetHandlerProvider(supportsGetRegisteredServices); @@ -31,8 +29,7 @@ public void GetMethodHandler_Parameterless(bool supportsGetRegisteredServices) Assert.Same(TestParameterlessMethodHandler.Instance, methodHandler); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public void GetMethodHandler_Notification(bool supportsGetRegisteredServices) { var handlerProvider = GetHandlerProvider(supportsGetRegisteredServices); @@ -41,8 +38,7 @@ public void GetMethodHandler_Notification(bool supportsGetRegisteredServices) Assert.Same(TestNotificationHandler.Instance, methodHandler); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public void GetMethodHandler_ParameterlessNotification(bool supportsGetRegisteredServices) { var handlerProvider = GetHandlerProvider(supportsGetRegisteredServices); @@ -67,8 +63,7 @@ public void GetMethodHandler_WrongResponseType_Throws() Assert.Throws(() => handlerProvider.GetMethodHandler(TestMethodHandler.Name, TestMethodHandler.RequestType, responseType: typeof(long), LanguageServerConstants.DefaultLanguageName)); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public void GetRegisteredMethods(bool supportsGetRegisteredServices) { var handlerProvider = GetHandlerProvider(supportsGetRegisteredServices); diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractHandlerProvider.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractHandlerProvider.cs index 4af41c4bd96bc..7e36101ec4d64 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractHandlerProvider.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractHandlerProvider.cs @@ -13,11 +13,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// /// Manages handler discovery and distribution. /// -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public abstract class AbstractHandlerProvider -#else internal abstract class AbstractHandlerProvider -#endif { /// /// Gets the s for all registered methods. diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs index f140ff52b78ee..8dddabcf31dbe 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs @@ -18,16 +18,10 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public abstract class AbstractLanguageServer -#else internal abstract class AbstractLanguageServer -#endif { private readonly JsonRpc _jsonRpc; -#pragma warning disable IDE1006 // Naming Styles - Required for API compat, TODO - https://github.com/dotnet/roslyn/issues/72251 - protected readonly ILspLogger _logger; -#pragma warning restore IDE1006 // Naming Styles + protected readonly ILspLogger Logger; protected readonly JsonSerializer _jsonSerializer; @@ -67,7 +61,7 @@ protected AbstractLanguageServer( JsonSerializer jsonSerializer, ILspLogger logger) { - _logger = logger; + Logger = logger; _jsonRpc = jsonRpc; _jsonSerializer = jsonSerializer; @@ -93,35 +87,20 @@ public void Initialize() /// This should only be called once, and then cached. protected abstract ILspServices ConstructLspServices(); - [Obsolete($"Use {nameof(HandlerProvider)} property instead.", error: false)] - protected virtual IHandlerProvider GetHandlerProvider() - { - var lspServices = _lspServices.Value; - var handlerProvider = new HandlerProvider(lspServices); - SetupRequestDispatcher(handlerProvider); - - return handlerProvider; - } - protected virtual AbstractHandlerProvider HandlerProvider { get { -#pragma warning disable CS0618 // Type or member is obsolete - var handlerProvider = GetHandlerProvider(); -#pragma warning restore CS0618 // Type or member is obsolete - if (handlerProvider is AbstractHandlerProvider abstractHandlerProvider) - { - return abstractHandlerProvider; - } - - return new WrappedHandlerProvider(handlerProvider); + var lspServices = _lspServices.Value; + var handlerProvider = new HandlerProvider(lspServices); + SetupRequestDispatcher(handlerProvider); + return handlerProvider; } } public ILspServices GetLspServices() => _lspServices.Value; - protected virtual void SetupRequestDispatcher(IHandlerProvider handlerProvider) + protected virtual void SetupRequestDispatcher(AbstractHandlerProvider handlerProvider) { var entryPointMethodInfo = typeof(DelegatingEntryPoint).GetMethod(nameof(DelegatingEntryPoint.ExecuteRequestAsync))!; // Get unique set of methods from the handler provider for the default language. @@ -187,7 +166,7 @@ public virtual void OnInitialized() protected virtual IRequestExecutionQueue ConstructRequestExecutionQueue() { var handlerProvider = HandlerProvider; - var queue = new RequestExecutionQueue(this, _logger, handlerProvider); + var queue = new RequestExecutionQueue(this, Logger, handlerProvider); queue.Start(); @@ -201,7 +180,7 @@ protected IRequestExecutionQueue GetRequestExecutionQueue() protected virtual string GetLanguageForRequest(string methodName, JToken? parameters) { - _logger.LogInformation($"Using default language handler for {methodName}"); + Logger.LogInformation($"Using default language handler for {methodName}"); return LanguageServerConstants.DefaultLanguageName; } @@ -328,7 +307,7 @@ async Task Shutdown_NoLockAsync(string message) // Immediately yield so that this does not run under the lock. await Task.Yield(); - _logger.LogInformation(message); + Logger.LogInformation(message); // Allow implementations to do any additional cleanup on shutdown. var lifeCycleManager = GetLspServices().GetRequiredService(); @@ -386,7 +365,7 @@ async Task Exit_NoLockAsync() } finally { - _logger.LogInformation("Exiting server"); + Logger.LogInformation("Exiting server"); _serverExitedSource.TrySetResult(null); } } diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLspLogger.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLspLogger.cs index 41c4bf4001219..b849768f60027 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLspLogger.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLspLogger.cs @@ -9,11 +9,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public abstract class AbstractLspLogger : ILspLogger -#else internal abstract class AbstractLspLogger : ILspLogger -#endif { public abstract void LogDebug(string message, params object[] @params); public abstract void LogStartContext(string message, params object[] @params); diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestContextFactory.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestContextFactory.cs index adcc85e1c484b..84849c484dc7a 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestContextFactory.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestContextFactory.cs @@ -21,11 +21,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// /// /// The type of the RequestContext to be used by the handler. -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public abstract class AbstractRequestContextFactory -#else internal abstract class AbstractRequestContextFactory -#endif { /// /// Create a object from the given . diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestScope.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestScope.cs index 7e5ea5c8aeb5a..27762af48782d 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestScope.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestScope.cs @@ -8,12 +8,7 @@ using System; namespace Microsoft.CommonLanguageServerProtocol.Framework; - -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public abstract class AbstractRequestScope(string name) : IDisposable -#else internal abstract class AbstractRequestScope(string name) : IDisposable -#endif { public string Name { get; } = name; diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractTelemetryService.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractTelemetryService.cs index 7df8edce2f545..c5ccdf85ab942 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractTelemetryService.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractTelemetryService.cs @@ -7,11 +7,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public abstract class AbstractTelemetryService -#else internal abstract class AbstractTelemetryService -#endif { public abstract AbstractRequestScope CreateRequestScope(string lspMethodName); } diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/HandlerProvider.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/HandlerProvider.cs index ac0e0bd0ad03b..93d0b2f64e2b5 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/HandlerProvider.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/HandlerProvider.cs @@ -15,7 +15,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// -internal class HandlerProvider : AbstractHandlerProvider, IHandlerProvider +internal class HandlerProvider : AbstractHandlerProvider { private readonly ILspServices _lspServices; private ImmutableDictionary>? _requestHandlers; diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Handlers/InitializeHandler.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Handlers/InitializeHandler.cs index aeb2a3d698f06..564898a683de1 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Handlers/InitializeHandler.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Handlers/InitializeHandler.cs @@ -11,11 +11,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework.Handlers; [LanguageServerEndpoint("initialize", LanguageServerConstants.DefaultLanguageName)] -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public class InitializeHandler -#else internal class InitializeHandler -#endif : IRequestHandler { private readonly IInitializeManager _capabilitiesManager; diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Handlers/InitializedHandler.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Handlers/InitializedHandler.cs index 46f87a4c52df4..d4a14851e4a70 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Handlers/InitializedHandler.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Handlers/InitializedHandler.cs @@ -12,11 +12,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework.Handlers; [LanguageServerEndpoint("initialized", LanguageServerConstants.DefaultLanguageName)] -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public class InitializedHandler : INotificationHandler -#else internal class InitializedHandler : INotificationHandler -#endif { private bool HasBeenInitialized = false; diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IHandlerProvider.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IHandlerProvider.cs deleted file mode 100644 index 837cd4b051de5..0000000000000 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IHandlerProvider.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -// This is consumed as 'generated' code in a source package and therefore requires an explicit nullable enable -#nullable enable - -using System; -using System.Collections.Immutable; - -namespace Microsoft.CommonLanguageServerProtocol.Framework; - -/// -/// Manages handler discovery and distribution. -/// -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface IHandlerProvider -#else -internal interface IHandlerProvider -#endif -{ - ImmutableArray GetRegisteredMethods(); - - IMethodHandler GetMethodHandler(string method, Type? requestType, Type? responseType); -} diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IInitializeManager.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IInitializeManager.cs index 13e9fc9293a3e..2a73d500c0919 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IInitializeManager.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IInitializeManager.cs @@ -6,12 +6,7 @@ #nullable enable namespace Microsoft.CommonLanguageServerProtocol.Framework; - -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface IInitializeManager -#else internal interface IInitializeManager -#endif { /// /// Gets a response to be used for "initialize", completing the negoticaitons between client and server. diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILifeCycleManager.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILifeCycleManager.cs index 73b17b89df6ad..b8f704c68d15f 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILifeCycleManager.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILifeCycleManager.cs @@ -13,11 +13,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// An optional component to run additional logic when LSP shutdown and exit are called, /// for example logging messages, cleaning up custom resources, etc. /// -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface ILifeCycleManager -#else internal interface ILifeCycleManager -#endif { /// /// Called when the server recieves the LSP exit notification. diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILspLogger.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILspLogger.cs index 3bfe075fc3e0e..e7ac139a4ddc2 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILspLogger.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILspLogger.cs @@ -8,12 +8,7 @@ using System; namespace Microsoft.CommonLanguageServerProtocol.Framework; - -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface ILspLogger -#else internal interface ILspLogger -#endif { void LogStartContext(string message, params object[] @params); void LogEndContext(string message, params object[] @params); diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILspServices.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILspServices.cs index 14ed351fb7e7c..7b094ed23ea64 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILspServices.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILspServices.cs @@ -10,12 +10,7 @@ using System.Collections.Immutable; namespace Microsoft.CommonLanguageServerProtocol.Framework; - -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface ILspServices : IDisposable -#else internal interface ILspServices : IDisposable -#endif { T GetRequiredService() where T : notnull; diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IMethodHandler.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IMethodHandler.cs index d8299e27250a1..6c637662c618d 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IMethodHandler.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IMethodHandler.cs @@ -10,11 +10,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// /// Top level type for LSP request handler. /// -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface IMethodHandler -#else internal interface IMethodHandler -#endif { /// /// Whether or not the solution state on the server is modified as a part of handling this request. diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/INotificationHandler.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/INotificationHandler.cs index 22c8d8f2c38a1..513a78dcc9c70 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/INotificationHandler.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/INotificationHandler.cs @@ -14,11 +14,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// An interface for handlers of methods which do not return a response and receive no parameters. /// /// The type of the RequestContext to be used by this handler. -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface INotificationHandler : IMethodHandler -#else internal interface INotificationHandler : IMethodHandler -#endif { Task HandleNotificationAsync(TRequestContext requestContext, CancellationToken cancellationToken); } @@ -28,11 +24,7 @@ internal interface INotificationHandler : IMethodHandler /// /// The type of the Request parameter to be received. /// The type of the RequestContext to be used by this handler. -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface INotificationHandler : IMethodHandler -#else internal interface INotificationHandler : IMethodHandler -#endif { Task HandleNotificationAsync(TRequest request, TRequestContext requestContext, CancellationToken cancellationToken); } diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IQueueItem.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IQueueItem.cs index 832bbf4fdeae2..23f1ea4cd188e 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IQueueItem.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IQueueItem.cs @@ -15,11 +15,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// An item to be queued for execution. /// /// The type of the request context to be passed along to the handler. -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface IQueueItem -#else internal interface IQueueItem -#endif { /// /// Executes the work specified by this queue item. diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestContextFactory.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestContextFactory.cs deleted file mode 100644 index ee07e97cfdc0a..0000000000000 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestContextFactory.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Threading; -using System.Threading.Tasks; - -// This is consumed as 'generated' code in a source package and therefore requires an explicit nullable enable -#nullable enable - -namespace Microsoft.CommonLanguageServerProtocol.Framework; - -/// -/// -/// A factory for creating objects from 's. -/// -/// -/// RequestContext's are useful for passing document context, since by default -/// is run on the queue thread (and thus no mutating requests may be executing simultaneously, preventing possible race conditions). -/// It also allows somewhere to pass things like the or which are useful on a wide variety of requests. -/// -/// -/// The type of the RequestContext to be used by the handler. -[Obsolete($"Use {nameof(AbstractRequestContextFactory)} instead.", error: false)] -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface IRequestContextFactory -#else -internal interface IRequestContextFactory -#endif -{ - /// - /// Create a object from the given . - /// Note - throwing in the implementation of this method will cause the server to shutdown. - /// - /// The from which to create a request. - /// The request parameters. - /// - /// The for this request. - /// This method is called on the queue thread to allow context to be retrieved serially, without the possibility of race conditions from Mutating requests. - Task CreateRequestContextAsync(IQueueItem queueItem, TRequestParam requestParam, CancellationToken cancellationToken); -} diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestExecutionQueue.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestExecutionQueue.cs index 1268202d4328f..463e597c70cf3 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestExecutionQueue.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestExecutionQueue.cs @@ -15,11 +15,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// Queues requests to be executed in the proper order. /// /// The type of the RequestContext to be used by the handler. -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface IRequestExecutionQueue : IAsyncDisposable -#else internal interface IRequestExecutionQueue : IAsyncDisposable -#endif { /// /// Queue a request. diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestHandler.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestHandler.cs index 2bc711aabe3a2..7d58b68427183 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestHandler.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestHandler.cs @@ -9,12 +9,7 @@ using System.Threading.Tasks; namespace Microsoft.CommonLanguageServerProtocol.Framework; - -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface IRequestHandler : IMethodHandler -#else internal interface IRequestHandler : IMethodHandler -#endif { /// /// Handles an LSP request in the context of the supplied document and/or solution. @@ -26,11 +21,7 @@ internal interface IRequestHandler : IMeth Task HandleRequestAsync(TRequest request, TRequestContext context, CancellationToken cancellationToken); } -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface IRequestHandler : IMethodHandler -#else internal interface IRequestHandler : IMethodHandler -#endif { /// /// Handles an LSP request in the context of the supplied document and/or solution. diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ITextDocumentIdentifierHandler.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ITextDocumentIdentifierHandler.cs index eaf30967bc0b9..8c746ab34f188 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ITextDocumentIdentifierHandler.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ITextDocumentIdentifierHandler.cs @@ -7,11 +7,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface ITextDocumentIdentifierHandler : ITextDocumentIdentifierHandler -#else internal interface ITextDocumentIdentifierHandler : ITextDocumentIdentifierHandler -#endif { /// /// Gets the identifier of the document from the request, if the request provides one. @@ -19,10 +15,6 @@ internal interface ITextDocumentIdentifierHandler /// Default language name for use with and . diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/LanguageServerEndpointAttribute.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/LanguageServerEndpointAttribute.cs index 1f8c71ce2d7ab..a7bb1e13ac058 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/LanguageServerEndpointAttribute.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/LanguageServerEndpointAttribute.cs @@ -14,11 +14,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// An attribute which identifies the method which an implements. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method, AllowMultiple = false)] -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public class LanguageServerEndpointAttribute : Attribute -#else internal class LanguageServerEndpointAttribute : Attribute -#endif { /// /// Contains the method that this implements. @@ -45,6 +41,6 @@ public LanguageServerEndpointAttribute(string method) public LanguageServerEndpointAttribute(string method, string language, params string[] additionalLanguages) { Method = method; - Languages = new[] { language }.Concat(additionalLanguages).ToArray(); + Languages = [language, .. additionalLanguages]; } } diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Microsoft.CommonLanguageServerProtocol.Framework.Shared.projitems b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Microsoft.CommonLanguageServerProtocol.Framework.Shared.projitems index 3e94a73112030..4f299de12be5e 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Microsoft.CommonLanguageServerProtocol.Framework.Shared.projitems +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Microsoft.CommonLanguageServerProtocol.Framework.Shared.projitems @@ -18,7 +18,6 @@ - @@ -26,7 +25,6 @@ - @@ -36,7 +34,6 @@ - \ No newline at end of file diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/QueueItem.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/QueueItem.cs index 5cf8e93cc2383..31dff9c3b2b81 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/QueueItem.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/QueueItem.cs @@ -86,30 +86,16 @@ public static (IQueueItem, Task) Create( return (queueItem, queueItem._completionSource.Task); } -#pragma warning disable CS0618 // Type or member is obsolete public async Task CreateRequestContextAsync(IMethodHandler handler, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); _requestTelemetryScope?.RecordExecutionStart(); - var requestContextFactory = (AbstractRequestContextFactory?)LspServices.TryGetService(typeof(AbstractRequestContextFactory)); - if (requestContextFactory is not null) - { - var context = await requestContextFactory.CreateRequestContextAsync(this, handler, _request, cancellationToken).ConfigureAwait(false); - return context; - } - - var obsoleteContextFactory = (IRequestContextFactory?)LspServices.TryGetService(typeof(IRequestContextFactory)); - if (obsoleteContextFactory is not null) - { - var context = await obsoleteContextFactory.CreateRequestContextAsync(this, _request, cancellationToken).ConfigureAwait(false); - return context; - } - - throw new InvalidOperationException($"No {nameof(AbstractRequestContextFactory)} or {nameof(IRequestContextFactory)} was registered with {nameof(ILspServices)}."); + var requestContextFactory = LspServices.GetRequiredService>(); + var context = await requestContextFactory.CreateRequestContextAsync(this, handler, _request, cancellationToken).ConfigureAwait(false); + return context; } -#pragma warning restore CS0618 // Type or member is obsolete /// /// Processes the queued request. Exceptions will be sent to the task completion source diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestExecutionQueue.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestExecutionQueue.cs index b24ca7e75352d..cd200a1ca0ec7 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestExecutionQueue.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestExecutionQueue.cs @@ -50,11 +50,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// more messages, and a new queue will need to be created. /// /// -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public class RequestExecutionQueue : IRequestExecutionQueue -#else internal class RequestExecutionQueue : IRequestExecutionQueue -#endif { protected readonly ILspLogger _logger; protected readonly AbstractHandlerProvider _handlerProvider; @@ -75,19 +71,6 @@ internal class RequestExecutionQueue : IRequestExecutionQueue _cancelSource.Token; - [Obsolete($"Use constructor with {nameof(AbstractHandlerProvider)} instead.", error: false)] - public RequestExecutionQueue(AbstractLanguageServer languageServer, ILspLogger logger, IHandlerProvider handlerProvider) - { - _languageServer = languageServer; - _logger = logger; - if (handlerProvider is AbstractHandlerProvider abstractHandlerProvider) - { - _handlerProvider = abstractHandlerProvider; - } - - _handlerProvider = new WrappedHandlerProvider(handlerProvider); - } - public RequestExecutionQueue(AbstractLanguageServer languageServer, ILspLogger logger, AbstractHandlerProvider handlerProvider) { _languageServer = languageServer; diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestHandlerMetadata.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestHandlerMetadata.cs index e9f7a2be78ed5..c0b0bd4254d43 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestHandlerMetadata.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestHandlerMetadata.cs @@ -8,12 +8,7 @@ using System; namespace Microsoft.CommonLanguageServerProtocol.Framework; - -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public record RequestHandlerMetadata(string MethodName, Type? RequestType, Type? ResponseType, string Language) -#else internal record RequestHandlerMetadata(string MethodName, Type? RequestType, Type? ResponseType, string Language) -#endif { internal string HandlerDescription { get; } = $"{MethodName} ({Language})"; } diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestShutdownEventArgs.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestShutdownEventArgs.cs index e689d852425f1..9ad2023834c93 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestShutdownEventArgs.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestShutdownEventArgs.cs @@ -8,12 +8,7 @@ using System; namespace Microsoft.CommonLanguageServerProtocol.Framework; - -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public class RequestShutdownEventArgs : EventArgs -#else internal class RequestShutdownEventArgs : EventArgs -#endif { public string Message { get; } diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/WrappedHandlerProvider.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/WrappedHandlerProvider.cs deleted file mode 100644 index 120b54dfb0c21..0000000000000 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/WrappedHandlerProvider.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -// This is consumed as 'generated' code in a source package and therefore requires an explicit nullable enable -#nullable enable - -using System; -using System.Collections.Immutable; - -namespace Microsoft.CommonLanguageServerProtocol.Framework; - -/// -/// Wraps an . -/// -internal sealed class WrappedHandlerProvider : AbstractHandlerProvider -{ - private readonly IHandlerProvider _handlerProvider; - - public WrappedHandlerProvider(IHandlerProvider handlerProvider) - { - _handlerProvider = handlerProvider; - } - - public override IMethodHandler GetMethodHandler(string method, Type? requestType, Type? responseType, string language) - => _handlerProvider.GetMethodHandler(method, requestType, responseType); - - public override ImmutableArray GetRegisteredMethods() - => _handlerProvider.GetRegisteredMethods(); -} diff --git a/src/Features/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs b/src/Features/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs index 2b2b974ac5a7c..406ea9a4c2de0 100644 --- a/src/Features/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs +++ b/src/Features/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs @@ -98,7 +98,7 @@ public ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) Range = true, Legend = new SemanticTokensLegend { - TokenTypes = SemanticTokensSchema.GetSchema(clientCapabilities.HasVisualStudioLspCapability()).AllTokenTypes.ToArray(), + TokenTypes = [.. SemanticTokensSchema.GetSchema(clientCapabilities.HasVisualStudioLspCapability()).AllTokenTypes], TokenModifiers = SemanticTokensSchema.TokenModifiers } }; @@ -120,16 +120,6 @@ public ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) // Using VS server capabilities because we have our own custom client. capabilities.OnAutoInsertProvider = new VSInternalDocumentOnAutoInsertOptions { TriggerCharacters = ["'", "/", "\n"] }; - if (!supportsVsExtensions) - { - capabilities.DiagnosticOptions = new DiagnosticOptions - { - InterFileDependencies = true, - WorkDoneProgress = true, - WorkspaceDiagnostics = true, - }; - } - return capabilities; } diff --git a/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs b/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs index 890759ebb9ceb..d9b8e4078f89f 100644 --- a/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs +++ b/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs @@ -10,8 +10,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.FindUsages; -using Microsoft.CodeAnalysis.LanguageServer.Handler; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; diff --git a/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs b/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs index c6a06c30f422c..b46fbf4e76b96 100644 --- a/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs +++ b/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs @@ -199,6 +199,22 @@ public static Uri CreateAbsoluteUri(string absolutePath) } } + internal static Uri CreateRelativePatternBaseUri(string path) + { + // According to VSCode LSP RelativePattern spec, + // found at https://github.com/microsoft/vscode/blob/9e1974682eb84eebb073d4ae775bad1738c281f6/src/vscode-dts/vscode.d.ts#L2226 + // the baseUri should not end in a trailing separator, nor should it + // have any relative segmeents (., ..) + if (path[^1] == System.IO.Path.DirectorySeparatorChar) + { + path = path[..^1]; + } + + Debug.Assert(!path.Split(System.IO.Path.DirectorySeparatorChar).Any(p => p == "." || p == "..")); + + return CreateAbsoluteUri(path); + } + // Implements workaround for https://github.com/dotnet/runtime/issues/89538: internal static string GetAbsoluteUriString(string absolutePath) { @@ -390,7 +406,7 @@ public static LSP.Range TextSpanToRange(TextSpan textSpan, SourceText text) else { var newText = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - textChanges = newText.GetTextChanges(oldText).ToImmutableArray(); + textChanges = [.. newText.GetTextChanges(oldText)]; } // Map all the text changes' spans for this document. diff --git a/src/Features/LanguageServer/Protocol/Features/CodeFixes/CodeFixService.ProjectCodeFixProvider.cs b/src/Features/LanguageServer/Protocol/Features/CodeFixes/CodeFixService.ProjectCodeFixProvider.cs index f30b9ac390f0d..fde0bc07d9891 100644 --- a/src/Features/LanguageServer/Protocol/Features/CodeFixes/CodeFixService.ProjectCodeFixProvider.cs +++ b/src/Features/LanguageServer/Protocol/Features/CodeFixes/CodeFixService.ProjectCodeFixProvider.cs @@ -15,7 +15,7 @@ private class ProjectCodeFixProvider : AbstractProjectExtensionProvider { protected override ImmutableArray GetLanguages(ExportCodeFixProviderAttribute exportAttribute) - => exportAttribute.Languages.ToImmutableArray(); + => [.. exportAttribute.Languages]; protected override bool TryGetExtensionsFromReference(AnalyzerReference reference, out ImmutableArray extensions) { diff --git a/src/Features/LanguageServer/Protocol/Features/Diagnostics/DiagnosticAnalyzerService.cs b/src/Features/LanguageServer/Protocol/Features/Diagnostics/DiagnosticAnalyzerService.cs index aac07fce0e0f2..be32623daf749 100644 --- a/src/Features/LanguageServer/Protocol/Features/Diagnostics/DiagnosticAnalyzerService.cs +++ b/src/Features/LanguageServer/Protocol/Features/Diagnostics/DiagnosticAnalyzerService.cs @@ -121,12 +121,6 @@ public Task> GetCachedDiagnosticsAsync(Workspace return analyzer.GetCachedDiagnosticsAsync(workspace.CurrentSolution, projectId, documentId, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics, cancellationToken); } - public Task> GetDiagnosticsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, bool includeSuppressedDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) - { - var analyzer = CreateIncrementalAnalyzer(solution.Workspace); - return analyzer.GetDiagnosticsAsync(solution, projectId, documentId, includeSuppressedDiagnostics, includeNonLocalDocumentDiagnostics, cancellationToken); - } - public async Task ForceAnalyzeProjectAsync(Project project, CancellationToken cancellationToken) { var analyzer = CreateIncrementalAnalyzer(project.Solution.Workspace); @@ -134,10 +128,10 @@ public async Task ForceAnalyzeProjectAsync(Project project, CancellationToken ca } public Task> GetDiagnosticsForIdsAsync( - Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) + Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, Func>? getDocuments, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) { var analyzer = CreateIncrementalAnalyzer(solution.Workspace); - return analyzer.GetDiagnosticsForIdsAsync(solution, projectId, documentId, diagnosticIds, shouldIncludeAnalyzer, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics, cancellationToken); + return analyzer.GetDiagnosticsForIdsAsync(solution, projectId, documentId, diagnosticIds, shouldIncludeAnalyzer, getDocuments, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics, cancellationToken); } public Task> GetProjectDiagnosticsForIdsAsync( diff --git a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs index 05b432309c757..68cdf1724c25b 100644 --- a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs +++ b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs @@ -24,7 +24,7 @@ internal partial class DiagnosticIncrementalAnalyzer /// Return all diagnostics that belong to given project for the given StateSets (analyzers) either from cache or by calculating them /// private async Task GetProjectAnalysisDataAsync( - CompilationWithAnalyzers? compilationWithAnalyzers, Project project, IdeAnalyzerOptions ideOptions, ImmutableArray stateSets, bool forceAnalyzerRun, CancellationToken cancellationToken) + CompilationWithAnalyzers? compilationWithAnalyzers, Project project, IdeAnalyzerOptions ideOptions, ImmutableArray stateSets, CancellationToken cancellationToken) { using (Logger.LogBlock(FunctionId.Diagnostics_ProjectDiagnostic, GetProjectLogMessage, project, stateSets, cancellationToken)) { @@ -42,64 +42,7 @@ private async Task GetProjectAnalysisDataAsync( return existingData; } - // PERF: Check whether we want to analyze this project or not. - var fullAnalysisEnabled = GlobalOptions.IsFullSolutionAnalysisEnabled(project.Language, out var compilerFullAnalysisEnabled, out var analyzersFullAnalysisEnabled); - if (forceAnalyzerRun) - { - // We are forcing full solution analysis for all diagnostics. - fullAnalysisEnabled = true; - compilerFullAnalysisEnabled = true; - analyzersFullAnalysisEnabled = true; - } - - if (!fullAnalysisEnabled) - { - Logger.Log(FunctionId.Diagnostics_ProjectDiagnostic, p => $"FSA off ({p.FilePath ?? p.Name})", project); - - // If we are producing document diagnostics for some other document in this project, we still want to show - // certain project-level diagnostics that would cause file-level diagnostics to be broken. We will only do this though if - // some file that's open is depending on this project though -- that way we're going to only be analyzing projects - // that have already had compilations produced for. - var shouldProduceOutput = false; - - var projectDependencyGraph = project.Solution.GetProjectDependencyGraph(); - - foreach (var openDocumentId in project.Solution.Workspace.GetOpenDocumentIds()) - { - if (openDocumentId.ProjectId == project.Id || projectDependencyGraph.DoesProjectTransitivelyDependOnProject(openDocumentId.ProjectId, project.Id)) - { - shouldProduceOutput = true; - break; - } - } - - var results = ImmutableDictionary.Empty; - - if (shouldProduceOutput) - { - (results, _) = await UpdateWithDocumentLoadAndGeneratorFailuresAsync( - results, - project, - version, - cancellationToken).ConfigureAwait(false); - } - - return new ProjectAnalysisData(project.Id, VersionStamp.Default, existingData.Result, results); - } - - // Reduce the state sets to analyze based on individual full solution analysis values - // for compiler diagnostics and analyzers. - if (!compilerFullAnalysisEnabled) - { - Debug.Assert(analyzersFullAnalysisEnabled); - stateSets = stateSets.WhereAsArray(s => !s.Analyzer.IsCompilerAnalyzer()); - } - else if (!analyzersFullAnalysisEnabled) - { - stateSets = stateSets.WhereAsArray(s => s.Analyzer.IsCompilerAnalyzer() || s.Analyzer.IsWorkspaceDiagnosticAnalyzer()); - } - - var result = await ComputeDiagnosticsAsync(compilationWithAnalyzers, project, ideOptions, stateSets, forceAnalyzerRun, existingData.Result, cancellationToken).ConfigureAwait(false); + var result = await ComputeDiagnosticsAsync(compilationWithAnalyzers, project, ideOptions, stateSets, existingData.Result, cancellationToken).ConfigureAwait(false); // If project is not loaded successfully, get rid of any semantic errors from compiler analyzer. // Note: In the past when project was not loaded successfully we did not run any analyzers on the project. @@ -169,7 +112,7 @@ private static async Task private async Task> ComputeDiagnosticsAsync( - CompilationWithAnalyzers? compilationWithAnalyzers, Project project, ImmutableArray ideAnalyzers, bool forcedAnalysis, CancellationToken cancellationToken) + CompilationWithAnalyzers? compilationWithAnalyzers, Project project, ImmutableArray ideAnalyzers, CancellationToken cancellationToken) { try { @@ -179,8 +122,8 @@ private async Task 0) { // calculate regular diagnostic analyzers diagnostics - var resultMap = await _diagnosticAnalyzerRunner.AnalyzeProjectAsync(project, compilationWithAnalyzers, - forcedAnalysis, logPerformanceInfo: false, getTelemetryInfo: true, cancellationToken).ConfigureAwait(false); + var resultMap = await _diagnosticAnalyzerRunner.AnalyzeProjectAsync( + project, compilationWithAnalyzers, logPerformanceInfo: false, getTelemetryInfo: true, cancellationToken).ConfigureAwait(false); result = resultMap.AnalysisResult; @@ -198,7 +141,7 @@ private async Task> ComputeDiagnosticsAsync( - CompilationWithAnalyzers? compilationWithAnalyzers, Project project, IdeAnalyzerOptions ideOptions, ImmutableArray stateSets, bool forcedAnalysis, + CompilationWithAnalyzers? compilationWithAnalyzers, Project project, IdeAnalyzerOptions ideOptions, ImmutableArray stateSets, ImmutableDictionary existing, CancellationToken cancellationToken) { try @@ -225,12 +168,12 @@ await DocumentAnalysisExecutor.CreateCompilationWithAnalyzersAsync( compilationWithAnalyzers.AnalysisOptions.ReportSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - var result = await ComputeDiagnosticsAsync(compilationWithReducedAnalyzers, project, ideAnalyzers, forcedAnalysis, cancellationToken).ConfigureAwait(false); + var result = await ComputeDiagnosticsAsync(compilationWithReducedAnalyzers, project, ideAnalyzers, cancellationToken).ConfigureAwait(false); return MergeExistingDiagnostics(version, existing, result); } // we couldn't reduce the set. - return await ComputeDiagnosticsAsync(compilationWithAnalyzers, project, ideAnalyzers, forcedAnalysis, cancellationToken).ConfigureAwait(false); + return await ComputeDiagnosticsAsync(compilationWithAnalyzers, project, ideAnalyzers, cancellationToken).ConfigureAwait(false); } catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) { diff --git a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.InProcOrRemoteHostAnalyzerRunner.cs b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.InProcOrRemoteHostAnalyzerRunner.cs index 17751bcd9e7d0..88214593ae6d4 100644 --- a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.InProcOrRemoteHostAnalyzerRunner.cs +++ b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.InProcOrRemoteHostAnalyzerRunner.cs @@ -50,12 +50,11 @@ public Task> AnalyzeProjectAsync( Project project, CompilationWithAnalyzers compilationWithAnalyzers, - bool forceExecuteAllAnalyzers, bool logPerformanceInfo, bool getTelemetryInfo, CancellationToken cancellationToken) => AnalyzeAsync(documentAnalysisScope: null, project, compilationWithAnalyzers, - isExplicit: false, forceExecuteAllAnalyzers, logPerformanceInfo, getTelemetryInfo, cancellationToken); + isExplicit: false, forceExecuteAllAnalyzers: true, logPerformanceInfo, getTelemetryInfo, cancellationToken); private async Task> AnalyzeAsync( DocumentAnalysisScope? documentAnalysisScope, @@ -214,7 +213,7 @@ private static async Task a // order statesets // order will be in this order // BuiltIn Compiler Analyzer (C#/VB) < Regular DiagnosticAnalyzers < Document/ProjectDiagnosticAnalyzers - OrderedStateSets = StateSetMap.Values.OrderBy(PriorityComparison).ToImmutableArray(); + OrderedStateSets = [.. StateSetMap.Values.OrderBy(PriorityComparison)]; } public HostAnalyzerStateSets WithExcludedAnalyzers(ImmutableHashSet excludedAnalyzers) diff --git a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs index cb1dc8c68d0b9..f24820a50f801 100644 --- a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs +++ b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs @@ -18,14 +18,11 @@ internal partial class DiagnosticIncrementalAnalyzer public Task> GetCachedDiagnosticsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) => new IdeCachedDiagnosticGetter(this, solution, projectId, documentId, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics).GetDiagnosticsAsync(cancellationToken); - public Task> GetDiagnosticsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, bool includeSuppressedDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) - => new IdeLatestDiagnosticGetter(this, solution, projectId, documentId, diagnosticIds: null, shouldIncludeAnalyzer: null, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics: true, includeNonLocalDocumentDiagnostics).GetDiagnosticsAsync(cancellationToken); - - public Task> GetDiagnosticsForIdsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) - => new IdeLatestDiagnosticGetter(this, solution, projectId, documentId, diagnosticIds, shouldIncludeAnalyzer, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics).GetDiagnosticsAsync(cancellationToken); + public Task> GetDiagnosticsForIdsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, Func>? getDocuments, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) + => new IdeLatestDiagnosticGetter(this, solution, projectId, documentId, diagnosticIds, shouldIncludeAnalyzer, getDocuments, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics).GetDiagnosticsAsync(cancellationToken); public Task> GetProjectDiagnosticsForIdsAsync(Solution solution, ProjectId? projectId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, bool includeSuppressedDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) - => new IdeLatestDiagnosticGetter(this, solution, projectId, documentId: null, diagnosticIds, shouldIncludeAnalyzer, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics: false, includeNonLocalDocumentDiagnostics).GetProjectDiagnosticsAsync(cancellationToken); + => new IdeLatestDiagnosticGetter(this, solution, projectId, documentId: null, diagnosticIds, shouldIncludeAnalyzer, getDocuments: null, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics: false, includeNonLocalDocumentDiagnostics).GetProjectDiagnosticsAsync(cancellationToken); private abstract class DiagnosticGetter { @@ -38,6 +35,8 @@ private abstract class DiagnosticGetter protected readonly bool IncludeLocalDocumentDiagnostics; protected readonly bool IncludeNonLocalDocumentDiagnostics; + private readonly Func> _getDocuments; + private ImmutableArray.Builder? _lazyDataBuilder; public DiagnosticGetter( @@ -45,6 +44,7 @@ public DiagnosticGetter( Solution solution, ProjectId? projectId, DocumentId? documentId, + Func>? getDocuments, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics) @@ -53,6 +53,7 @@ public DiagnosticGetter( Solution = solution; DocumentId = documentId; + _getDocuments = getDocuments ?? (static (project, documentId) => documentId != null ? [documentId] : project.DocumentIds); ProjectId = projectId ?? documentId?.ProjectId; IncludeSuppressedDiagnostics = includeSuppressedDiagnostics; @@ -79,7 +80,7 @@ public async Task> GetDiagnosticsAsync(Cancellati return GetDiagnosticData(); } - var documentIds = DocumentId != null ? [DocumentId] : project.DocumentIds; + var documentIds = _getDocuments(project, DocumentId); // return diagnostics specific to one project or document var includeProjectNonLocalResult = DocumentId == null; @@ -132,7 +133,7 @@ private bool ShouldIncludeSuppressedDiagnostic(DiagnosticData diagnostic) private sealed class IdeCachedDiagnosticGetter : DiagnosticGetter { public IdeCachedDiagnosticGetter(DiagnosticIncrementalAnalyzer owner, Solution solution, ProjectId? projectId, DocumentId? documentId, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics) - : base(owner, solution, projectId, documentId, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics) + : base(owner, solution, projectId, documentId, getDocuments: null, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics) { } @@ -232,8 +233,9 @@ private sealed class IdeLatestDiagnosticGetter : DiagnosticGetter public IdeLatestDiagnosticGetter( DiagnosticIncrementalAnalyzer owner, Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, - bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics) - : base(owner, solution, projectId, documentId, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics) + Func>? getDocuments, + bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics) + : base(owner, solution, projectId, documentId, getDocuments, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics) { _diagnosticIds = diagnosticIds; _shouldIncludeAnalyzer = shouldIncludeAnalyzer; @@ -269,7 +271,7 @@ protected override async Task AppendDiagnosticsAsync(Project project, IEnumerabl // unlike the suppressed (disabled) analyzer, we will include hidden diagnostic only analyzers here. var compilation = await CreateCompilationWithAnalyzersAsync(project, ideOptions, stateSets, IncludeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - var result = await Owner.GetProjectAnalysisDataAsync(compilation, project, ideOptions, stateSets, forceAnalyzerRun: true, cancellationToken).ConfigureAwait(false); + var result = await Owner.GetProjectAnalysisDataAsync(compilation, project, ideOptions, stateSets, cancellationToken).ConfigureAwait(false); foreach (var stateSet in stateSets) { diff --git a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs index af943468881a4..e7054c9de2c94 100644 --- a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs +++ b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs @@ -37,7 +37,7 @@ public async Task> ForceAnalyzeProjectAsync(Proje compilationWithAnalyzers = await DocumentAnalysisExecutor.CreateCompilationWithAnalyzersAsync(project, ideOptions, activeAnalyzers, includeSuppressedDiagnostics: true, cancellationToken).ConfigureAwait(false); - var result = await GetProjectAnalysisDataAsync(compilationWithAnalyzers, project, ideOptions, stateSets, forceAnalyzerRun: true, cancellationToken).ConfigureAwait(false); + var result = await GetProjectAnalysisDataAsync(compilationWithAnalyzers, project, ideOptions, stateSets, cancellationToken).ConfigureAwait(false); using var _ = ArrayBuilder.GetInstance(out var diagnostics); diff --git a/src/Features/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_OpenDocument.cs b/src/Features/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_OpenDocument.cs index 4f6b49fea36b6..deb7877c1b234 100644 --- a/src/Features/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_OpenDocument.cs +++ b/src/Features/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_OpenDocument.cs @@ -21,7 +21,7 @@ private sealed class OpenDocumentSource(Document document) : AbstractDocumentDia public override bool IsLiveSource() => true; - public override async Task> GetDiagnosticsAsync(IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) + public override async Task> GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken) { var designTimeDocument = Document; var designTimeSolution = designTimeDocument.Project.Solution; diff --git a/src/Features/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_Workspace.cs b/src/Features/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_Workspace.cs index d74ca12db6421..b84fee77d2030 100644 --- a/src/Features/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_Workspace.cs +++ b/src/Features/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_Workspace.cs @@ -22,7 +22,7 @@ private sealed class ProjectSource(Project project, ImmutableArray true; - public override Task> GetDiagnosticsAsync(IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) + public override Task> GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken) => Task.FromResult(diagnostics); } @@ -31,7 +31,7 @@ private sealed class ClosedDocumentSource(TextDocument document, ImmutableArray< public override bool IsLiveSource() => true; - public override Task> GetDiagnosticsAsync(IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) + public override Task> GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken) => Task.FromResult(diagnostics); } diff --git a/src/Features/LanguageServer/Protocol/Features/Options/SolutionCrawlerOptionsStorage.cs b/src/Features/LanguageServer/Protocol/Features/Options/SolutionCrawlerOptionsStorage.cs index 8aacbe48a42fe..cc7fc24bc3875 100644 --- a/src/Features/LanguageServer/Protocol/Features/Options/SolutionCrawlerOptionsStorage.cs +++ b/src/Features/LanguageServer/Protocol/Features/Options/SolutionCrawlerOptionsStorage.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Options; diff --git a/src/Features/LanguageServer/Protocol/Features/UnifiedSuggestions/UnifiedSuggestedActionsSource.cs b/src/Features/LanguageServer/Protocol/Features/UnifiedSuggestions/UnifiedSuggestedActionsSource.cs index a5c972d5a0a15..cf1b8f407a589 100644 --- a/src/Features/LanguageServer/Protocol/Features/UnifiedSuggestions/UnifiedSuggestedActionsSource.cs +++ b/src/Features/LanguageServer/Protocol/Features/UnifiedSuggestions/UnifiedSuggestedActionsSource.cs @@ -401,7 +401,7 @@ private static void AddUnifiedSuggestedActionsSet( sets.Add(new UnifiedSuggestedActionSet( originalSolution, category, - group.ToImmutableArray(), + [.. group], title: null, priority, applicableToSpan: groupKey.Item1.DataLocation.UnmappedFileSpan.GetClampedTextSpan(text))); @@ -711,9 +711,7 @@ private static ImmutableArray GetInitiallyOrderedActi private static ImmutableArray OrderActionSets( ImmutableArray actionSets, TextSpan? selectionOpt) { - return actionSets.OrderByDescending(s => s.Priority) - .ThenBy(s => s, new UnifiedSuggestedActionSetComparer(selectionOpt)) - .ToImmutableArray(); + return [.. actionSets.OrderByDescending(s => s.Priority).ThenBy(s => s, new UnifiedSuggestedActionSetComparer(selectionOpt))]; } private static UnifiedSuggestedActionSet WithPriority( diff --git a/src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionHelpers.cs b/src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionHelpers.cs index b8ac33f20ddb6..44136ac1f6479 100644 --- a/src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionHelpers.cs +++ b/src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionHelpers.cs @@ -124,12 +124,12 @@ private static LSP.CodeAction[] GenerateCodeActions( { CommandIdentifier = CodeActionsHandler.RunNestedCodeActionCommandName, Title = title, - Arguments = [new CodeActionResolveData(title, codeAction.CustomTags, request.Range, request.TextDocument, codeActionPathList.ToArray(), fixAllFlavors: null, nestedCodeActions: nestedCodeActions)] + Arguments = [new CodeActionResolveData(title, codeAction.CustomTags, request.Range, request.TextDocument, [.. codeActionPathList], fixAllFlavors: null, nestedCodeActions: nestedCodeActions)] }; } AddLSPCodeActions(builder, codeAction, request, codeActionKind, diagnosticsForFix, nestedCodeActionCommand, - nestedCodeActions, codeActionPathList.ToArray(), suggestedAction); + nestedCodeActions, [.. codeActionPathList], suggestedAction); return builder.ToArray(); } @@ -161,7 +161,7 @@ private static LSP.CodeAction[] GenerateCodeActions( if (!isTopLevelCodeAction) { AddLSPCodeActions(nestedCodeActions, codeAction, request, codeActionKind, diagnosticsForFix, - nestedCodeActionCommand: null, nestedCodeActions: null, pathOfParentAction.ToArray(), suggestedAction); + nestedCodeActionCommand: null, nestedCodeActions: null, [.. pathOfParentAction], suggestedAction); } } @@ -240,7 +240,7 @@ private static VSInternalCodeAction GenerateVSCodeAction( Priority = UnifiedSuggestedActionSetPriorityToPriorityLevel(setPriority), Group = $"Roslyn{currentSetNumber}", ApplicableRange = applicableRange, - Data = new CodeActionResolveData(codeAction.Title, codeAction.CustomTags, request.Range, request.TextDocument, fixAllFlavors: null, nestedCodeActions: null, codeActionPath: codeActionPathList.ToArray()) + Data = new CodeActionResolveData(codeAction.Title, codeAction.CustomTags, request.Range, request.TextDocument, fixAllFlavors: null, nestedCodeActions: null, codeActionPath: [.. codeActionPathList]) }; static VSInternalCodeAction[] GenerateNestedVSCodeActions( diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractDocumentPullDiagnosticHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractDocumentPullDiagnosticHandler.cs index dc20144b584f1..106d2dc6bfc78 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractDocumentPullDiagnosticHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractDocumentPullDiagnosticHandler.cs @@ -2,17 +2,21 @@ // 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.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; using Microsoft.CommonLanguageServerProtocol.Framework; using Roslyn.LanguageServer.Protocol; -using LSP = Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; internal abstract class AbstractDocumentPullDiagnosticHandler( IDiagnosticAnalyzerService diagnosticAnalyzerService, IDiagnosticsRefresher diagnosticRefresher, + IDiagnosticSourceManager diagnosticSourceManager, IGlobalOptionService globalOptions) : AbstractPullDiagnosticHandler( diagnosticAnalyzerService, @@ -20,5 +24,31 @@ internal abstract class AbstractDocumentPullDiagnosticHandler where TDiagnosticsParams : IPartialResultParams { - public abstract LSP.TextDocumentIdentifier? GetTextDocumentIdentifier(TDiagnosticsParams diagnosticsParams); + protected readonly IDiagnosticSourceManager DiagnosticSourceManager = diagnosticSourceManager; + + public abstract TextDocumentIdentifier? GetTextDocumentIdentifier(TDiagnosticsParams diagnosticsParams); + + protected override ValueTask> GetOrderedDiagnosticSourcesAsync(TDiagnosticsParams diagnosticsParams, string? requestDiagnosticCategory, RequestContext context, CancellationToken cancellationToken) + { + // Note: context.Document may be null in the case where the client is asking about a document that we have + // since removed from the workspace. In this case, we don't really have anything to process. + // GetPreviousResults will be used to properly realize this and notify the client that the doc is gone. + // + // Only consider open documents here (and only closed ones in the WorkspacePullDiagnosticHandler). Each + // handler treats those as separate worlds that they are responsible for. + var textDocument = context.TextDocument; + if (textDocument is null) + { + context.TraceInformation("Ignoring diagnostics request because no text document was provided"); + return new([]); + } + + if (!context.IsTracking(textDocument.GetURI())) + { + context.TraceWarning($"Ignoring diagnostics request for untracked document: {textDocument.GetURI()}"); + return new([]); + } + + return DiagnosticSourceManager.CreateDocumentDiagnosticSourcesAsync(context, requestDiagnosticCategory, cancellationToken); + } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractPullDiagnosticHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractPullDiagnosticHandler.cs index 755e5d771299d..94bb3f537e1c9 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractPullDiagnosticHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractPullDiagnosticHandler.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; @@ -38,8 +37,6 @@ internal abstract partial class AbstractPullDiagnosticHandler protected const int WorkspaceDiagnosticIdentifier = 1; protected const int DocumentDiagnosticIdentifier = 2; - // internal for testing purposes - internal const int DocumentNonLocalDiagnosticIdentifier = 3; private readonly IDiagnosticsRefresher _diagnosticRefresher; protected readonly IGlobalOptionService GlobalOptions; @@ -68,8 +65,6 @@ protected AbstractPullDiagnosticHandler( GlobalOptions = globalOptions; } - protected virtual string? GetDiagnosticSourceIdentifier(TDiagnosticsParams diagnosticsParams) => null; - /// /// Retrieve the previous results we reported. Used so we can avoid resending data for unchanged files. Also /// used so we can report which documents were removed and can have all their diagnostics cleared. @@ -80,7 +75,7 @@ protected AbstractPullDiagnosticHandler( /// Returns all the documents that should be processed in the desired order to process them in. /// protected abstract ValueTask> GetOrderedDiagnosticSourcesAsync( - TDiagnosticsParams diagnosticsParams, RequestContext context, CancellationToken cancellationToken); + TDiagnosticsParams diagnosticsParams, string? requestDiagnosticCategory, RequestContext context, CancellationToken cancellationToken); /// /// Creates the appropriate LSP type to report a new set of diagnostics and resultId. @@ -106,7 +101,7 @@ protected abstract ValueTask> GetOrderedDiagno /// protected abstract DiagnosticTag[] ConvertTags(DiagnosticData diagnosticData, bool isLiveSource); - protected abstract string? GetDiagnosticCategory(TDiagnosticsParams diagnosticsParams); + protected abstract string? GetRequestDiagnosticCategory(TDiagnosticsParams diagnosticsParams); /// /// Used by public workspace pull diagnostics to allow it to keep the connection open until @@ -134,9 +129,8 @@ protected virtual Task WaitForChangesAsync(RequestContext context, CancellationT Contract.ThrowIfNull(context.Solution); var clientCapabilities = context.GetRequiredClientCapabilities(); - var category = GetDiagnosticCategory(diagnosticsParams) ?? ""; - var sourceIdentifier = GetDiagnosticSourceIdentifier(diagnosticsParams) ?? ""; - var handlerName = $"{this.GetType().Name}(category: {category}, source: {sourceIdentifier})"; + var category = GetRequestDiagnosticCategory(diagnosticsParams); + var handlerName = $"{this.GetType().Name}(category: {category})"; context.TraceInformation($"{handlerName} started getting diagnostics"); var versionedCache = _categoryToVersionedCache.GetOrAdd(handlerName, static handlerName => new(handlerName)); @@ -160,7 +154,7 @@ protected virtual Task WaitForChangesAsync(RequestContext context, CancellationT // Next process each file in priority order. Determine if diagnostics are changed or unchanged since the // last time we notified the client. Report back either to the client so they can update accordingly. var orderedSources = await GetOrderedDiagnosticSourcesAsync( - diagnosticsParams, context, cancellationToken).ConfigureAwait(false); + diagnosticsParams, category, context, cancellationToken).ConfigureAwait(false); context.TraceInformation($"Processing {orderedSources.Length} documents"); @@ -295,7 +289,7 @@ private async Task ComputeAndReportCurrentDiagnosticsAsync( CancellationToken cancellationToken) { using var _ = ArrayBuilder.GetInstance(out var result); - var diagnostics = await diagnosticSource.GetDiagnosticsAsync(DiagnosticAnalyzerService, context, cancellationToken).ConfigureAwait(false); + var diagnostics = await diagnosticSource.GetDiagnosticsAsync(context, cancellationToken).ConfigureAwait(false); // If we can't get a text document identifier we can't report diagnostics for this source. // This can happen for 'fake' projects (e.g. used for TS script blocks). diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractWorkspacePullDiagnosticsHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractWorkspacePullDiagnosticsHandler.cs index f3911586aad23..7e3d6e4619791 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractWorkspacePullDiagnosticsHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractWorkspacePullDiagnosticsHandler.cs @@ -3,21 +3,13 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.EditAndContinue; -using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Shared.Collections; -using Microsoft.CodeAnalysis.SolutionCrawler; -using Microsoft.CodeAnalysis.TaskList; using Roslyn.LanguageServer.Protocol; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; @@ -27,6 +19,7 @@ internal abstract class AbstractWorkspacePullDiagnosticsHandler /// Flag that represents whether the LSP view of the world has changed. @@ -40,9 +33,11 @@ protected AbstractWorkspacePullDiagnosticsHandler( LspWorkspaceManager workspaceManager, LspWorkspaceRegistrationService registrationService, IDiagnosticAnalyzerService diagnosticAnalyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticRefresher, IGlobalOptionService globalOptions) : base(diagnosticAnalyzerService, diagnosticRefresher, globalOptions) { + DiagnosticSourceManager = diagnosticSourceManager; _workspaceManager = workspaceManager; _workspaceRegistrationService = registrationService; @@ -56,32 +51,17 @@ public void Dispose() _workspaceRegistrationService.LspSolutionChanged -= OnLspSolutionChanged; } - protected override async ValueTask> GetOrderedDiagnosticSourcesAsync(TDiagnosticsParams diagnosticsParams, RequestContext context, CancellationToken cancellationToken) + protected override ValueTask> GetOrderedDiagnosticSourcesAsync(TDiagnosticsParams diagnosticsParams, string? requestDiagnosticCategory, RequestContext context, CancellationToken cancellationToken) { - // If we're being called from razor, we do not support WorkspaceDiagnostics at all. For razor, workspace - // diagnostics will be handled by razor itself, which will operate by calling into Roslyn and asking for - // document-diagnostics instead. if (context.ServerKind == WellKnownLspServerKinds.RazorLspServer) - return []; - - Contract.ThrowIfNull(context.Solution); - - var category = GetDiagnosticCategory(diagnosticsParams); - - // TODO: Implement as extensibility point. https://github.com/dotnet/roslyn/issues/72896 - - if (category == PullDiagnosticCategories.Task) - return GetTaskListDiagnosticSources(context, GlobalOptions); - - if (category == PullDiagnosticCategories.EditAndContinue) - return await EditAndContinueDiagnosticSource.CreateWorkspaceDiagnosticSourcesAsync(context.Solution, document => context.IsTracking(document.GetURI()), cancellationToken).ConfigureAwait(false); - - // if this request doesn't have a category at all (legacy behavior, assume they're asking about everything). - if (category == null || category == PullDiagnosticCategories.WorkspaceDocumentsAndProject) - return await GetDiagnosticSourcesAsync(context, GlobalOptions, cancellationToken).ConfigureAwait(false); + { + // If we're being called from razor, we do not support WorkspaceDiagnostics at all. For razor, workspace + // diagnostics will be handled by razor itself, which will operate by calling into Roslyn and asking for + // document-diagnostics instead. + return new([]); + } - // if it's a category we don't recognize, return nothing. - return []; + return DiagnosticSourceManager.CreateWorkspaceDiagnosticSourcesAsync(context, requestDiagnosticCategory, cancellationToken); } private void OnLspSolutionChanged(object? sender, WorkspaceChangeEventArgs e) @@ -115,159 +95,6 @@ protected override async Task WaitForChangesAsync(RequestContext context, Cancel return; } - private static ImmutableArray GetTaskListDiagnosticSources( - RequestContext context, IGlobalOptionService globalOptions) - { - Contract.ThrowIfNull(context.Solution); - - // Only compute task list items for closed files if the option is on for it. - var taskListEnabled = globalOptions.GetTaskListOptions().ComputeForClosedFiles; - if (!taskListEnabled) - return []; - - using var _ = ArrayBuilder.GetInstance(out var result); - - foreach (var project in GetProjectsInPriorityOrder(context.Solution, context.SupportedLanguages)) - { - foreach (var document in project.Documents) - { - if (!ShouldSkipDocument(context, document)) - result.Add(new TaskListDiagnosticSource(document, globalOptions)); - } - } - - return result.ToImmutableAndClear(); - } - - /// - /// There are three potential sources for reporting workspace diagnostics: - /// - /// 1. Full solution analysis: If the user has enabled Full solution analysis, we always run analysis on the latest - /// project snapshot and return up-to-date diagnostics computed from this analysis. - /// - /// 2. Code analysis service: Otherwise, if full solution analysis is disabled, and if we have diagnostics from an explicitly - /// triggered code analysis execution on either the current or a prior project snapshot, we return - /// diagnostics from this execution. These diagnostics may be stale with respect to the current - /// project snapshot, but they match user's intent of not enabling continuous background analysis - /// for always having up-to-date workspace diagnostics, but instead computing them explicitly on - /// specific project snapshots by manually running the "Run Code Analysis" command on a project or solution. - /// - /// 3. EnC analysis: Emit and debugger diagnostics associated with a closed document or not associated with any document. - /// - /// If full solution analysis is disabled AND code analysis was never executed for the given project, - /// we have no workspace diagnostics to report and bail out. - /// - public static async ValueTask> GetDiagnosticSourcesAsync( - RequestContext context, IGlobalOptionService globalOptions, CancellationToken cancellationToken) - { - Contract.ThrowIfNull(context.Solution); - - using var _ = ArrayBuilder.GetInstance(out var result); - - var solution = context.Solution; - var enableDiagnosticsInSourceGeneratedFiles = solution.Services.GetService()?.EnableDiagnosticsInSourceGeneratedFiles == true; - var codeAnalysisService = solution.Services.GetRequiredService(); - - foreach (var project in GetProjectsInPriorityOrder(solution, context.SupportedLanguages)) - await AddDocumentsAndProjectAsync(project, cancellationToken).ConfigureAwait(false); - - return result.ToImmutableAndClear(); - - async Task AddDocumentsAndProjectAsync(Project project, CancellationToken cancellationToken) - { - var fullSolutionAnalysisEnabled = globalOptions.IsFullSolutionAnalysisEnabled(project.Language, out var compilerFullSolutionAnalysisEnabled, out var analyzersFullSolutionAnalysisEnabled); - if (!fullSolutionAnalysisEnabled && !codeAnalysisService.HasProjectBeenAnalyzed(project.Id)) - return; - - Func? shouldIncludeAnalyzer = !compilerFullSolutionAnalysisEnabled || !analyzersFullSolutionAnalysisEnabled - ? ShouldIncludeAnalyzer : null; - - AddDocumentSources(project.Documents); - AddDocumentSources(project.AdditionalDocuments); - - // If all features are enabled for source generated documents, then compute todo-comments/diagnostics for them. - if (enableDiagnosticsInSourceGeneratedFiles) - { - var sourceGeneratedDocuments = await project.GetSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false); - AddDocumentSources(sourceGeneratedDocuments); - } - - // Finally, add the appropriate FSA or CodeAnalysis project source to get project specific diagnostics, not associated with any document. - AddProjectSource(); - - return; - - void AddDocumentSources(IEnumerable documents) - { - foreach (var document in documents) - { - if (!ShouldSkipDocument(context, document)) - { - // Add the appropriate FSA or CodeAnalysis document source to get document diagnostics. - var documentDiagnosticSource = fullSolutionAnalysisEnabled - ? AbstractWorkspaceDocumentDiagnosticSource.CreateForFullSolutionAnalysisDiagnostics(document, shouldIncludeAnalyzer) - : AbstractWorkspaceDocumentDiagnosticSource.CreateForCodeAnalysisDiagnostics(document, codeAnalysisService); - result.Add(documentDiagnosticSource); - } - } - } - - void AddProjectSource() - { - var projectDiagnosticSource = fullSolutionAnalysisEnabled - ? AbstractProjectDiagnosticSource.CreateForFullSolutionAnalysisDiagnostics(project, shouldIncludeAnalyzer) - : AbstractProjectDiagnosticSource.CreateForCodeAnalysisDiagnostics(project, codeAnalysisService); - result.Add(projectDiagnosticSource); - } - - bool ShouldIncludeAnalyzer(DiagnosticAnalyzer analyzer) - => analyzer.IsCompilerAnalyzer() ? compilerFullSolutionAnalysisEnabled : analyzersFullSolutionAnalysisEnabled; - } - } - - private static IEnumerable GetProjectsInPriorityOrder( - Solution solution, ImmutableArray supportedLanguages) - { - return GetProjectsInPriorityOrderWorker(solution) - .WhereNotNull() - .Distinct() - .Where(p => supportedLanguages.Contains(p.Language)); - - static IEnumerable GetProjectsInPriorityOrderWorker(Solution solution) - { - var documentTrackingService = solution.Services.GetRequiredService(); - - // Collect all the documents from the solution in the order we'd like to get diagnostics for. This will - // prioritize the files from currently active projects, but then also include all other docs in all projects - // (depending on current FSA settings). - - var activeDocument = documentTrackingService.GetActiveDocument(solution); - var visibleDocuments = documentTrackingService.GetVisibleDocuments(solution); - - yield return activeDocument?.Project; - foreach (var doc in visibleDocuments) - yield return doc.Project; - - foreach (var project in solution.Projects) - yield return project; - } - } - - private static bool ShouldSkipDocument(RequestContext context, TextDocument document) - { - // Only consider closed documents here (and only open ones in the DocumentPullDiagnosticHandler). - // Each handler treats those as separate worlds that they are responsible for. - if (context.IsTracking(document.GetURI())) - { - context.TraceInformation($"Skipping tracked document: {document.GetURI()}"); - return true; - } - - // Do not attempt to get workspace diagnostics for Razor files, Razor will directly ask us for document diagnostics - // for any razor file they are interested in. - return document.IsRazorDocument(); - } - internal abstract TestAccessor GetTestAccessor(); internal readonly struct TestAccessor(AbstractWorkspacePullDiagnosticsHandler handler) diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/DiagnosticSourceManager.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/DiagnosticSourceManager.cs new file mode 100644 index 0000000000000..2af9fa8710706 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/DiagnosticSourceManager.cs @@ -0,0 +1,164 @@ +// 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.Composition; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; +using Microsoft.CodeAnalysis.PooledObjects; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; + +[Export(typeof(IDiagnosticSourceManager)), Shared] +internal sealed class DiagnosticSourceManager : IDiagnosticSourceManager +{ + /// + /// Document level providers ordered by name. + /// + private readonly ImmutableDictionary _nameToDocumentProviderMap; + + /// + /// Workspace level providers ordered by name. + /// + private readonly ImmutableDictionary _nameToWorkspaceProviderMap; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public DiagnosticSourceManager([ImportMany] IEnumerable sourceProviders) + { + _nameToDocumentProviderMap = sourceProviders + .Where(p => p.IsDocument) + .ToImmutableDictionary(kvp => kvp.Name, kvp => kvp); + + _nameToWorkspaceProviderMap = sourceProviders + .Where(p => !p.IsDocument) + .ToImmutableDictionary(kvp => kvp.Name, kvp => kvp); + } + + public ImmutableArray GetDocumentSourceProviderNames(ClientCapabilities clientCapabilities) + => _nameToDocumentProviderMap.Where(kvp => kvp.Value.IsEnabled(clientCapabilities)).SelectAsArray(kvp => kvp.Key); + + public ImmutableArray GetWorkspaceSourceProviderNames(ClientCapabilities clientCapabilities) + => _nameToWorkspaceProviderMap.Where(kvp => kvp.Value.IsEnabled(clientCapabilities)).SelectAsArray(kvp => kvp.Key); + + public ValueTask> CreateDocumentDiagnosticSourcesAsync(RequestContext context, string? providerName, CancellationToken cancellationToken) + => CreateDiagnosticSourcesAsync(context, providerName, _nameToDocumentProviderMap, isDocument: true, cancellationToken); + + public ValueTask> CreateWorkspaceDiagnosticSourcesAsync(RequestContext context, string? providerName, CancellationToken cancellationToken) + => CreateDiagnosticSourcesAsync(context, providerName, _nameToWorkspaceProviderMap, isDocument: false, cancellationToken); + + private static async ValueTask> CreateDiagnosticSourcesAsync( + RequestContext context, + string? providerName, + ImmutableDictionary nameToProviderMap, + bool isDocument, + CancellationToken cancellationToken) + { + if (providerName != null) + { + // VS does not distinguish between document and workspace sources. Thus it can request + // document diagnostics with workspace source name. We need to handle this case. + if (nameToProviderMap.TryGetValue(providerName, out var provider)) + { + Contract.ThrowIfFalse(provider.IsEnabled(context.GetRequiredClientCapabilities())); + return await provider.CreateDiagnosticSourcesAsync(context, cancellationToken).ConfigureAwait(false); + } + + return []; + } + else + { + // VS Code (and legacy VS ?) pass null sourceName when requesting all sources. + using var _ = ArrayBuilder.GetInstance(out var sourcesBuilder); + foreach (var (name, provider) in nameToProviderMap) + { + if (!provider.IsEnabled(context.GetRequiredClientCapabilities())) + { + continue; + } + + var namedSources = await provider.CreateDiagnosticSourcesAsync(context, cancellationToken).ConfigureAwait(false); + sourcesBuilder.AddRange(namedSources); + } + + var sources = sourcesBuilder.ToImmutableAndClear(); + return AggregateSourcesIfNeeded(sources, isDocument); + } + } + + public static ImmutableArray AggregateSourcesIfNeeded(ImmutableArray sources, bool isDocument) + { + if (sources.Length <= 1) + { + return sources; + } + + 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 + { + // We ASSUME that all sources with the same ProjectOrDocumentID and IsLiveSource + // 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)) + .ToImmutableArray(); + } + + return sources; + } + + /// + /// Aggregates multiple s into a single source. + /// + /// Sources to aggregate + /// + /// Aggregation is usually needed for clients like VS Code which supports single source per request. + /// + private sealed class AggregatedDocumentDiagnosticSource(ImmutableArray sources) : IDiagnosticSource + { + public static ImmutableArray AggregateIfNeeded(IEnumerable sources) + { + var result = sources.ToImmutableArray(); + if (result.Length > 1) + { + result = [new AggregatedDocumentDiagnosticSource(result)]; + } + + return result; + } + + public bool IsLiveSource() => true; + public Project GetProject() => sources[0].GetProject(); + public ProjectOrDocumentId GetId() => sources[0].GetId(); + public TextDocumentIdentifier? GetDocumentIdentifier() => sources[0].GetDocumentIdentifier(); + public string ToDisplayString() => $"{this.GetType().Name}: count={sources.Length}"; + + public async Task> GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var diagnostics); + foreach (var source in sources) + { + var namedDiagnostics = await source.GetDiagnosticsAsync(context, cancellationToken).ConfigureAwait(false); + diagnostics.AddRange(namedDiagnostics); + } + + return diagnostics.ToImmutableAndClear(); + } + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/DocumentSyntaxAndSemanticDiagnosticSourceProvider.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/DocumentSyntaxAndSemanticDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..85fca7423bdd3 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/DocumentSyntaxAndSemanticDiagnosticSourceProvider.cs @@ -0,0 +1,70 @@ +// 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.Diagnostics; +using Microsoft.CodeAnalysis.Host.Mef; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; + +internal abstract class AbstractDocumentSyntaxAndSemanticDiagnosticSourceProvider( + IDiagnosticAnalyzerService diagnosticAnalyzerService, DiagnosticKind kind, string sourceName) + : IDiagnosticSourceProvider +{ + public bool IsDocument => true; + public string Name => sourceName; + + public bool IsEnabled(ClientCapabilities clientCapabilities) => true; + + public ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + if (context.GetTrackedDocument() is { } document) + { + return new([new DocumentDiagnosticSource(diagnosticAnalyzerService, kind, document)]); + } + + return new([]); + } + + [Export(typeof(IDiagnosticSourceProvider)), Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + private sealed class DocumentCompilerSyntaxDiagnosticSourceProvider([Import] IDiagnosticAnalyzerService diagnosticAnalyzerService) + : AbstractDocumentSyntaxAndSemanticDiagnosticSourceProvider(diagnosticAnalyzerService, + DiagnosticKind.CompilerSyntax, PullDiagnosticCategories.DocumentCompilerSyntax) + { + } + + [Export(typeof(IDiagnosticSourceProvider)), Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + private sealed class DocumentCompilerSemanticDiagnosticSourceProvider([Import] IDiagnosticAnalyzerService diagnosticAnalyzerService) + : AbstractDocumentSyntaxAndSemanticDiagnosticSourceProvider( + diagnosticAnalyzerService, DiagnosticKind.CompilerSemantic, PullDiagnosticCategories.DocumentCompilerSemantic) + { + } + + [Export(typeof(IDiagnosticSourceProvider)), Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + private sealed class DocumentAnalyzerSyntaxDiagnosticSourceProvider([Import] IDiagnosticAnalyzerService diagnosticAnalyzerService) + : AbstractDocumentSyntaxAndSemanticDiagnosticSourceProvider(diagnosticAnalyzerService, + DiagnosticKind.AnalyzerSyntax, PullDiagnosticCategories.DocumentAnalyzerSyntax) + { + } + + [Export(typeof(IDiagnosticSourceProvider)), Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + private sealed class DocumentAnalyzerSemanticDiagnosticSourceProvider([Import] IDiagnosticAnalyzerService diagnosticAnalyzerService) + : AbstractDocumentSyntaxAndSemanticDiagnosticSourceProvider(diagnosticAnalyzerService, + DiagnosticKind.AnalyzerSemantic, PullDiagnosticCategories.DocumentAnalyzerSemantic) + { + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/IDiagnosticSourceManager.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/IDiagnosticSourceManager.cs new file mode 100644 index 0000000000000..25fc19acaca03 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/IDiagnosticSourceManager.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; + +/// +/// Provides centralized/singleton management of MEF based s. +/// Consumers - like diagnostic handlers - use it to get diagnostics from one or more providers. +/// +internal interface IDiagnosticSourceManager +{ + /// + /// Returns the names of document level s. + /// + ImmutableArray GetDocumentSourceProviderNames(ClientCapabilities clientCapabilities); + + /// + /// Returns the names of workspace level s. + /// + ImmutableArray GetWorkspaceSourceProviderNames(ClientCapabilities clientCapabilities); + + /// + /// Creates document diagnostic sources for the given . + /// + /// + /// Optional provider name. If then diagnostics from all providers are used. + /// A cancellation token that can be used to cancel the request processing. + ValueTask> CreateDocumentDiagnosticSourcesAsync(RequestContext context, string? providerName, CancellationToken cancellationToken); + + /// + /// Creates workspace diagnostic sources for the given . + /// + /// + /// Optional provider name. If not specified then diagnostics from all providers are used. + /// A cancellation token that can be used to cancel the request processing. + ValueTask> CreateWorkspaceDiagnosticSourcesAsync(RequestContext context, string? providerName, CancellationToken cancellationToken); +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/IDiagnosticSourceProvider.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/IDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..773f3a309c94e --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/IDiagnosticSourceProvider.cs @@ -0,0 +1,36 @@ +// 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 Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; + +/// +/// Provides diagnostic sources. +/// +internal interface IDiagnosticSourceProvider +{ + /// + /// if this provider is for documents, if it is for workspace. + /// + bool IsDocument { get; } + + /// + /// Provider's name. Each should have a unique name within scope. + /// + string Name { get; } + + bool IsEnabled(ClientCapabilities clientCapabilities); + + /// + /// Creates the diagnostic sources. + /// + /// + /// A cancellation token that can be used to cancel the request processing. + ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken); +} + diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/WorkspaceDiagnosticSourceHelpers.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/WorkspaceDiagnosticSourceHelpers.cs new file mode 100644 index 0000000000000..8956840dbbfa2 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/WorkspaceDiagnosticSourceHelpers.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis.Host; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; + +internal static class WorkspaceDiagnosticSourceHelpers +{ + public static IEnumerable GetProjectsInPriorityOrder(Solution solution, ImmutableArray supportedLanguages) + { + return GetProjectsInPriorityOrderWorker(solution) + .WhereNotNull() + .Distinct() + .Where(p => supportedLanguages.Contains(p.Language)); + + static IEnumerable GetProjectsInPriorityOrderWorker(Solution solution) + { + var documentTrackingService = solution.Services.GetRequiredService(); + + // Collect all the documents from the solution in the order we'd like to get diagnostics for. This will + // prioritize the files from currently active projects, but then also include all other docs in all projects + // (depending on current FSA settings). + + var activeDocument = documentTrackingService.GetActiveDocument(solution); + var visibleDocuments = documentTrackingService.GetVisibleDocuments(solution); + + yield return activeDocument?.Project; + foreach (var doc in visibleDocuments) + yield return doc.Project; + + foreach (var project in solution.Projects) + yield return project; + } + } + + public static bool ShouldSkipDocument(RequestContext context, TextDocument document) + { + // Only consider closed documents here (and only open ones in the DocumentPullDiagnosticHandler). + // Each handler treats those as separate worlds that they are responsible for. + if (context.IsTracking(document.GetURI())) + { + context.TraceInformation($"Skipping tracked document: {document.GetURI()}"); + return true; + } + + // Do not attempt to get workspace diagnostics for Razor files, Razor will directly ask us for document diagnostics + // for any razor file they are interested in. + return document.IsRazorDocument(); + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/WorkspaceDocumentsAndProjectDiagnosticSourceProvider.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/WorkspaceDocumentsAndProjectDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..58cca636c91f5 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/WorkspaceDocumentsAndProjectDiagnosticSourceProvider.cs @@ -0,0 +1,118 @@ +// 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.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.SolutionCrawler; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; + +[Export(typeof(IDiagnosticSourceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class WorkspaceDocumentsAndProjectDiagnosticSourceProvider( + [Import] IDiagnosticAnalyzerService diagnosticAnalyzerService, + [Import] IGlobalOptionService globalOptions) + : IDiagnosticSourceProvider +{ + public bool IsDocument => false; + public string Name => PullDiagnosticCategories.WorkspaceDocumentsAndProject; + + public bool IsEnabled(ClientCapabilities clientCapabilities) => true; + + /// + /// There are three potential sources for reporting workspace diagnostics: + /// + /// 1. Full solution analysis: If the user has enabled Full solution analysis, we always run analysis on the latest + /// project snapshot and return up-to-date diagnostics computed from this analysis. + /// + /// 2. Code analysis service: Otherwise, if full solution analysis is disabled, and if we have diagnostics from an explicitly + /// triggered code analysis execution on either the current or a prior project snapshot, we return + /// diagnostics from this execution. These diagnostics may be stale with respect to the current + /// project snapshot, but they match user's intent of not enabling continuous background analysis + /// for always having up-to-date workspace diagnostics, but instead computing them explicitly on + /// specific project snapshots by manually running the "Run Code Analysis" command on a project or solution. + /// + /// 3. EnC analysis: Emit and debugger diagnostics associated with a closed document or not associated with any document. + /// + /// If full solution analysis is disabled AND code analysis was never executed for the given project, + /// we have no workspace diagnostics to report and bail out. + /// + public async ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + Contract.ThrowIfNull(context.Solution); + + using var _ = ArrayBuilder.GetInstance(out var result); + + var solution = context.Solution; + var enableDiagnosticsInSourceGeneratedFiles = solution.Services.GetService()?.EnableDiagnosticsInSourceGeneratedFiles == true; + var codeAnalysisService = solution.Services.GetRequiredService(); + + foreach (var project in WorkspaceDiagnosticSourceHelpers.GetProjectsInPriorityOrder(solution, context.SupportedLanguages)) + await AddDocumentsAndProjectAsync(project, diagnosticAnalyzerService, cancellationToken).ConfigureAwait(false); + + return result.ToImmutableAndClear(); + + async Task AddDocumentsAndProjectAsync(Project project, IDiagnosticAnalyzerService diagnosticAnalyzerService, CancellationToken cancellationToken) + { + var fullSolutionAnalysisEnabled = globalOptions.IsFullSolutionAnalysisEnabled(project.Language, out var compilerFullSolutionAnalysisEnabled, out var analyzersFullSolutionAnalysisEnabled); + if (!fullSolutionAnalysisEnabled && !codeAnalysisService.HasProjectBeenAnalyzed(project.Id)) + return; + + Func? shouldIncludeAnalyzer = !compilerFullSolutionAnalysisEnabled || !analyzersFullSolutionAnalysisEnabled + ? ShouldIncludeAnalyzer : null; + + AddDocumentSources(project.Documents); + AddDocumentSources(project.AdditionalDocuments); + + // If all features are enabled for source generated documents, then compute todo-comments/diagnostics for them. + if (enableDiagnosticsInSourceGeneratedFiles) + { + var sourceGeneratedDocuments = await project.GetSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false); + AddDocumentSources(sourceGeneratedDocuments); + } + + // Finally, add the appropriate FSA or CodeAnalysis project source to get project specific diagnostics, not associated with any document. + AddProjectSource(); + + return; + + void AddDocumentSources(IEnumerable documents) + { + foreach (var document in documents) + { + if (!WorkspaceDiagnosticSourceHelpers.ShouldSkipDocument(context, document)) + { + // Add the appropriate FSA or CodeAnalysis document source to get document diagnostics. + var documentDiagnosticSource = fullSolutionAnalysisEnabled + ? AbstractWorkspaceDocumentDiagnosticSource.CreateForFullSolutionAnalysisDiagnostics(document, diagnosticAnalyzerService, shouldIncludeAnalyzer) + : AbstractWorkspaceDocumentDiagnosticSource.CreateForCodeAnalysisDiagnostics(document, codeAnalysisService); + result.Add(documentDiagnosticSource); + } + } + } + + void AddProjectSource() + { + var projectDiagnosticSource = fullSolutionAnalysisEnabled + ? AbstractProjectDiagnosticSource.CreateForFullSolutionAnalysisDiagnostics(project, diagnosticAnalyzerService, shouldIncludeAnalyzer) + : AbstractProjectDiagnosticSource.CreateForCodeAnalysisDiagnostics(project, codeAnalysisService); + result.Add(projectDiagnosticSource); + } + + bool ShouldIncludeAnalyzer(DiagnosticAnalyzer analyzer) + => analyzer.IsCompilerAnalyzer() ? compilerFullSolutionAnalysisEnabled : analyzersFullSolutionAnalysisEnabled; + } + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractDocumentDiagnosticSource.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractDocumentDiagnosticSource.cs index 0de186f4e9ac1..f88810864a7bb 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractDocumentDiagnosticSource.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractDocumentDiagnosticSource.cs @@ -18,7 +18,7 @@ internal abstract class AbstractDocumentDiagnosticSource(TDocument do public abstract bool IsLiveSource(); public abstract Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken); + RequestContext context, CancellationToken cancellationToken); public ProjectOrDocumentId GetId() => new(Document.Id); public Project GetProject() => Document.Project; diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractProjectDiagnosticSource.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractProjectDiagnosticSource.cs index d34ef1d906826..ac49b0a9da7c6 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractProjectDiagnosticSource.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractProjectDiagnosticSource.cs @@ -16,14 +16,14 @@ internal abstract class AbstractProjectDiagnosticSource(Project project) { protected Project Project => project; - public static AbstractProjectDiagnosticSource CreateForFullSolutionAnalysisDiagnostics(Project project, Func? shouldIncludeAnalyzer) - => new FullSolutionAnalysisDiagnosticSource(project, shouldIncludeAnalyzer); + public static AbstractProjectDiagnosticSource CreateForFullSolutionAnalysisDiagnostics(Project project, IDiagnosticAnalyzerService diagnosticAnalyzerService, Func? shouldIncludeAnalyzer) + => new FullSolutionAnalysisDiagnosticSource(project, diagnosticAnalyzerService, shouldIncludeAnalyzer); public static AbstractProjectDiagnosticSource CreateForCodeAnalysisDiagnostics(Project project, ICodeAnalysisDiagnosticAnalyzerService codeAnalysisService) => new CodeAnalysisDiagnosticSource(project, codeAnalysisService); public abstract bool IsLiveSource(); - public abstract Task> GetDiagnosticsAsync(IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken); + public abstract Task> GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken); public ProjectOrDocumentId GetId() => new(Project.Id); public Project GetProject() => Project; @@ -33,7 +33,8 @@ public static AbstractProjectDiagnosticSource CreateForCodeAnalysisDiagnostics(P : null; public string ToDisplayString() => Project.Name; - private sealed class FullSolutionAnalysisDiagnosticSource(Project project, Func? shouldIncludeAnalyzer) : AbstractProjectDiagnosticSource(project) + private sealed class FullSolutionAnalysisDiagnosticSource(Project project, IDiagnosticAnalyzerService diagnosticAnalyzerService, Func? shouldIncludeAnalyzer) + : AbstractProjectDiagnosticSource(project) { /// /// This is a normal project source that represents live/fresh diagnostics that should supersede everything else. @@ -42,7 +43,6 @@ public override bool IsLiveSource() => true; public override async Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) { @@ -55,7 +55,8 @@ public override async Task> GetDiagnosticsAsync( } } - private sealed class CodeAnalysisDiagnosticSource(Project project, ICodeAnalysisDiagnosticAnalyzerService codeAnalysisService) : AbstractProjectDiagnosticSource(project) + 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 @@ -66,7 +67,6 @@ public override bool IsLiveSource() => false; public override Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) { diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractWorkspaceDocumentDiagnosticSource.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractWorkspaceDocumentDiagnosticSource.cs index 4cc153fcc9739..6777d956c2c88 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractWorkspaceDocumentDiagnosticSource.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractWorkspaceDocumentDiagnosticSource.cs @@ -3,24 +3,36 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections; +using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; internal abstract class AbstractWorkspaceDocumentDiagnosticSource(TextDocument document) : AbstractDocumentDiagnosticSource(document) { - public static AbstractWorkspaceDocumentDiagnosticSource CreateForFullSolutionAnalysisDiagnostics(TextDocument document, Func? shouldIncludeAnalyzer) - => new FullSolutionAnalysisDiagnosticSource(document, shouldIncludeAnalyzer); + public static AbstractWorkspaceDocumentDiagnosticSource CreateForFullSolutionAnalysisDiagnostics(TextDocument document, IDiagnosticAnalyzerService diagnosticAnalyzerService, Func? shouldIncludeAnalyzer) + => new FullSolutionAnalysisDiagnosticSource(document, diagnosticAnalyzerService, shouldIncludeAnalyzer); public static AbstractWorkspaceDocumentDiagnosticSource CreateForCodeAnalysisDiagnostics(TextDocument document, ICodeAnalysisDiagnosticAnalyzerService codeAnalysisService) => new CodeAnalysisDiagnosticSource(document, codeAnalysisService); - private sealed class FullSolutionAnalysisDiagnosticSource(TextDocument document, Func? shouldIncludeAnalyzer) + private sealed class FullSolutionAnalysisDiagnosticSource(TextDocument document, IDiagnosticAnalyzerService diagnosticAnalyzerService, Func? shouldIncludeAnalyzer) : AbstractWorkspaceDocumentDiagnosticSource(document) { + /// + /// Cached mapping between a project instance and all the diagnostics computed for it. This is used so that + /// once we compute the diagnostics once for a particular project, we don't need to recompute them again as we + /// walk every document within it. + /// + private static readonly ConditionalWeakTable>> s_projectToDiagnostics = new(); + /// /// This is a normal document source that represents live/fresh diagnostics that should supersede everything else. /// @@ -28,7 +40,6 @@ public override bool IsLiveSource() => true; public override async Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) { @@ -40,14 +51,39 @@ public override async Task> GetDiagnosticsAsync( } else { - // We call GetDiagnosticsForIdsAsync as we want to ensure we get the full set of diagnostics for this document - // including those reported as a compilation end diagnostic. These are not included in document pull (uses GetDiagnosticsForSpan) due to cost. - // However we can include them as a part of workspace pull when FSA is on. - var documentDiagnostics = await diagnosticAnalyzerService.GetDiagnosticsForIdsAsync( - Document.Project.Solution, Document.Project.Id, Document.Id, - diagnosticIds: null, shouldIncludeAnalyzer, includeSuppressedDiagnostics: false, - includeLocalDocumentDiagnostics: true, includeNonLocalDocumentDiagnostics: true, cancellationToken).ConfigureAwait(false); - return documentDiagnostics; + var projectDiagnostics = await GetProjectDiagnosticsAsync(diagnosticAnalyzerService, cancellationToken).ConfigureAwait(false); + return projectDiagnostics.WhereAsArray(d => d.DocumentId == Document.Id); + } + } + + private async ValueTask> GetProjectDiagnosticsAsync( + IDiagnosticAnalyzerService diagnosticAnalyzerService, CancellationToken cancellationToken) + { + if (!s_projectToDiagnostics.TryGetValue(Document.Project, out var lazyDiagnostics)) + { + // Extracted into local to prevent captures. + lazyDiagnostics = GetLazyDiagnostics(); + } + + var result = await lazyDiagnostics.GetValueAsync(cancellationToken).ConfigureAwait(false); + return result[Document.Id].ToImmutableArray(); + + AsyncLazy> GetLazyDiagnostics() + { + return s_projectToDiagnostics.GetValue( + Document.Project, + _ => AsyncLazy.Create( + async cancellationToken => + { + var allDiagnostics = await diagnosticAnalyzerService.GetDiagnosticsForIdsAsync( + Document.Project.Solution, Document.Project.Id, documentId: null, + diagnosticIds: null, shouldIncludeAnalyzer, + // Ensure we compute and return diagnostics for both the normal docs and the additional docs in this project. + static (project, _) => [.. project.DocumentIds.Concat(project.AdditionalDocumentIds)], + includeSuppressedDiagnostics: false, + includeLocalDocumentDiagnostics: true, includeNonLocalDocumentDiagnostics: true, cancellationToken).ConfigureAwait(false); + return allDiagnostics.Where(d => d.DocumentId != null).ToLookup(d => d.DocumentId!); + })); } } } @@ -64,7 +100,6 @@ public override bool IsLiveSource() => false; public override Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) { diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/DocumentDiagnosticSource.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/DocumentDiagnosticSource.cs index 54f5c6ce5deb2..e9a5ad49650bf 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/DocumentDiagnosticSource.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/DocumentDiagnosticSource.cs @@ -7,11 +7,10 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Copilot; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.EditAndContinue; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; -internal sealed class DocumentDiagnosticSource(DiagnosticKind diagnosticKind, TextDocument document) +internal sealed class DocumentDiagnosticSource(IDiagnosticAnalyzerService diagnosticAnalyzerService, DiagnosticKind diagnosticKind, TextDocument document) : AbstractDocumentDiagnosticSource(document) { public DiagnosticKind DiagnosticKind { get; } = diagnosticKind; @@ -23,14 +22,14 @@ public override bool IsLiveSource() => true; public override async Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) + RequestContext context, CancellationToken cancellationToken) { // We call GetDiagnosticsForSpanAsync here instead of GetDiagnosticsForIdsAsync as it has faster perf // characteristics. GetDiagnosticsForIdsAsync runs analyzers against the entire compilation whereas // GetDiagnosticsForSpanAsync will only run analyzers against the request document. // Also ensure we pass in "includeSuppressedDiagnostics = true" for unnecessary suppressions to be reported. var allSpanDiagnostics = await diagnosticAnalyzerService.GetDiagnosticsForSpanAsync( - Document, range: null, diagnosticKind: this.DiagnosticKind, includeSuppressedDiagnostics: true, cancellationToken: cancellationToken).ConfigureAwait(false); + Document, range: null, diagnosticKind: this.DiagnosticKind, includeSuppressedDiagnostics: true, cancellationToken: cancellationToken).ConfigureAwait(false); // Add cached Copilot diagnostics when computing analyzer semantic diagnostics. // TODO: move to a separate diagnostic source. https://github.com/dotnet/roslyn/issues/72896 diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/IDiagnosticSource.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/IDiagnosticSource.cs index 34d110e4adb4c..e8cb70bc1de4c 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/IDiagnosticSource.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/IDiagnosticSource.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; @@ -33,7 +32,6 @@ internal interface IDiagnosticSource bool IsLiveSource(); Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken); } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/NonLocalDocumentDiagnosticSource.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/NonLocalDocumentDiagnosticSource.cs index 8ffd2d9b75fb9..d370f15240a60 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/NonLocalDocumentDiagnosticSource.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/NonLocalDocumentDiagnosticSource.cs @@ -10,7 +10,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; -internal sealed class NonLocalDocumentDiagnosticSource(TextDocument document, Func? shouldIncludeAnalyzer) +internal sealed class NonLocalDocumentDiagnosticSource(TextDocument document, IDiagnosticAnalyzerService diagnosticAnalyzerService, Func? shouldIncludeAnalyzer) : AbstractDocumentDiagnosticSource(document) { private readonly Func? _shouldIncludeAnalyzer = shouldIncludeAnalyzer; @@ -19,7 +19,6 @@ public override bool IsLiveSource() => true; public override async Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) { diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/TaskListDiagnosticSource.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/TaskListDiagnosticSource.cs index df8a3491ccdcc..fe6ebc182c6f9 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/TaskListDiagnosticSource.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/TaskListDiagnosticSource.cs @@ -33,7 +33,7 @@ public override bool IsLiveSource() => true; public override async Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) + RequestContext context, CancellationToken cancellationToken) { var service = this.Document.GetLanguageService(); if (service == null) diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs index eace41f6259c5..16ee2f87d2bf2 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs @@ -3,15 +3,10 @@ // See the LICENSE file in the project root for more information. using System.Collections.Immutable; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.EditAndContinue; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.SolutionCrawler; using Roslyn.LanguageServer.Protocol; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; @@ -21,13 +16,14 @@ internal partial class DocumentPullDiagnosticHandler { public DocumentPullDiagnosticHandler( IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticRefresher, IGlobalOptionService globalOptions) - : base(analyzerService, diagnosticRefresher, globalOptions) + : base(analyzerService, diagnosticRefresher, diagnosticSourceManager, globalOptions) { } - protected override string? GetDiagnosticCategory(VSInternalDocumentDiagnosticsParams diagnosticsParams) + protected override string? GetRequestDiagnosticCategory(VSInternalDocumentDiagnosticsParams diagnosticsParams) => diagnosticsParams.QueryingDiagnosticKind?.Value; public override TextDocumentIdentifier? GetTextDocumentIdentifier(VSInternalDocumentDiagnosticsParams diagnosticsParams) @@ -72,59 +68,7 @@ protected override DiagnosticTag[] ConvertTags(DiagnosticData diagnosticData, bo => ConvertTags(diagnosticData, isLiveSource, potentialDuplicate: false); protected override VSInternalDiagnosticReport[]? CreateReturn(BufferedProgress progress) - => progress.GetFlattenedValues(); - - protected override ValueTask> GetOrderedDiagnosticSourcesAsync( - VSInternalDocumentDiagnosticsParams diagnosticsParams, RequestContext context, CancellationToken cancellationToken) - => new(GetDiagnosticSource(diagnosticsParams, context) is { } diagnosticSource ? [diagnosticSource] : []); - - private IDiagnosticSource? GetDiagnosticSource(VSInternalDocumentDiagnosticsParams diagnosticsParams, RequestContext context) - { - var category = diagnosticsParams.QueryingDiagnosticKind?.Value; - - // TODO: Implement as extensibility point. https://github.com/dotnet/roslyn/issues/72896 - - if (category == PullDiagnosticCategories.Task) - return context.GetTrackedDocument() is { } document ? new TaskListDiagnosticSource(document, GlobalOptions) : null; - - if (category == PullDiagnosticCategories.EditAndContinue) - return GetEditAndContinueDiagnosticSource(context); - - var diagnosticKind = category switch - { - PullDiagnosticCategories.DocumentCompilerSyntax => DiagnosticKind.CompilerSyntax, - PullDiagnosticCategories.DocumentCompilerSemantic => DiagnosticKind.CompilerSemantic, - PullDiagnosticCategories.DocumentAnalyzerSyntax => DiagnosticKind.AnalyzerSyntax, - PullDiagnosticCategories.DocumentAnalyzerSemantic => DiagnosticKind.AnalyzerSemantic, - // if this request doesn't have a category at all (legacy behavior, assume they're asking about everything). - null => DiagnosticKind.All, - // if it's a category we don't recognize, return nothing. - _ => (DiagnosticKind?)null, - }; - - if (diagnosticKind is null) - return null; - - return GetDiagnosticSource(diagnosticKind.Value, context); - } - - internal static IDiagnosticSource? GetEditAndContinueDiagnosticSource(RequestContext context) - => context.GetTrackedDocument() is { } document ? EditAndContinueDiagnosticSource.CreateOpenDocumentSource(document) : null; - - internal static IDiagnosticSource? GetDiagnosticSource(DiagnosticKind diagnosticKind, RequestContext context) - => context.GetTrackedDocument() is { } textDocument ? new DocumentDiagnosticSource(diagnosticKind, textDocument) : null; - - internal static IDiagnosticSource? GetNonLocalDiagnosticSource(RequestContext context, IGlobalOptionService globalOptions) { - var textDocument = context.GetTrackedDocument(); - if (textDocument == null) - return null; - - // Non-local document diagnostics are reported only when full solution analysis is enabled for analyzer execution. - if (globalOptions.GetBackgroundAnalysisScope(textDocument.Project.Language) != BackgroundAnalysisScope.FullSolution) - return null; - - // NOTE: Compiler does not report any non-local diagnostics, so we bail out for compiler analyzer. - return new NonLocalDocumentDiagnosticSource(textDocument, shouldIncludeAnalyzer: static analyzer => !analyzer.IsCompilerAnalyzer()); + return progress.GetFlattenedValues(); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandlerFactory.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandlerFactory.cs index d731fe70276cd..07a6cfcaabc17 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandlerFactory.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandlerFactory.cs @@ -6,6 +6,7 @@ using System.Composition; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; @@ -14,6 +15,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; internal class DocumentPullDiagnosticHandlerFactory : ILspServiceFactory { private readonly IDiagnosticAnalyzerService _analyzerService; + private readonly IDiagnosticSourceManager _diagnosticSourceManager; private readonly IDiagnosticsRefresher _diagnosticsRefresher; private readonly IGlobalOptionService _globalOptions; @@ -21,14 +23,16 @@ internal class DocumentPullDiagnosticHandlerFactory : ILspServiceFactory [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public DocumentPullDiagnosticHandlerFactory( IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticsRefresher, IGlobalOptionService globalOptions) { _analyzerService = analyzerService; + _diagnosticSourceManager = diagnosticSourceManager; _diagnosticsRefresher = diagnosticsRefresher; _globalOptions = globalOptions; } public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) - => new DocumentPullDiagnosticHandler(_analyzerService, _diagnosticsRefresher, _globalOptions); + => new DocumentPullDiagnosticHandler(_analyzerService, _diagnosticSourceManager, _diagnosticsRefresher, _globalOptions); } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentNonLocalDiagnosticSourceProvider.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentNonLocalDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..5ac98fefc7389 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentNonLocalDiagnosticSourceProvider.cs @@ -0,0 +1,44 @@ +// 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.Diagnostics; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.SolutionCrawler; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; + +[Export(typeof(IDiagnosticSourceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class PublicDocumentNonLocalDiagnosticSourceProvider( + [Import] IGlobalOptionService globalOptions, + [Import] IDiagnosticAnalyzerService diagnosticAnalyzerService) + : IDiagnosticSourceProvider +{ + public const string NonLocal = nameof(NonLocal); + public bool IsDocument => true; + public string Name => NonLocal; + + public bool IsEnabled(ClientCapabilities clientCapabilities) => true; + + public ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + // Non-local document diagnostics are reported only when full solution analysis is enabled for analyzer execution. + if (context.GetTrackedDocument() is { } textDocument && + globalOptions.GetBackgroundAnalysisScope(textDocument.Project.Language) == BackgroundAnalysisScope.FullSolution) + { + // NOTE: Compiler does not report any non-local diagnostics, so we bail out for compiler analyzer. + return new([new NonLocalDocumentDiagnosticSource(textDocument, diagnosticAnalyzerService, a => !a.IsCompilerAnalyzer())]); + } + + return new([]); + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticHandlerFactory.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticHandlerFactory.cs index e369e4180ed43..3ae460c780e84 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticHandlerFactory.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticHandlerFactory.cs @@ -5,10 +5,9 @@ using System; using System.Composition; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.EditAndContinue; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; -using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; @@ -19,6 +18,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; internal sealed class PublicDocumentPullDiagnosticHandlerFactory : ILspServiceFactory { private readonly IDiagnosticAnalyzerService _analyzerService; + private readonly IDiagnosticSourceManager _diagnosticSourceManager; private readonly IDiagnosticsRefresher _diagnosticRefresher; private readonly IGlobalOptionService _globalOptions; @@ -26,10 +26,12 @@ internal sealed class PublicDocumentPullDiagnosticHandlerFactory : ILspServiceFa [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public PublicDocumentPullDiagnosticHandlerFactory( IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticRefresher, IGlobalOptionService globalOptions) { _analyzerService = analyzerService; + _diagnosticSourceManager = diagnosticSourceManager; _diagnosticRefresher = diagnosticRefresher; _globalOptions = globalOptions; } @@ -37,6 +39,6 @@ public PublicDocumentPullDiagnosticHandlerFactory( public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) { var clientLanguageServerManager = lspServices.GetRequiredService(); - return new PublicDocumentPullDiagnosticsHandler(clientLanguageServerManager, _analyzerService, _diagnosticRefresher, _globalOptions); + return new PublicDocumentPullDiagnosticsHandler(clientLanguageServerManager, _analyzerService, _diagnosticSourceManager, _diagnosticRefresher, _globalOptions); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler.cs index 400087cd41ef7..426148bed2f11 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler.cs @@ -2,52 +2,42 @@ // 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.Linq; -using System.Threading; -using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.EditAndContinue; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; using Roslyn.LanguageServer.Protocol; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; -using DocumentDiagnosticReport = SumType; - // A document diagnostic partial report is defined as having the first literal send = DocumentDiagnosticReport (aka changed / unchanged) followed // by n DocumentDiagnosticPartialResult literals. -// See https://github.com/microsoft/vscode-languageserver-node/blob/main/protocol/src/common/proposed.diagnostics.md#textDocument_diagnostic +// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#documentDiagnosticParams using DocumentDiagnosticPartialReport = SumType; +using DocumentDiagnosticReport = SumType; [Method(Methods.TextDocumentDiagnosticName)] internal sealed partial class PublicDocumentPullDiagnosticsHandler : AbstractDocumentPullDiagnosticHandler { - private readonly string _nonLocalDiagnosticsSourceRegistrationId; private readonly IClientLanguageServerManager _clientLanguageServerManager; public PublicDocumentPullDiagnosticsHandler( IClientLanguageServerManager clientLanguageServerManager, IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticsRefresher, IGlobalOptionService globalOptions) - : base(analyzerService, diagnosticsRefresher, globalOptions) + : base(analyzerService, diagnosticsRefresher, diagnosticSourceManager, globalOptions) { - _nonLocalDiagnosticsSourceRegistrationId = Guid.NewGuid().ToString(); _clientLanguageServerManager = clientLanguageServerManager; } - /// - /// Public API doesn't support categories (yet). - /// - protected override string? GetDiagnosticCategory(DocumentDiagnosticParams diagnosticsParams) - => null; + protected override string? GetRequestDiagnosticCategory(DocumentDiagnosticParams diagnosticsParams) + => diagnosticsParams.Identifier; - public override TextDocumentIdentifier GetTextDocumentIdentifier(DocumentDiagnosticParams diagnosticsParams) => diagnosticsParams.TextDocument; - - protected override string? GetDiagnosticSourceIdentifier(DocumentDiagnosticParams diagnosticsParams) => diagnosticsParams.Identifier; + public override TextDocumentIdentifier GetTextDocumentIdentifier(DocumentDiagnosticParams diagnosticsParams) + => diagnosticsParams.TextDocument; protected override DiagnosticTag[] ConvertTags(DiagnosticData diagnosticData, bool isLiveSource) => ConvertTags(diagnosticData, isLiveSource, potentialDuplicate: false); @@ -92,18 +82,6 @@ protected override bool TryCreateUnchangedReport(TextDocumentIdentifier identifi return null; } - protected override ValueTask> GetOrderedDiagnosticSourcesAsync(DocumentDiagnosticParams diagnosticParams, RequestContext context, CancellationToken cancellationToken) - { - var nonLocalDocumentDiagnostics = diagnosticParams.Identifier == DocumentNonLocalDiagnosticIdentifier.ToString(); - - // Task list items are not reported through the public LSP diagnostic API. - var source = nonLocalDocumentDiagnostics - ? DocumentPullDiagnosticHandler.GetNonLocalDiagnosticSource(context, GlobalOptions) - : DocumentPullDiagnosticHandler.GetDiagnosticSource(DiagnosticKind.All, context); - - return new(source != null ? [source] : []); - } - protected override ImmutableArray? GetPreviousResults(DocumentDiagnosticParams diagnosticsParams) { if (diagnosticsParams.PreviousResultId != null && diagnosticsParams.TextDocument != null) diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler_IOnInitialized.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler_IOnInitialized.cs index 940723c0e652a..539100d7a23b2 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler_IOnInitialized.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler_IOnInitialized.cs @@ -2,61 +2,62 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.SolutionCrawler; using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; - -using DocumentDiagnosticReport = SumType; - // A document diagnostic partial report is defined as having the first literal send = DocumentDiagnosticReport (aka changed / unchanged) followed // by n DocumentDiagnosticPartialResult literals. // See https://github.com/microsoft/vscode-languageserver-node/blob/main/protocol/src/common/proposed.diagnostics.md#textDocument_diagnostic -using DocumentDiagnosticPartialReport = SumType; -internal sealed partial class PublicDocumentPullDiagnosticsHandler : AbstractDocumentPullDiagnosticHandler, IOnInitialized +internal sealed partial class PublicDocumentPullDiagnosticsHandler : IOnInitialized { public async Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) { - // Dynamically register a non-local document diagnostic source if Full solution background analysis is enabled - // for analyzer execution. This diagnostic source reports diagnostics in open documents that are reported - // when analyzing other documents or at compilation end. - if (clientCapabilities?.TextDocument?.Diagnostic?.DynamicRegistration is true && IsFsaEnabled()) + // Dynamically register for all relevant diagnostic sources. + if (clientCapabilities?.TextDocument?.Diagnostic?.DynamicRegistration is true) { // TODO: Hookup an option changed handler for changes to BackgroundAnalysisScopeOption // to dynamically register/unregister the non-local document diagnostic source. + var documentSources = DiagnosticSourceManager.GetDocumentSourceProviderNames(clientCapabilities); + var workspaceSources = DiagnosticSourceManager.GetWorkspaceSourceProviderNames(clientCapabilities); + + // All diagnostic sources have to be registered under the document pull method name, + // See https://github.com/microsoft/language-server-protocol/issues/1723 + // + // Additionally if a source name is used by both document and workspace pull (e.g. enc) + // we don't want to send two registrations, instead we should send a single registration + // that also sets the workspace pull option. + // + // So we build up a unique set of source names and mark if each one is also a workspace source. + var allSources = documentSources + .AddRange(workspaceSources) + .ToSet() + .Select(name => (Name: name, IsWorkspaceSource: workspaceSources.Contains(name))); + + var registrations = allSources.Select(FromSourceName).ToArray(); await _clientLanguageServerManager.SendRequestAsync( methodName: Methods.ClientRegisterCapabilityName, @params: new RegistrationParams() { - Registrations = - [ - new Registration - { - Id = _nonLocalDiagnosticsSourceRegistrationId, - Method = Methods.TextDocumentDiagnosticName, - RegisterOptions = new DiagnosticRegistrationOptions - { - Identifier = DocumentNonLocalDiagnosticIdentifier.ToString() - } - } - ] + Registrations = registrations }, cancellationToken).ConfigureAwait(false); } - bool IsFsaEnabled() + static Registration FromSourceName((string Name, bool IsWorkspaceSource) source) { - foreach (var language in context.SupportedLanguages) + return new() { - if (GlobalOptions.GetBackgroundAnalysisScope(language) == BackgroundAnalysisScope.FullSolution) - return true; - } - - return false; + Id = Guid.NewGuid().ToString(), + Method = Methods.TextDocumentDiagnosticName, + RegisterOptions = new DiagnosticRegistrationOptions { Identifier = source.Name, InterFileDependencies = true, WorkspaceDiagnostics = source.IsWorkspaceSource, WorkDoneProgress = source.IsWorkspaceSource } + }; } } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticHandlerFactory.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticHandlerFactory.cs index 90970a521fe53..ea0814316fbfc 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticHandlerFactory.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticHandlerFactory.cs @@ -6,6 +6,7 @@ using System.Composition; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; @@ -16,12 +17,13 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; internal sealed class PublicWorkspacePullDiagnosticHandlerFactory( LspWorkspaceRegistrationService registrationService, IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticsRefresher, IGlobalOptionService globalOptions) : ILspServiceFactory { public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) { var workspaceManager = lspServices.GetRequiredService(); - return new PublicWorkspacePullDiagnosticsHandler(workspaceManager, registrationService, analyzerService, diagnosticsRefresher, globalOptions); + return new PublicWorkspacePullDiagnosticsHandler(workspaceManager, registrationService, analyzerService, diagnosticSourceManager, diagnosticsRefresher, globalOptions); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler.cs index 6c2beb5517041..71f78c36902a0 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler.cs @@ -7,6 +7,7 @@ using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; using Roslyn.LanguageServer.Protocol; using Roslyn.Utilities; @@ -19,21 +20,21 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; using WorkspaceDiagnosticPartialReport = SumType; [Method(Methods.WorkspaceDiagnosticName)] -internal sealed class PublicWorkspacePullDiagnosticsHandler( - LspWorkspaceManager workspaceManager, - LspWorkspaceRegistrationService registrationService, - IDiagnosticAnalyzerService analyzerService, - IDiagnosticsRefresher diagnosticRefresher, - IGlobalOptionService globalOptions) - : AbstractWorkspacePullDiagnosticsHandler( - workspaceManager, registrationService, analyzerService, diagnosticRefresher, globalOptions), IDisposable +internal sealed partial class PublicWorkspacePullDiagnosticsHandler : AbstractWorkspacePullDiagnosticsHandler, IDisposable { + public PublicWorkspacePullDiagnosticsHandler( + LspWorkspaceManager workspaceManager, + LspWorkspaceRegistrationService registrationService, + IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, + IDiagnosticsRefresher diagnosticRefresher, + IGlobalOptionService globalOptions) + : base(workspaceManager, registrationService, analyzerService, diagnosticSourceManager, diagnosticRefresher, globalOptions) + { + } - /// - /// Public API doesn't support categories (yet). - /// - protected override string? GetDiagnosticCategory(WorkspaceDiagnosticParams diagnosticsParams) - => null; + protected override string? GetRequestDiagnosticCategory(WorkspaceDiagnosticParams diagnosticsParams) + => diagnosticsParams.Identifier; protected override DiagnosticTag[] ConvertTags(DiagnosticData diagnosticData, bool isLiveSource) => ConvertTags(diagnosticData, isLiveSource, potentialDuplicate: false); diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandler.cs index 66893cacf32fb..62f6e9b5b8332 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandler.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; using Roslyn.LanguageServer.Protocol; using Roslyn.Utilities; @@ -17,26 +18,27 @@ internal sealed partial class WorkspacePullDiagnosticHandler( LspWorkspaceManager workspaceManager, LspWorkspaceRegistrationService registrationService, IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticsRefresher, IGlobalOptionService globalOptions) : AbstractWorkspacePullDiagnosticsHandler( - workspaceManager, registrationService, analyzerService, diagnosticsRefresher, globalOptions) + workspaceManager, registrationService, analyzerService, diagnosticSourceManager, diagnosticsRefresher, globalOptions) { - protected override string? GetDiagnosticCategory(VSInternalWorkspaceDiagnosticsParams diagnosticsParams) + protected override string? GetRequestDiagnosticCategory(VSInternalWorkspaceDiagnosticsParams diagnosticsParams) => diagnosticsParams.QueryingDiagnosticKind?.Value; protected override VSInternalWorkspaceDiagnosticReport[] CreateReport(TextDocumentIdentifier identifier, Roslyn.LanguageServer.Protocol.Diagnostic[]? diagnostics, string? resultId) - => [ - new VSInternalWorkspaceDiagnosticReport - { - TextDocument = identifier, - Diagnostics = diagnostics, - ResultId = resultId, - // Mark these diagnostics as having come from us. They will be superseded by any diagnostics for the - // same file produced by the DocumentPullDiagnosticHandler. - Identifier = WorkspaceDiagnosticIdentifier, - } - ]; + => [ + new VSInternalWorkspaceDiagnosticReport + { + TextDocument = identifier, + Diagnostics = diagnostics, + ResultId = resultId, + // Mark these diagnostics as having come from us. They will be superseded by any diagnostics for the + // same file produced by the DocumentPullDiagnosticHandler. + Identifier = WorkspaceDiagnosticIdentifier, + } + ]; protected override VSInternalWorkspaceDiagnosticReport[] CreateRemovedReport(TextDocumentIdentifier identifier) => CreateReport(identifier, diagnostics: null, resultId: null); diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandlerFactory.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandlerFactory.cs index b928cf51f3910..7c13f4d46b74a 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandlerFactory.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandlerFactory.cs @@ -6,6 +6,7 @@ using System.Composition; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; @@ -16,12 +17,13 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; internal class WorkspacePullDiagnosticHandlerFactory( LspWorkspaceRegistrationService registrationService, IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticsRefresher, IGlobalOptionService globalOptions) : ILspServiceFactory { public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) { var workspaceManager = lspServices.GetRequiredService(); - return new WorkspacePullDiagnosticHandler(workspaceManager, registrationService, analyzerService, diagnosticsRefresher, globalOptions); + return new WorkspacePullDiagnosticHandler(workspaceManager, registrationService, analyzerService, diagnosticSourceManager, diagnosticsRefresher, globalOptions); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/EditAndContinue/DocumentEditAndContinueDiagnosticSourceProvider.cs b/src/Features/LanguageServer/Protocol/Handler/EditAndContinue/DocumentEditAndContinueDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..5f66ca41cdddb --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/EditAndContinue/DocumentEditAndContinueDiagnosticSourceProvider.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.EditAndContinue; +using Microsoft.CodeAnalysis.Host.Mef; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; + +[Export(typeof(IDiagnosticSourceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class DocumentEditAndContinueDiagnosticSourceProvider() : IDiagnosticSourceProvider +{ + public bool IsDocument => true; + public string Name => PullDiagnosticCategories.EditAndContinue; + + public bool IsEnabled(ClientCapabilities capabilities) => true; + + public ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + if (context.GetTrackedDocument() is { } document) + { + return new([EditAndContinueDiagnosticSource.CreateOpenDocumentSource(document)]); + } + + return new([]); + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/EditAndContinue/WorkspaceEditAndContinueDiagnosticSourceProvider.cs b/src/Features/LanguageServer/Protocol/Handler/EditAndContinue/WorkspaceEditAndContinueDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..af92108b86cae --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/EditAndContinue/WorkspaceEditAndContinueDiagnosticSourceProvider.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.EditAndContinue; +using Microsoft.CodeAnalysis.Host.Mef; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; + +[Export(typeof(IDiagnosticSourceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class WorkspaceEditAndContinueDiagnosticSourceProvider() : IDiagnosticSourceProvider +{ + public bool IsDocument => false; + public string Name => PullDiagnosticCategories.EditAndContinue; + + public bool IsEnabled(ClientCapabilities capabilities) => true; + + public ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + Contract.ThrowIfNull(context.Solution); + return EditAndContinueDiagnosticSource.CreateWorkspaceDiagnosticSourcesAsync(context.Solution!, document => context.IsTracking(document.GetURI()), cancellationToken); + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Highlights/DocumentHighlightHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Highlights/DocumentHighlightHandler.cs index 176c3f17b0739..806d070ff0463 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Highlights/DocumentHighlightHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Highlights/DocumentHighlightHandler.cs @@ -53,14 +53,14 @@ public DocumentHighlightsHandler(IHighlightingService highlightingService, IGlob var keywordHighlights = await GetKeywordHighlightsAsync(document, text, position, cancellationToken).ConfigureAwait(false); if (keywordHighlights.Any()) { - return keywordHighlights.ToArray(); + return [.. keywordHighlights]; } // Not a keyword, check if it is a reference that needs highlighting. var referenceHighlights = await GetReferenceHighlightsAsync(document, text, position, cancellationToken).ConfigureAwait(false); if (referenceHighlights.Any()) { - return referenceHighlights.ToArray(); + return [.. referenceHighlights]; } // No keyword or references to highlight at this location. diff --git a/src/Features/LanguageServer/Protocol/Handler/OnAutoInsert/OnAutoInsertHandler.cs b/src/Features/LanguageServer/Protocol/Handler/OnAutoInsert/OnAutoInsertHandler.cs index a488aefea3f12..7ea64d0c5d474 100644 --- a/src/Features/LanguageServer/Protocol/Handler/OnAutoInsert/OnAutoInsertHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/OnAutoInsert/OnAutoInsertHandler.cs @@ -166,7 +166,7 @@ public OnAutoInsertHandler( var indentedText = GetIndentedText(newSourceText, caretLine, desiredCaretLinePosition, options); // Get the overall text changes between the original text and the formatted + indented text. - textChanges = indentedText.GetTextChanges(sourceText).ToImmutableArray(); + textChanges = [.. indentedText.GetTextChanges(sourceText)]; newSourceText = indentedText; // If tabs were inserted the desired caret column can remain beyond the line text. diff --git a/src/Features/LanguageServer/Protocol/Handler/ProjectContext/GetTextDocumentWithContextHandler.cs b/src/Features/LanguageServer/Protocol/Handler/ProjectContext/GetTextDocumentWithContextHandler.cs index 1fd14087e596d..ee897994efe61 100644 --- a/src/Features/LanguageServer/Protocol/Handler/ProjectContext/GetTextDocumentWithContextHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/ProjectContext/GetTextDocumentWithContextHandler.cs @@ -63,7 +63,7 @@ public GetTextDocumentWithContextHandler() return Task.FromResult(new VSProjectContextList { - ProjectContexts = contexts.ToArray(), + ProjectContexts = [.. contexts], DefaultIndex = documentIds.IndexOf(d => d == currentContextDocumentId) }); } diff --git a/src/Features/LanguageServer/Protocol/Handler/References/FindUsagesLSPContext.cs b/src/Features/LanguageServer/Protocol/Handler/References/FindUsagesLSPContext.cs index 2338d5b383c31..9e57963e23ef3 100644 --- a/src/Features/LanguageServer/Protocol/Handler/References/FindUsagesLSPContext.cs +++ b/src/Features/LanguageServer/Protocol/Handler/References/FindUsagesLSPContext.cs @@ -259,7 +259,7 @@ public override async ValueTask OnReferenceFoundAsync(SourceReferenceItem refere var docText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); var classifiedTextRuns = GetClassifiedTextRuns(_id, definitionId, documentSpan.Value, isWrittenTo, classifiedSpans, docText); - return new ClassifiedTextElement(classifiedTextRuns.ToArray()); + return new ClassifiedTextElement([.. classifiedTextRuns]); } // Certain definitions may not have a DocumentSpan, such as namespace and metadata definitions @@ -317,7 +317,7 @@ private static ClassifiedTextRun[] GetClassifiedTextRuns( private ValueTask ReportReferencesAsync(ImmutableSegmentedList> referencesToReport, CancellationToken cancellationToken) { // We can report outside of the lock here since _progress is thread-safe. - _progress.Report(referencesToReport.ToArray()); + _progress.Report([.. referencesToReport]); return ValueTaskFactory.CompletedTask; } } diff --git a/src/Features/LanguageServer/Protocol/Handler/RequestTelemetryLogger.cs b/src/Features/LanguageServer/Protocol/Handler/RequestTelemetryLogger.cs index 1ff18f536c1e3..7341d04b9c5d9 100644 --- a/src/Features/LanguageServer/Protocol/Handler/RequestTelemetryLogger.cs +++ b/src/Features/LanguageServer/Protocol/Handler/RequestTelemetryLogger.cs @@ -36,6 +36,8 @@ public RequestTelemetryLogger(string serverTypeName) _requestCounters = new(); _findDocumentResults = new(); _usedForkedSolutionCounter = new(); + + TelemetryLogging.Flushed += OnFlushed; } public void UpdateFindDocumentTelemetryData(bool success, string? workspaceKind) @@ -92,6 +94,14 @@ public void Dispose() return; } + // Flush all telemetry logged through TelemetryLogging + TelemetryLogging.Flush(); + + TelemetryLogging.Flushed -= OnFlushed; + } + + private void OnFlushed(object? sender, EventArgs e) + { foreach (var kvp in _requestCounters) { TelemetryLogging.Log(FunctionId.LSP_RequestCounter, KeyValueLogMessage.Create(LogType.Trace, m => @@ -124,9 +134,6 @@ public void Dispose() } })); - // Flush all telemetry logged through TelemetryLogging - TelemetryLogging.Flush(); - _requestCounters.Clear(); } diff --git a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensSchema.cs b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensSchema.cs index 752ec12df2eeb..03c19b8338212 100644 --- a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensSchema.cs +++ b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensSchema.cs @@ -102,7 +102,7 @@ public SemanticTokensSchema(IReadOnlyDictionary tokenTypeMap) .Order() .ToImmutableArray(); - AllTokenTypes = SemanticTokenTypes.AllTypes.Concat(customTokenTypes).ToImmutableArray(); + AllTokenTypes = [.. SemanticTokenTypes.AllTypes, .. customTokenTypes]; var tokenTypeToIndex = new Dictionary(); diff --git a/src/Features/LanguageServer/Protocol/Handler/Symbols/WorkspaceSymbolsHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Symbols/WorkspaceSymbolsHandler.cs index 852a0d777a22a..f0ff5255f831b 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Symbols/WorkspaceSymbolsHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Symbols/WorkspaceSymbolsHandler.cs @@ -70,20 +70,26 @@ private sealed class LSPNavigateToCallback( BufferedProgress progress) : INavigateToSearchCallback { - public async Task AddItemAsync(Project project, INavigateToSearchResult result, CancellationToken cancellationToken) + public async Task AddResultsAsync(ImmutableArray results, CancellationToken cancellationToken) { - var document = await result.NavigableItem.Document.GetRequiredDocumentAsync(project.Solution, cancellationToken).ConfigureAwait(false); + Contract.ThrowIfNull(context.Solution); + var solution = context.Solution; - var location = await ProtocolConversions.TextSpanToLocationAsync( - document, result.NavigableItem.SourceSpan, result.NavigableItem.IsStale, context, cancellationToken).ConfigureAwait(false); - if (location == null) - return; + foreach (var result in results) + { + var document = await result.NavigableItem.Document.GetRequiredDocumentAsync(solution, cancellationToken).ConfigureAwait(false); - var service = project.Solution.Services.GetRequiredService(); - var symbolInfo = service.Create( - result.Name, result.AdditionalInformation, ProtocolConversions.NavigateToKindToSymbolKind(result.Kind), location, result.NavigableItem.Glyph); + var location = await ProtocolConversions.TextSpanToLocationAsync( + document, result.NavigableItem.SourceSpan, result.NavigableItem.IsStale, context, cancellationToken).ConfigureAwait(false); + if (location == null) + return; - progress.Report(symbolInfo); + var service = solution.Services.GetRequiredService(); + var symbolInfo = service.Create( + result.Name, result.AdditionalInformation, ProtocolConversions.NavigateToKindToSymbolKind(result.Kind), location, result.NavigableItem.Glyph); + + progress.Report(symbolInfo); + } } public void Done(bool isFullyLoaded) diff --git a/src/Features/LanguageServer/Protocol/Handler/Tasks/DocumentTaskDiagnosticSourceProvider.cs b/src/Features/LanguageServer/Protocol/Handler/Tasks/DocumentTaskDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..5e78b219c22f3 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Tasks/DocumentTaskDiagnosticSourceProvider.cs @@ -0,0 +1,36 @@ +// 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.Mef; +using Microsoft.CodeAnalysis.Options; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Tasks; + +[Export(typeof(IDiagnosticSourceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class DocumentTaskDiagnosticSourceProvider([Import] IGlobalOptionService globalOptions) : IDiagnosticSourceProvider +{ + public bool IsDocument => true; + public string Name => PullDiagnosticCategories.Task; + + public bool IsEnabled(ClientCapabilities capabilities) => capabilities.HasVisualStudioLspCapability(); + + public ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + if (context.GetTrackedDocument() is { } document) + { + return new([new TaskListDiagnosticSource(document, globalOptions)]); + } + + return new([]); + } +} + diff --git a/src/Features/LanguageServer/Protocol/Handler/Tasks/WorkspaceTaskDiagnosticSourceProvider.cs b/src/Features/LanguageServer/Protocol/Handler/Tasks/WorkspaceTaskDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..2a273d85d398a --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Tasks/WorkspaceTaskDiagnosticSourceProvider.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.TaskList; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Tasks; + +[Export(typeof(IDiagnosticSourceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class WorkspaceTaskDiagnosticSourceProvider([Import] IGlobalOptionService globalOptions) : IDiagnosticSourceProvider +{ + public bool IsDocument => false; + public string Name => PullDiagnosticCategories.Task; + + public bool IsEnabled(ClientCapabilities capabilities) => capabilities.HasVisualStudioLspCapability(); + + public ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + Contract.ThrowIfNull(context.Solution); + + // Only compute task list items for closed files if the option is on for it. + if (globalOptions.GetTaskListOptions().ComputeForClosedFiles) + { + using var _ = ArrayBuilder.GetInstance(out var result); + foreach (var project in WorkspaceDiagnosticSourceHelpers.GetProjectsInPriorityOrder(context.Solution, context.SupportedLanguages)) + { + foreach (var document in project.Documents) + { + if (!WorkspaceDiagnosticSourceHelpers.ShouldSkipDocument(context, document)) + result.Add(new TaskListDiagnosticSource(document, globalOptions)); + } + } + + return new(result.ToImmutableAndClear()); + } + + return new([]); + } +} diff --git a/src/Features/LanguageServer/Protocol/LspServices/LspServices.cs b/src/Features/LanguageServer/Protocol/LspServices/LspServices.cs index f604d0049d19f..099a38a5f3889 100644 --- a/src/Features/LanguageServer/Protocol/LspServices/LspServices.cs +++ b/src/Features/LanguageServer/Protocol/LspServices/LspServices.cs @@ -153,7 +153,7 @@ public void Dispose() ImmutableArray disposableServices; lock (_gate) { - disposableServices = _servicesToDispose.ToImmutableArray(); + disposableServices = [.. _servicesToDispose]; _servicesToDispose.Clear(); } diff --git a/src/Features/LanguageServer/Protocol/Protocol/Internal/Converters/VSCodeInternalExtensionUtilities.cs b/src/Features/LanguageServer/Protocol/Protocol/Internal/Converters/VSCodeInternalExtensionUtilities.cs new file mode 100644 index 0000000000000..a1d482e7a480a --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Protocol/Internal/Converters/VSCodeInternalExtensionUtilities.cs @@ -0,0 +1,52 @@ +// 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 Roslyn.LanguageServer.Protocol +{ + using Newtonsoft.Json; + + /// + /// Utilities to aid work with VS Code LSP Extensions. + /// + internal static class VSCodeInternalExtensionUtilities + { + /// + /// Adds necessary to deserialize + /// JSON stream into objects which include VS Code-specific extensions. + /// + /// + /// If is used in parallel to execution of this method, + /// its access needs to be synchronized with this method call, to guarantee that + /// collection is not modified when in use. + /// + /// Instance of which is guaranteed to not work in parallel to this method call. + public static void AddVSCodeInternalExtensionConverters(this JsonSerializer serializer) + { + // Reading the number of converters before we start adding new ones + var existingConvertersCount = serializer.Converters.Count; + + AddOrReplaceConverter(); + AddOrReplaceConverter(); + + void AddOrReplaceConverter() + where TExtension : TBase + { + for (var i = 0; i < existingConvertersCount; i++) + { + var existingConverterType = serializer.Converters[i].GetType(); + if (existingConverterType.IsGenericType && + existingConverterType.GetGenericTypeDefinition() == typeof(VSExtensionConverter<,>) && + existingConverterType.GenericTypeArguments[0] == typeof(TBase)) + { + serializer.Converters.RemoveAt(i); + existingConvertersCount--; + break; + } + } + + serializer.Converters.Add(new VSExtensionConverter()); + } + } + } +} diff --git a/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalHover.cs b/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalHover.cs index ea9a5ec9094f6..de87ad88193bc 100644 --- a/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalHover.cs +++ b/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalHover.cs @@ -10,6 +10,7 @@ namespace Roslyn.LanguageServer.Protocol /// /// Extension to Hover which adds additional data for colorization. /// + [DataContract] internal class VSInternalHover : Hover { /// diff --git a/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalInlineCompletionContext.cs b/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalInlineCompletionContext.cs index 284ffa5d505ae..a3ab7943d4821 100644 --- a/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalInlineCompletionContext.cs +++ b/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalInlineCompletionContext.cs @@ -12,6 +12,7 @@ namespace Roslyn.LanguageServer.Protocol /// Context for inline completion request. /// See https://github.com/microsoft/vscode/blob/075ba020e8493f40dba89891b1a08453f2c067e9/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts#L27. /// + [DataContract] internal class VSInternalInlineCompletionContext { /// diff --git a/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalInlineCompletionItem.cs b/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalInlineCompletionItem.cs index 73fb5c5285852..740c6799c26d9 100644 --- a/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalInlineCompletionItem.cs +++ b/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalInlineCompletionItem.cs @@ -13,6 +13,7 @@ namespace Roslyn.LanguageServer.Protocol /// /// See https://github.com/microsoft/vscode/blob/075ba020e8493f40dba89891b1a08453f2c067e9/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts#L78. /// + [DataContract] internal class VSInternalInlineCompletionItem { /// @@ -45,4 +46,4 @@ internal class VSInternalInlineCompletionItem [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public InsertTextFormat? TextFormat { get; set; } = InsertTextFormat.Plaintext; } -} \ No newline at end of file +} diff --git a/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalInlineCompletionList.cs b/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalInlineCompletionList.cs index 62f4b20ba0d07..57c7957df661b 100644 --- a/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalInlineCompletionList.cs +++ b/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalInlineCompletionList.cs @@ -12,6 +12,7 @@ namespace Roslyn.LanguageServer.Protocol /// /// See https://github.com/microsoft/vscode/blob/075ba020e8493f40dba89891b1a08453f2c067e9/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts#L72. /// + [DataContract] internal class VSInternalInlineCompletionList { /// @@ -21,4 +22,4 @@ internal class VSInternalInlineCompletionList [JsonProperty(Required = Required.Always)] public VSInternalInlineCompletionItem[] Items { get; set; } } -} \ No newline at end of file +} diff --git a/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalInlineCompletionRequest.cs b/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalInlineCompletionRequest.cs index fe3840400b762..a57c4f294d4db 100644 --- a/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalInlineCompletionRequest.cs +++ b/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalInlineCompletionRequest.cs @@ -12,6 +12,7 @@ namespace Roslyn.LanguageServer.Protocol /// /// See https://github.com/microsoft/vscode/blob/075ba020e8493f40dba89891b1a08453f2c067e9/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts#L24. /// + [DataContract] internal class VSInternalInlineCompletionRequest : ITextDocumentParams { /// diff --git a/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalReferenceParams.cs b/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalReferenceParams.cs index a302b2baeb3ce..ef0a031bbb6dd 100644 --- a/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalReferenceParams.cs +++ b/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalReferenceParams.cs @@ -10,6 +10,7 @@ namespace Roslyn.LanguageServer.Protocol /// /// Class which represents extensions of passed as parameter of find reference requests. /// + [DataContract] internal class VSInternalReferenceParams : ReferenceParams { /// diff --git a/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalSelectedCompletionInfo.cs b/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalSelectedCompletionInfo.cs index 566acebde174c..963cc264e0acf 100644 --- a/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalSelectedCompletionInfo.cs +++ b/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalSelectedCompletionInfo.cs @@ -12,6 +12,7 @@ namespace Roslyn.LanguageServer.Protocol /// /// See https://github.com/microsoft/vscode/blob/075ba020e8493f40dba89891b1a08453f2c067e9/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts#L48. /// + [DataContract] internal class VSInternalSelectedCompletionInfo { /// @@ -42,4 +43,4 @@ internal class VSInternalSelectedCompletionInfo [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] public bool IsSnippetText { get; set; } } -} \ No newline at end of file +} diff --git a/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalTextDocumentClientCapabilities.cs b/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalTextDocumentClientCapabilities.cs index 1ae592269a85e..abc8b2b3749ed 100644 --- a/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalTextDocumentClientCapabilities.cs +++ b/src/Features/LanguageServer/Protocol/Protocol/Internal/VSInternalTextDocumentClientCapabilities.cs @@ -10,6 +10,7 @@ namespace Roslyn.LanguageServer.Protocol /// /// Text document capabilities specific to Visual Studio. /// + [DataContract] internal class VSInternalTextDocumentClientCapabilities : TextDocumentClientCapabilities { /// diff --git a/src/Features/LanguageServer/Protocol/RoslynLanguageServer.cs b/src/Features/LanguageServer/Protocol/RoslynLanguageServer.cs index 3dc9a54510650..8570c4d0db31b 100644 --- a/src/Features/LanguageServer/Protocol/RoslynLanguageServer.cs +++ b/src/Features/LanguageServer/Protocol/RoslynLanguageServer.cs @@ -39,6 +39,8 @@ public RoslynLanguageServer( _lspServiceProvider = lspServiceProvider; _serverKind = serverKind; + VSCodeInternalExtensionUtilities.AddVSCodeInternalExtensionConverters(serializer); + // Create services that require base dependencies (jsonrpc) or are more complex to create to the set manually. _baseServices = GetBaseServices(jsonRpc, logger, capabilitiesProvider, hostServices, serverKind, supportedLanguages); @@ -54,7 +56,7 @@ protected override ILspServices ConstructLspServices() protected override IRequestExecutionQueue ConstructRequestExecutionQueue() { var provider = GetLspServices().GetRequiredService>(); - return provider.CreateRequestExecutionQueue(this, _logger, HandlerProvider); + return provider.CreateRequestExecutionQueue(this, Logger, HandlerProvider); } private ImmutableDictionary>> GetBaseServices( @@ -115,7 +117,7 @@ protected override string GetLanguageForRequest(string methodName, JToken? param { if (parameters == null) { - _logger.LogInformation("No request parameters given, using default language handler"); + Logger.LogInformation("No request parameters given, using default language handler"); return LanguageServerConstants.DefaultLanguageName; } @@ -140,7 +142,7 @@ protected override string GetLanguageForRequest(string methodName, JToken? param var uri = uriToken.ToObject(_jsonSerializer); Contract.ThrowIfNull(uri, "Failed to deserialize uri property"); var language = lspWorkspaceManager.GetLanguageForUri(uri); - _logger.LogInformation($"Using {language} from request text document"); + Logger.LogInformation($"Using {language} from request text document"); return language; } @@ -154,12 +156,12 @@ protected override string GetLanguageForRequest(string methodName, JToken? param var data = dataToken.ToObject(_jsonSerializer); Contract.ThrowIfNull(data, "Failed to document resolve data object"); var language = lspWorkspaceManager.GetLanguageForUri(data.TextDocument.Uri); - _logger.LogInformation($"Using {language} from data text document"); + Logger.LogInformation($"Using {language} from data text document"); return language; } // This request is not for a textDocument and is not a resolve request. - _logger.LogInformation("Request did not contain a textDocument, using default language handler"); + Logger.LogInformation("Request did not contain a textDocument, using default language handler"); return LanguageServerConstants.DefaultLanguageName; static bool ShouldUseDefaultLanguage(string methodName) diff --git a/src/Features/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs index b2b813d4c07ed..cfa16156b82ce 100644 --- a/src/Features/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs +++ b/src/Features/LanguageServer/ProtocolUnitTests/Completion/CompletionFeaturesTests.cs @@ -496,8 +496,7 @@ public async Task TestUsingServerDefaultCommitCharacters(bool mutatingLspWorkspa } } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] [WorkItem("https://github.com/dotnet/roslyn/issues/26488")] public async Task TestCompletionForObsoleteSymbol(bool mutatingLspWorkspace) { diff --git a/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/AbstractPullDiagnosticTestsBase.cs b/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/AbstractPullDiagnosticTestsBase.cs index 201f98f4233cf..556c9a4cc1282 100644 --- a/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/AbstractPullDiagnosticTestsBase.cs +++ b/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/AbstractPullDiagnosticTestsBase.cs @@ -72,7 +72,7 @@ private protected static async Task> RunGet } else { - return await RunPublicGetWorkspacePullDiagnosticsAsync(testLspServer, previousResults, useProgress, triggerConnectionClose); + return await RunPublicGetWorkspacePullDiagnosticsAsync(testLspServer, previousResults, useProgress, category, triggerConnectionClose); } } @@ -114,6 +114,7 @@ private protected static async Task> RunPub TestLspServer testLspServer, ImmutableArray<(string resultId, TextDocumentIdentifier identifier)>? previousResults, bool useProgress, + string? category, bool triggerConnectionClose) { await testLspServer.WaitForDiagnosticsAsync(); @@ -121,7 +122,7 @@ private protected static async Task> RunPub BufferedProgress? progress = useProgress ? BufferedProgress.Create(null) : null; var diagnosticsTask = testLspServer.ExecuteRequestAsync( Methods.WorkspaceDiagnosticName, - CreateProposedWorkspaceDiagnosticParams(previousResults, progress), + CreateProposedWorkspaceDiagnosticParams(previousResults, progress, category), CancellationToken.None).ConfigureAwait(false); if (triggerConnectionClose) @@ -147,8 +148,9 @@ private protected static async Task> RunPub } private static WorkspaceDiagnosticParams CreateProposedWorkspaceDiagnosticParams( - ImmutableArray<(string resultId, TextDocumentIdentifier identifier)>? previousResults = null, - IProgress? progress = null) + ImmutableArray<(string resultId, TextDocumentIdentifier identifier)>? previousResults, + IProgress? progress, + string? category) { var previousResultsLsp = previousResults?.Select(r => new PreviousResultId { @@ -158,7 +160,8 @@ private static WorkspaceDiagnosticParams CreateProposedWorkspaceDiagnosticParams return new WorkspaceDiagnosticParams { PreviousResultId = previousResultsLsp, - PartialResultToken = progress + PartialResultToken = progress, + Identifier = category }; } @@ -166,12 +169,12 @@ private static TestDiagnosticResult ConvertWorkspaceDiagnosticResult(SumType CreateDiagnosticParamsFromPreviousReports(ImmutableArray results) { - - return results.Select(r => (r.ResultId, r.TextDocument)).ToImmutableArray(); + // If there was no resultId provided in the response, we cannot create previous results for it. + return results.Where(r => r.ResultId != null).Select(r => (r.ResultId!, r.TextDocument)).ToImmutableArray(); } private protected static VSInternalDocumentDiagnosticsParams CreateDocumentDiagnosticParams( @@ -231,10 +234,9 @@ private protected static Task> RunGetDocume bool useVSDiagnostics, string? previousResultId = null, bool useProgress = false, - string? category = null, - bool testNonLocalDiagnostics = false) + string? category = null) { - return RunGetDocumentPullDiagnosticsAsync(testLspServer, new VSTextDocumentIdentifier { Uri = uri }, useVSDiagnostics, previousResultId, useProgress, category, testNonLocalDiagnostics); + return RunGetDocumentPullDiagnosticsAsync(testLspServer, new VSTextDocumentIdentifier { Uri = uri }, useVSDiagnostics, previousResultId, useProgress, category); } private protected static async Task> RunGetDocumentPullDiagnosticsAsync( @@ -243,14 +245,13 @@ private protected static async Task> RunGet bool useVSDiagnostics, string? previousResultId = null, bool useProgress = false, - string? category = null, - bool testNonLocalDiagnostics = false) + string? category = null) { await testLspServer.WaitForDiagnosticsAsync(); if (useVSDiagnostics) { - Assert.False(testNonLocalDiagnostics, "NonLocalDiagnostics are only supported for public DocumentPullHandler"); + Assert.False(category == PublicDocumentNonLocalDiagnosticSourceProvider.NonLocal, "NonLocalDiagnostics are only supported for public DocumentPullHandler"); BufferedProgress? progress = useProgress ? BufferedProgress.Create(null) : null; var diagnostics = await testLspServer.ExecuteRequestAsync( VSInternalMethods.DocumentPullDiagnosticName, @@ -271,7 +272,7 @@ private protected static async Task> RunGet BufferedProgress? progress = useProgress ? BufferedProgress.Create(null) : null; var diagnostics = await testLspServer.ExecuteRequestAsync?>( Methods.TextDocumentDiagnosticName, - CreateProposedDocumentDiagnosticParams(vsTextDocumentIdentifier, previousResultId, progress, testNonLocalDiagnostics), + CreateProposedDocumentDiagnosticParams(vsTextDocumentIdentifier, previousResultId, category, progress), CancellationToken.None).ConfigureAwait(false); if (useProgress) { @@ -298,12 +299,12 @@ private protected static async Task> RunGet static DocumentDiagnosticParams CreateProposedDocumentDiagnosticParams( VSTextDocumentIdentifier vsTextDocumentIdentifier, string? previousResultId, - IProgress? progress, - bool testNonLocalDiagnostics) + string? category, + IProgress? progress) { return new DocumentDiagnosticParams { - Identifier = testNonLocalDiagnostics ? DocumentPullDiagnosticHandler.DocumentNonLocalDiagnosticIdentifier.ToString() : null, + Identifier = category, PreviousResultId = previousResultId, PartialResultToken = progress, TextDocument = vsTextDocumentIdentifier, @@ -361,7 +362,7 @@ private protected static InitializationOptions GetInitializationOptions( /// Helper type to store unified LSP diagnostic results. /// Diagnostics are null when unchanged. /// - private protected record TestDiagnosticResult(TextDocumentIdentifier TextDocument, string ResultId, LSP.Diagnostic[]? Diagnostics) + private protected record TestDiagnosticResult(TextDocumentIdentifier TextDocument, string? ResultId, LSP.Diagnostic[]? Diagnostics) { public Uri Uri { get; } = TextDocument.Uri; } diff --git a/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/DiagnosticRegistrationTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/DiagnosticRegistrationTests.cs new file mode 100644 index 0000000000000..cb85dd18a0c98 --- /dev/null +++ b/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/DiagnosticRegistrationTests.cs @@ -0,0 +1,117 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; +using Newtonsoft.Json.Linq; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Test.Utilities; +using StreamJsonRpc; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.Diagnostics; + +public class DiagnosticRegistrationTests : AbstractLanguageServerProtocolTests +{ + public DiagnosticRegistrationTests(ITestOutputHelper? testOutputHelper) : base(testOutputHelper) + { + } + + [Theory, CombinatorialData] + public async Task TestPublicDiagnosticSourcesAreRegisteredWhenSupported(bool mutatingLspWorkspace) + { + var clientCapabilities = new ClientCapabilities + { + TextDocument = new TextDocumentClientCapabilities + { + Diagnostic = new DiagnosticSetting + { + DynamicRegistration = true, + } + } + }; + var clientCallbackTarget = new ClientCallbackTarget(); + var initializationOptions = new InitializationOptions() + { + CallInitialized = true, + ClientCapabilities = clientCapabilities, + ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer, + ClientTarget = clientCallbackTarget, + }; + + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, initializationOptions); + + var registrations = clientCallbackTarget.GetRegistrations(); + + // 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 => ((JObject)r.RegisterOptions!).ToObject()!); + + 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) + { + var options = Assert.Single(diagnosticRegistrations, (r) => r.Identifier == workspaceSource); + Assert.True(options.WorkspaceDiagnostics); + Assert.True(options.InterFileDependencies); + Assert.True(options.WorkDoneProgress); + } + + // Verify task diagnostics are not present. + Assert.DoesNotContain(diagnosticRegistrations, (r) => r.Identifier == PullDiagnosticCategories.Task); + } + + /// + /// Implements a client side callback target for client/registerCapability to inspect what was registered. + /// + private class ClientCallbackTarget() + { + private readonly List _registrations = new(); + + [JsonRpcMethod(Methods.ClientRegisterCapabilityName, UseSingleObjectParameterDeserialization = true)] + public void ClientRegisterCapability(RegistrationParams registrationParams, CancellationToken _) + { + _registrations.AddRange(registrationParams.Registrations); + } + + /// + /// This is safe to call after 'initialized' has completed because capabilties are dynamically registered in the + /// implementation of the initialized request. Additionally, client/registerCapability is a request (not a notification) + /// which means the server will wait for the client to finish handling it before the server returns from 'initialized'. + /// + public ImmutableArray GetRegistrations() + { + return _registrations.ToImmutableArray(); + } + } +} diff --git a/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/NonLocalDiagnosticTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/NonLocalDiagnosticTests.cs index 5f9e9694c4ec4..24b0be51a26dc 100644 --- a/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/NonLocalDiagnosticTests.cs +++ b/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/NonLocalDiagnosticTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.SolutionCrawler; using Microsoft.CodeAnalysis.Test.Utilities; @@ -39,7 +40,7 @@ internal async Task TestNonLocalDocumentDiagnosticsAreReportedWhenFSAEnabled(boo // and not reported here. await OpenDocumentAsync(testLspServer, document); - var results = await RunGetDocumentPullDiagnosticsAsync(testLspServer, document.GetURI(), useVSDiagnostics: false, testNonLocalDiagnostics: true); + var results = await RunGetDocumentPullDiagnosticsAsync(testLspServer, document.GetURI(), useVSDiagnostics: false, category: PublicDocumentNonLocalDiagnosticSourceProvider.NonLocal); if (fsaEnabled) { Assert.Equal(1, results.Length); @@ -50,7 +51,7 @@ internal async Task TestNonLocalDocumentDiagnosticsAreReportedWhenFSAEnabled(boo Assert.Equal(document.GetURI(), results[0].Uri); // Asking again should give us back unchanged diagnostics. - var results2 = await RunGetDocumentPullDiagnosticsAsync(testLspServer, document.GetURI(), useVSDiagnostics: false, previousResultId: results.Single().ResultId, testNonLocalDiagnostics: true); + var results2 = await RunGetDocumentPullDiagnosticsAsync(testLspServer, document.GetURI(), useVSDiagnostics: false, previousResultId: results.Single().ResultId, category: PublicDocumentNonLocalDiagnosticSourceProvider.NonLocal); Assert.Null(results2[0].Diagnostics); Assert.Equal(results[0].ResultId, results2[0].ResultId); } @@ -59,7 +60,7 @@ internal async Task TestNonLocalDocumentDiagnosticsAreReportedWhenFSAEnabled(boo Assert.Empty(results); // Asking again should give us back unchanged diagnostics. - var results2 = await RunGetDocumentPullDiagnosticsAsync(testLspServer, document.GetURI(), useVSDiagnostics: false, testNonLocalDiagnostics: true); + var results2 = await RunGetDocumentPullDiagnosticsAsync(testLspServer, document.GetURI(), useVSDiagnostics: false, category: PublicDocumentNonLocalDiagnosticSourceProvider.NonLocal); Assert.Empty(results2); } } diff --git a/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/PullDiagnosticTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/PullDiagnosticTests.cs index 6f32ed5d1bb8a..1141ce6e1a12d 100644 --- a/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/PullDiagnosticTests.cs +++ b/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/PullDiagnosticTests.cs @@ -73,14 +73,14 @@ public async Task TestDocumentDiagnosticsForOpenFilesWithFSAOff(bool useVSDiagno } [Theory, CombinatorialData, WorkItem("https://github.com/dotnet/fsharp/issues/15972")] - public async Task TestDocumentDiagnosticsForOpenFilesWithFSAOff_Categories(bool mutatingLspWorkspace) + public async Task TestDocumentDiagnosticsForOpenFilesWithFSAOff_Categories(bool useVSDiagnostics, bool mutatingLspWorkspace) { var markup = @"class A : B {"; var additionalAnalyzers = new DiagnosticAnalyzer[] { new CSharpSyntaxAnalyzer(), new CSharpSemanticAnalyzer() }; await using var testLspServer = await CreateTestWorkspaceWithDiagnosticsAsync( - markup, mutatingLspWorkspace, BackgroundAnalysisScope.OpenFiles, useVSDiagnostics: true, additionalAnalyzers: additionalAnalyzers); + markup, mutatingLspWorkspace, BackgroundAnalysisScope.OpenFiles, useVSDiagnostics, additionalAnalyzers: additionalAnalyzers); // Calling GetTextBuffer will effectively open the file. testLspServer.TestWorkspace.Documents.Single().GetTextBuffer(); @@ -90,35 +90,35 @@ public async Task TestDocumentDiagnosticsForOpenFilesWithFSAOff_Categories(bool await OpenDocumentAsync(testLspServer, document); var syntaxResults = await RunGetDocumentPullDiagnosticsAsync( - testLspServer, document.GetURI(), useVSDiagnostics: true, category: PullDiagnosticCategories.DocumentCompilerSyntax); + testLspServer, document.GetURI(), useVSDiagnostics, category: PullDiagnosticCategories.DocumentCompilerSyntax); var semanticResults = await RunGetDocumentPullDiagnosticsAsync( - testLspServer, document.GetURI(), useVSDiagnostics: true, category: PullDiagnosticCategories.DocumentCompilerSemantic); + testLspServer, document.GetURI(), useVSDiagnostics, category: PullDiagnosticCategories.DocumentCompilerSemantic); Assert.Equal("CS1513", syntaxResults.Single().Diagnostics.Single().Code); Assert.Equal("CS0246", semanticResults.Single().Diagnostics.Single().Code); var syntaxResults2 = await RunGetDocumentPullDiagnosticsAsync( - testLspServer, document.GetURI(), useVSDiagnostics: true, previousResultId: syntaxResults.Single().ResultId, category: PullDiagnosticCategories.DocumentCompilerSyntax); + testLspServer, document.GetURI(), useVSDiagnostics, previousResultId: syntaxResults.Single().ResultId, category: PullDiagnosticCategories.DocumentCompilerSyntax); var semanticResults2 = await RunGetDocumentPullDiagnosticsAsync( - testLspServer, document.GetURI(), useVSDiagnostics: true, previousResultId: semanticResults.Single().ResultId, category: PullDiagnosticCategories.DocumentCompilerSemantic); + testLspServer, document.GetURI(), useVSDiagnostics, previousResultId: semanticResults.Single().ResultId, category: PullDiagnosticCategories.DocumentCompilerSemantic); Assert.Equal(syntaxResults.Single().ResultId, syntaxResults2.Single().ResultId); Assert.Equal(semanticResults.Single().ResultId, semanticResults2.Single().ResultId); var syntaxAnalyzerResults = await RunGetDocumentPullDiagnosticsAsync( - testLspServer, document.GetURI(), useVSDiagnostics: true, category: PullDiagnosticCategories.DocumentAnalyzerSyntax); + testLspServer, document.GetURI(), useVSDiagnostics, category: PullDiagnosticCategories.DocumentAnalyzerSyntax); var semanticAnalyzerResults = await RunGetDocumentPullDiagnosticsAsync( - testLspServer, document.GetURI(), useVSDiagnostics: true, category: PullDiagnosticCategories.DocumentAnalyzerSemantic); + testLspServer, document.GetURI(), useVSDiagnostics, category: PullDiagnosticCategories.DocumentAnalyzerSemantic); Assert.Equal(CSharpSyntaxAnalyzer.RuleId, syntaxAnalyzerResults.Single().Diagnostics.Single().Code); Assert.Equal(CSharpSemanticAnalyzer.RuleId, semanticAnalyzerResults.Single().Diagnostics.Single().Code); var syntaxAnalyzerResults2 = await RunGetDocumentPullDiagnosticsAsync( - testLspServer, document.GetURI(), useVSDiagnostics: true, previousResultId: syntaxAnalyzerResults.Single().ResultId, category: PullDiagnosticCategories.DocumentAnalyzerSyntax); + testLspServer, document.GetURI(), useVSDiagnostics, previousResultId: syntaxAnalyzerResults.Single().ResultId, category: PullDiagnosticCategories.DocumentAnalyzerSyntax); var semanticAnalyzerResults2 = await RunGetDocumentPullDiagnosticsAsync( - testLspServer, document.GetURI(), useVSDiagnostics: true, previousResultId: semanticAnalyzerResults.Single().ResultId, category: PullDiagnosticCategories.DocumentAnalyzerSemantic); + testLspServer, document.GetURI(), useVSDiagnostics, previousResultId: semanticAnalyzerResults.Single().ResultId, category: PullDiagnosticCategories.DocumentAnalyzerSemantic); Assert.Equal(syntaxAnalyzerResults.Single().ResultId, syntaxAnalyzerResults2.Single().ResultId); Assert.Equal(semanticAnalyzerResults.Single().ResultId, semanticAnalyzerResults2.Single().ResultId); @@ -181,37 +181,14 @@ static void Main(string[] args) } [Theory, CombinatorialData] - public async Task TestDocumentTodoCommentsDiagnosticsForOpenFile_NoCategory(bool useVSDiagnostics, bool mutatingLspWorkspace) + public async Task TestDocumentTodoCommentsDiagnosticsForOpenFile_Category(bool mutatingLspWorkspace) { var markup = @" // todo: goo class A { }"; - await using var testLspServer = await CreateTestWorkspaceWithDiagnosticsAsync(markup, mutatingLspWorkspace, BackgroundAnalysisScope.OpenFiles, useVSDiagnostics); - - // Calling GetTextBuffer will effectively open the file. - testLspServer.TestWorkspace.Documents.Single().GetTextBuffer(); - - var document = testLspServer.GetCurrentSolution().Projects.Single().Documents.Single(); - - await OpenDocumentAsync(testLspServer, document); - - var results = await RunGetDocumentPullDiagnosticsAsync( - testLspServer, document.GetURI(), useVSDiagnostics); - - Assert.Empty(results.Single().Diagnostics); - } - - [Theory, CombinatorialData] - public async Task TestDocumentTodoCommentsDiagnosticsForOpenFile_Category(bool useVSDiagnostics, bool mutatingLspWorkspace) - { - var markup = -@" -// todo: goo -class A { -}"; - await using var testLspServer = await CreateTestWorkspaceWithDiagnosticsAsync(markup, mutatingLspWorkspace, BackgroundAnalysisScope.OpenFiles, useVSDiagnostics); + await using var testLspServer = await CreateTestWorkspaceWithDiagnosticsAsync(markup, mutatingLspWorkspace, BackgroundAnalysisScope.OpenFiles, useVSDiagnostics: true); // Calling GetTextBuffer will effectively open the file. testLspServer.TestWorkspace.Documents.Single().GetTextBuffer(); @@ -221,17 +198,10 @@ class A { await OpenDocumentAsync(testLspServer, document); var results = await RunGetDocumentPullDiagnosticsAsync( - testLspServer, document.GetURI(), useVSDiagnostics, category: PullDiagnosticCategories.Task); + testLspServer, document.GetURI(), useVSDiagnostics: true, category: PullDiagnosticCategories.Task); - if (useVSDiagnostics) - { - Assert.Equal("TODO", results.Single().Diagnostics.Single().Code); - Assert.Equal("todo: goo", results.Single().Diagnostics.Single().Message); - } - else - { - Assert.Empty(results.Single().Diagnostics); - } + Assert.Equal("TODO", results.Single().Diagnostics.Single().Code); + Assert.Equal("todo: goo", results.Single().Diagnostics.Single().Message); } [Theory, CombinatorialData] @@ -1035,7 +1005,7 @@ public async Task TestWorkspaceDiagnosticsForClosedFilesWithWithRunCodeAnalysisF } [Theory, CombinatorialData] - public async Task TestWorkspaceTodoForClosedFilesWithFSAOffAndTodoOff(bool useVSDiagnostics, bool mutatingLspWorkspace) + public async Task TestWorkspaceTodoForClosedFilesWithFSAOffAndTodoOff(bool mutatingLspWorkspace) { var markup1 = @" @@ -1043,15 +1013,15 @@ public async Task TestWorkspaceTodoForClosedFilesWithFSAOffAndTodoOff(bool useVS class A { }"; await using var testLspServer = await CreateTestWorkspaceWithDiagnosticsAsync( - [markup1], mutatingLspWorkspace, BackgroundAnalysisScope.OpenFiles, useVSDiagnostics); + [markup1], mutatingLspWorkspace, BackgroundAnalysisScope.OpenFiles, useVSDiagnostics: true); - var results = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics, includeTaskListItems: false, category: PullDiagnosticCategories.Task); + var results = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics: true, includeTaskListItems: false, category: PullDiagnosticCategories.Task); Assert.Equal(0, results.Length); } [Theory, CombinatorialData] - public async Task TestWorkspaceTodoForClosedFilesWithFSAOffAndTodoOn(bool useVSDiagnostics, bool mutatingLspWorkspace) + public async Task TestWorkspaceTodoForClosedFilesWithFSAOffAndTodoOn(bool mutatingLspWorkspace) { var markup1 = @" @@ -1059,21 +1029,14 @@ public async Task TestWorkspaceTodoForClosedFilesWithFSAOffAndTodoOn(bool useVSD class A { }"; await using var testLspServer = await CreateTestWorkspaceWithDiagnosticsAsync( - [markup1], mutatingLspWorkspace, BackgroundAnalysisScope.OpenFiles, useVSDiagnostics); + [markup1], mutatingLspWorkspace, BackgroundAnalysisScope.OpenFiles, useVSDiagnostics: true); - var results = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics, includeTaskListItems: true, category: PullDiagnosticCategories.Task); + var results = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics: true, includeTaskListItems: true, category: PullDiagnosticCategories.Task); - if (useVSDiagnostics) - { - Assert.Equal(1, results.Length); - Assert.Equal("TODO", results[0].Diagnostics.Single().Code); - Assert.Equal("todo: goo", results[0].Diagnostics.Single().Message); - Assert.Equal(VSDiagnosticRank.Default, ((VSDiagnostic)results[0].Diagnostics.Single()).DiagnosticRank); - } - else - { - Assert.Empty(results); - } + Assert.Equal(1, results.Length); + Assert.Equal("TODO", results[0].Diagnostics.Single().Code); + Assert.Equal("todo: goo", results[0].Diagnostics.Single().Message); + Assert.Equal(VSDiagnosticRank.Default, ((VSDiagnostic)results[0].Diagnostics.Single()).DiagnosticRank); } [Theory] @@ -1108,7 +1071,7 @@ class A { } [Theory, CombinatorialData] - public async Task TestWorkspaceTodoForClosedFilesWithFSAOnAndTodoOff(bool useVSDiagnostics, bool mutatingLspWorkspace) + public async Task TestWorkspaceTodoForClosedFilesWithFSAOnAndTodoOff(bool mutatingLspWorkspace) { var markup1 = @" @@ -1116,24 +1079,15 @@ public async Task TestWorkspaceTodoForClosedFilesWithFSAOnAndTodoOff(bool useVSD class A { }"; await using var testLspServer = await CreateTestWorkspaceWithDiagnosticsAsync( - [markup1], mutatingLspWorkspace, BackgroundAnalysisScope.FullSolution, useVSDiagnostics); + [markup1], mutatingLspWorkspace, BackgroundAnalysisScope.FullSolution, useVSDiagnostics: true); - var results = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics, includeTaskListItems: false, category: PullDiagnosticCategories.Task); + var results = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics: true, includeTaskListItems: false, category: PullDiagnosticCategories.Task); - if (useVSDiagnostics) - { - Assert.Equal(0, results.Length); - } - else - { - Assert.Equal(2, results.Length); - Assert.Empty(results[0].Diagnostics); - Assert.Empty(results[1].Diagnostics); - } + Assert.Equal(0, results.Length); } [Theory, CombinatorialData] - public async Task TestWorkspaceTodoForClosedFilesWithFSAOnAndTodoOn(bool useVSDiagnostics, bool mutatingLspWorkspace) + public async Task TestWorkspaceTodoForClosedFilesWithFSAOnAndTodoOn(bool mutatingLspWorkspace) { var markup1 = @" @@ -1141,28 +1095,18 @@ public async Task TestWorkspaceTodoForClosedFilesWithFSAOnAndTodoOn(bool useVSDi class A { }"; await using var testLspServer = await CreateTestWorkspaceWithDiagnosticsAsync( - [markup1], mutatingLspWorkspace, BackgroundAnalysisScope.FullSolution, useVSDiagnostics); - - var results = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics, includeTaskListItems: true, category: PullDiagnosticCategories.Task); + [markup1], mutatingLspWorkspace, BackgroundAnalysisScope.FullSolution, useVSDiagnostics: true); - if (useVSDiagnostics) - { - Assert.Equal(1, results.Length); + var results = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics: true, includeTaskListItems: true, category: PullDiagnosticCategories.Task); - Assert.Equal("TODO", results[0].Diagnostics.Single().Code); - Assert.Equal("todo: goo", results[0].Diagnostics.Single().Message); - } - else - { - Assert.Equal(2, results.Length); + Assert.Equal(1, results.Length); - Assert.Empty(results[0].Diagnostics); - Assert.Empty(results[1].Diagnostics); - } + Assert.Equal("TODO", results[0].Diagnostics.Single().Code); + Assert.Equal("todo: goo", results[0].Diagnostics.Single().Message); } [Theory, CombinatorialData] - public async Task TestWorkspaceTodoAndDiagnosticForClosedFilesWithFSAOnAndTodoOn(bool useVSDiagnostics, bool mutatingLspWorkspace) + public async Task TestWorkspaceTodoAndDiagnosticForClosedFilesWithFSAOnAndTodoOn(bool mutatingLspWorkspace) { var markup1 = @" @@ -1170,24 +1114,12 @@ public async Task TestWorkspaceTodoAndDiagnosticForClosedFilesWithFSAOnAndTodoOn class A { "; await using var testLspServer = await CreateTestWorkspaceWithDiagnosticsAsync( - [markup1], mutatingLspWorkspace, BackgroundAnalysisScope.FullSolution, useVSDiagnostics); - - var results = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics, includeTaskListItems: true, category: PullDiagnosticCategories.Task); - - if (useVSDiagnostics) - { - Assert.Equal(1, results.Length); - - Assert.Equal("TODO", results[0].Diagnostics![0].Code); - } - else - { - Assert.Equal(2, results.Length); + [markup1], mutatingLspWorkspace, BackgroundAnalysisScope.FullSolution, useVSDiagnostics: true); - Assert.Equal("CS1513", results[0].Diagnostics![0].Code); + var results = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics: true, includeTaskListItems: true, category: PullDiagnosticCategories.Task); - Assert.Empty(results[1].Diagnostics); - } + Assert.Equal(1, results.Length); + Assert.Equal("TODO", results[0].Diagnostics![0].Code); } [Theory, CombinatorialData] @@ -1206,9 +1138,9 @@ public async Task EditAndContinue_NoActiveSession(bool mutatingLspWorkspace) } [Theory, CombinatorialData] - public async Task EditAndContinue(bool mutatingLspWorkspace) + public async Task EditAndContinue(bool useVSDiagnostics, bool mutatingLspWorkspace) { - var options = GetInitializationOptions(BackgroundAnalysisScope.OpenFiles, compilerDiagnosticsScope: null, useVSDiagnostics: true); + var options = GetInitializationOptions(BackgroundAnalysisScope.OpenFiles, compilerDiagnosticsScope: null, useVSDiagnostics); var composition = Composition .AddExcludedPartTypes(typeof(EditAndContinueService)) .AddParts(typeof(MockEditAndContinueService)); @@ -1234,7 +1166,7 @@ public async Task EditAndContinue(bool mutatingLspWorkspace) encSessionState.ApplyChangesDiagnostics = [projectDiagnostic, openDocumentDiagnostic1, closedDocumentDiagnostic]; encService.GetDocumentDiagnosticsImpl = (_, _) => [openDocumentDiagnostic2]; - var documentResults1 = await RunGetDocumentPullDiagnosticsAsync(testLspServer, openDocument.GetURI(), useVSDiagnostics: true, category: PullDiagnosticCategories.EditAndContinue); + var documentResults1 = await RunGetDocumentPullDiagnosticsAsync(testLspServer, openDocument.GetURI(), useVSDiagnostics, category: PullDiagnosticCategories.EditAndContinue); // both diagnostics located in the open document are reported: AssertEx.Equal( @@ -1242,7 +1174,7 @@ public async Task EditAndContinue(bool mutatingLspWorkspace) "file:///C:/test1.cs -> [ENC_OPEN_DOC1,ENC_OPEN_DOC2]", ], documentResults1.Select(Inspect)); - var workspaceResults1 = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics: true, includeTaskListItems: false, category: PullDiagnosticCategories.EditAndContinue); + var workspaceResults1 = await RunGetWorkspacePullDiagnosticsAsync(testLspServer, useVSDiagnostics, includeTaskListItems: false, category: PullDiagnosticCategories.EditAndContinue); AssertEx.Equal( [ @@ -1256,7 +1188,7 @@ public async Task EditAndContinue(bool mutatingLspWorkspace) diagnosticsRefresher.RequestWorkspaceRefresh(); var documentResults2 = await RunGetDocumentPullDiagnosticsAsync( - testLspServer, openDocument.GetURI(), previousResultId: documentResults1.Single().ResultId, useVSDiagnostics: true, category: PullDiagnosticCategories.EditAndContinue); + testLspServer, openDocument.GetURI(), previousResultId: documentResults1.Single().ResultId, useVSDiagnostics: useVSDiagnostics, category: PullDiagnosticCategories.EditAndContinue); AssertEx.Equal( [ @@ -1264,7 +1196,7 @@ public async Task EditAndContinue(bool mutatingLspWorkspace) ], documentResults2.Select(Inspect)); var workspaceResults2 = await RunGetWorkspacePullDiagnosticsAsync( - testLspServer, useVSDiagnostics: true, previousResults: workspaceResults1.SelectAsArray(r => (r.ResultId, r.TextDocument)), includeTaskListItems: false, category: PullDiagnosticCategories.EditAndContinue); + testLspServer, useVSDiagnostics, previousResults: CreateDiagnosticParamsFromPreviousReports(workspaceResults1), includeTaskListItems: false, category: PullDiagnosticCategories.EditAndContinue); AssertEx.Equal( [ "file:///C:/test2.cs -> []", @@ -1277,14 +1209,14 @@ public async Task EditAndContinue(bool mutatingLspWorkspace) diagnosticsRefresher.RequestWorkspaceRefresh(); var documentResults3 = await RunGetDocumentPullDiagnosticsAsync( - testLspServer, openDocument.GetURI(), previousResultId: documentResults2.Single().ResultId, useVSDiagnostics: true, category: PullDiagnosticCategories.EditAndContinue); + testLspServer, openDocument.GetURI(), previousResultId: documentResults2.Single().ResultId, useVSDiagnostics: useVSDiagnostics, category: PullDiagnosticCategories.EditAndContinue); AssertEx.Equal( [ "file:///C:/test1.cs -> []", ], documentResults3.Select(Inspect)); var workspaceResults3 = await RunGetWorkspacePullDiagnosticsAsync( - testLspServer, useVSDiagnostics: true, previousResults: workspaceResults2.SelectAsArray(r => (r.ResultId, r.TextDocument)), includeTaskListItems: false, category: PullDiagnosticCategories.EditAndContinue); + testLspServer, useVSDiagnostics, previousResults: CreateDiagnosticParamsFromPreviousReports(workspaceResults2), includeTaskListItems: false, category: PullDiagnosticCategories.EditAndContinue); AssertEx.Equal([], workspaceResults3.Select(Inspect)); static DiagnosticData CreateDiagnostic(string id, Document? document = null, Project? project = null) diff --git a/src/Features/LanguageServer/ProtocolUnitTests/ProtocolConversionsTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/ProtocolConversionsTests.cs index 8685d125fc2a5..a9d20fa0ccee6 100644 --- a/src/Features/LanguageServer/ProtocolUnitTests/ProtocolConversionsTests.cs +++ b/src/Features/LanguageServer/ProtocolUnitTests/ProtocolConversionsTests.cs @@ -103,6 +103,47 @@ public void CreateAbsoluteUri_LocalPaths_Unix(string filePath, string expectedAb Assert.Equal(filePath, uri.LocalPath); } + [ConditionalTheory(typeof(WindowsOnly))] + [InlineData("C:\\a\\b", "file:///C:/a/b")] + [InlineData("C:\\a\\b\\", "file:///C:/a/b")] + [InlineData("C:\\a\\\\b", "file:///C:/a//b")] + [InlineData("C:\\%25\ue25b/a\\b", "file:///C:/%2525%EE%89%9B/a/b")] + [InlineData("C:\\%25\ue25b/a\\\\b", "file:///C:/%2525%EE%89%9B/a//b")] + [InlineData("C:\\\u0089\uC7BD", "file:///C:/%C2%89%EC%9E%BD")] + [InlineData("/\\server\ue25b\\%25\ue25b\\b", "file://server/%2525%EE%89%9B/b")] + [InlineData("\\\\server\ue25b\\%25\ue25b\\b", "file://server/%2525%EE%89%9B/b")] + [InlineData("\\\\server\ue25b\\%25\ue25b\\b\\", "file://server/%2525%EE%89%9B/b")] + [InlineData("C:\\ !$&'()+,-;=@[]_~#", "file:///C:/%20!$&'()+,-;=@[]_~%23")] + [InlineData("C:\\ !$&'()+,-;=@[]_~#\ue25b", "file:///C:/%20!$&'()+,-;=@[]_~%23%EE%89%9B")] + [InlineData("C:\\\u0073\u0323\u0307", "file:///C:/s%CC%A3%CC%87")] // combining marks + [InlineData("A:/\\\u200e//", "file:///A://%E2%80%8E//")] // cases from https://github.com/dotnet/runtime/issues/1487 + [InlineData("B:\\/\u200e", "file:///B://%E2%80%8E")] + [InlineData("C:/\\\\-Ā\r", "file:///C:///-%C4%80%0D")] + [InlineData("D:\\\\\\\\\\\u200e", "file:///D://///%E2%80%8E")] + public void CreateRelativePatternBaseUri_LocalPaths_Windows(string filePath, string expectedUri) + { + var uri = ProtocolConversions.CreateRelativePatternBaseUri(filePath); + Assert.Equal(expectedUri, uri.AbsoluteUri); + } + + [ConditionalTheory(typeof(UnixLikeOnly))] + [InlineData("/", "file://")] + [InlineData("/u", "file:///u")] + [InlineData("/unix/", "file:///unix")] + [InlineData("/unix/path", "file:///unix/path")] + [InlineData("/%25\ue25b/\u0089\uC7BD", "file:///%2525%EE%89%9B/%C2%89%EC%9E%BD")] + [InlineData("/!$&'()+,-;=@[]_~#", "file:///!$&'()+,-;=@[]_~%23")] + [InlineData("/!$&'()+,-;=@[]_~#", "file:///!$&'()+,-;=@[]_~%23%EE%89%9B")] + [InlineData("/\\\u200e//", "file:////%E2%80%8E//")] // cases from https://github.com/dotnet/runtime/issues/1487 + [InlineData("\\/\u200e", "file:////%E2%80%8E")] + [InlineData("/\\\\-Ā\r", "file://///-%C4%80%0D")] + [InlineData("\\\\\\\\\\\u200e", "file:///////%E2%80%8E")] + public void CreateRelativePatternBaseUri_LocalPaths_Unix(string filePath, string expectedRelativeUri) + { + var uri = ProtocolConversions.CreateRelativePatternBaseUri(filePath); + Assert.Equal(expectedRelativeUri, uri.AbsoluteUri); + } + [ConditionalTheory(typeof(UnixLikeOnly))] [InlineData("/a/./b", "file:///a/./b", "file:///a/b")] [InlineData("/a/../b", "file:///a/../b", "file:///b")] diff --git a/src/Features/LanguageServer/ProtocolUnitTests/Symbols/DocumentSymbolsTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/Symbols/DocumentSymbolsTests.cs index ed5bbf322c5e3..f4abf45dec3ce 100644 --- a/src/Features/LanguageServer/ProtocolUnitTests/Symbols/DocumentSymbolsTests.cs +++ b/src/Features/LanguageServer/ProtocolUnitTests/Symbols/DocumentSymbolsTests.cs @@ -181,7 +181,7 @@ private static LSP.DocumentSymbol CreateDocumentSymbol(LSP.SymbolKind kind, stri { var children = parent.Children.ToList(); children.Add(documentSymbol); - parent.Children = children.ToArray(); + parent.Children = [.. children]; } return documentSymbol; diff --git a/src/Features/Lsif/Generator/Generator.cs b/src/Features/Lsif/Generator/Generator.cs index 87db8cfbcc8f6..57dfd14b41197 100644 --- a/src/Features/Lsif/Generator/Generator.cs +++ b/src/Features/Lsif/Generator/Generator.cs @@ -130,7 +130,10 @@ public async Task GenerateForProjectAsync( } }; - var documents = (await project.GetAllRegularAndSourceGeneratedDocumentsAsync(cancellationToken)).ToList(); + var documents = new List(); + await foreach (var document in project.GetAllRegularAndSourceGeneratedDocumentsAsync(cancellationToken)) + documents.Add(document); + var tasks = new List(); foreach (var document in documents) { diff --git a/src/Features/Test/EditAndContinue/ActiveStatementsMapTests.cs b/src/Features/Test/EditAndContinue/ActiveStatementsMapTests.cs index d3bea48ce86cc..8f2b12dc7db8c 100644 --- a/src/Features/Test/EditAndContinue/ActiveStatementsMapTests.cs +++ b/src/Features/Test/EditAndContinue/ActiveStatementsMapTests.cs @@ -262,8 +262,7 @@ SourceFileSpan Span(int startLine, int startColumn, int endLine, int endColumn) => new("a.cs", new(new(startLine, startColumn), new(endLine, endColumn))); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public void NonRemappableRegionOrdering(bool reverse) { var source1 = @@ -312,8 +311,7 @@ static void M() Assert.Equal(unmappedActiveStatements[0].Statement.Span, activeStatement.FileSpan.Span); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public void SubSpan(bool reverse) { var source1 = diff --git a/src/Features/Test/EditAndContinue/EditAndContinueMethodDebugInfoReaderTests.cs b/src/Features/Test/EditAndContinue/EditAndContinueMethodDebugInfoReaderTests.cs index a6c802e7ab9ee..3534e172218e2 100644 --- a/src/Features/Test/EditAndContinue/EditAndContinueMethodDebugInfoReaderTests.cs +++ b/src/Features/Test/EditAndContinue/EditAndContinueMethodDebugInfoReaderTests.cs @@ -110,7 +110,7 @@ public static void Main() Assert.False(reader.TryGetDocumentChecksum("/A/C.cs", out _, out _)); Assert.True(reader.TryGetDocumentChecksum("/a/c.cs", out var actualChecksum, out var actualAlgorithm)); - Assert.Equal("21-C8-B2-D7-A3-6B-49-C7-57-DF-67-B8-1F-75-DF-6A-64-FD-59-22", BitConverter.ToString(actualChecksum.ToArray())); + Assert.Equal("21-C8-B2-D7-A3-6B-49-C7-57-DF-67-B8-1F-75-DF-6A-64-FD-59-22", BitConverter.ToString([.. actualChecksum])); Assert.Equal(new Guid("ff1816ec-aa5e-4d10-87f7-6f4963833460"), actualAlgorithm); } } diff --git a/src/Features/TestUtilities/Diagnostics/TestDiagnosticAnalyzerDriver.cs b/src/Features/TestUtilities/Diagnostics/TestDiagnosticAnalyzerDriver.cs index 8419a40314bd9..f5767ebb02d68 100644 --- a/src/Features/TestUtilities/Diagnostics/TestDiagnosticAnalyzerDriver.cs +++ b/src/Features/TestUtilities/Diagnostics/TestDiagnosticAnalyzerDriver.cs @@ -55,7 +55,9 @@ private async Task> GetDiagnosticsAsync( if (getDocumentDiagnostics) { var text = await document.GetTextAsync().ConfigureAwait(false); - var dxs = await _diagnosticAnalyzerService.GetDiagnosticsAsync(project.Solution, project.Id, document.Id, _includeSuppressedDiagnostics, _includeNonLocalDocumentDiagnostics, CancellationToken.None); + var dxs = await _diagnosticAnalyzerService.GetDiagnosticsForIdsAsync( + project.Solution, project.Id, document.Id, diagnosticIds: null, shouldIncludeAnalyzer: null, + _includeSuppressedDiagnostics, includeLocalDocumentDiagnostics: true, _includeNonLocalDocumentDiagnostics, CancellationToken.None); documentDiagnostics = await CodeAnalysis.Diagnostics.Extensions.ToDiagnosticsAsync( filterSpan is null ? dxs.Where(d => d.DataLocation.DocumentId != null) @@ -66,7 +68,9 @@ filterSpan is null if (getProjectDiagnostics) { - var dxs = await _diagnosticAnalyzerService.GetDiagnosticsAsync(project.Solution, project.Id, documentId: null, _includeSuppressedDiagnostics, _includeNonLocalDocumentDiagnostics, CancellationToken.None); + var dxs = await _diagnosticAnalyzerService.GetDiagnosticsForIdsAsync( + project.Solution, project.Id, documentId: null, diagnosticIds: null, shouldIncludeAnalyzer: null, + _includeSuppressedDiagnostics, includeLocalDocumentDiagnostics: true, _includeNonLocalDocumentDiagnostics, CancellationToken.None); projectDiagnostics = await CodeAnalysis.Diagnostics.Extensions.ToDiagnosticsAsync(dxs.Where(d => d.DocumentId is null), project, CancellationToken.None); } diff --git a/src/Features/TestUtilities/EditAndContinue/ActiveStatementsDescription.cs b/src/Features/TestUtilities/EditAndContinue/ActiveStatementsDescription.cs index 916f551d29251..ec33eac92f782 100644 --- a/src/Features/TestUtilities/EditAndContinue/ActiveStatementsDescription.cs +++ b/src/Features/TestUtilities/EditAndContinue/ActiveStatementsDescription.cs @@ -100,7 +100,7 @@ internal static ImmutableArray CreateActiveStatementMap var activeStatementMarkers = SourceMarkers.GetActiveSpans(markedSource).ToArray(); var exceptionRegionMarkers = SourceMarkers.GetExceptionRegions(markedSource); - return activeStatementMarkers.Aggregate( + return [.. activeStatementMarkers.Aggregate( new List(), (list, marker) => { @@ -126,7 +126,7 @@ internal static ImmutableArray CreateActiveStatementMap documentActiveStatements.Add(unmappedActiveStatement.Statement); return SourceMarkers.SetListItem(list, ordinal, unmappedActiveStatement); - }).ToImmutableArray(); + })]; } internal static ImmutableArray GetUnmappedActiveStatements( diff --git a/src/Features/TestUtilities/EditAndContinue/EditAndContinueTestHelpers.cs b/src/Features/TestUtilities/EditAndContinue/EditAndContinueTestHelpers.cs index 7b64d9003233a..df6d379295bcf 100644 --- a/src/Features/TestUtilities/EditAndContinue/EditAndContinueTestHelpers.cs +++ b/src/Features/TestUtilities/EditAndContinue/EditAndContinueTestHelpers.cs @@ -31,7 +31,8 @@ internal abstract class EditAndContinueTestHelpers EditAndContinueCapabilities.AddInstanceFieldToExistingType | EditAndContinueCapabilities.AddStaticFieldToExistingType | EditAndContinueCapabilities.AddMethodToExistingType | - EditAndContinueCapabilities.NewTypeDefinition; + EditAndContinueCapabilities.NewTypeDefinition | + EditAndContinueCapabilities.AddExplicitInterfaceImplementation; public const EditAndContinueCapabilities Net6RuntimeCapabilities = Net5RuntimeCapabilities | diff --git a/src/Features/TestUtilities/EditAndContinue/SourceMarkers.cs b/src/Features/TestUtilities/EditAndContinue/SourceMarkers.cs index 0efea7cc9ab11..1c43208de1796 100644 --- a/src/Features/TestUtilities/EditAndContinue/SourceMarkers.cs +++ b/src/Features/TestUtilities/EditAndContinue/SourceMarkers.cs @@ -106,7 +106,7 @@ public static (int id, TextSpan span)[] GetTrackingSpans(string src) Contract.ThrowIfTrue(result.Any(span => span == default)); - return result.ToArray(); + return [.. result]; } public static ImmutableArray> GetExceptionRegions(string markedSource) diff --git a/src/Features/VisualBasic/Portable/QuickInfo/VisualBasicSemanticQuickInfoProvider.vb b/src/Features/VisualBasic/Portable/QuickInfo/VisualBasicSemanticQuickInfoProvider.vb index 77f2abd72d4b7..695f312588b15 100644 --- a/src/Features/VisualBasic/Portable/QuickInfo/VisualBasicSemanticQuickInfoProvider.vb +++ b/src/Features/VisualBasic/Portable/QuickInfo/VisualBasicSemanticQuickInfoProvider.vb @@ -197,7 +197,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.QuickInfo Return QuickInfoItem.Create(token.Span, sections:=ImmutableArray.Create(QuickInfoSection.Create(QuickInfoSectionKinds.Description, ImmutableArray.Create(New TaggedText(TextTags.Text, VBFeaturesResources.Multiple_Types))))) End If - Return Await CreateContentAsync(services, semanticModel, token, New TokenInformation(types), supportedPlatforms:=Nothing, options, cancellationToken).ConfigureAwait(False) + Return Await CreateContentAsync(services, semanticModel, token, New TokenInformation(types), supportedPlatforms:=Nothing, options, onTheFlyDocsElement:=Nothing, cancellationToken).ConfigureAwait(False) End Function Private Shared Function BuildContentForIntrinsicOperator( diff --git a/src/Features/VisualBasicTest/EditAndContinue/ActiveStatementTests.vb b/src/Features/VisualBasicTest/EditAndContinue/ActiveStatementTests.vb index 95b726d40e429..e00b01e5dd200 100644 --- a/src/Features/VisualBasicTest/EditAndContinue/ActiveStatementTests.vb +++ b/src/Features/VisualBasicTest/EditAndContinue/ActiveStatementTests.vb @@ -5137,7 +5137,7 @@ End Class edits.VerifySemanticDiagnostics( active, - capabilities:=EditAndContinueCapabilities.NewTypeDefinition) + capabilities:=EditAndContinueCapabilities.NewTypeDefinition Or EditAndContinueCapabilities.AddExplicitInterfaceImplementation) End Sub @@ -5167,7 +5167,7 @@ End Class edits.VerifySemanticDiagnostics( active, - capabilities:=EditAndContinueCapabilities.NewTypeDefinition) + capabilities:=EditAndContinueCapabilities.NewTypeDefinition Or EditAndContinueCapabilities.AddExplicitInterfaceImplementation) End Sub @@ -5284,7 +5284,7 @@ End Class edits.VerifySemanticDiagnostics( active, - capabilities:=EditAndContinueCapabilities.NewTypeDefinition) + capabilities:=EditAndContinueCapabilities.NewTypeDefinition Or EditAndContinueCapabilities.AddExplicitInterfaceImplementation) End Sub @@ -5403,7 +5403,7 @@ End Class edits.VerifySemanticDiagnostics( active, - capabilities:=EditAndContinueCapabilities.NewTypeDefinition) + capabilities:=EditAndContinueCapabilities.NewTypeDefinition Or EditAndContinueCapabilities.AddExplicitInterfaceImplementation) End Sub @@ -5432,7 +5432,7 @@ End Class Dim active = GetActiveStatements(src1, src2) edits.VerifySemanticDiagnostics(active, - capabilities:=EditAndContinueCapabilities.NewTypeDefinition) + capabilities:=EditAndContinueCapabilities.NewTypeDefinition Or EditAndContinueCapabilities.AddExplicitInterfaceImplementation) End Sub @@ -5515,7 +5515,7 @@ End Class ' No rude edit since the AS is within the nested function. edits.VerifySemanticDiagnostics(active, - capabilities:=EditAndContinueCapabilities.NewTypeDefinition) + capabilities:=EditAndContinueCapabilities.NewTypeDefinition Or EditAndContinueCapabilities.AddExplicitInterfaceImplementation) End Sub diff --git a/src/Features/VisualBasicTest/EditAndContinue/StatementEditingTests.vb b/src/Features/VisualBasicTest/EditAndContinue/StatementEditingTests.vb index d31fbfda9c867..a6550aca7c311 100644 --- a/src/Features/VisualBasicTest/EditAndContinue/StatementEditingTests.vb +++ b/src/Features/VisualBasicTest/EditAndContinue/StatementEditingTests.vb @@ -5309,7 +5309,7 @@ End Class VerifySemanticDiagnostics( editScript:=edits, targetFrameworks:={TargetFramework.Mscorlib40AndSystemCore}, - capabilities:=EditAndContinueCapabilities.NewTypeDefinition) + capabilities:=EditAndContinueCapabilities.NewTypeDefinition Or EditAndContinueCapabilities.AddExplicitInterfaceImplementation) End Sub #End Region @@ -5419,7 +5419,7 @@ End Class VerifySemanticDiagnostics( edits, targetFrameworks:={TargetFramework.MinimalAsync}, - capabilities:=EditAndContinueCapabilities.NewTypeDefinition) + capabilities:=EditAndContinueCapabilities.NewTypeDefinition Or EditAndContinueCapabilities.AddExplicitInterfaceImplementation) End Sub diff --git a/src/Features/VisualBasicTest/EditAndContinue/TopLevelEditingTests.vb b/src/Features/VisualBasicTest/EditAndContinue/TopLevelEditingTests.vb index 3eaf69d316e74..b1dd74eafa024 100644 --- a/src/Features/VisualBasicTest/EditAndContinue/TopLevelEditingTests.vb +++ b/src/Features/VisualBasicTest/EditAndContinue/TopLevelEditingTests.vb @@ -4789,6 +4789,10 @@ End Structure "Update [Function F() As Task(Of String)]@11 -> [Async Function F() As Task(Of String)]@11") edits.VerifySemanticDiagnostics( + capabilities:=EditAndContinueCapabilities.NewTypeDefinition Or EditAndContinueCapabilities.AddExplicitInterfaceImplementation) + + edits.VerifySemanticDiagnostics( + {Diagnostic(RudeEditKind.MakeMethodAsyncNotSupportedByRuntime, "Async Function F()")}, capabilities:=EditAndContinueCapabilities.NewTypeDefinition) End Sub @@ -5268,7 +5272,7 @@ End Class" "Update [Function F() As Task(Of String)]@11 -> [Iterator Function F() As Task(Of String)]@11") edits.VerifySemanticDiagnostics( - capabilities:=EditAndContinueCapabilities.NewTypeDefinition) + capabilities:=EditAndContinueCapabilities.NewTypeDefinition Or EditAndContinueCapabilities.AddExplicitInterfaceImplementation) End Sub diff --git a/src/Scripting/CSharpTest/InteractiveSessionTests.cs b/src/Scripting/CSharpTest/InteractiveSessionTests.cs index 10c6016032737..24e7362698c70 100644 --- a/src/Scripting/CSharpTest/InteractiveSessionTests.cs +++ b/src/Scripting/CSharpTest/InteractiveSessionTests.cs @@ -1215,7 +1215,7 @@ public interface I public class C : I { public int F() => 1; -}", new MetadataReference[] { NetStandard13.SystemRuntime, lib1.ToMetadataReference() }); +}", new MetadataReference[] { NetStandard13.References.SystemRuntime, lib1.ToMetadataReference() }); lib2.Emit(file2.Path); @@ -1277,7 +1277,7 @@ public void ExtensionPriority1() var main = CreateCSharpCompilation( @"public static class M { public static readonly C X = new C(); }", - new MetadataReference[] { NetStandard13.SystemRuntime, libExe.ToMetadataReference() }, + new MetadataReference[] { NetStandard13.References.SystemRuntime, libExe.ToMetadataReference() }, mainName); var exeImage = libExe.EmitToArray(); @@ -1307,7 +1307,7 @@ public void ExtensionPriority2() var main = CreateCSharpCompilation( @"public static class M { public static readonly C X = new C(); }", - new MetadataReference[] { NetStandard13.SystemRuntime, libExe.ToMetadataReference() }, + new MetadataReference[] { NetStandard13.References.SystemRuntime, libExe.ToMetadataReference() }, mainName); var exeImage = libExe.EmitToArray(); diff --git a/src/Scripting/CoreTestUtilities/TestCompilationFactory.cs b/src/Scripting/CoreTestUtilities/TestCompilationFactory.cs index b48f3ddb82ca7..eb729b278204d 100644 --- a/src/Scripting/CoreTestUtilities/TestCompilationFactory.cs +++ b/src/Scripting/CoreTestUtilities/TestCompilationFactory.cs @@ -22,7 +22,7 @@ internal static Compilation CreateCSharpCompilationWithCorlib(string source, str return CSharpCompilation.Create( assemblyName ?? Guid.NewGuid().ToString(), new[] { CSharp.SyntaxFactory.ParseSyntaxTree(SourceText.From(source, encoding: null, SourceHashAlgorithms.Default)) }, - new[] { NetStandard13.SystemRuntime }, + new[] { NetStandard13.References.SystemRuntime }, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); } @@ -31,7 +31,7 @@ internal static Compilation CreateVisualBasicCompilationWithCorlib(string source return VisualBasicCompilation.Create( assemblyName ?? Guid.NewGuid().ToString(), new[] { VisualBasic.SyntaxFactory.ParseSyntaxTree(SourceText.From(source, encoding: null, SourceHashAlgorithms.Default)) }, - new[] { NetStandard13.SystemRuntime }, + new[] { NetStandard13.References.SystemRuntime }, new VisualBasicCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); } diff --git a/src/Tools/AnalyzerRunner/IncrementalAnalyzerRunner.cs b/src/Tools/AnalyzerRunner/IncrementalAnalyzerRunner.cs index 400df749f61f7..a501677df6270 100644 --- a/src/Tools/AnalyzerRunner/IncrementalAnalyzerRunner.cs +++ b/src/Tools/AnalyzerRunner/IncrementalAnalyzerRunner.cs @@ -53,7 +53,7 @@ public async Task RunAsync(CancellationToken cancellationToken) if (usePersistentStorage) { var persistentStorageService = _workspace.Services.SolutionServices.GetPersistentStorageService(); - await using var persistentStorage = await persistentStorageService.GetStorageAsync(SolutionKey.ToSolutionKey(_workspace.CurrentSolution), cancellationToken).ConfigureAwait(false); + var persistentStorage = await persistentStorageService.GetStorageAsync(SolutionKey.ToSolutionKey(_workspace.CurrentSolution), cancellationToken).ConfigureAwait(false); if (persistentStorage is NoOpPersistentStorage) { throw new InvalidOperationException("Benchmark is not configured to use persistent storage."); diff --git a/src/Tools/BuildBoss/SolutionCheckerUtil.cs b/src/Tools/BuildBoss/SolutionCheckerUtil.cs index 5b63e86c46a2f..92256e6695dc3 100644 --- a/src/Tools/BuildBoss/SolutionCheckerUtil.cs +++ b/src/Tools/BuildBoss/SolutionCheckerUtil.cs @@ -109,7 +109,7 @@ private bool CheckDuplicate(TextWriter textWriter, out Dictionary private bool CheckProjectSystemGuid(TextWriter textWriter, IEnumerable dataList) { - Guid getExpectedGuid(ProjectData data) + static Guid getExpectedGuid(ProjectData data) { var util = data.ProjectUtil; switch (ProjectEntryUtil.GetProjectFileType(data.FilePath)) diff --git a/src/Tools/ExternalAccess/Copilot/Analyzer/IExternalCSharpCopilotCodeAnalysisService.cs b/src/Tools/ExternalAccess/Copilot/Analyzer/IExternalCSharpCopilotCodeAnalysisService.cs index 0314ebdb89ff3..e2e5d1d11dffb 100644 --- a/src/Tools/ExternalAccess/Copilot/Analyzer/IExternalCSharpCopilotCodeAnalysisService.cs +++ b/src/Tools/ExternalAccess/Copilot/Analyzer/IExternalCSharpCopilotCodeAnalysisService.cs @@ -17,4 +17,5 @@ internal interface IExternalCSharpCopilotCodeAnalysisService Task> AnalyzeDocumentAsync(Document document, TextSpan? span, string promptTitle, CancellationToken cancellationToken); Task> GetCachedDiagnosticsAsync(Document document, string promptTitle, CancellationToken cancellationToken); Task StartRefinementSessionAsync(Document oldDocument, Document newDocument, Diagnostic? primaryDiagnostic, CancellationToken cancellationToken); + Task GetOnTheFlyDocsAsync(string symbolSignature, ImmutableArray declarationCode, string language, CancellationToken cancellationToken); } diff --git a/src/Tools/ExternalAccess/Copilot/Internal/Analyzer/AbstractCopilotCodeAnalysisService.cs b/src/Tools/ExternalAccess/Copilot/Internal/Analyzer/AbstractCopilotCodeAnalysisService.cs index aae8ad275f898..0f65dd06ac618 100644 --- a/src/Tools/ExternalAccess/Copilot/Internal/Analyzer/AbstractCopilotCodeAnalysisService.cs +++ b/src/Tools/ExternalAccess/Copilot/Internal/Analyzer/AbstractCopilotCodeAnalysisService.cs @@ -28,7 +28,7 @@ internal abstract class AbstractCopilotCodeAnalysisService(IDiagnosticsRefresher { // The _diagnosticsCache is a cache for computed diagnostics via `AnalyzeDocumentAsync`. // Each document maps to a dictionary, which in tern maps a prompt title to a list of existing Diagnostics and a boolean flag. - // The list of diangostics represents the diagnostics computed for the document under the given prompt title, + // The list of diagnostics represents the diagnostics computed for the document under the given prompt title, // the boolean flag indicates whether the diagnostics result is for the entire document. // This cache is used to avoid duplicate analysis calls by storing the computed diagnostics for each document and prompt title. private readonly ConditionalWeakTable Diagnostics, bool IsCompleteResult)>> _diagnosticsCache = new(); @@ -38,6 +38,7 @@ internal abstract class AbstractCopilotCodeAnalysisService(IDiagnosticsRefresher protected abstract Task> AnalyzeDocumentCoreAsync(Document document, TextSpan? span, string promptTitle, CancellationToken cancellationToken); protected abstract Task> GetCachedDiagnosticsCoreAsync(Document document, string promptTitle, CancellationToken cancellationToken); protected abstract Task StartRefinementSessionCoreAsync(Document oldDocument, Document newDocument, Diagnostic? primaryDiagnostic, CancellationToken cancellationToken); + protected abstract Task GetOnTheFlyDocsCoreAsync(string symbolSignature, ImmutableArray declarationCode, string language, CancellationToken cancellationToken); public Task IsAvailableAsync(CancellationToken cancellationToken) => IsAvailableCoreAsync(cancellationToken); @@ -169,4 +170,12 @@ public async Task StartRefinementSessionAsync(Document oldDocument, Document new if (await service.IsRefineOptionEnabledAsync().ConfigureAwait(false)) await StartRefinementSessionCoreAsync(oldDocument, newDocument, primaryDiagnostic, cancellationToken).ConfigureAwait(false); } + + public async Task GetOnTheFlyDocsAsync(string symbolSignature, ImmutableArray declarationCode, string language, CancellationToken cancellationToken) + { + if (!await IsAvailableAsync(cancellationToken).ConfigureAwait(false)) + return string.Empty; + + return await GetOnTheFlyDocsCoreAsync(symbolSignature, declarationCode, language, cancellationToken).ConfigureAwait(false); + } } diff --git a/src/Tools/ExternalAccess/Copilot/Internal/Analyzer/CSharp/CSharpCopilotCodeAnalysisService.ReflectionWrapper.cs b/src/Tools/ExternalAccess/Copilot/Internal/Analyzer/CSharp/CSharpCopilotCodeAnalysisService.ReflectionWrapper.cs index adb3f8264ab57..425f5366bf931 100644 --- a/src/Tools/ExternalAccess/Copilot/Internal/Analyzer/CSharp/CSharpCopilotCodeAnalysisService.ReflectionWrapper.cs +++ b/src/Tools/ExternalAccess/Copilot/Internal/Analyzer/CSharp/CSharpCopilotCodeAnalysisService.ReflectionWrapper.cs @@ -19,6 +19,7 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Copilot.Internal.Analyzer.CSharp using GetCachedDiagnosticsAsyncDelegateType = Func>>; using IsAvailableAsyncDelegateType = Func>; using StartRefinementSessionAsyncDelegateType = Func; +using GetOnTheFlyDocsAsyncDelegateType = Func, string, CancellationToken, Task>; internal sealed partial class CSharpCopilotCodeAnalysisService { @@ -33,6 +34,7 @@ private sealed class ReflectionWrapper : IExternalCSharpCopilotCodeAnalysisServi private const string AnalyzeDocumentAsyncMethodName = "AnalyzeDocumentAsync"; private const string GetCachedDiagnosticsAsyncMethodName = "GetCachedDiagnosticsAsync"; private const string StartRefinementSessionAsyncMethodName = "StartRefinementSessionAsync"; + private const string GetOnTheFlyDocsAsyncMethodName = "GetOnTheFlyDocsAsync"; // Create and cache closed delegate to ensure we use a singleton object and with better performance. private readonly Type? _analyzerType; @@ -42,6 +44,7 @@ private sealed class ReflectionWrapper : IExternalCSharpCopilotCodeAnalysisServi private readonly Lazy _lazyAnalyzeDocumentAsyncDelegate; private readonly Lazy _lazyGetCachedDiagnosticsAsyncDelegate; private readonly Lazy _lazyStartRefinementSessionAsyncDelegate; + private readonly Lazy _lazyGetOnTheFlyDocsAsyncDelegate; public ReflectionWrapper(IServiceProvider serviceProvider, IVsService brokeredServiceContainer) { @@ -69,6 +72,7 @@ public ReflectionWrapper(IServiceProvider serviceProvider, IVsService(string methodName, Type[] types) where T : Delegate @@ -104,6 +108,9 @@ public ReflectionWrapper(IServiceProvider serviceProvider, IVsService CreateDelegate(StartRefinementSessionAsyncMethodName, [typeof(Document), typeof(Document), typeof(Diagnostic), typeof(CancellationToken)]); + private GetOnTheFlyDocsAsyncDelegateType? CreateGetOnTheFlyDocsAsyncDelegate() + => CreateDelegate(GetOnTheFlyDocsAsyncMethodName, [typeof(string), typeof(ImmutableArray), typeof(string), typeof(CancellationToken)]); + public async Task IsAvailableAsync(CancellationToken cancellationToken) { if (_lazyIsAvailableAsyncDelegate.Value is null) @@ -143,5 +150,13 @@ public Task StartRefinementSessionAsync(Document oldDocument, Document newDocume return _lazyStartRefinementSessionAsyncDelegate.Value(oldDocument, newDocument, primaryDiagnostic, cancellationToken); } + + public async Task GetOnTheFlyDocsAsync(string symbolSignature, ImmutableArray declarationCode, string language, CancellationToken cancellationToken) + { + if (_lazyGetOnTheFlyDocsAsyncDelegate.Value is null) + return string.Empty; + + return await _lazyGetOnTheFlyDocsAsyncDelegate.Value(symbolSignature, declarationCode, language, cancellationToken).ConfigureAwait(false); + } } } diff --git a/src/Tools/ExternalAccess/Copilot/Internal/Analyzer/CSharp/CSharpCopilotCodeAnalysisService.cs b/src/Tools/ExternalAccess/Copilot/Internal/Analyzer/CSharp/CSharpCopilotCodeAnalysisService.cs index 2bac22ef7fb92..e9f1006c59e01 100644 --- a/src/Tools/ExternalAccess/Copilot/Internal/Analyzer/CSharp/CSharpCopilotCodeAnalysisService.cs +++ b/src/Tools/ExternalAccess/Copilot/Internal/Analyzer/CSharp/CSharpCopilotCodeAnalysisService.cs @@ -57,6 +57,9 @@ protected override Task IsAvailableCoreAsync(CancellationToken cancellatio protected override Task StartRefinementSessionCoreAsync(Document oldDocument, Document newDocument, Diagnostic? primaryDiagnostic, CancellationToken cancellationToken) => _lazyExternalCopilotService.Value.StartRefinementSessionAsync(oldDocument, newDocument, primaryDiagnostic, cancellationToken); + protected override Task GetOnTheFlyDocsCoreAsync(string symbolSignature, ImmutableArray declarationCode, string language, CancellationToken cancellationToken) + => _lazyExternalCopilotService.Value.GetOnTheFlyDocsAsync(symbolSignature, declarationCode, language, cancellationToken); + protected override async Task> GetDiagnosticsIntersectWithSpanAsync( Document document, IReadOnlyList diagnostics, TextSpan span, CancellationToken cancellationToken) { diff --git a/src/Tools/ExternalAccess/Copilot/Internal/CodeMapper/CopilotCSharpMapCodeService.cs b/src/Tools/ExternalAccess/Copilot/Internal/CodeMapper/CopilotCSharpMapCodeService.cs index aa8265e7d4dbb..7a20c142784b2 100644 --- a/src/Tools/ExternalAccess/Copilot/Internal/CodeMapper/CopilotCSharpMapCodeService.cs +++ b/src/Tools/ExternalAccess/Copilot/Internal/CodeMapper/CopilotCSharpMapCodeService.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.ExternalAccess.Copilot.CodeMapper; @@ -16,8 +15,6 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Copilot.Internal.CodeMapper; -using MapCodeAsyncDelegateType = Func, ImmutableArray<(Document Document, TextSpan TextSpan)>, Dictionary, CancellationToken, Task?>>; - [ExportLanguageService(typeof(IMapCodeService), language: LanguageNames.CSharp), Shared] internal sealed class CSharpMapCodeService : IMapCodeService { @@ -25,9 +22,9 @@ internal sealed class CSharpMapCodeService : IMapCodeService [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpMapCodeService([Import(AllowDefault = true)] ICSharpCopilotMapCodeService? service) + public CSharpMapCodeService(ICSharpCopilotMapCodeService service) { - _service = service ?? new ReflectionWrapper(); + _service = service; } public Task?> MapCodeAsync(Document document, ImmutableArray contents, ImmutableArray<(Document, TextSpan)> focusLocations, CancellationToken cancellationToken) @@ -35,49 +32,4 @@ public CSharpMapCodeService([Import(AllowDefault = true)] ICSharpCopilotMapCodeS var options = new Dictionary(); return _service.MapCodeAsync(document, contents, focusLocations, options, cancellationToken); } - - private sealed class ReflectionWrapper : ICSharpCopilotMapCodeService - { - private const string CodeMapperDllName = "Microsoft.VisualStudio.Copilot.CodeMappers.CSharp, Version=0.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"; - private const string MapCodeServiceTypeFullName = "Microsoft.VisualStudio.Conversations.CodeMappers.CSharp.CSharpMapCodeService"; - private const string MapCodeAsyncMethodName = "MapCodeAsync"; - - // Create and cache the delegate to ensure we use a singleton and better performance. - private readonly Lazy _lazyMapCodeAsyncDelegate = new(CreateDelegate, LazyThreadSafetyMode.PublicationOnly); - - private static MapCodeAsyncDelegateType? CreateDelegate() - { - try - { - var assembly = Assembly.Load(CodeMapperDllName); - var type = assembly.GetType(MapCodeServiceTypeFullName); - if (type is null) - return null; - - var serviceInstance = Activator.CreateInstance(type); - if (serviceInstance is null) - return null; - - if (type.GetMethod(MapCodeAsyncMethodName, [typeof(Document), typeof(ImmutableArray), typeof(ImmutableArray<(Document Document, TextSpan TextSpan)>), typeof(Dictionary), typeof(CancellationToken)]) is not MethodInfo mapCodeAsyncMethod) - return null; - - return (MapCodeAsyncDelegateType)Delegate.CreateDelegate(typeof(MapCodeAsyncDelegateType), serviceInstance, mapCodeAsyncMethod); - - } - catch - { - // Catch all here since failure is expected if user has no copilot chat or an older version of it installed. - } - - return null; - } - - public async Task?> MapCodeAsync(Document document, ImmutableArray contents, ImmutableArray<(Document document, TextSpan textSpan)> prioritizedFocusLocations, Dictionary options, CancellationToken cancellationToken) - { - if (_lazyMapCodeAsyncDelegate.Value is null) - return null; - - return await _lazyMapCodeAsyncDelegate.Value(document, contents, prioritizedFocusLocations, options, cancellationToken).ConfigureAwait(false); - } - } } diff --git a/src/Tools/ExternalAccess/Copilot/InternalAPI.Unshipped.txt b/src/Tools/ExternalAccess/Copilot/InternalAPI.Unshipped.txt index f2eec4444e3eb..0f4024be9597d 100644 --- a/src/Tools/ExternalAccess/Copilot/InternalAPI.Unshipped.txt +++ b/src/Tools/ExternalAccess/Copilot/InternalAPI.Unshipped.txt @@ -8,6 +8,7 @@ Microsoft.CodeAnalysis.ExternalAccess.Copilot.IExternalCSharpCopilotCodeAnalysis Microsoft.CodeAnalysis.ExternalAccess.Copilot.IExternalCSharpCopilotCodeAnalysisService.AnalyzeDocumentAsync(Microsoft.CodeAnalysis.Document! document, Microsoft.CodeAnalysis.Text.TextSpan? span, string! promptTitle, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task>! Microsoft.CodeAnalysis.ExternalAccess.Copilot.IExternalCSharpCopilotCodeAnalysisService.GetAvailablePromptTitlesAsync(Microsoft.CodeAnalysis.Document! document, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task>! Microsoft.CodeAnalysis.ExternalAccess.Copilot.IExternalCSharpCopilotCodeAnalysisService.GetCachedDiagnosticsAsync(Microsoft.CodeAnalysis.Document! document, string! promptTitle, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task>! +Microsoft.CodeAnalysis.ExternalAccess.Copilot.IExternalCSharpCopilotCodeAnalysisService.GetOnTheFlyDocsAsync(string! symbolSignature, System.Collections.Immutable.ImmutableArray declarationCode, string! language, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Microsoft.CodeAnalysis.ExternalAccess.Copilot.IExternalCSharpCopilotCodeAnalysisService.IsAvailableAsync(System.Threading.CancellationToken cancellation) -> System.Threading.Tasks.Task! Microsoft.CodeAnalysis.ExternalAccess.Copilot.IExternalCSharpCopilotCodeAnalysisService.StartRefinementSessionAsync(Microsoft.CodeAnalysis.Document! oldDocument, Microsoft.CodeAnalysis.Document! newDocument, Microsoft.CodeAnalysis.Diagnostic? primaryDiagnostic, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! override Microsoft.CodeAnalysis.ExternalAccess.Copilot.CopilotChecksumWrapper.Equals(object? obj) -> bool diff --git a/src/Tools/ExternalAccess/FSharp/Internal/NavigateTo/FSharpNavigateToSearchService.cs b/src/Tools/ExternalAccess/FSharp/Internal/NavigateTo/FSharpNavigateToSearchService.cs index 42525a23e8f98..fe18ef4273008 100644 --- a/src/Tools/ExternalAccess/FSharp/Internal/NavigateTo/FSharpNavigateToSearchService.cs +++ b/src/Tools/ExternalAccess/FSharp/Internal/NavigateTo/FSharpNavigateToSearchService.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Immutable; using System.Composition; -using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -37,12 +36,12 @@ public async Task SearchDocumentAsync( Document document, string searchPattern, IImmutableSet kinds, - Func onResultFound, + Func, Task> onResultsFound, CancellationToken cancellationToken) { var results = await _service.SearchDocumentAsync(document, searchPattern, kinds, cancellationToken).ConfigureAwait(false); - foreach (var result in results) - await onResultFound(new InternalFSharpNavigateToSearchResult(result)).ConfigureAwait(false); + if (results.Length > 0) + await onResultsFound(results.SelectAsArray(result => (INavigateToSearchResult)new InternalFSharpNavigateToSearchResult(result))).ConfigureAwait(false); } public async Task SearchProjectsAsync( @@ -52,7 +51,7 @@ public async Task SearchProjectsAsync( string searchPattern, IImmutableSet kinds, Document? activeDocument, - Func onResultFound, + Func, Task> onResultsFound, Func onProjectCompleted, CancellationToken cancellationToken) { @@ -62,8 +61,8 @@ public async Task SearchProjectsAsync( foreach (var project in projects) { var results = await _service.SearchProjectAsync(project, priorityDocuments, searchPattern, kinds, cancellationToken).ConfigureAwait(false); - foreach (var result in results) - await onResultFound(project, new InternalFSharpNavigateToSearchResult(result)).ConfigureAwait(false); + if (results.Length > 0) + await onResultsFound(results.SelectAsArray(result => (INavigateToSearchResult)new InternalFSharpNavigateToSearchResult(result))).ConfigureAwait(false); await onProjectCompleted().ConfigureAwait(false); } diff --git a/src/Tools/ExternalAccess/OmniSharp/NavigateTo/OmniSharpNavigateToSearchService.cs b/src/Tools/ExternalAccess/OmniSharp/NavigateTo/OmniSharpNavigateToSearchService.cs index 0f5bcdcd96a74..49275c191a188 100644 --- a/src/Tools/ExternalAccess/OmniSharp/NavigateTo/OmniSharpNavigateToSearchService.cs +++ b/src/Tools/ExternalAccess/OmniSharp/NavigateTo/OmniSharpNavigateToSearchService.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.ExternalAccess.OmniSharp.Navigation; using Microsoft.CodeAnalysis.NavigateTo; +using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.TestHooks; namespace Microsoft.CodeAnalysis.ExternalAccess.OmniSharp.NavigateTo; @@ -25,7 +26,7 @@ public static Task SearchAsync( var searcher = NavigateToSearcher.Create( solution, AsynchronousOperationListenerProvider.NullListener, - new OmniSharpNavigateToCallbackImpl(callback), + new OmniSharpNavigateToCallbackImpl(solution, callback), searchPattern, kinds, disposalToken: CancellationToken.None); @@ -33,23 +34,27 @@ public static Task SearchAsync( return searcher.SearchAsync(NavigateToSearchScope.Solution, cancellationToken); } - private sealed class OmniSharpNavigateToCallbackImpl(OmniSharpNavigateToCallback callback) : INavigateToSearchCallback + private sealed class OmniSharpNavigateToCallbackImpl(Solution solution, OmniSharpNavigateToCallback callback) : INavigateToSearchCallback { - public async Task AddItemAsync(Project project, INavigateToSearchResult result, CancellationToken cancellationToken) + public async Task AddResultsAsync(ImmutableArray results, CancellationToken cancellationToken) { - var document = await result.NavigableItem.Document.GetRequiredDocumentAsync(project.Solution, cancellationToken).ConfigureAwait(false); - var omniSharpResult = new OmniSharpNavigateToSearchResult( - result.AdditionalInformation, - result.Kind, - (OmniSharpNavigateToMatchKind)result.MatchKind, - result.IsCaseSensitive, - result.Name, - result.NameMatchSpans, - result.SecondarySort, - result.Summary!, - new OmniSharpNavigableItem(result.NavigableItem.DisplayTaggedParts, document, result.NavigableItem.SourceSpan)); + foreach (var result in results) + { + var project = solution.GetRequiredProject(result.NavigableItem.Document.Project.Id); + var document = await result.NavigableItem.Document.GetRequiredDocumentAsync(project.Solution, cancellationToken).ConfigureAwait(false); + var omniSharpResult = new OmniSharpNavigateToSearchResult( + result.AdditionalInformation, + result.Kind, + (OmniSharpNavigateToMatchKind)result.MatchKind, + result.IsCaseSensitive, + result.Name, + result.NameMatchSpans, + result.SecondarySort, + result.Summary!, + new OmniSharpNavigableItem(result.NavigableItem.DisplayTaggedParts, document, result.NavigableItem.SourceSpan)); - await callback(project, omniSharpResult, cancellationToken).ConfigureAwait(false); + await callback(project, omniSharpResult, cancellationToken).ConfigureAwait(false); + } } public void Done(bool isFullyLoaded) diff --git a/src/Tools/ExternalAccess/Razor/ChecksumWrapper.cs b/src/Tools/ExternalAccess/Razor/ChecksumWrapper.cs new file mode 100644 index 0000000000000..e8986f09ca04d --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/ChecksumWrapper.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor; + +internal readonly struct ChecksumWrapper(Checksum checksum) : IEquatable +{ + private readonly Checksum _value = checksum; + + public override int GetHashCode() + { + return _value.GetHashCode(); + } + + public override bool Equals(object? obj) + { + if (obj is ChecksumWrapper wrapper) + { + return Equals(wrapper); + } + return false; + } + + public bool Equals(ChecksumWrapper other) + { + return _value.Equals(other._value); + } + + public override string ToString() + => _value.ToString(); +} diff --git a/src/Tools/ExternalAccess/Razor/Cohost/Constants.cs b/src/Tools/ExternalAccess/Razor/Cohost/Constants.cs index 1c361a9fc7003..71a70b2de65d9 100644 --- a/src/Tools/ExternalAccess/Razor/Cohost/Constants.cs +++ b/src/Tools/ExternalAccess/Razor/Cohost/Constants.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.Collections.Immutable; using Microsoft.CodeAnalysis.LanguageServer; @@ -16,4 +17,7 @@ internal static class Constants public const string RazorLanguageContract = ProtocolConstants.RazorCohostContract; public static readonly ImmutableArray RazorLanguage = ImmutableArray.Create("Razor"); + + // The UI context is provided by Razor, so this guid must match the one in https://github.com/dotnet/razor/blob/main/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorConstants.cs + public static readonly Guid RazorCohostingUIContext = new Guid("6d5b86dc-6b8a-483b-ae30-098a3c7d6774"); } diff --git a/src/Tools/ExternalAccess/Razor/Cohost/RazorDynamicRegistrationServiceFactory.cs b/src/Tools/ExternalAccess/Razor/Cohost/RazorDynamicRegistrationServiceFactory.cs index f3f84a2e888e5..425cfce0e740c 100644 --- a/src/Tools/ExternalAccess/Razor/Cohost/RazorDynamicRegistrationServiceFactory.cs +++ b/src/Tools/ExternalAccess/Razor/Cohost/RazorDynamicRegistrationServiceFactory.cs @@ -6,55 +6,84 @@ using System.Composition; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageServer; using Microsoft.CodeAnalysis.LanguageServer.Handler; using Newtonsoft.Json; using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; [ExportCSharpVisualBasicLspServiceFactory(typeof(RazorDynamicRegistrationService), WellKnownLspServerKinds.AlwaysActiveVSLspServer), Shared] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] -internal sealed class RazorDynamicRegistrationServiceFactory([Import(AllowDefault = true)] IRazorCohostDynamicRegistrationService? dynamicRegistrationService) : ILspServiceFactory +internal sealed class RazorDynamicRegistrationServiceFactory( + [Import(AllowDefault = true)] IUIContextActivationService? uIContextActivationService, + [Import(AllowDefault = true)] Lazy? dynamicRegistrationService) : ILspServiceFactory { public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) { var clientLanguageServerManager = lspServices.GetRequiredService(); - return new RazorDynamicRegistrationService(dynamicRegistrationService, clientLanguageServerManager); + return new RazorDynamicRegistrationService(uIContextActivationService, dynamicRegistrationService, clientLanguageServerManager); } - private class RazorDynamicRegistrationService : ILspService, IOnInitialized + private class RazorDynamicRegistrationService( + IUIContextActivationService? uIContextActivationService, + Lazy? dynamicRegistrationService, + IClientLanguageServerManager? clientLanguageServerManager) : ILspService, IOnInitialized, IDisposable { - private readonly IRazorCohostDynamicRegistrationService? _dynamicRegistrationService; - private readonly IClientLanguageServerManager _clientLanguageServerManager; - private readonly JsonSerializerSettings _serializerSettings; + private readonly CancellationTokenSource _disposalTokenSource = new(); - public RazorDynamicRegistrationService(IRazorCohostDynamicRegistrationService? dynamicRegistrationService, IClientLanguageServerManager clientLanguageServerManager) + public void Dispose() { - _dynamicRegistrationService = dynamicRegistrationService; - _clientLanguageServerManager = clientLanguageServerManager; - - var serializer = new JsonSerializer(); - serializer.AddVSInternalExtensionConverters(); - _serializerSettings = new JsonSerializerSettings { Converters = serializer.Converters }; + _disposalTokenSource.Cancel(); } public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) { - if (_dynamicRegistrationService is null) + if (dynamicRegistrationService is null || clientLanguageServerManager is null) { return Task.CompletedTask; } + if (uIContextActivationService is null) + { + // Outside of VS, we want to initialize immediately.. I think? + InitializeRazor(); + } + else + { + uIContextActivationService.ExecuteWhenActivated(Constants.RazorCohostingUIContext, InitializeRazor); + } + + return Task.CompletedTask; + + void InitializeRazor() + { + this.InitializeRazor(clientCapabilities, context, _disposalTokenSource.Token); + } + } + + private void InitializeRazor(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) + { + // The LSP server will dispose us when the server exits, but VS could decide to activate us later. + // If a new instance of the server is created, a new instance of this class will be created and the + // UIContext will already be active, so this method will be immediately called on the new instance. + if (cancellationToken.IsCancellationRequested) return; + + var serializer = new JsonSerializer(); + serializer.AddVSInternalExtensionConverters(); + var serializerSettings = new JsonSerializerSettings { Converters = serializer.Converters }; + // We use a string to pass capabilities to/from Razor to avoid version issues with the Protocol DLL - var serializedClientCapabilities = JsonConvert.SerializeObject(clientCapabilities, _serializerSettings); - var razorCohostClientLanguageServerManager = new RazorCohostClientLanguageServerManager(_clientLanguageServerManager); + var serializedClientCapabilities = JsonConvert.SerializeObject(clientCapabilities, serializerSettings); + var razorCohostClientLanguageServerManager = new RazorCohostClientLanguageServerManager(clientLanguageServerManager!); var requestContext = new RazorCohostRequestContext(context); - return _dynamicRegistrationService.RegisterAsync(serializedClientCapabilities, requestContext, cancellationToken); + dynamicRegistrationService!.Value.RegisterAsync(serializedClientCapabilities, requestContext, cancellationToken).ReportNonFatalErrorAsync(); } } } diff --git a/src/Tools/ExternalAccess/Razor/RazorUri.cs b/src/Tools/ExternalAccess/Razor/RazorUri.cs index e83ddc1dffb67..dd0e03df5dab1 100644 --- a/src/Tools/ExternalAccess/Razor/RazorUri.cs +++ b/src/Tools/ExternalAccess/Razor/RazorUri.cs @@ -4,7 +4,6 @@ using System; using Microsoft.CodeAnalysis.LanguageServer; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.ExternalAccess.Razor; @@ -13,8 +12,6 @@ internal static class RazorUri public static Uri CreateAbsoluteUri(string absolutePath) => ProtocolConversions.CreateAbsoluteUri(absolutePath); - public static Uri CreateUri(this TextDocument document) - { - return document.GetURI(); - } + public static string GetDocumentFilePathFromUri(Uri uri) + => ProtocolConversions.GetDocumentFilePathFromUri(uri); } diff --git a/src/Tools/ExternalAccess/Razor/SolutionExtensions.cs b/src/Tools/ExternalAccess/Razor/SolutionExtensions.cs new file mode 100644 index 0000000000000..358ea29bf613b --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/SolutionExtensions.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor; + +internal static class SolutionExtensions +{ + public static ImmutableArray GetTextDocuments(this Solution solution, Uri documentUri) + => LanguageServer.Extensions.GetTextDocuments(solution, documentUri); + + public static ImmutableArray GetDocumentIds(this Solution solution, Uri documentUri) + => LanguageServer.Extensions.GetDocumentIds(solution, documentUri); + + public static int GetWorkspaceVersion(this Solution solution) + => solution.WorkspaceVersion; +} diff --git a/src/Tools/ExternalAccess/Razor/TextDocumentExtensions.cs b/src/Tools/ExternalAccess/Razor/TextDocumentExtensions.cs new file mode 100644 index 0000000000000..d9ef4e5898b5a --- /dev/null +++ b/src/Tools/ExternalAccess/Razor/TextDocumentExtensions.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.LanguageServer; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Razor; + +internal static class TextDocumentExtensions +{ + public static Uri CreateUri(this TextDocument document) + => document.GetURI(); + + public static async Task GetChecksumAsync(this TextDocument document, CancellationToken cancellationToken) + => new ChecksumWrapper(await document.State.GetChecksumAsync(cancellationToken).ConfigureAwait(false)); +} diff --git a/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/HotReloadRequestContext.cs b/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/HotReloadRequestContext.cs new file mode 100644 index 0000000000000..c7b511ee906c2 --- /dev/null +++ b/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/HotReloadRequestContext.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; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using LSP = Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts; + +internal sealed class HotReloadRequestContext(RequestContext context) +{ + internal LSP.ClientCapabilities ClientCapabilities => context.GetRequiredClientCapabilities(); + public TextDocument? TextDocument => context.TextDocument; + public Solution? Solution => context.Solution; + public bool IsTracking(TextDocument textDocument) => context.IsTracking(textDocument.GetURI()); +} diff --git a/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticManager.cs b/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticManager.cs new file mode 100644 index 0000000000000..f1fe7a600dae4 --- /dev/null +++ b/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticManager.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts; + +internal interface IHotReloadDiagnosticManager +{ + /// + /// Refreshes hot reload diagnostics. + /// + void RequestRefresh(); + + /// + /// Registers providers of hot reload diagnostics. Callers are responsible for refreshing diagnostics after registration. + /// + void Register(IEnumerable providers); + + /// + /// Unregisters providers of hot reload diagnostics. Callers are responsible for refreshing diagnostics after un-registration. + /// + void Unregister(IEnumerable providers); + + /// + /// Providers. + /// + ImmutableArray Providers { get; } +} diff --git a/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticSource.cs b/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticSource.cs new file mode 100644 index 0000000000000..85a8004f1db8d --- /dev/null +++ b/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticSource.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; + +namespace Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts; + +/// +/// Source of hot reload diagnostics. +/// +internal interface IHotReloadDiagnosticSource +{ + /// + /// Text document for which diagnostics are provided. + /// + DocumentId DocumentId { get; } + + /// + /// Provides list of diagnostics for the given document. + /// + ValueTask> GetDiagnosticsAsync(HotReloadRequestContext request, CancellationToken cancellationToken); +} diff --git a/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticSourceProvider.cs b/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..586a6a2d8ec90 --- /dev/null +++ b/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticSourceProvider.cs @@ -0,0 +1,27 @@ +// 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; + +namespace Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts; + +/// +/// Provides diagnostic sources. +/// +internal interface IHotReloadDiagnosticSourceProvider +{ + /// + /// True if this provider is for documents. False if it is for a workspace, i.e. for unopened documents. + /// + bool IsDocument { get; } + + /// + /// Creates the diagnostic sources. + /// + /// The context. + /// The cancellation token. + ValueTask> CreateDiagnosticSourcesAsync(HotReloadRequestContext context, CancellationToken cancellationToken); +} diff --git a/src/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticManager.cs b/src/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticManager.cs new file mode 100644 index 0000000000000..8a6071ea4228a --- /dev/null +++ b/src/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticManager.cs @@ -0,0 +1,49 @@ +// 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.Composition; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal; + +[Export(typeof(IHotReloadDiagnosticManager)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class HotReloadDiagnosticManager([Import] IDiagnosticsRefresher diagnosticsRefresher) : IHotReloadDiagnosticManager +{ + private readonly object _syncLock = new(); + public ImmutableArray Providers { get; private set; } = []; + + public void RequestRefresh() + => diagnosticsRefresher.RequestWorkspaceRefresh(); + + public void Register(IEnumerable providers) + { + // We use array instead of e.g. HashSet because we expect the number of sources to be small. + // Usually 2, one workspace and one document provider. + lock (_syncLock) + { + foreach (var provider in providers) + { + if (!Providers.Contains(provider)) + Providers = Providers.Add(provider); + } + } + } + + public void Unregister(IEnumerable providers) + { + lock (_syncLock) + { + foreach (var provider in Providers) + Providers = Providers.Remove(provider); + } + } + +} diff --git a/src/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticSource.cs b/src/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticSource.cs new file mode 100644 index 0000000000000..64d157b4a1343 --- /dev/null +++ b/src/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticSource.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal; + +internal sealed class HotReloadDiagnosticSource(IHotReloadDiagnosticSource source, TextDocument textDocument) : IDiagnosticSource +{ + 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(); + return result; + } + + public TextDocumentIdentifier? GetDocumentIdentifier() => new() { Uri = 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/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticSourceProvider.cs b/src/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..88bd5cceb9f90 --- /dev/null +++ b/src/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticSourceProvider.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; +using Microsoft.CodeAnalysis.PooledObjects; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal; + +internal abstract class HotReloadDiagnosticSourceProvider(IHotReloadDiagnosticManager diagnosticManager, bool isDocument) : IDiagnosticSourceProvider +{ + public string Name => "HotReloadDiagnostics"; + public bool IsDocument => isDocument; + + public bool IsEnabled(ClientCapabilities clientCapabilities) => true; + + public async ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + if (context.Solution is not Solution solution) + { + return ImmutableArray.Empty; + } + + var hotReloadContext = new HotReloadRequestContext(context); + using var _ = ArrayBuilder.GetInstance(out var sources); + foreach (var provider in diagnosticManager.Providers) + { + if (provider.IsDocument == isDocument) + { + var hotReloadSources = await provider.CreateDiagnosticSourcesAsync(hotReloadContext, cancellationToken).ConfigureAwait(false); + foreach (var hotReloadSource in hotReloadSources) + { + // Look for additional document first. Currently most common hot reload diagnostics come from *.xaml files. + TextDocument? textDocument = + solution.GetAdditionalDocument(hotReloadSource.DocumentId) ?? + solution.GetDocument(hotReloadSource.DocumentId); + if (textDocument != null) + { + sources.Add(new HotReloadDiagnosticSource(hotReloadSource, textDocument)); + } + } + } + } + + var result = sources.ToImmutableAndClear(); + return DiagnosticSourceManager.AggregateSourcesIfNeeded(result, isDocument); + } + + [Export(typeof(IDiagnosticSourceProvider)), Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + private sealed class DocumentHotReloadDiagnosticSourceProvider([Import] IHotReloadDiagnosticManager diagnosticManager) + : HotReloadDiagnosticSourceProvider(diagnosticManager, isDocument: true) + { + } + + [Export(typeof(IDiagnosticSourceProvider)), Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + private sealed class WorkspaceHotReloadDiagnosticSourceProvider([Import] IHotReloadDiagnosticManager diagnosticManager) + : HotReloadDiagnosticSourceProvider(diagnosticManager, isDocument: false) + { + } +} diff --git a/src/Tools/ExternalAccess/VisualDiagnostics/InternalAPI.Unshipped.txt b/src/Tools/ExternalAccess/VisualDiagnostics/InternalAPI.Unshipped.txt index 504106533fe6b..a831db9ed8865 100644 --- a/src/Tools/ExternalAccess/VisualDiagnostics/InternalAPI.Unshipped.txt +++ b/src/Tools/ExternalAccess/VisualDiagnostics/InternalAPI.Unshipped.txt @@ -1,15 +1,47 @@ +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.HotReloadRequestContext +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.HotReloadRequestContext.ClientCapabilities.get -> Roslyn.LanguageServer.Protocol.ClientCapabilities! +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.HotReloadRequestContext.HotReloadRequestContext(Microsoft.CodeAnalysis.LanguageServer.Handler.RequestContext context) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.HotReloadRequestContext.IsTracking(Microsoft.CodeAnalysis.TextDocument! textDocument) -> bool +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.HotReloadRequestContext.Solution.get -> Microsoft.CodeAnalysis.Solution? +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.HotReloadRequestContext.TextDocument.get -> Microsoft.CodeAnalysis.TextDocument? +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticManager +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticManager.Providers.get -> System.Collections.Immutable.ImmutableArray +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticManager.Register(System.Collections.Generic.IEnumerable! providers) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticManager.RequestRefresh() -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticManager.Unregister(System.Collections.Generic.IEnumerable! providers) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticSource +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticSource.DocumentId.get -> Microsoft.CodeAnalysis.DocumentId! +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticSource.GetDiagnosticsAsync(Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.HotReloadRequestContext! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask> +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticSourceProvider +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticSourceProvider.CreateDiagnosticSourcesAsync(Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.HotReloadRequestContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask> +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticSourceProvider.IsDocument.get -> bool Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IVisualDiagnosticsLanguageService Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IVisualDiagnosticsLanguageService.HandleDiagnosticSessionStartAsync(Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.ProcessInfo info, System.Threading.CancellationToken token) -> System.Threading.Tasks.Task! Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IVisualDiagnosticsLanguageService.HandleDiagnosticSessionStopAsync(Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.ProcessInfo info, System.Threading.CancellationToken token) -> System.Threading.Tasks.Task! Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IVisualDiagnosticsLanguageService.InitializeAsync(Microsoft.ServiceHub.Framework.IServiceBroker! serviceBroker, System.Threading.CancellationToken token) -> System.Threading.Tasks.Task! Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IVisualDiagnosticsLanguageService.RequestDataBridgeConnectionAsync(string! connectionId, System.Threading.CancellationToken token) -> System.Threading.Tasks.Task! -Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IVisualDiagnosticsServiceBroker -Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IVisualDiagnosticsServiceBroker.GetServiceBrokerAsync() -> System.Threading.Tasks.Task! -Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.VisualDiagnosticsServiceBroker -Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.VisualDiagnosticsServiceBroker.NotifyServiceBrokerInitialized.get -> Microsoft.CodeAnalysis.BrokeredServices.IOnServiceBrokerInitialized? -Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.VisualDiagnosticsServiceBroker.NotifyServiceBrokerInitialized.set -> void -Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.VisualDiagnosticsServiceBroker.OnServiceBrokerInitialized(Microsoft.ServiceHub.Framework.IServiceBroker! serviceBroker) -> void -Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.VisualDiagnosticsServiceBroker.VisualDiagnosticsServiceBroker() -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticManager +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticManager.HotReloadDiagnosticManager(Microsoft.CodeAnalysis.Diagnostics.IDiagnosticsRefresher! diagnosticsRefresher) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticManager.Providers.get -> System.Collections.Immutable.ImmutableArray +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticManager.Register(System.Collections.Generic.IEnumerable! providers) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticManager.RequestRefresh() -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticManager.Unregister(System.Collections.Generic.IEnumerable! providers) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSource +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSource.GetDiagnosticsAsync(Microsoft.CodeAnalysis.LanguageServer.Handler.RequestContext context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task>! +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSource.GetDocumentIdentifier() -> Roslyn.LanguageServer.Protocol.TextDocumentIdentifier? +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSource.GetId() -> Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.ProjectOrDocumentId +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSource.GetProject() -> Microsoft.CodeAnalysis.Project! +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSource.HotReloadDiagnosticSource(Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticSource! source, Microsoft.CodeAnalysis.TextDocument! textDocument) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSource.IsLiveSource() -> bool +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSource.ToDisplayString() -> string! +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSourceProvider +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSourceProvider.CreateDiagnosticSourcesAsync(Microsoft.CodeAnalysis.LanguageServer.Handler.RequestContext context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask> +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSourceProvider.DocumentHotReloadDiagnosticSourceProvider.DocumentHotReloadDiagnosticSourceProvider(Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticManager! diagnosticManager) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSourceProvider.HotReloadDiagnosticSourceProvider(Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticManager! diagnosticManager, bool isDocument) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSourceProvider.IsDocument.get -> bool +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSourceProvider.IsEnabled(Roslyn.LanguageServer.Protocol.ClientCapabilities! clientCapabilities) -> bool +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSourceProvider.Name.get -> string! +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSourceProvider.WorkspaceHotReloadDiagnosticSourceProvider.WorkspaceHotReloadDiagnosticSourceProvider(Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticManager! diagnosticManager) -> void Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.RunningProcessEntry Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.RunningProcessEntry.RunningProcessEntry() -> void Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.VisualDiagnosticsServiceFactory diff --git a/src/Tools/ExternalAccess/VisualDiagnostics/Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.csproj b/src/Tools/ExternalAccess/VisualDiagnostics/Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.csproj index ada2581f782b5..f63b387f65552 100644 --- a/src/Tools/ExternalAccess/VisualDiagnostics/Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.csproj +++ b/src/Tools/ExternalAccess/VisualDiagnostics/Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/Tools/ExternalAccess/Xaml/External/IXamlDiagnosticSource.cs b/src/Tools/ExternalAccess/Xaml/External/IXamlDiagnosticSource.cs new file mode 100644 index 0000000000000..0b55c2089007d --- /dev/null +++ b/src/Tools/ExternalAccess/Xaml/External/IXamlDiagnosticSource.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Xaml; + +internal interface IXamlDiagnosticSource +{ + Task> GetDiagnosticsAsync( + XamlRequestContext context, + CancellationToken cancellationToken); +} diff --git a/src/Tools/ExternalAccess/Xaml/Internal/ClientCapabilityProvider.cs b/src/Tools/ExternalAccess/Xaml/Internal/ClientCapabilityProvider.cs index e0f7cf9e77eda..812a79e593eef 100644 --- a/src/Tools/ExternalAccess/Xaml/Internal/ClientCapabilityProvider.cs +++ b/src/Tools/ExternalAccess/Xaml/Internal/ClientCapabilityProvider.cs @@ -73,6 +73,12 @@ public bool IsDynamicRegistrationSupported(string methodName) return _clientCapabilities?.Workspace?.DidChangeConfiguration?.DynamicRegistration == true; case LSP.Methods.WorkspaceDidChangeWatchedFilesName: return _clientCapabilities?.Workspace?.DidChangeWatchedFiles?.DynamicRegistration == true; + case LSP.VSInternalMethods.OnAutoInsertName: + if (_clientCapabilities.TextDocument is VSInternalTextDocumentClientCapabilities internalTextDocumentClientCapabilities) + { + return internalTextDocumentClientCapabilities.OnAutoInsert?.DynamicRegistration == true; + } + return false; default: throw new InvalidOperationException($"Unsupported dynamic registration method: {methodName}"); } diff --git a/src/Tools/ExternalAccess/Xaml/Internal/XamlDiagnosticSource.cs b/src/Tools/ExternalAccess/Xaml/Internal/XamlDiagnosticSource.cs new file mode 100644 index 0000000000000..1cee9dbe1b173 --- /dev/null +++ b/src/Tools/ExternalAccess/Xaml/Internal/XamlDiagnosticSource.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; +using Roslyn.LanguageServer.Protocol; + +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() { Uri = document.GetURI() }; + string IDiagnosticSource.ToDisplayString() => $"{this.GetType().Name}: {document.FilePath ?? document.Name} in {document.Project.Name}"; + + async Task> IDiagnosticSource.GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken) + { + var xamlRequestContext = XamlRequestContext.FromRequestContext(context); + var diagnostics = await xamlDiagnosticSource.GetDiagnosticsAsync(xamlRequestContext, cancellationToken).ConfigureAwait(false); + var result = diagnostics.Select(e => DiagnosticData.Create(e, document)).ToImmutableArray(); + return result; + } +} diff --git a/src/Tools/ExternalAccess/Xaml/Internal/XamlDiagnosticSourceProvider.cs b/src/Tools/ExternalAccess/Xaml/Internal/XamlDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..526a26f5e57e8 --- /dev/null +++ b/src/Tools/ExternalAccess/Xaml/Internal/XamlDiagnosticSourceProvider.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Xaml; + +[Export(typeof(IDiagnosticSourceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class XamlDiagnosticSourceProvider([Import(AllowDefault = true)] IXamlDiagnosticSource? xamlDiagnosticSource) : IDiagnosticSourceProvider +{ + bool IDiagnosticSourceProvider.IsDocument => true; + + string IDiagnosticSourceProvider.Name => "XamlDiagnostics"; + + bool IDiagnosticSourceProvider.IsEnabled(ClientCapabilities clientCapabilities) => true; + + ValueTask> IDiagnosticSourceProvider.CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + if (xamlDiagnosticSource != null && context.TextDocument is { } document && + document.Project.GetAdditionalDocument(document.Id) != null) + { + return new([new XamlDiagnosticSource(xamlDiagnosticSource, document)]); + } + + return new([]); + } +} diff --git a/src/Tools/ExternalAccess/Xaml/InternalAPI.Unshipped.txt b/src/Tools/ExternalAccess/Xaml/InternalAPI.Unshipped.txt index d62fe5bb29af1..c8d63468defae 100644 --- a/src/Tools/ExternalAccess/Xaml/InternalAPI.Unshipped.txt +++ b/src/Tools/ExternalAccess/Xaml/InternalAPI.Unshipped.txt @@ -37,6 +37,8 @@ Microsoft.CodeAnalysis.ExternalAccess.Xaml.ILocationService.GetSymbolLocationsAs Microsoft.CodeAnalysis.ExternalAccess.Xaml.IResolveCachedDataService Microsoft.CodeAnalysis.ExternalAccess.Xaml.IResolveCachedDataService.FromResolveData(object? resolveData) -> (object? data, System.Uri? uri) Microsoft.CodeAnalysis.ExternalAccess.Xaml.IResolveCachedDataService.ToResolveData(object! data, System.Uri! uri) -> object! +Microsoft.CodeAnalysis.ExternalAccess.Xaml.IXamlDiagnosticSource +Microsoft.CodeAnalysis.ExternalAccess.Xaml.IXamlDiagnosticSource.GetDiagnosticsAsync(Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlRequestContext context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task>! Microsoft.CodeAnalysis.ExternalAccess.Xaml.IXamlRequestHandler Microsoft.CodeAnalysis.ExternalAccess.Xaml.IXamlRequestHandler.HandleRequestAsync(TRequest request, Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlRequestContext context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Microsoft.CodeAnalysis.ExternalAccess.Xaml.IXamlRequestHandler.MutatesSolutionState.get -> bool @@ -51,6 +53,10 @@ Microsoft.CodeAnalysis.ExternalAccess.Xaml.ResolveDataConversions Microsoft.CodeAnalysis.ExternalAccess.Xaml.StringConstants Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlCommandAttribute Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlCommandAttribute.XamlCommandAttribute(string! command) -> void +Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlDiagnosticSource +Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlDiagnosticSource.XamlDiagnosticSource(Microsoft.CodeAnalysis.ExternalAccess.Xaml.IXamlDiagnosticSource! xamlDiagnosticSource, Microsoft.CodeAnalysis.TextDocument! document) -> void +Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlDiagnosticSourceProvider +Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlDiagnosticSourceProvider.XamlDiagnosticSourceProvider(Microsoft.CodeAnalysis.ExternalAccess.Xaml.IXamlDiagnosticSource? xamlDiagnosticSource) -> void Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlMethodAttribute Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlMethodAttribute.XamlMethodAttribute(string! method) -> void Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlRequestContext diff --git a/src/Tools/IdeBenchmarks/SQLitePersistentStorageBenchmark.cs b/src/Tools/IdeBenchmarks/SQLitePersistentStorageBenchmark.cs index 778a6dbe08607..290022941f109 100644 --- a/src/Tools/IdeBenchmarks/SQLitePersistentStorageBenchmark.cs +++ b/src/Tools/IdeBenchmarks/SQLitePersistentStorageBenchmark.cs @@ -61,13 +61,12 @@ public void GlobalSetup() "); - var connectionPoolService = _workspace.ExportProvider.GetExportedValue(); var asyncListener = _workspace.ExportProvider.GetExportedValue().GetListener(FeatureAttribute.PersistentStorage); - _storageService = new SQLitePersistentStorageService(connectionPoolService, new StorageConfiguration(), asyncListener); + _storageService = new SQLitePersistentStorageService(new StorageConfiguration(), asyncListener); var solution = _workspace.CurrentSolution; - _storage = _storageService.GetStorageWorkerAsync(SolutionKey.ToSolutionKey(solution), CancellationToken.None).AsTask().GetAwaiter().GetResult(); + _storage = _storageService.GetStorageAsync(SolutionKey.ToSolutionKey(solution), CancellationToken.None).AsTask().GetAwaiter().GetResult(); Console.WriteLine("Storage type: " + _storage.GetType()); _document = _workspace.CurrentSolution.Projects.Single().Documents.Single(); @@ -83,7 +82,6 @@ public void GlobalCleanup() } _document = null!; - _storage.Dispose(); _storage = null!; _storageService = null!; _workspace.Dispose(); diff --git a/src/Tools/IdeCoreBenchmarks/FindReferencesBenchmarks.cs b/src/Tools/IdeCoreBenchmarks/FindReferencesBenchmarks.cs index 7318e6a776139..bae7a906f62a3 100644 --- a/src/Tools/IdeCoreBenchmarks/FindReferencesBenchmarks.cs +++ b/src/Tools/IdeCoreBenchmarks/FindReferencesBenchmarks.cs @@ -84,10 +84,8 @@ private async Task LoadSolutionAsync() if (storageService == null) throw new ArgumentException("Couldn't get storage service"); - using (var storage = await storageService.GetStorageAsync(SolutionKey.ToSolutionKey(_workspace.CurrentSolution), CancellationToken.None)) - { - Console.WriteLine("Sucessfully got persistent storage instance"); - } + var storage = await storageService.GetStorageAsync(SolutionKey.ToSolutionKey(_workspace.CurrentSolution), CancellationToken.None); + Console.WriteLine("Successfully got persistent storage instance"); // There might be multiple projects with this name. That's ok. FAR goes and finds all the linked-projects // anyways to perform the search on all the equivalent symbols from them. So the end perf cost is the diff --git a/src/Tools/IdeCoreBenchmarks/NavigateToBenchmarks.cs b/src/Tools/IdeCoreBenchmarks/NavigateToBenchmarks.cs index be535cf145ab5..8b79dd838e9f9 100644 --- a/src/Tools/IdeCoreBenchmarks/NavigateToBenchmarks.cs +++ b/src/Tools/IdeCoreBenchmarks/NavigateToBenchmarks.cs @@ -183,24 +183,24 @@ public async Task RunFullParallelIndexing() Console.WriteLine("Starting indexing"); var storageService = _workspace.Services.SolutionServices.GetPersistentStorageService(); - using (var storage = await storageService.GetStorageAsync(SolutionKey.ToSolutionKey(_workspace.CurrentSolution), CancellationToken.None)) - { - Console.WriteLine("Successfully got persistent storage instance"); - var start = DateTime.Now; - var indexTime = TimeSpan.Zero; - var tasks = _workspace.CurrentSolution.Projects.SelectMany(p => p.Documents).Select(d => Task.Run( - async () => - { - var tree = await d.GetSyntaxRootAsync(); - var stopwatch = SharedStopwatch.StartNew(); - await TopLevelSyntaxTreeIndex.GetIndexAsync(d, default); - await SyntaxTreeIndex.GetIndexAsync(d, default); - indexTime += stopwatch.Elapsed; - })).ToList(); - await Task.WhenAll(tasks); - Console.WriteLine("Indexing time : " + indexTime); - Console.WriteLine("Solution parallel: " + (DateTime.Now - start)); - } + var storage = await storageService.GetStorageAsync(SolutionKey.ToSolutionKey(_workspace.CurrentSolution), CancellationToken.None); + + Console.WriteLine("Successfully got persistent storage instance"); + var start = DateTime.Now; + var indexTime = TimeSpan.Zero; + var tasks = _workspace.CurrentSolution.Projects.SelectMany(p => p.Documents).Select(d => Task.Run( + async () => + { + var tree = await d.GetSyntaxRootAsync(); + var stopwatch = SharedStopwatch.StartNew(); + await TopLevelSyntaxTreeIndex.GetIndexAsync(d, default); + await SyntaxTreeIndex.GetIndexAsync(d, default); + indexTime += stopwatch.Elapsed; + })).ToList(); + await Task.WhenAll(tasks); + Console.WriteLine("Indexing time : " + indexTime); + Console.WriteLine("Solution parallel: " + (DateTime.Now - start)); + Console.WriteLine("DB flushed"); Console.ReadLine(); } @@ -229,10 +229,10 @@ private async Task SearchAsync(Solution solution, IGrouping(); await service.SearchProjectsAsync( solution, grouping.ToImmutableArray(), priorityDocuments, "Syntax", service.KindsProvided, activeDocument: null, - (_, r) => + r => { lock (results) - results.Add(r); + results.AddRange(r); return Task.CompletedTask; }, diff --git a/src/Tools/Replay/README.md b/src/Tools/Replay/README.md index 9e6e6986efcdc..66d752c869343 100644 --- a/src/Tools/Replay/README.md +++ b/src/Tools/Replay/README.md @@ -23,4 +23,41 @@ e:\code\roslyn\src\Tools\Replay> dotnet run --framework net472 e:\code\example\m This runs all of the compilation events in the binary log against the compiler and outputs the results to `e:\temp`. -[binary-log]: https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Binary-Log.md \ No newline at end of file +[binary-log]: https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Binary-Log.md + +## Example Usage + +### dotnet trace + +To profile with `dotnet trace` first run with the `-w` option to get the PID of the compiler server. Then start up `dotnet trace` against that PID and have replay continue + +Console 1 + +```cmd +e:\code\roslyn\src\Tools\Replay> dotnet run --framework net472 e:\code\example\msbuild.binlog -w +Binary Log: E:\code\example\msbuild.binlog +Client Directory: E:\code\roslyn\artifacts\bin\Replay\Release\net8.0\ +Output Directory: E:\code\roslyn\src\Tools\Replay\output +Pipe Name: 0254ccf8-294e-4b8f-a606-70f105b9e4a1 +Parallel: 6 + +Starting server +Process Id: 48752 +Press any key to continue +``` + +Console 2 + +```cmd +e:\users\jaredpar> dotnet trace collect --profile gc-verbose -p 48752 + +Provider Name Keywords Level Enabled By +Microsoft-Windows-DotNETRuntime 0x0000000000008003 Verbose(5) --profile + +Process : C:\Program Files\dotnet\dotnet.exe +Output File : C:\Users\jaredpar\dotnet.exe_20240502_083035.nettrace + +[00:00:01:13] Recording trace 379.5907 (MB) +``` + +Then go back to Console 1 and press any key to continue. The trace will automatically stop once the replay operation is complete. \ No newline at end of file diff --git a/src/Tools/Replay/Replay.cs b/src/Tools/Replay/Replay.cs index 444b79364ae03..47c4550450a52 100644 --- a/src/Tools/Replay/Replay.cs +++ b/src/Tools/Replay/Replay.cs @@ -56,7 +56,7 @@ static ReplayOptions ParseOptions(string[] args) if (string.IsNullOrEmpty(outputDirectory)) { - outputDirectory = Path.Combine(Environment.CurrentDirectory, "output"); + outputDirectory = Path.Combine(Path.GetTempPath(), "replay"); } return new ReplayOptions( diff --git a/src/VisualStudio/CSharp/Impl/ChangeSignature/CSharpChangeSignatureViewModelFactoryService.cs b/src/VisualStudio/CSharp/Impl/ChangeSignature/CSharpChangeSignatureViewModelFactoryService.cs index 450ff38f04e2f..1453ad36fe7dc 100644 --- a/src/VisualStudio/CSharp/Impl/ChangeSignature/CSharpChangeSignatureViewModelFactoryService.cs +++ b/src/VisualStudio/CSharp/Impl/ChangeSignature/CSharpChangeSignatureViewModelFactoryService.cs @@ -47,7 +47,7 @@ public override SymbolDisplayPart[] GeneratePreviewDisplayParts(AddedParameterVi parts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.Text, null, addedParameterViewModel.Default)); } - return parts.ToArray(); + return [.. parts]; } // Use LangVersion Preview to ensure that all types parse correctly. If the user types in a type only available diff --git a/src/VisualStudio/CSharp/Impl/Options/CSharpVisualStudioCopilotOptionsService.cs b/src/VisualStudio/CSharp/Impl/Options/CSharpVisualStudioCopilotOptionsService.cs index 5012e6c7c4acb..f5fcb69f0bb5f 100644 --- a/src/VisualStudio/CSharp/Impl/Options/CSharpVisualStudioCopilotOptionsService.cs +++ b/src/VisualStudio/CSharp/Impl/Options/CSharpVisualStudioCopilotOptionsService.cs @@ -42,6 +42,7 @@ internal sealed class CSharpVisualStudioCopilotOptionsService : ICopilotOptionsS private const string CopilotOptionNamePrefix = "Microsoft.VisualStudio.Conversations"; private const string CopilotCodeAnalysisOptionName = "EnableCSharpCodeAnalysis"; private const string CopilotRefineOptionName = "EnableCSharpRefineQuickActionSuggestion"; + private const string CopilotOnTheFlyDocsOptionName = "EnableCSharpOnTheFlyDocs"; private static readonly UIContext s_copilotHasLoadedUIContext = UIContext.FromUIContextGuid(new Guid(CopilotHasLoadedGuid)); private static readonly UIContext s_gitHubAccountStatusDeterminedContext = UIContext.FromUIContextGuid(new Guid(GitHubAccountStatusDetermined)); @@ -84,4 +85,7 @@ public Task IsCodeAnalysisOptionEnabledAsync() public Task IsRefineOptionEnabledAsync() => IsCopilotOptionEnabledAsync(CopilotRefineOptionName); + + public Task IsOnTheFlyDocsOptionEnabledAsync() + => IsCopilotOptionEnabledAsync(CopilotOnTheFlyDocsOptionName); } diff --git a/src/VisualStudio/CSharp/Impl/PackageRegistration.pkgdef b/src/VisualStudio/CSharp/Impl/PackageRegistration.pkgdef index ee45c4813cee4..852593e3f17a3 100644 --- a/src/VisualStudio/CSharp/Impl/PackageRegistration.pkgdef +++ b/src/VisualStudio/CSharp/Impl/PackageRegistration.pkgdef @@ -198,4 +198,4 @@ [$RootKey$\SettingsManifests\{13c3bbb4-f18f-4111-9f54-a0fb010d9194}] @="Microsoft.VisualStudio.LanguageServices.CSharp.LanguageService.CSharpPackage" "ManifestPath"="$PackageFolder$\UnifiedSettings\csharpSettings.registration.json" -"CacheTag"=qword:08DC1824DFE0117B +"CacheTag"=qword:CE76341698AB8CD3 diff --git a/src/VisualStudio/CSharp/Test/Microsoft.VisualStudio.LanguageServices.CSharp.UnitTests.csproj b/src/VisualStudio/CSharp/Test/Microsoft.VisualStudio.LanguageServices.CSharp.UnitTests.csproj index a1822e09b908e..b42bdacc847c3 100644 --- a/src/VisualStudio/CSharp/Test/Microsoft.VisualStudio.LanguageServices.CSharp.UnitTests.csproj +++ b/src/VisualStudio/CSharp/Test/Microsoft.VisualStudio.LanguageServices.CSharp.UnitTests.csproj @@ -82,5 +82,6 @@ + \ No newline at end of file diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/AbstractPersistentStorageTests.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/AbstractPersistentStorageTests.cs index fb895470cd613..4364077c46c92 100644 --- a/src/VisualStudio/CSharp/Test/PersistentStorage/AbstractPersistentStorageTests.cs +++ b/src/VisualStudio/CSharp/Test/PersistentStorage/AbstractPersistentStorageTests.cs @@ -85,12 +85,6 @@ protected AbstractPersistentStorageTests() ThreadPool.SetMinThreads(Math.Max(workerThreads, NumThreads), completionPortThreads); } - internal abstract AbstractPersistentStorageService GetStorageService( - IMefHostExportProvider exportProvider, - IPersistentStorageConfiguration configuration, - IPersistentStorageFaultInjector? faultInjector, - string rootFolder); - public void Dispose() { // This should cause the service to release the cached connection it maintains for the primary workspace @@ -121,7 +115,7 @@ public async Task TestNullFilePaths() var streamName = "stream"; - await using var storage = await GetStorageAsync(solution); + var storage = await GetStorageAsync(solution); var project = solution.Projects.First(); var document = project.Documents.First(); Assert.False(await storage.WriteStreamAsync(project, streamName, EncodeString(""))); @@ -142,14 +136,14 @@ public async Task CacheDirectoryInPathWithSingleQuote(Size size, bool withChecks var streamName1 = "PersistentService_Solution_WriteReadDifferentInstances1"; var streamName2 = "PersistentService_Solution_WriteReadDifferentInstances2"; - await using (var storage = await GetStorageAsync(solution, folder)) { + var storage = await GetStorageAsync(solution, folder); Assert.True(await storage.WriteStreamAsync(streamName1, EncodeString(GetData1(size)), GetChecksum1(withChecksum))); Assert.True(await storage.WriteStreamAsync(streamName2, EncodeString(GetData2(size)), GetChecksum2(withChecksum))); } - await using (var storage = await GetStorageAsync(solution, folder)) { + var storage = await GetStorageAsync(solution, folder); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(streamName1, GetChecksum1(withChecksum)))); Assert.Equal(GetData2(size), ReadStringToEnd(await storage.ReadStreamAsync(streamName2, GetChecksum2(withChecksum)))); } @@ -163,14 +157,14 @@ public async Task PersistentService_Solution_WriteReadDifferentInstances(Size si var streamName1 = "PersistentService_Solution_WriteReadDifferentInstances1"; var streamName2 = "PersistentService_Solution_WriteReadDifferentInstances2"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.WriteStreamAsync(streamName1, EncodeString(GetData1(size)), GetChecksum1(withChecksum))); Assert.True(await storage.WriteStreamAsync(streamName2, EncodeString(GetData2(size)), GetChecksum2(withChecksum))); } - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(streamName1, GetChecksum1(withChecksum)))); Assert.Equal(GetData2(size), ReadStringToEnd(await storage.ReadStreamAsync(streamName2, GetChecksum2(withChecksum)))); } @@ -184,16 +178,16 @@ public async Task PersistentService_Solution_WriteReadReopenSolution(Size size, var streamName1 = "PersistentService_Solution_WriteReadReopenSolution1"; var streamName2 = "PersistentService_Solution_WriteReadReopenSolution2"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.WriteStreamAsync(streamName1, EncodeString(GetData1(size)), GetChecksum1(withChecksum))); Assert.True(await storage.WriteStreamAsync(streamName2, EncodeString(GetData2(size)), GetChecksum2(withChecksum))); } solution = CreateOrOpenSolution(); - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(streamName1, GetChecksum1(withChecksum)))); Assert.Equal(GetData2(size), ReadStringToEnd(await storage.ReadStreamAsync(streamName2, GetChecksum2(withChecksum)))); } @@ -207,7 +201,7 @@ public async Task PersistentService_Solution_WriteReadSameInstance(Size size, bo var streamName1 = "PersistentService_Solution_WriteReadSameInstance1"; var streamName2 = "PersistentService_Solution_WriteReadSameInstance2"; - await using var storage = await GetStorageAsync(solution); + var storage = await GetStorageAsync(solution); Assert.True(await storage.WriteStreamAsync(streamName1, EncodeString(GetData1(size)), GetChecksum1(withChecksum))); Assert.True(await storage.WriteStreamAsync(streamName2, EncodeString(GetData2(size)), GetChecksum2(withChecksum))); @@ -223,7 +217,7 @@ public async Task PersistentService_Project_WriteReadSameInstance(Size size, boo var streamName1 = "PersistentService_Project_WriteReadSameInstance1"; var streamName2 = "PersistentService_Project_WriteReadSameInstance2"; - await using var storage = await GetStorageAsync(solution); + var storage = await GetStorageAsync(solution); var project = solution.Projects.Single(); Assert.True(await storage.WriteStreamAsync(project, streamName1, EncodeString(GetData1(size)), GetChecksum1(withChecksum))); @@ -241,7 +235,7 @@ public async Task PersistentService_Document_WriteReadSameInstance(Size size, bo var streamName1 = "PersistentService_Document_WriteReadSameInstance1"; var streamName2 = "PersistentService_Document_WriteReadSameInstance2"; - await using var storage = await GetStorageAsync(solution); + var storage = await GetStorageAsync(solution); var document = solution.Projects.Single().Documents.Single(); Assert.True(await storage.WriteStreamAsync(document, streamName1, EncodeString(GetData1(size)), GetChecksum1(withChecksum))); @@ -259,7 +253,7 @@ public async Task PersistentService_Solution_SimultaneousWrites([CombinatorialRa var streamName1 = "PersistentService_Solution_SimultaneousWrites1"; - await using var storage = await GetStorageAsync(solution); + var storage = await GetStorageAsync(solution); DoSimultaneousWrites(s => storage.WriteStreamAsync(streamName1, EncodeString(s))); var value = int.Parse(ReadStringToEnd(await storage.ReadStreamAsync(streamName1))); Assert.True(value >= 0); @@ -274,7 +268,7 @@ public async Task PersistentService_Project_SimultaneousWrites([CombinatorialRan var streamName1 = "PersistentService_Project_SimultaneousWrites1"; - await using var storage = await GetStorageAsync(solution); + var storage = await GetStorageAsync(solution); DoSimultaneousWrites(s => storage.WriteStreamAsync(solution.Projects.Single(), streamName1, EncodeString(s))); var value = int.Parse(ReadStringToEnd(await storage.ReadStreamAsync(solution.Projects.Single(), streamName1))); Assert.True(value >= 0); @@ -289,7 +283,7 @@ public async Task PersistentService_Document_SimultaneousWrites([CombinatorialRa var streamName1 = "PersistentService_Document_SimultaneousWrites1"; - await using var storage = await GetStorageAsync(solution); + var storage = await GetStorageAsync(solution); DoSimultaneousWrites(s => storage.WriteStreamAsync(solution.Projects.Single().Documents.Single(), streamName1, EncodeString(s))); var value = int.Parse(ReadStringToEnd(await storage.ReadStreamAsync(solution.Projects.Single().Documents.Single(), streamName1))); Assert.True(value >= 0); @@ -303,7 +297,7 @@ public async Task PersistentService_Solution_SimultaneousReads(Size size, bool w var solution = CreateOrOpenSolution(); var streamName1 = "PersistentService_Solution_SimultaneousReads1"; - await using var storage = await GetStorageAsync(solution); + var storage = await GetStorageAsync(solution); Assert.True(await storage.WriteStreamAsync(streamName1, EncodeString(GetData1(size)), GetChecksum1(withChecksum))); DoSimultaneousReads(async () => ReadStringToEnd(await storage.ReadStreamAsync(streamName1, GetChecksum1(withChecksum))), GetData1(size)); } @@ -315,7 +309,7 @@ public async Task PersistentService_Project_SimultaneousReads(Size size, bool wi var solution = CreateOrOpenSolution(); var streamName1 = "PersistentService_Project_SimultaneousReads1"; - await using var storage = await GetStorageAsync(solution); + var storage = await GetStorageAsync(solution); Assert.True(await storage.WriteStreamAsync(solution.Projects.Single(), streamName1, EncodeString(GetData1(size)), GetChecksum1(withChecksum))); DoSimultaneousReads(async () => ReadStringToEnd(await storage.ReadStreamAsync(solution.Projects.Single(), streamName1, GetChecksum1(withChecksum))), GetData1(size)); } @@ -328,7 +322,7 @@ public async Task PersistentService_Document_SimultaneousReads(Size size, bool w var solution = CreateOrOpenSolution(); var streamName1 = "PersistentService_Document_SimultaneousReads1"; - await using var storage = await GetStorageAsync(solution); + var storage = await GetStorageAsync(solution); Assert.True(await storage.WriteStreamAsync(solution.Projects.Single().Documents.Single(), streamName1, EncodeString(GetData1(size)), GetChecksum1(withChecksum))); DoSimultaneousReads(async () => ReadStringToEnd(await storage.ReadStreamAsync(solution.Projects.Single().Documents.Single(), streamName1, GetChecksum1(withChecksum))), GetData1(size)); } @@ -341,7 +335,7 @@ public async Task TestReadChecksumReturnsNullWhenNeverWritten([CombinatorialRang var streamName1 = "TestReadChecksumReturnsNullWhenNeverWritten"; - await using var storage = await GetStorageAsync(solution); + var storage = await GetStorageAsync(solution); Assert.False(await storage.ChecksumMatchesAsync(streamName1, s_checksum1)); } @@ -353,13 +347,13 @@ public async Task TestCanReadWithNullChecksumSomethingWrittenWithNonNullChecksum var streamName1 = "TestCanReadWithNullChecksumSomethingWrittenWithNonNullChecksum"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.WriteStreamAsync(streamName1, EncodeString(GetData1(size)), s_checksum1)); } - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(streamName1, checksum: null))); } } @@ -372,13 +366,13 @@ public async Task TestCannotReadWithMismatchedChecksums(Size size, [Combinatoria var streamName1 = "TestCannotReadWithMismatchedChecksums"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.WriteStreamAsync(streamName1, EncodeString(GetData1(size)), s_checksum1)); } - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.Null(await storage.ReadStreamAsync(streamName1, s_checksum2)); } } @@ -391,13 +385,13 @@ public async Task TestCannotReadChecksumIfWriteDidNotIncludeChecksum(Size size, var streamName1 = "TestCannotReadChecksumIfWriteDidNotIncludeChecksum"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.WriteStreamAsync(streamName1, EncodeString(GetData1(size)), checksum: null)); } - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.False(await storage.ChecksumMatchesAsync(streamName1, s_checksum1)); } } @@ -410,13 +404,13 @@ public async Task TestReadChecksumProducesWrittenChecksum(Size size, [Combinator var streamName1 = "TestReadChecksumProducesWrittenChecksum"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.WriteStreamAsync(streamName1, EncodeString(GetData1(size)), checksum: s_checksum1)); } - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.ChecksumMatchesAsync(streamName1, s_checksum1)); } } @@ -429,14 +423,14 @@ public async Task TestReadChecksumProducesLastWrittenChecksum1(Size size, [Combi var streamName1 = "TestReadChecksumProducesLastWrittenChecksum1"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.WriteStreamAsync(streamName1, EncodeString(GetData1(size)), checksum: s_checksum1)); Assert.True(await storage.WriteStreamAsync(streamName1, EncodeString(GetData1(size)), checksum: null)); } - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.False(await storage.ChecksumMatchesAsync(streamName1, s_checksum1)); } } @@ -449,14 +443,14 @@ public async Task TestReadChecksumProducesLastWrittenChecksum2(Size size, [Combi var streamName1 = "TestReadChecksumProducesLastWrittenChecksum2"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.WriteStreamAsync(streamName1, EncodeString(GetData1(size)), checksum: null)); Assert.True(await storage.WriteStreamAsync(streamName1, EncodeString(GetData1(size)), checksum: s_checksum1)); } - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.ChecksumMatchesAsync(streamName1, s_checksum1)); } } @@ -469,14 +463,14 @@ public async Task TestReadChecksumProducesLastWrittenChecksum3(Size size, [Combi var streamName1 = "TestReadChecksumProducesLastWrittenChecksum3"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.WriteStreamAsync(streamName1, EncodeString(GetData1(size)), checksum: s_checksum1)); Assert.True(await storage.WriteStreamAsync(streamName1, EncodeString(GetData1(size)), checksum: s_checksum2)); } - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.ChecksumMatchesAsync(streamName1, s_checksum2)); } } @@ -490,13 +484,13 @@ public async Task TestOpenWithSolutionKeyReadWithDocumentKey(Size size, [Combina var streamName1 = "stream"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); await storage.WriteStreamAsync(document, streamName1, EncodeString(GetData1(size)), checksum: s_checksum1); } - await using (var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution))) { + var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution)); Assert.True(await storage.ChecksumMatchesAsync(DocumentKey.ToDocumentKey(document), streamName1, s_checksum1)); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(DocumentKey.ToDocumentKey(document), streamName1))); } @@ -511,13 +505,13 @@ public async Task TestOpenWithSolutionKeyReadWithDocument(Size size, [Combinator var streamName1 = "stream"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); await storage.WriteStreamAsync(document, streamName1, EncodeString(GetData1(size)), checksum: s_checksum1); } - await using (var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution))) { + var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution)); Assert.True(await storage.ChecksumMatchesAsync(document, streamName1, s_checksum1)); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(document, streamName1))); } @@ -532,13 +526,13 @@ public async Task TestOpenWithSolutionReadWithDocumentKey(Size size, [Combinator var streamName1 = "stream"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); await storage.WriteStreamAsync(document, streamName1, EncodeString(GetData1(size)), checksum: s_checksum1); } - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.ChecksumMatchesAsync(DocumentKey.ToDocumentKey(document), streamName1, s_checksum1)); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(DocumentKey.ToDocumentKey(document), streamName1))); } @@ -553,13 +547,13 @@ public async Task TestOpenWithSolutionReadWithDocument(Size size, [Combinatorial var streamName1 = "stream"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); await storage.WriteStreamAsync(document, streamName1, EncodeString(GetData1(size)), checksum: s_checksum1); } - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.ChecksumMatchesAsync(document, streamName1, s_checksum1)); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(document, streamName1))); } @@ -574,13 +568,13 @@ public async Task TestOpenWithSolutionReadWithDocumentKeyAndDocument1(Size size, var streamName1 = "stream"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); await storage.WriteStreamAsync(document, streamName1, EncodeString(GetData1(size)), checksum: s_checksum1); } - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.ChecksumMatchesAsync(DocumentKey.ToDocumentKey(document), streamName1, s_checksum1)); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(DocumentKey.ToDocumentKey(document), streamName1))); @@ -598,13 +592,13 @@ public async Task TestOpenWithSolutionReadWithDocumentKeyAndDocument2(Size size, var streamName1 = "stream"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); await storage.WriteStreamAsync(document, streamName1, EncodeString(GetData1(size)), checksum: s_checksum1); } - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.ChecksumMatchesAsync(document, streamName1, s_checksum1)); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(document, streamName1))); @@ -622,13 +616,13 @@ public async Task TestOpenWithSolutionKeyReadWithDocumentKeyAndDocument1(Size si var streamName1 = "stream"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); await storage.WriteStreamAsync(document, streamName1, EncodeString(GetData1(size)), checksum: s_checksum1); } - await using (var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution))) { + var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution)); Assert.True(await storage.ChecksumMatchesAsync(DocumentKey.ToDocumentKey(document), streamName1, s_checksum1)); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(DocumentKey.ToDocumentKey(document), streamName1))); @@ -646,13 +640,13 @@ public async Task TestOpenWithSolutionKeyReadWithDocumentKeyAndDocument2(Size si var streamName1 = "stream"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); await storage.WriteStreamAsync(document, streamName1, EncodeString(GetData1(size)), checksum: s_checksum1); } - await using (var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution))) { + var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution)); Assert.True(await storage.ChecksumMatchesAsync(document, streamName1, s_checksum1)); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(document, streamName1))); @@ -670,13 +664,13 @@ public async Task TestOpenWithSolutionKeyReadWithDocumentKey_WriteWithSolutionKe var streamName1 = "stream"; - await using (var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution))) { + var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution)); await storage.WriteStreamAsync(document, streamName1, EncodeString(GetData1(size)), checksum: s_checksum1); } - await using (var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution))) { + var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution)); Assert.True(await storage.ChecksumMatchesAsync(DocumentKey.ToDocumentKey(document), streamName1, s_checksum1)); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(DocumentKey.ToDocumentKey(document), streamName1))); } @@ -691,13 +685,13 @@ public async Task TestOpenWithSolutionKeyReadWithDocument_WriteWithSolutionKey(S var streamName1 = "stream"; - await using (var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution))) { + var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution)); await storage.WriteStreamAsync(document, streamName1, EncodeString(GetData1(size)), checksum: s_checksum1); } - await using (var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution))) { + var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution)); Assert.True(await storage.ChecksumMatchesAsync(document, streamName1, s_checksum1)); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(document, streamName1))); } @@ -712,13 +706,13 @@ public async Task TestOpenWithSolutionReadWithDocumentKey_WriteWithSolutionKey(S var streamName1 = "stream"; - await using (var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution))) { + var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution)); await storage.WriteStreamAsync(document, streamName1, EncodeString(GetData1(size)), checksum: s_checksum1); } - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.ChecksumMatchesAsync(DocumentKey.ToDocumentKey(document), streamName1, s_checksum1)); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(DocumentKey.ToDocumentKey(document), streamName1))); } @@ -733,13 +727,13 @@ public async Task TestOpenWithSolutionReadWithDocument_WriteWithSolutionKey(Size var streamName1 = "stream"; - await using (var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution))) { + var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution)); await storage.WriteStreamAsync(document, streamName1, EncodeString(GetData1(size)), checksum: s_checksum1); } - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.ChecksumMatchesAsync(document, streamName1, s_checksum1)); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(document, streamName1))); } @@ -754,13 +748,13 @@ public async Task TestOpenWithSolutionReadWithDocumentKeyAndDocument1_WriteWithS var streamName1 = "stream"; - await using (var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution))) { + var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution)); await storage.WriteStreamAsync(document, streamName1, EncodeString(GetData1(size)), checksum: s_checksum1); } - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.ChecksumMatchesAsync(DocumentKey.ToDocumentKey(document), streamName1, s_checksum1)); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(DocumentKey.ToDocumentKey(document), streamName1))); @@ -778,13 +772,13 @@ public async Task TestOpenWithSolutionReadWithDocumentKeyAndDocument2_WriteWithS var streamName1 = "stream"; - await using (var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution))) { + var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution)); await storage.WriteStreamAsync(document, streamName1, EncodeString(GetData1(size)), checksum: s_checksum1); } - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.ChecksumMatchesAsync(document, streamName1, s_checksum1)); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(document, streamName1))); @@ -802,13 +796,13 @@ public async Task TestOpenWithSolutionKeyReadWithDocumentKeyAndDocument1_WriteWi var streamName1 = "stream"; - await using (var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution))) { + var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution)); await storage.WriteStreamAsync(document, streamName1, EncodeString(GetData1(size)), checksum: s_checksum1); } - await using (var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution))) { + var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution)); Assert.True(await storage.ChecksumMatchesAsync(DocumentKey.ToDocumentKey(document), streamName1, s_checksum1)); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(DocumentKey.ToDocumentKey(document), streamName1))); @@ -826,13 +820,13 @@ public async Task TestOpenWithSolutionKeyReadWithDocumentKeyAndDocument2_WriteWi var streamName1 = "stream"; - await using (var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution))) { + var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution)); await storage.WriteStreamAsync(document, streamName1, EncodeString(GetData1(size)), checksum: s_checksum1); } - await using (var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution))) { + var storage = await GetStorageFromKeyAsync(solution.Workspace.Services, SolutionKey.ToSolutionKey(solution)); Assert.True(await storage.ChecksumMatchesAsync(document, streamName1, s_checksum1)); Assert.Equal(GetData1(size), ReadStringToEnd(await storage.ReadStreamAsync(document, streamName1))); @@ -859,13 +853,13 @@ public async Task PersistentService_ReadByteTwice(Size size, bool withChecksum, var solution = CreateOrOpenSolution(); var streamName1 = "PersistentService_ReadByteTwice"; - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); Assert.True(await storage.WriteStreamAsync(streamName1, EncodeString(GetData1(size)), GetChecksum1(withChecksum))); } - await using (var storage = await GetStorageAsync(solution)) { + var storage = await GetStorageAsync(solution); using var stream = await storage.ReadStreamAsync(streamName1, GetChecksum1(withChecksum)); Contract.ThrowIfNull(stream); stream.ReadByte(); @@ -883,8 +877,8 @@ public async Task TestPersistSyntaxTreeIndex([CombinatorialRange(0, Iterations)] var document = solution.GetRequiredDocument(id); - await using (var storage = await GetStorageAsync(solution)) { + _ = await GetStorageAsync(solution); var index = await SyntaxTreeIndex.GetRequiredIndexAsync(document, default); await index.SaveAsync(document, _storageService!); @@ -903,8 +897,8 @@ public async Task TestPersistTopLevelSyntaxTreeIndex([CombinatorialRange(0, Iter var document = solution.GetRequiredDocument(id); - await using (var storage = await GetStorageAsync(solution)) { + _ = await GetStorageAsync(solution); var index = await TopLevelSyntaxTreeIndex.GetRequiredIndexAsync(document, default); await index.SaveAsync(document, _storageService!); @@ -981,6 +975,7 @@ private static void DoSimultaneousWrites(Func write) protected Solution CreateOrOpenSolution(TempDirectory? persistentFolder = null, bool nullPaths = false) { persistentFolder ??= _persistentFolder; + _storageService?.GetTestAccessor().Shutdown(); var solutionFile = persistentFolder.CreateOrOpenFile("Solution1.sln").WriteAllText(""); var info = SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Create(), solutionFile.Path); @@ -1016,13 +1011,14 @@ internal async Task GetStorageAsync( _storageService?.GetTestAccessor().Shutdown(); var configuration = new MockPersistentStorageConfiguration(solution.Id, persistentFolder.Path, throwOnFailure); - _storageService = GetStorageService(solution.Workspace.Services.SolutionServices.ExportProvider, configuration, faultInjector, _persistentFolder.Path); - var storage = await _storageService.GetStorageAsync(SolutionKey.ToSolutionKey(solution), CancellationToken.None); + _storageService = (AbstractPersistentStorageService)solution.Workspace.Services.SolutionServices.GetPersistentStorageService(); + var storage = await _storageService.GetStorageAsync( + SolutionKey.ToSolutionKey(solution), configuration, faultInjector, CancellationToken.None); // If we're injecting faults, we expect things to be strange if (faultInjector == null) { - Assert.NotEqual(NoOpPersistentStorage.TestAccessor.StorageInstance, storage); + Assert.NotEqual(NoOpPersistentStorage.TestAccessor.GetStorageInstance(SolutionKey.ToSolutionKey(solution)), storage); } return storage; @@ -1031,17 +1027,16 @@ internal async Task GetStorageAsync( internal async Task GetStorageFromKeyAsync( HostWorkspaceServices services, SolutionKey solutionKey, IPersistentStorageFaultInjector? faultInjector = null) { - // If we handed out one for a previous test, we need to shut that down first - _storageService?.GetTestAccessor().Shutdown(); var configuration = new MockPersistentStorageConfiguration(solutionKey.Id, _persistentFolder.Path, throwOnFailure: true); - _storageService = GetStorageService(services.SolutionServices.ExportProvider, configuration, faultInjector, _persistentFolder.Path); - var storage = await _storageService.GetStorageAsync(solutionKey, CancellationToken.None); + _storageService = (AbstractPersistentStorageService)services.SolutionServices.GetPersistentStorageService(); + var storage = await _storageService.GetStorageAsync( + solutionKey, configuration, faultInjector, CancellationToken.None); // If we're injecting faults, we expect things to be strange if (faultInjector == null) { - Assert.NotEqual(NoOpPersistentStorage.TestAccessor.StorageInstance, storage); + Assert.NotEqual(NoOpPersistentStorage.TestAccessor.GetStorageInstance(solutionKey), storage); } return storage; diff --git a/src/VisualStudio/CSharp/Test/PersistentStorage/SQLiteV2PersistentStorageTests.cs b/src/VisualStudio/CSharp/Test/PersistentStorage/SQLiteV2PersistentStorageTests.cs index ef6a87a205418..6f1ef7522c274 100644 --- a/src/VisualStudio/CSharp/Test/PersistentStorage/SQLiteV2PersistentStorageTests.cs +++ b/src/VisualStudio/CSharp/Test/PersistentStorage/SQLiteV2PersistentStorageTests.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.SQLite.v2; using Microsoft.CodeAnalysis.Storage; @@ -23,13 +22,6 @@ namespace Microsoft.CodeAnalysis.UnitTests.WorkspaceServices /// public class SQLiteV2PersistentStorageTests : AbstractPersistentStorageTests { - internal override AbstractPersistentStorageService GetStorageService(IMefHostExportProvider exportProvider, IPersistentStorageConfiguration configuration, IPersistentStorageFaultInjector? faultInjector, string relativePathBase) - => new SQLitePersistentStorageService( - exportProvider.GetExports().Single().Value, - configuration, - exportProvider.GetExports().Single().Value.GetListener(FeatureAttribute.PersistentStorage), - faultInjector); - [Fact] public async Task TestCrashInNewConnection() { @@ -46,7 +38,7 @@ public async Task TestCrashInNewConnection() // Because instantiating the connection will fail, we will not get back // a working persistent storage. We are testing a fault recovery code path. - await using (var storage = await GetStorageAsync(solution, faultInjector: faultInjector, throwOnFailure: false)) + var storage = await GetStorageAsync(solution, faultInjector: faultInjector, throwOnFailure: false); using (var memStream = new MemoryStream()) using (var streamWriter = new StreamWriter(memStream)) { diff --git a/src/VisualStudio/CSharp/Test/UnifiedSettings/CSharpUnifiedSettingsTests.cs b/src/VisualStudio/CSharp/Test/UnifiedSettings/CSharpUnifiedSettingsTests.cs index 4258c8a03cd5b..cd9d0faf9bdc3 100644 --- a/src/VisualStudio/CSharp/Test/UnifiedSettings/CSharpUnifiedSettingsTests.cs +++ b/src/VisualStudio/CSharp/Test/UnifiedSettings/CSharpUnifiedSettingsTests.cs @@ -102,7 +102,7 @@ internal override object GetOptionsDefaultValue(IOption2 option) [Fact] public async Task IntelliSensePageTests() { - var registrationFileStream = typeof(CSharpUnifiedSettingsTests).GetTypeInfo().Assembly.GetManifestResourceStream("Roslyn.VisualStudio.CSharp.UnitTests.csharpSettings.registration.json"); + using var registrationFileStream = typeof(CSharpUnifiedSettingsTests).GetTypeInfo().Assembly.GetManifestResourceStream("Roslyn.VisualStudio.CSharp.UnitTests.csharpSettings.registration.json"); using var reader = new StreamReader(registrationFileStream); var registrationFile = await reader.ReadToEndAsync().ConfigureAwait(false); var registrationJsonObject = JObject.Parse(registrationFile, new JsonLoadSettings() { CommentHandling = CommentHandling.Ignore }); @@ -110,7 +110,10 @@ public async Task IntelliSensePageTests() Assert.Equal("C#", actual: categoriesTitle.ToString()); var optionPageId = registrationJsonObject.SelectToken("$.categories['textEditor.csharp.intellisense'].legacyOptionPageId"); Assert.Equal(Guids.CSharpOptionPageIntelliSenseIdString, optionPageId!.ToString()); - TestUnifiedSettingsCategory(registrationJsonObject, categoryBasePath: "textEditor.csharp.intellisense", languageName: LanguageNames.CSharp); + using var pkgdefFileStream = typeof(CSharpUnifiedSettingsTests).GetTypeInfo().Assembly.GetManifestResourceStream("Roslyn.VisualStudio.CSharp.UnitTests.PackageRegistration.pkgdef"); + using var pkgdefReader = new StreamReader(pkgdefFileStream); + var pkgdefFile = await pkgdefReader.ReadToEndAsync().ConfigureAwait(false); + TestUnifiedSettingsCategory(registrationJsonObject, categoryBasePath: "textEditor.csharp.intellisense", languageName: LanguageNames.CSharp, pkgdefFile); } } } diff --git a/src/VisualStudio/Core/Def/AnalyzerDependency/AnalyzerFileWatcherService.cs b/src/VisualStudio/Core/Def/AnalyzerDependency/AnalyzerFileWatcherService.cs deleted file mode 100644 index 570fe959cf266..0000000000000 --- a/src/VisualStudio/Core/Def/AnalyzerDependency/AnalyzerFileWatcherService.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.ComponentModel.Composition; -using System.IO; -using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; -using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Shell.Interop; - -namespace Microsoft.VisualStudio.LanguageServices.Implementation; - -[Export(typeof(AnalyzerFileWatcherService))] -[method: ImportingConstructor] -[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] -internal sealed class AnalyzerFileWatcherService(SVsServiceProvider serviceProvider) -{ - private readonly IVsFileChangeEx _fileChangeService = (IVsFileChangeEx)serviceProvider.GetService(typeof(SVsFileChangeEx)); - - private readonly Dictionary _fileChangeTrackers = new(StringComparer.OrdinalIgnoreCase); - - /// - /// Holds a list of assembly modified times that we can use to detect a file change prior to the being in place. - /// Once it's in place and subscribed, we'll remove the entry because any further changes will be detected that way. - /// - private readonly Dictionary _assemblyUpdatedTimesUtc = new(StringComparer.OrdinalIgnoreCase); - - private readonly object _guard = new(); - - private static DateTime? GetLastUpdateTimeUtc(string fullPath) - { - try - { - var creationTimeUtc = File.GetCreationTimeUtc(fullPath); - var writeTimeUtc = File.GetLastWriteTimeUtc(fullPath); - - return writeTimeUtc > creationTimeUtc ? writeTimeUtc : creationTimeUtc; - } - catch (IOException) - { - return null; - } - catch (UnauthorizedAccessException) - { - return null; - } - } - - internal void TrackFilePathAndReportErrorIfChanged(string filePath) - { - lock (_guard) - { - if (!_fileChangeTrackers.TryGetValue(filePath, out var tracker)) - { - tracker = new FileChangeTracker(_fileChangeService, filePath); - tracker.UpdatedOnDisk += Tracker_UpdatedOnDisk; - _ = tracker.StartFileChangeListeningAsync(); - - _fileChangeTrackers.Add(filePath, tracker); - } - - if (_assemblyUpdatedTimesUtc.TryGetValue(filePath, out var assemblyUpdatedTime)) - { - var currentFileUpdateTime = GetLastUpdateTimeUtc(filePath); - - if (currentFileUpdateTime != null) - { - // If the the tracker is in place, at this point we can stop checking any further for this assembly - if (tracker.PreviousCallToStartFileChangeHasAsynchronouslyCompleted) - { - _assemblyUpdatedTimesUtc.Remove(filePath); - } - } - } - else - { - // We don't have an assembly updated time. This means we either haven't ever checked it, or we have a file watcher in place. - // If the file watcher is in place, then nothing further to do. Otherwise we'll add the update time to the map for future checking - if (!tracker.PreviousCallToStartFileChangeHasAsynchronouslyCompleted) - { - var currentFileUpdateTime = GetLastUpdateTimeUtc(filePath); - - if (currentFileUpdateTime != null) - { - _assemblyUpdatedTimesUtc[filePath] = currentFileUpdateTime.Value; - } - } - } - } - } - - private void Tracker_UpdatedOnDisk(object sender, EventArgs e) - { - var tracker = (FileChangeTracker)sender; - var filePath = tracker.FilePath; - - lock (_guard) - { - // Once we've created a diagnostic for a given analyzer file, there's - // no need to keep watching it. - _fileChangeTrackers.Remove(filePath); - } - - tracker.Dispose(); - tracker.UpdatedOnDisk -= Tracker_UpdatedOnDisk; - } -} diff --git a/src/VisualStudio/Core/Def/CodeCleanup/AbstractCodeCleanUpFixer.cs b/src/VisualStudio/Core/Def/CodeCleanup/AbstractCodeCleanUpFixer.cs index ab3025275b5a2..b695530f678da 100644 --- a/src/VisualStudio/Core/Def/CodeCleanup/AbstractCodeCleanUpFixer.cs +++ b/src/VisualStudio/Core/Def/CodeCleanup/AbstractCodeCleanUpFixer.cs @@ -11,13 +11,10 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeCleanup; -using Microsoft.CodeAnalysis.Editor; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; -using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Progress; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; @@ -69,7 +66,13 @@ private async Task FixHierarchyContentAsync(IVsHierarchyCodeCleanupScope h var hierarchy = hierarchyContent.Hierarchy; if (hierarchy == null) { - return await FixSolutionAsync(_workspace.CurrentSolution, context).ConfigureAwait(true); + var solution = _workspace.CurrentSolution; + return await FixAsync( + _workspace, + // Just defer to FixProjectsAsync, passing in all fixable projects in the solution. + (progress, cancellationToken) => FixProjectsAsync( + _globalOptions, solution, solution.Projects.Where(p => p.SupportsCompilation).ToImmutableArray(), context.EnabledFixIds, progress, cancellationToken), + context).ConfigureAwait(false); } // Map the hierarchy to a ProjectId. For hierarchies mapping to multitargeted projects, we first try to @@ -90,9 +93,7 @@ private async Task FixHierarchyContentAsync(IVsHierarchyCodeCleanupScope h { var projectHierarchyItem = _vsHierarchyItemManager.GetHierarchyItem(hierarchyContent.Hierarchy, (uint)VSConstants.VSITEMID.Root); if (!hierarchyToProjectMap.TryGetProjectId(projectHierarchyItem, targetFrameworkMoniker: null, out projectId)) - { return false; - } } var itemId = hierarchyContent.ItemId; @@ -101,12 +102,15 @@ private async Task FixHierarchyContentAsync(IVsHierarchyCodeCleanupScope h await TaskScheduler.Default; var project = _workspace.CurrentSolution.GetProject(projectId); - if (project == null) - { + if (project == null || !project.SupportsCompilation) return false; - } - return await FixProjectAsync(project, context).ConfigureAwait(true); + return await FixAsync( + _workspace, + // Just defer to FixProjectsAsync, passing in this single project to fix. + (progress, cancellationToken) => FixProjectsAsync( + _globalOptions, project.Solution, [project], context.EnabledFixIds, progress, cancellationToken), + context).ConfigureAwait(false); } else if (hierarchy.GetCanonicalName(itemId, out var path) == 0) { @@ -126,54 +130,25 @@ private async Task FixHierarchyContentAsync(IVsHierarchyCodeCleanupScope h var documentIds = solution.GetDocumentIdsWithFilePath(path); var documentId = documentIds.FirstOrDefault(id => id.ProjectId == projectId); if (documentId is null) - { return false; - } var document = solution.GetRequiredDocument(documentId); var options = _globalOptions.GetCodeActionOptions(document.Project.Services); - return await FixDocumentAsync(document, options, context).ConfigureAwait(true); + + return await FixAsync( + _workspace, + async (progress, cancellationToken) => + { + var newDocument = await FixDocumentAsync(document, context.EnabledFixIds, progress, options, cancellationToken).ConfigureAwait(true); + return newDocument.Project.Solution; + }, + context).ConfigureAwait(false); } } return false; } - private Task FixSolutionAsync(Solution solution, ICodeCleanUpExecutionContext context) - { - return FixAsync(_workspace, ApplyFixAsync, context); - - // Local function - Task ApplyFixAsync(IProgress progress, CancellationToken cancellationToken) - { - return FixSolutionAsync(solution, context.EnabledFixIds, progress, cancellationToken); - } - } - - private Task FixProjectAsync(Project project, ICodeCleanUpExecutionContext context) - { - return FixAsync(_workspace, ApplyFixAsync, context); - - // Local function - async Task ApplyFixAsync(IProgress progress, CancellationToken cancellationToken) - { - var newProject = await FixProjectAsync(project, context.EnabledFixIds, progress, addProgressItemsForDocuments: true, cancellationToken).ConfigureAwait(true); - return newProject.Solution; - } - } - - private Task FixDocumentAsync(Document document, CodeActionOptions options, ICodeCleanUpExecutionContext context) - { - return FixAsync(document.Project.Solution.Workspace, ApplyFixAsync, context); - - // Local function - async Task ApplyFixAsync(IProgress progress, CancellationToken cancellationToken) - { - var newDocument = await FixDocumentAsync(document, context.EnabledFixIds, progress, options, cancellationToken).ConfigureAwait(true); - return newDocument.Project.Solution; - } - } - private Task FixTextBufferAsync(TextBufferCodeCleanUpScope textBufferScope, ICodeCleanUpExecutionContext context) { var buffer = textBufferScope.SubjectBuffer; @@ -215,9 +190,7 @@ private async Task FixAsync( { var workspaceStatusService = workspace.Services.GetService(); if (workspaceStatusService != null) - { await workspaceStatusService.WaitUntilFullyLoadedAsync(context.OperationContext.UserCancellationToken).ConfigureAwait(true); - } } using (var scope = context.OperationContext.AddScope(allowCancellation: true, description: EditorFeaturesResources.Applying_changes)) @@ -233,76 +206,58 @@ private async Task FixAsync( } } - private async Task FixSolutionAsync( + private static async Task FixProjectsAsync( + IGlobalOptionService globalOptions, Solution solution, + ImmutableArray projects, FixIdContainer enabledFixIds, IProgress progressTracker, CancellationToken cancellationToken) { - // Prepopulate the solution progress tracker with the total number of documents to process - foreach (var projectId in solution.ProjectIds) - { - var project = solution.GetRequiredProject(projectId); - if (!CanCleanupProject(project)) - { - continue; - } - - progressTracker.AddItems(project.DocumentIds.Count); - } - - foreach (var projectId in solution.ProjectIds) - { - cancellationToken.ThrowIfCancellationRequested(); - - var project = solution.GetRequiredProject(projectId); - var newProject = await FixProjectAsync(project, enabledFixIds, progressTracker, addProgressItemsForDocuments: false, cancellationToken).ConfigureAwait(false); - solution = newProject.Solution; - } - - return solution; - } - - private async Task FixProjectAsync( - Project project, - FixIdContainer enabledFixIds, - IProgress progressTracker, - bool addProgressItemsForDocuments, - CancellationToken cancellationToken) - { - if (!CanCleanupProject(project)) - { - return project; - } - - if (addProgressItemsForDocuments) - { - progressTracker.AddItems(project.DocumentIds.Count); - } - - var ideOptions = _globalOptions.GetCodeActionOptions(project.Services); - - foreach (var documentId in project.DocumentIds) - { - cancellationToken.ThrowIfCancellationRequested(); - - var document = project.GetRequiredDocument(documentId); - progressTracker.Report(CodeAnalysisProgress.Description(document.Name)); + // Add an item for each document in all the projects we're processing. + progressTracker.AddItems(projects.Sum(static p => p.DocumentIds.Count)); - // FixDocumentAsync reports progress within a document, but we only want to report progress at the - // project granularity. So we pass CodeAnalysisProgress.None here so that inner progress updates don't - // affect us. - var fixedDocument = await FixDocumentAsync(document, enabledFixIds, CodeAnalysisProgress.None, ideOptions, cancellationToken).ConfigureAwait(false); - project = fixedDocument.Project; - progressTracker.ItemCompleted(); - } - - return project; + // Run in parallel across all projects. + return await ProducerConsumer<(DocumentId documentId, SyntaxNode newRoot)>.RunParallelAsync( + source: projects, + produceItems: static async (project, callback, args, cancellationToken) => + { + Contract.ThrowIfFalse(project.SupportsCompilation); + cancellationToken.ThrowIfCancellationRequested(); + + var ideOptions = args.globalOptions.GetCodeActionOptions(project.Services); + + // And for each project, process all the documents in parallel. + await RoslynParallel.ForEachAsync( + source: project.Documents, + cancellationToken, + async (document, cancellationToken) => + { + using var _ = args.progressTracker.ItemCompletedScope(); + + // FixDocumentAsync reports progress within a document, but we only want to report progress at + // the document granularity. So we pass CodeAnalysisProgress.None here so that inner progress + // updates don't affect us. + var fixedDocument = await FixDocumentAsync(document, args.enabledFixIds, CodeAnalysisProgress.None, ideOptions, cancellationToken).ConfigureAwait(false); + if (fixedDocument == document) + return; + + callback((document.Id, await fixedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false))); + }).ConfigureAwait(false); + }, + consumeItems: static async (stream, args, cancellationToken) => + { + // Now consume the changed documents, applying their new roots to the solution. + var currentSolution = args.solution; + await foreach (var (documentId, newRoot) in stream) + currentSolution = currentSolution.WithDocumentSyntaxRoot(documentId, newRoot); + + return currentSolution; + }, + args: (globalOptions, solution, enabledFixIds, progressTracker), + cancellationToken).ConfigureAwait(false); } - private static bool CanCleanupProject(Project project) - => project.Services.GetService() != null; - private static async Task FixDocumentAsync( Document document, FixIdContainer enabledFixIds, @@ -310,10 +265,9 @@ private static async Task FixDocumentAsync( CodeActionOptions ideOptions, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); if (document.IsGeneratedCode(cancellationToken)) - { return document; - } var codeCleanupService = document.GetRequiredLanguageService(); diff --git a/src/VisualStudio/Core/Def/EditorConfigSettings/SettingsEditorControl.xaml.cs b/src/VisualStudio/Core/Def/EditorConfigSettings/SettingsEditorControl.xaml.cs index 6544889756b73..c5481121e2963 100644 --- a/src/VisualStudio/Core/Def/EditorConfigSettings/SettingsEditorControl.xaml.cs +++ b/src/VisualStudio/Core/Def/EditorConfigSettings/SettingsEditorControl.xaml.cs @@ -66,7 +66,7 @@ public SettingsEditorControl(ISettingsEditorView whitespaceView, analyzerView ]; - _tableControls = _views.SelectAsArray(view => view.TableControl).ToArray(); + _tableControls = [.. _views.SelectAsArray(view => view.TableControl)]; InitializeComponent(); } diff --git a/src/VisualStudio/Core/Def/ErrorReporting/VisualStudioErrorReportingService.cs b/src/VisualStudio/Core/Def/ErrorReporting/VisualStudioErrorReportingService.cs index 9e5a817ba411b..49794d90e20f2 100644 --- a/src/VisualStudio/Core/Def/ErrorReporting/VisualStudioErrorReportingService.cs +++ b/src/VisualStudio/Core/Def/ErrorReporting/VisualStudioErrorReportingService.cs @@ -75,7 +75,7 @@ public void ShowFeatureNotAvailableErrorInfo(string message, TelemetryFeatureNam closeAfterAction: true)); } - ShowGlobalErrorInfo(message, featureName, exception, infoBarUIs.ToArray()); + ShowGlobalErrorInfo(message, featureName, exception, [.. infoBarUIs]); } private void LogGlobalErrorToActivityLog(string message, string? detailedError) diff --git a/src/VisualStudio/Core/Def/Implementation/VisualStudioMetadataAsSourceFileSupportService.cs b/src/VisualStudio/Core/Def/Implementation/VisualStudioMetadataAsSourceFileSupportService.cs deleted file mode 100644 index 3f623ce2e8ef7..0000000000000 --- a/src/VisualStudio/Core/Def/Implementation/VisualStudioMetadataAsSourceFileSupportService.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -#nullable disable - -using System; -using System.ComponentModel.Composition; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Editor.Shared.Utilities; -using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.MetadataAsSource; -using Microsoft.VisualStudio.Shell; -using Microsoft.VisualStudio.Shell.Interop; - -namespace Microsoft.VisualStudio.LanguageServices.Implementation; - -[Export(typeof(VisualStudioMetadataAsSourceFileSupportService))] -internal sealed class VisualStudioMetadataAsSourceFileSupportService : IVsSolutionEvents -{ - private readonly IThreadingContext _threadingContext; - private readonly IMetadataAsSourceFileService _metadataAsSourceFileService; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public VisualStudioMetadataAsSourceFileSupportService( - IThreadingContext threadingContext, - IMetadataAsSourceFileService metadataAsSourceFileService) - { - _threadingContext = threadingContext; - _metadataAsSourceFileService = metadataAsSourceFileService; - } - - public async Task InitializeAsync(IAsyncServiceProvider serviceProvider, CancellationToken cancellationToken) - { - var solution = await serviceProvider.GetServiceAsync(_threadingContext.JoinableTaskFactory).ConfigureAwait(false); - await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - // Intentionally ignore the event-cookie we get back out. We never stop listening to solution events. - ErrorHandler.ThrowOnFailure(solution.AdviseSolutionEvents(this, out _)); - } - - public int OnAfterCloseSolution(object pUnkReserved) - { - _metadataAsSourceFileService.CleanupGeneratedFiles(); - - return VSConstants.S_OK; - } - - public int OnAfterLoadProject(IVsHierarchy pStubHierarchy, IVsHierarchy pRealHierarchy) - => VSConstants.E_NOTIMPL; - - public int OnAfterOpenProject(IVsHierarchy pHierarchy, int fAdded) - => VSConstants.E_NOTIMPL; - - public int OnAfterOpenSolution(object pUnkReserved, int fNewSolution) - => VSConstants.E_NOTIMPL; - - public int OnBeforeCloseProject(IVsHierarchy pHierarchy, int fRemoved) - => VSConstants.E_NOTIMPL; - - public int OnBeforeCloseSolution(object pUnkReserved) - => VSConstants.E_NOTIMPL; - - public int OnBeforeUnloadProject(IVsHierarchy pRealHierarchy, IVsHierarchy pStubHierarchy) - => VSConstants.E_NOTIMPL; - - public int OnQueryCloseProject(IVsHierarchy pHierarchy, int fRemoving, ref int pfCancel) - => VSConstants.E_NOTIMPL; - - public int OnQueryCloseSolution(object pUnkReserved, ref int pfCancel) - => VSConstants.E_NOTIMPL; - - public int OnQueryUnloadProject(IVsHierarchy pRealHierarchy, ref int pfCancel) - => VSConstants.E_NOTIMPL; -} diff --git a/src/VisualStudio/Core/Def/Implementation/VisualStudioUIContextActivationService.cs b/src/VisualStudio/Core/Def/Implementation/VisualStudioUIContextActivationService.cs new file mode 100644 index 0000000000000..d04d43a5cd83c --- /dev/null +++ b/src/VisualStudio/Core/Def/Implementation/VisualStudioUIContextActivationService.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Composition; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.VisualStudio.Shell; + +namespace Microsoft.VisualStudio.LanguageServices.Implementation; + +[Export(typeof(IUIContextActivationService)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class VisualStudioUIContextActivationService() : IUIContextActivationService +{ + public void ExecuteWhenActivated(Guid uiContext, Action action) + { + var context = UIContext.FromUIContextGuid(uiContext); + context.WhenActivated(action); + } +} diff --git a/src/VisualStudio/Core/Def/Implementation/VsRefactorNotifyService.cs b/src/VisualStudio/Core/Def/Implementation/VsRefactorNotifyService.cs index 2cc9eb4b5c06b..a2a015311f028 100644 --- a/src/VisualStudio/Core/Def/Implementation/VsRefactorNotifyService.cs +++ b/src/VisualStudio/Core/Def/Implementation/VsRefactorNotifyService.cs @@ -46,7 +46,7 @@ public bool TryOnBeforeGlobalSymbolRenamed(Workspace workspace, IEnumerable new InheritanceMarginAutomationPeer(this); + private void LazyInitializeContextMenu() { if (ContextMenu is not InheritanceMarginContextMenu) @@ -211,4 +215,12 @@ private void ResetFocus() } } } + + private sealed class InheritanceMarginAutomationPeer(InheritanceMarginGlyph owner) : ButtonAutomationPeer(owner) + { + protected override AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Group; + } + } } diff --git a/src/VisualStudio/Core/Def/KeybindingReset/KeybindingResetDetector.cs b/src/VisualStudio/Core/Def/KeybindingReset/KeybindingResetDetector.cs index f0eab888879e2..0711327e2ebf1 100644 --- a/src/VisualStudio/Core/Def/KeybindingReset/KeybindingResetDetector.cs +++ b/src/VisualStudio/Core/Def/KeybindingReset/KeybindingResetDetector.cs @@ -101,15 +101,14 @@ public KeybindingResetDetector( _infoBar = new VisualStudioInfoBar(threadingContext, vsInfoBarUIFactory, vsShell, listenerProvider, windowFrame: null); } - public Task InitializeAsync() + public async Task InitializeAsync(CancellationToken cancellationToken) { // Immediately bail if the user has asked to never see this bar again. if (_globalOptions.GetOption(KeybindingResetOptionsStorage.NeverShowAgain)) - { - return Task.CompletedTask; - } + return; - return _threadingContext.InvokeBelowInputPriorityAsync(InitializeCore); + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken); + InitializeCore(); } private void InitializeCore() diff --git a/src/VisualStudio/Core/Def/LanguageService/AbstractLanguageService`2.IVsLanguageDebugInfo.cs b/src/VisualStudio/Core/Def/LanguageService/AbstractLanguageService`2.IVsLanguageDebugInfo.cs index 9b5be14eb0845..9820e02d3087e 100644 --- a/src/VisualStudio/Core/Def/LanguageService/AbstractLanguageService`2.IVsLanguageDebugInfo.cs +++ b/src/VisualStudio/Core/Def/LanguageService/AbstractLanguageService`2.IVsLanguageDebugInfo.cs @@ -20,7 +20,7 @@ int IVsLanguageDebugInfo.GetLanguageID(IVsTextBuffer pBuffer, int iLine, int iCo { try { - return LanguageDebugInfo.GetLanguageID(pBuffer, iLine, iCol, out pguidLanguageID); + return _languageDebugInfo.GetLanguageID(pBuffer, iLine, iCol, out pguidLanguageID); } catch (Exception e) when (FatalError.ReportAndPropagate(e)) { @@ -32,7 +32,7 @@ int IVsLanguageDebugInfo.GetLocationOfName(string pszName, out string pbstrMkDoc { try { - return LanguageDebugInfo.GetLocationOfName(pszName, out pbstrMkDoc, out pspanLocation); + return _languageDebugInfo.GetLocationOfName(pszName, out pbstrMkDoc, out pspanLocation); } catch (Exception e) when (FatalError.ReportAndPropagate(e)) { @@ -44,7 +44,7 @@ int IVsLanguageDebugInfo.GetNameOfLocation(IVsTextBuffer pBuffer, int iLine, int { try { - return LanguageDebugInfo.GetNameOfLocation(pBuffer, iLine, iCol, out pbstrName, out piLineOffset); + return _languageDebugInfo.GetNameOfLocation(pBuffer, iLine, iCol, out pbstrName, out piLineOffset); } catch (Exception e) when (FatalError.ReportAndPropagate(e)) { @@ -56,7 +56,7 @@ int IVsLanguageDebugInfo.GetProximityExpressions(IVsTextBuffer pBuffer, int iLin { try { - return LanguageDebugInfo.GetProximityExpressions(pBuffer, iLine, iCol, cLines, out ppEnum); + return _languageDebugInfo.GetProximityExpressions(pBuffer, iLine, iCol, cLines, out ppEnum); } catch (Exception e) when (FatalError.ReportAndPropagate(e)) { @@ -68,7 +68,7 @@ int IVsLanguageDebugInfo.IsMappedLocation(IVsTextBuffer pBuffer, int iLine, int { try { - return LanguageDebugInfo.IsMappedLocation(pBuffer, iLine, iCol); + return _languageDebugInfo.IsMappedLocation(pBuffer, iLine, iCol); } catch (Exception e) when (FatalError.ReportAndPropagate(e)) { @@ -80,7 +80,7 @@ int IVsLanguageDebugInfo.ResolveName(string pszName, uint dwFlags, out IVsEnumDe { try { - return LanguageDebugInfo.ResolveName(pszName, dwFlags, out ppNames); + return _languageDebugInfo.ResolveName(pszName, dwFlags, out ppNames); } catch (Exception e) when (FatalError.ReportAndPropagate(e)) { @@ -92,7 +92,7 @@ int IVsLanguageDebugInfo.ValidateBreakpointLocation(IVsTextBuffer pBuffer, int i { try { - return LanguageDebugInfo.ValidateBreakpointLocation(pBuffer, iLine, iCol, pCodeSpan); + return _languageDebugInfo.ValidateBreakpointLocation(pBuffer, iLine, iCol, pCodeSpan); } catch (Exception e) when (FatalError.ReportAndPropagate(e)) { diff --git a/src/VisualStudio/Core/Def/LanguageService/AbstractLanguageService`2.cs b/src/VisualStudio/Core/Def/LanguageService/AbstractLanguageService`2.cs index 867aa1f21ef16..bac0507b76bf7 100644 --- a/src/VisualStudio/Core/Def/LanguageService/AbstractLanguageService`2.cs +++ b/src/VisualStudio/Core/Def/LanguageService/AbstractLanguageService`2.cs @@ -2,11 +2,11 @@ // 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.Diagnostics; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; @@ -19,13 +19,12 @@ using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; using Microsoft.VisualStudio.Editor; using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; -using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList; using Microsoft.VisualStudio.LanguageServices.Implementation.Venus; -using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Text.Editor; using Microsoft.VisualStudio.Text.Outlining; using Microsoft.VisualStudio.TextManager.Interop; +using Microsoft.VisualStudio.Threading; using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; @@ -36,7 +35,8 @@ internal abstract partial class AbstractLanguageService { internal TPackage Package { get; } - internal VsLanguageDebugInfo LanguageDebugInfo { get; private set; } + + private readonly VsLanguageDebugInfo _languageDebugInfo; // DevDiv 753309: // We've redefined some VS interfaces that had incorrect PIAs. When @@ -51,15 +51,14 @@ internal abstract partial class AbstractLanguageService /// Whether or not we have been set up. This is set once everything is wired up and cleared once tear down has begun. @@ -71,9 +70,21 @@ internal abstract partial class AbstractLanguageService private bool _isSetUp; + protected abstract string ContentTypeName { get; } + protected abstract string LanguageName { get; } + protected abstract string RoslynLanguageName { get; } + protected abstract Guid DebuggerLanguageId { get; } + protected AbstractLanguageService(TPackage package) { Package = package; + + Debug.Assert(!this.Package.JoinableTaskFactory.Context.IsOnMainThread, "Language service should be instantiated on background thread"); + + this.EditorOptionsService = this.Package.ComponentModel.GetService(); + this.Workspace = this.Package.ComponentModel.GetService(); + this.EditorAdaptersFactoryService = this.Package.ComponentModel.GetService(); + this._languageDebugInfo = CreateLanguageDebugInfo(); } public override IServiceProvider SystemServiceProvider @@ -82,32 +93,28 @@ public override IServiceProvider SystemServiceProvider /// /// Setup and TearDown go in reverse order. /// - internal void Setup() + public async Task SetupAsync(CancellationToken cancellationToken) { - this.ComAggregate = CreateComAggregate(); - // First, acquire any services we need throughout our lifetime. - this.GetServices(); + // This method should only contain calls to acquire services off of the component model + // or service providers. Anything else which is more complicated should go in Initialize + // instead. - // TODO: Is the below access to component model required or can be removed? - _ = this.Package.ComponentModel; + // Start off a background task to prime some components we'll need for editing. + Task.Run(() => + { + var formatter = this.Workspace.Services.GetLanguageServices(RoslynLanguageName).GetService(); + formatter?.GetDefaultFormattingRules(); + }, cancellationToken).Forget(); - // Start off a background task to prime some components we'll need for editing - VsTaskLibraryHelper.CreateAndStartTask(VsTaskLibraryHelper.ServiceInstance, VsTaskRunContext.BackgroundThread, - () => PrimeLanguageServiceComponentsOnBackground()); + await this.Package.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - // Finally, once our connections are established, set up any initial state that we need. - // Note: we may be instantiated at any time (including when the IDE is already - // debugging). We must not assume anything about our initial state and must instead - // query for all the information we need at this point. - this.Initialize(); + // Creating the com aggregate has to happen on the UI thread. + this.ComAggregate = Interop.ComAggregate.CreateAggregatedObject(this); _isSetUp = true; } - private object CreateComAggregate() - => Interop.ComAggregate.CreateAggregatedObject(this); - internal void TearDown() { if (!_isSetUp) @@ -117,9 +124,6 @@ internal void TearDown() _isSetUp = false; GC.SuppressFinalize(this); - - this.Uninitialize(); - this.RemoveServices(); } ~AbstractLanguageService() @@ -130,50 +134,6 @@ internal void TearDown() } } - protected virtual void GetServices() - { - // This method should only contain calls to acquire services off of the component model - // or service providers. Anything else which is more complicated should go in Initialize - // instead. - this.EditorOptionsService = this.Package.ComponentModel.GetService(); - this.Workspace = this.Package.ComponentModel.GetService(); - this.EditorAdaptersFactoryService = this.Package.ComponentModel.GetService(); - this.AnalyzerFileWatcherService = this.Package.ComponentModel.GetService(); - } - - protected virtual void RemoveServices() - { - this.EditorAdaptersFactoryService = null; - this.Workspace = null; - } - - /// - /// Called right after we instantiate the language service. Used to set up any internal - /// state we need. - /// - /// Try to keep this method fairly clean. Any complicated logic should go in methods called - /// from this one. Initialize and Uninitialize go in reverse order - /// - protected virtual void Initialize() - { - InitializeLanguageDebugInfo(); - } - - protected virtual void Uninitialize() - { - UninitializeLanguageDebugInfo(); - } - - private void PrimeLanguageServiceComponentsOnBackground() - { - var formatter = this.Workspace.Services.GetLanguageServices(RoslynLanguageName).GetService(); - formatter?.GetDefaultFormattingRules(); - } - - protected abstract string ContentTypeName { get; } - protected abstract string LanguageName { get; } - protected abstract string RoslynLanguageName { get; } - protected virtual void SetupNewTextView(IVsTextView textView) { Contract.ThrowIfNull(textView); @@ -201,7 +161,7 @@ protected virtual void SetupNewTextView(IVsTextView textView) // If the file is metadata as source, and the user has the preference set to collapse them, then // always collapse all metadata as source var globalOptions = this.Package.ComponentModel.GetService(); - var options = BlockStructureOptionsStorage.GetBlockStructureOptions(globalOptions, openDocument.Project.Language, isMetadataAsSource: masWorkspace is not null); + var options = BlockStructureOptionsStorage.GetBlockStructureOptions(globalOptions, openDocument.Project.Language, isMetadataAsSource: true); collapseAllImplementations = masWorkspace.FileService.ShouldCollapseOnOpen(openDocument.FilePath, options); } @@ -255,15 +215,9 @@ private void ConditionallyCollapseOutliningRegions(IVsTextView textView, IWpfTex } } - private void InitializeLanguageDebugInfo() - => this.LanguageDebugInfo = this.CreateLanguageDebugInfo(); - - protected abstract Guid DebuggerLanguageId { get; } - private VsLanguageDebugInfo CreateLanguageDebugInfo() { - var workspace = this.Workspace; - var languageServices = workspace.Services.GetLanguageServices(RoslynLanguageName); + var languageServices = this.Workspace.Services.GetLanguageServices(RoslynLanguageName); return new VsLanguageDebugInfo( this.DebuggerLanguageId, @@ -273,9 +227,6 @@ private VsLanguageDebugInfo CreateLanguageDebugInfo() this.Package.ComponentModel.GetService()); } - private void UninitializeLanguageDebugInfo() - => this.LanguageDebugInfo = null; - protected virtual IVsContainedLanguage CreateContainedLanguage( IVsTextBufferCoordinator bufferCoordinator, ProjectSystemProject project, IVsHierarchy hierarchy, uint itemid) diff --git a/src/VisualStudio/Core/Def/LanguageService/AbstractPackage`2.cs b/src/VisualStudio/Core/Def/LanguageService/AbstractPackage`2.cs index b4c28e70037d4..d53c66478e03d 100644 --- a/src/VisualStudio/Core/Def/LanguageService/AbstractPackage`2.cs +++ b/src/VisualStudio/Core/Def/LanguageService/AbstractPackage`2.cs @@ -51,14 +51,16 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke RegisterEditorFactory(editorFactory); } - RegisterLanguageService(typeof(TLanguageService), async ct => + RegisterLanguageService(typeof(TLanguageService), async cancellationToken => { - await JoinableTaskFactory.SwitchToMainThreadAsync(ct); + // Ensure we're on the BG when creating the language service. + await TaskScheduler.Default; // Create the language service, tell it to set itself up, then store it in a field // so we can notify it that it's time to clean up. _languageService = CreateLanguageService(); - _languageService.Setup(); + await _languageService.SetupAsync(cancellationToken).ConfigureAwait(false); + return _languageService.ComAggregate; }); diff --git a/src/VisualStudio/Core/Def/NavigateTo/RoslynNavigateToSearchCallback.cs b/src/VisualStudio/Core/Def/NavigateTo/RoslynNavigateToSearchCallback.cs index 1c51b4865aa4b..cf0b5b0f3cc8b 100644 --- a/src/VisualStudio/Core/Def/NavigateTo/RoslynNavigateToSearchCallback.cs +++ b/src/VisualStudio/Core/Def/NavigateTo/RoslynNavigateToSearchCallback.cs @@ -3,9 +3,11 @@ // 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; +using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text.Shared.Extensions; using Microsoft.VisualStudio.Search.Data; using Microsoft.VisualStudio.Text.PatternMatching; @@ -21,13 +23,16 @@ internal sealed partial class RoslynSearchItemsSourceProvider /// private sealed class RoslynNavigateToSearchCallback : INavigateToSearchCallback { + private readonly Solution _solution; private readonly RoslynSearchItemsSourceProvider _provider; private readonly ISearchCallback _searchCallback; public RoslynNavigateToSearchCallback( + Solution solution, RoslynSearchItemsSourceProvider provider, ISearchCallback searchCallback) { + _solution = solution; _provider = provider; _searchCallback = searchCallback; } @@ -51,30 +56,34 @@ public void ReportIncomplete() _searchCallback.ReportIncomplete(IncompleteReason.Parsing); } - public Task AddItemAsync(Project project, INavigateToSearchResult result, CancellationToken cancellationToken) + public Task AddResultsAsync(ImmutableArray results, CancellationToken cancellationToken) { // Convert roslyn pattern matches to the platform type. - var matches = result.Matches.SelectAsArray(static m => new PatternMatch( + foreach (var result in results) + { + var matches = result.Matches.SelectAsArray(static m => new PatternMatch( ConvertKind(m.Kind), punctuationStripped: false, m.IsCaseSensitive, m.MatchedSpans.SelectAsArray(static s => s.ToSpan()))); - // Weight the items based on the overall pattern matching weights. We want the items that have the best - // pattern matches (low .Kind values) to have the highest float values (as higher is better for the VS - // api). - var perProviderItemPriority = float.MaxValue - Enumerable.Sum(result.Matches.Select(m => (int)m.Kind)); + // Weight the items based on the overall pattern matching weights. We want the items that have the best + // pattern matches (low .Kind values) to have the highest float values (as higher is better for the VS + // api). + var perProviderItemPriority = float.MaxValue - Enumerable.Sum(result.Matches.Select(m => (int)m.Kind)); - _searchCallback.AddItem(new RoslynCodeSearchResult( - _provider, - result, - GetResultType(result.Kind), - result.Name, - result.SecondarySort, - matches, - result.NavigableItem.Document.FilePath, - perProviderItemPriority, - project.Language)); + var project = _solution.GetRequiredProject(result.NavigableItem.Document.Project.Id); + _searchCallback.AddItem(new RoslynCodeSearchResult( + _provider, + result, + GetResultType(result.Kind), + result.Name, + result.SecondarySort, + matches, + result.NavigableItem.Document.FilePath, + perProviderItemPriority, + project.Language)); + } return Task.CompletedTask; } diff --git a/src/VisualStudio/Core/Def/NavigateTo/RoslynSearchItemsSource.cs b/src/VisualStudio/Core/Def/NavigateTo/RoslynSearchItemsSource.cs index d535811c7cea4..c0d8b86af61c9 100644 --- a/src/VisualStudio/Core/Def/NavigateTo/RoslynSearchItemsSource.cs +++ b/src/VisualStudio/Core/Def/NavigateTo/RoslynSearchItemsSource.cs @@ -89,10 +89,11 @@ private async Task PerformSearchWorkerAsync( // Create a nav-to callback that will take results and translate them to aiosp results for the // callback passed to us. + var solution = provider._workspace.CurrentSolution; var searcher = NavigateToSearcher.Create( - provider._workspace.CurrentSolution, + solution, provider._asyncListener, - new RoslynNavigateToSearchCallback(provider, searchCallback), + new RoslynNavigateToSearchCallback(solution, provider, searchCallback), searchValue, kinds, provider._threadingContext.DisposalToken); diff --git a/src/VisualStudio/Core/Def/PackageRegistration.pkgdef b/src/VisualStudio/Core/Def/PackageRegistration.pkgdef index 97c945e244fa0..25767a4f03745 100644 --- a/src/VisualStudio/Core/Def/PackageRegistration.pkgdef +++ b/src/VisualStudio/Core/Def/PackageRegistration.pkgdef @@ -39,6 +39,12 @@ "Title"="Run C#/VB code analysis with ServerGC (requires restart)" "PreviewPaneChannels"="IntPreview,int.main" +[$RootKey$\FeatureFlags\Roslyn\SemanticSearchEnabled] +"Description"="Enable C# Semantic Search." +"Value"=dword:00000000 +"Title"="Enable C# Semantic Search" +"PreviewPaneChannels"="IntPreview,int.main" + // Corresponds to WellKnownExperimentNames.LspPullDiagnosticsFeatureFlag [$RootKey$\FeatureFlags\Lsp\PullDiagnostics] "Description"="Enables the LSP-powered diagnostics for managed .Net projects" diff --git a/src/VisualStudio/Core/Def/Progression/GraphNodeIdCreation.cs b/src/VisualStudio/Core/Def/Progression/GraphNodeIdCreation.cs index 7f17e6f0096a8..bb05d21142e75 100644 --- a/src/VisualStudio/Core/Def/Progression/GraphNodeIdCreation.cs +++ b/src/VisualStudio/Core/Def/Progression/GraphNodeIdCreation.cs @@ -198,7 +198,7 @@ private static async Task GetPartialForNamedTypeAsync(INamedTypeSym partials.Add(GraphNodeId.GetArray( CodeGraphNodeIdName.GenericArgumentsIdentifier, - genericArguments.ToArray())); + [.. genericArguments])); } if (namedType.ContainingType != null) @@ -206,7 +206,7 @@ private static async Task GetPartialForNamedTypeAsync(INamedTypeSym partials.Add(await GetPartialForTypeAsync(namedType.ContainingType, CodeGraphNodeIdName.ParentType, solution, cancellationToken, hasGenericArguments).ConfigureAwait(false)); } - return GraphNodeId.GetPartial(nodeName, MakeCollectionIfNecessary(partials.ToArray())); + return GraphNodeId.GetPartial(nodeName, MakeCollectionIfNecessary([.. partials])); } } @@ -231,7 +231,7 @@ private static async Task GetPartialForPointerTypeAsync(IPointerTyp partials.Add(await GetPartialForTypeAsync(pointerType.PointedAtType.ContainingType, CodeGraphNodeIdName.ParentType, solution, cancellationToken).ConfigureAwait(false)); } - return GraphNodeId.GetPartial(nodeName, MakeCollectionIfNecessary(partials.ToArray())); + return GraphNodeId.GetPartial(nodeName, MakeCollectionIfNecessary([.. partials])); } private static async Task GetPartialForArrayTypeAsync(IArrayTypeSymbol arrayType, GraphNodeIdName nodeName, Solution solution, CancellationToken cancellationToken) @@ -252,7 +252,7 @@ private static async Task GetPartialForArrayTypeAsync(IArrayTypeSym partials.Add(GraphNodeId.GetPartial(CodeQualifiedName.ArrayRank, arrayType.Rank.ToString())); partials.Add(await GetPartialForTypeAsync(arrayType.ElementType, CodeGraphNodeIdName.ParentType, solution, cancellationToken).ConfigureAwait(false)); - return GraphNodeId.GetPartial(nodeName, MakeCollectionIfNecessary(partials.ToArray())); + return GraphNodeId.GetPartial(nodeName, MakeCollectionIfNecessary([.. partials])); } private static async Task GetPartialForTypeParameterSymbolAsync(ITypeParameterSymbol typeParameterSymbol, GraphNodeIdName nodeName, Solution solution, CancellationToken cancellationToken) @@ -317,7 +317,7 @@ public static async Task GetIdForMemberAsync(ISymbol member, Soluti nodes.Add(GraphNodeId.GetPartial(CodeGraphNodeIdName.ParamKind, ParamKind.Ref)); } - parameterTypeIds.Add(GraphNodeId.GetNested(nodes.ToArray())); + parameterTypeIds.Add(GraphNodeId.GetNested([.. nodes])); } if (member is IMethodSymbol methodSymbol && methodSymbol.MethodKind == MethodKind.Conversion) @@ -336,25 +336,25 @@ public static async Task GetIdForMemberAsync(ISymbol member, Soluti var returnTypePartial = nodes.ToList(); returnTypePartial.Add(GraphNodeId.GetPartial(CodeGraphNodeIdName.ParamKind, Microsoft.VisualStudio.GraphModel.CodeSchema.ParamKind.Return)); - var returnCollection = GraphNodeId.GetNested(returnTypePartial.ToArray()); + var returnCollection = GraphNodeId.GetNested([.. returnTypePartial]); parameterTypeIds.Add(returnCollection); } memberPartials.Add(GraphNodeId.GetArray( CodeGraphNodeIdName.OverloadingParameters, - parameterTypeIds.ToArray())); + [.. parameterTypeIds])); } partials.Add(GraphNodeId.GetPartial( CodeGraphNodeIdName.Member, - MakeCollectionIfNecessary(memberPartials.ToArray()))); + MakeCollectionIfNecessary([.. memberPartials]))); } else { partials.Add(GraphNodeId.GetPartial(CodeGraphNodeIdName.Member, member.MetadataName)); } - return GraphNodeId.GetNested(partials.ToArray()); + return GraphNodeId.GetNested([.. partials]); } private static object MakeCollectionIfNecessary(GraphNodeId[] array) diff --git a/src/VisualStudio/Core/Def/Progression/GraphQueries/ProgressionNavigateToSearchCallback.cs b/src/VisualStudio/Core/Def/Progression/GraphQueries/ProgressionNavigateToSearchCallback.cs index 5ad659283582a..1c2da80987c39 100644 --- a/src/VisualStudio/Core/Def/Progression/GraphQueries/ProgressionNavigateToSearchCallback.cs +++ b/src/VisualStudio/Core/Def/Progression/GraphQueries/ProgressionNavigateToSearchCallback.cs @@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis; using Microsoft.VisualStudio.GraphModel; using Microsoft.CodeAnalysis.NavigateTo; +using System.Collections.Immutable; namespace Microsoft.VisualStudio.LanguageServices.Implementation.Progression; @@ -14,11 +15,13 @@ internal sealed partial class SearchGraphQuery { private class ProgressionNavigateToSearchCallback : INavigateToSearchCallback { + private readonly Solution _solution; private readonly IGraphContext _context; private readonly GraphBuilder _graphBuilder; - public ProgressionNavigateToSearchCallback(IGraphContext context, GraphBuilder graphBuilder) + public ProgressionNavigateToSearchCallback(Solution solution, IGraphContext context, GraphBuilder graphBuilder) { + _solution = solution; _context = context; _graphBuilder = graphBuilder; } @@ -36,14 +39,17 @@ public void ReportIncomplete() { } - public async Task AddItemAsync(Project project, INavigateToSearchResult result, CancellationToken cancellationToken) + public async Task AddResultsAsync(ImmutableArray results, CancellationToken cancellationToken) { - var node = await _graphBuilder.CreateNodeAsync(project.Solution, result, cancellationToken).ConfigureAwait(false); - if (node != null) + foreach (var result in results) { - // _context.OutputNodes is not threadsafe. So ensure only one navto callback can mutate it at a time. - lock (this) - _context.OutputNodes.Add(node); + var node = await _graphBuilder.CreateNodeAsync(_solution, result, cancellationToken).ConfigureAwait(false); + if (node != null) + { + // _context.OutputNodes is not threadsafe. So ensure only one navto callback can mutate it at a time. + lock (this) + _context.OutputNodes.Add(node); + } } } } diff --git a/src/VisualStudio/Core/Def/Progression/GraphQueries/SearchGraphQuery.cs b/src/VisualStudio/Core/Def/Progression/GraphQueries/SearchGraphQuery.cs index 0d60c8feb2a90..2c95f2f1c2315 100644 --- a/src/VisualStudio/Core/Def/Progression/GraphQueries/SearchGraphQuery.cs +++ b/src/VisualStudio/Core/Def/Progression/GraphQueries/SearchGraphQuery.cs @@ -19,7 +19,7 @@ internal sealed partial class SearchGraphQuery( public async Task GetGraphAsync(Solution solution, IGraphContext context, CancellationToken cancellationToken) { var graphBuilder = await GraphBuilder.CreateForInputNodesAsync(solution, context.InputNodes, cancellationToken).ConfigureAwait(false); - var callback = new ProgressionNavigateToSearchCallback(context, graphBuilder); + var callback = new ProgressionNavigateToSearchCallback(solution, context, graphBuilder); // We have a specialized host for progression vs normal nav-to. Progression itself will tell the client if // the project is fully loaded or not. But after that point, the client will be considered fully loaded and diff --git a/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.MetadataCache.cs b/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.MetadataCache.cs deleted file mode 100644 index 8cd6e19b78c26..0000000000000 --- a/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.MetadataCache.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.CodeAnalysis; -using Roslyn.Utilities; - -namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; - -internal sealed partial class VisualStudioMetadataReferenceManager -{ - private sealed class MetadataCache - { - private readonly object _gate = new(); - - // value is ValueSource so that how metadata is re-acquired back are different per entry. - private readonly Dictionary _metadataCache = []; - - public bool TryGetMetadata(FileKey key, [NotNullWhen(true)] out AssemblyMetadata? metadata) - { - lock (_gate) - { - return TryGetMetadata_NoLock(key, out metadata); - } - } - - private bool TryGetMetadata_NoLock(FileKey key, [NotNullWhen(true)] out AssemblyMetadata? metadata) - => _metadataCache.TryGetValue(key, out metadata) && metadata != null; - - /// - /// Gets specified metadata from the cache, or retrieves metadata from given - /// and adds it to the cache if it's not there yet. - /// - /// - /// True if the metadata is retrieved from source, false if it already exists in the cache. - /// - public bool GetOrAddMetadata(FileKey key, AssemblyMetadata newMetadata, out AssemblyMetadata metadata) - { - lock (_gate) - { - if (TryGetMetadata_NoLock(key, out var cachedMetadata)) - { - metadata = cachedMetadata; - return false; - } - - // the source is expected to keep the metadata alive at this point - Contract.ThrowIfNull(newMetadata); - - // don't use "Add" since key might already exist with already released metadata - _metadataCache[key] = newMetadata; - metadata = newMetadata; - return true; - } - } - - public void ClearCache() - { - lock (_gate) - { - _metadataCache.Clear(); - } - } - } -} diff --git a/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.NativeMethods.cs b/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.NativeMethods.cs index 696f5d410793c..5cbdacdcbb16a 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.NativeMethods.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.NativeMethods.cs @@ -2,20 +2,19 @@ // 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.Runtime.InteropServices; -using Microsoft.CodeAnalysis; namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; internal sealed partial class VisualStudioMetadataReferenceManager { + private static readonly Guid s_IID_IMetaDataImport = new("7DAC8207-D3AE-4c75-9B67-92801A497D44"); + [ComImport] [Guid("7998EA64-7F95-48B8-86FC-17CAF48BF5CB")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - internal interface IMetaDataInfo + private interface IMetaDataInfo { // MetaData scope is opened (there's a reference to a MetaData interface for this scope). // Returns S_OK, COR_E_NOTSUPPORTED, or E_INVALIDARG (if NULL is passed). @@ -28,16 +27,19 @@ internal interface IMetaDataInfo } // Flags returned from IMetaDataInfo.GetFileMapping - internal enum CorFileMapping : uint + private enum CorFileMapping : uint { - Flat = 0, // Flat file mapping - file is mapped as data file (code:SEC_IMAGE flag was not - // passed to code:CreateFileMapping). - ExecutableImage = 1 // Executable image file mapping - file is mapped for execution - // (either via code:LoadLibrary or code:CreateFileMapping with code:SEC_IMAGE flag). + Flat = 0, // Flat file mapping - file is mapped as data file (code:SEC_IMAGE flag was not + // passed to code:CreateFileMapping). +#if false + ExecutableImage = 1 // Executable image file mapping - file is mapped for execution + // (either via code:LoadLibrary or code:CreateFileMapping with code:SEC_IMAGE flag). +#endif } - internal enum CorOpenFlags : uint + private enum CorOpenFlags : uint { +#if false Read = 0, Write = 1, ReadWriteMask = 1, @@ -45,10 +47,13 @@ internal enum CorOpenFlags : uint CopyMemory = 2, ManifestMetadata = 8, +#endif ReadOnly = 16, +#if false TakeOwnership = 32, CacheImage = 4, NoTypeLib = 128 +#endif } } diff --git a/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.cs b/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.cs index 8fe74c01b94e9..5c20f8ef178b4 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.cs @@ -14,14 +14,14 @@ using System.Runtime.InteropServices; using System.Threading; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.VisualStudio.Shell.Interop; using Roslyn.Utilities; namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; +using static TemporaryStorageService; + /// /// Manages metadata references for VS projects. /// @@ -32,42 +32,44 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; /// internal sealed partial class VisualStudioMetadataReferenceManager : IWorkspaceService, IDisposable { - private static readonly Guid s_IID_IMetaDataImport = new("7DAC8207-D3AE-4c75-9B67-92801A497D44"); - private static readonly ConditionalWeakTable s_lifetimeMap = new(); /// - /// Mapping from an we created, to the memory mapped files (mmf) corresponding to - /// the assembly and all the modules within it. This is kept around to make OOP syncing more efficient. - /// Specifically, since we know we read the assembly into an mmf, we can just send the mmf name/offset/length to - /// the remote process, and it can map that same memory in directly, instead of needing the host to send the - /// entire contents of the assembly over the channel to the OOP process. + /// Mapping from an we created, to the identifiers identifying the memory mapped + /// files (mmf) corresponding to that assembly and all the modules within it. This is kept around to make OOP + /// syncing more efficient. Specifically, since we know we dumped the assembly into an mmf, we can just send the mmf + /// name/offset/length to the remote process, and it can map that same memory in directly, instead of needing the + /// host to send the entire contents of the assembly over the channel to the OOP process. + /// + private static readonly ConditionalWeakTable> s_metadataToStorageHandles = new(); + + private readonly object _metadataCacheLock = new(); + + /// + /// Access locked with . /// - private static readonly ConditionalWeakTable> s_metadataToStorages = new(); + private readonly Dictionary _metadataCache = []; - private readonly MetadataCache _metadataCache = new(); private readonly ImmutableArray _runtimeDirectories; private readonly TemporaryStorageService _temporaryStorageService; - - internal IVsXMLMemberIndexService XmlMemberIndexService { get; } + private readonly IVsXMLMemberIndexService _xmlMemberIndexService; + private readonly ReaderWriterLockSlim _smartOpenScopeLock = new(); /// /// The smart open scope service. This can be null during shutdown when using the service might crash. Any - /// use of this field or derived types should be synchronized with to ensure + /// use of this field or derived types should be synchronized with to ensure /// you don't grab the field and then use it while shutdown continues. /// private IVsSmartOpenScope? SmartOpenScopeServiceOpt { get; set; } - private readonly ReaderWriterLockSlim _readerWriterLock = new(); - - internal VisualStudioMetadataReferenceManager( + public VisualStudioMetadataReferenceManager( IServiceProvider serviceProvider, TemporaryStorageService temporaryStorageService) { _runtimeDirectories = GetRuntimeDirectories(); - XmlMemberIndexService = (IVsXMLMemberIndexService)serviceProvider.GetService(typeof(SVsXMLMemberIndexService)); - Assumes.Present(XmlMemberIndexService); + _xmlMemberIndexService = (IVsXMLMemberIndexService)serviceProvider.GetService(typeof(SVsXMLMemberIndexService)); + Assumes.Present(_xmlMemberIndexService); SmartOpenScopeServiceOpt = (IVsSmartOpenScope)serviceProvider.GetService(typeof(SVsSmartOpenScope)); Assumes.Present(SmartOpenScopeServiceOpt); @@ -78,7 +80,7 @@ internal VisualStudioMetadataReferenceManager( public void Dispose() { - using (_readerWriterLock.DisposableWrite()) + using (_smartOpenScopeLock.DisposableWrite()) { // IVsSmartOpenScope can't be used as we shutdown, and this is pretty commonly hit according to // Windows Error Reporting as we try creating metadata for compilations. @@ -86,24 +88,27 @@ public void Dispose() } } - public IReadOnlyList? GetStorages(string fullPath, DateTime snapshotTimestamp) + private bool TryGetMetadata(FileKey key, [NotNullWhen(true)] out AssemblyMetadata? metadata) + { + lock (_metadataCacheLock) + return _metadataCache.TryGetValue(key, out metadata); + } + + public IReadOnlyList? GetStorageHandles(string fullPath, DateTime snapshotTimestamp) { var key = new FileKey(fullPath, snapshotTimestamp); // check existing metadata - if (_metadataCache.TryGetMetadata(key, out var source) && - s_metadataToStorages.TryGetValue(source, out var storages)) + if (TryGetMetadata(key, out var source) && + s_metadataToStorageHandles.TryGetValue(source, out var handles)) { - return storages; + return handles; } return null; } public PortableExecutableReference CreateMetadataReferenceSnapshot(string filePath, MetadataReferenceProperties properties) - => new VisualStudioMetadataReference.Snapshot(this, properties, filePath, fileChangeTrackerOpt: null); - - public void ClearCache() - => _metadataCache.ClearCache(); + => new VisualStudioPortableExecutableReference(this, properties, filePath, fileChangeTracker: null); private bool VsSmartScopeCandidate(string fullPath) => _runtimeDirectories.Any(static (d, fullPath) => fullPath.StartsWith(d, StringComparison.OrdinalIgnoreCase), fullPath); @@ -133,67 +138,71 @@ private static ImmutableArray GetRuntimeDirectories() internal Metadata GetMetadata(string fullPath, DateTime snapshotTimestamp) { var key = new FileKey(fullPath, snapshotTimestamp); + // check existing metadata - if (_metadataCache.TryGetMetadata(key, out var metadata)) - return metadata; + if (!TryGetMetadata(key, out var metadata)) + { + // wasn't in the cache. create a new instance. + metadata = GetMetadataWorker(fullPath); + Contract.ThrowIfNull(metadata); - var newMetadata = GetMetadataWorker(); + lock (_metadataCacheLock) + { + // Now try to create and add the metadata to the cache. If we fail to add it (because some other thread + // beat us to this), then Dispose the metadata we just created and will return the existing metadata + // instead. + if (_metadataCache.TryGetValue(key, out var cachedMetadata)) + { + metadata.Dispose(); + return cachedMetadata; + } - if (!_metadataCache.GetOrAddMetadata(key, newMetadata, out metadata)) - newMetadata.Dispose(); + // don't use "Add" since key might already exist with already released metadata + _metadataCache[key] = metadata; + return metadata; + } + } return metadata; - AssemblyMetadata GetMetadataWorker() + AssemblyMetadata GetMetadataWorker(string fullPath) { - if (VsSmartScopeCandidate(key.FullPath)) - { - var newMetadata = CreateAssemblyMetadataFromMetadataImporter(key); - return newMetadata; - } - else - { - // use temporary storage - using var _ = ArrayBuilder.GetInstance(out var storages); - var newMetadata = CreateAssemblyMetadata(key, key => - { - // - // - GetMetadataFromTemporaryStorage(key, out var storage, out var metadata); - storages.Add(storage); - return metadata; - }); - - var storagesArray = storages.ToImmutable(); + var (metadata, handles) = VsSmartScopeCandidate(fullPath) + ? CreateAssemblyMetadataFromMetadataImporter(fullPath) + : CreateAssemblyMetadata(fullPath, fullPath => GetMetadataFromTemporaryStorage(fullPath, _temporaryStorageService)); - s_metadataToStorages.Add(newMetadata, storagesArray); + if (handles != null) + s_metadataToStorageHandles.Add(metadata, handles); - return newMetadata; - } + return metadata; } } - private void GetMetadataFromTemporaryStorage( - FileKey moduleFileKey, out TemporaryStorageService.TemporaryStreamStorage storage, out ModuleMetadata metadata) + private static (ModuleMetadata metadata, TemporaryStorageStreamHandle storageHandle) GetMetadataFromTemporaryStorage( + string fullPath, TemporaryStorageService temporaryStorageService) { - GetStorageInfoFromTemporaryStorage(moduleFileKey, out storage, out var stream); + GetStorageInfoFromTemporaryStorage(fullPath, temporaryStorageService, out var storageHandle, out var stream); unsafe { - // For an unmanaged memory stream, ModuleMetadata can take ownership directly. - metadata = ModuleMetadata.CreateFromMetadata((IntPtr)stream.PositionPointer, (int)stream.Length, stream.Dispose); + // For an unmanaged memory stream, ModuleMetadata can take ownership directly. Passing in stream.Dispose + // here will also ensure that as long as this metdata is alive, we'll keep the memory-mapped-file it points + // to alive. + var metadata = ModuleMetadata.CreateFromMetadata((IntPtr)stream.PositionPointer, (int)stream.Length, stream.Dispose); + return (metadata, storageHandle); } - return; - - void GetStorageInfoFromTemporaryStorage( - FileKey moduleFileKey, out TemporaryStorageService.TemporaryStreamStorage storage, out UnmanagedMemoryStream stream) + static void GetStorageInfoFromTemporaryStorage( + string fullPath, TemporaryStorageService temporaryStorageService, out TemporaryStorageStreamHandle storageHandle, out UnmanagedMemoryStream stream) { int size; + + // Create a temp stream in memory to copy the metadata bytes into. using (var copyStream = SerializableBytes.CreateWritableStream()) { - // open a file and let it go as soon as possible - using (var fileStream = FileUtilities.OpenRead(moduleFileKey.FullPath)) + // Open a file on disk, find the metadata section, copy those bytes into the temp stream, and release + // the file immediately after. + using (var fileStream = FileUtilities.OpenRead(fullPath)) { var headers = new PEHeaders(fileStream); @@ -210,16 +219,15 @@ void GetStorageInfoFromTemporaryStorage( StreamCopy(fileStream, copyStream, offset, size); } - // copy over the data to temp storage and let pooled stream go - storage = _temporaryStorageService.CreateTemporaryStreamStorage(); - + // Now, copy over the metadata bytes into a memory mapped file. This will keep it fixed in a single + // location, so we can create a metadata value wrapping that. This will also let us share the memory + // for that metadata value with our OOP process. copyStream.Position = 0; - storage.WriteStream(copyStream); + storageHandle = temporaryStorageService.WriteToTemporaryStorage(copyStream, CancellationToken.None); } - // get stream that owns the underlying unmanaged memory. - stream = storage.ReadStream(CancellationToken.None); - + // Now, read the data from the memory-mapped-file back into a stream that we load into the metadata value. + stream = storageHandle.ReadFromTemporaryStorage(CancellationToken.None); // stream size must be same as what metadata reader said the size should be. Contract.ThrowIfFalse(stream.Length == size); } @@ -244,22 +252,21 @@ static void StreamCopy(Stream source, Stream destination, int start, int length) /// /// - private AssemblyMetadata CreateAssemblyMetadataFromMetadataImporter(FileKey fileKey) + private (AssemblyMetadata assemblyMetadata, IReadOnlyList? handles) CreateAssemblyMetadataFromMetadataImporter(string fullPath) { - return CreateAssemblyMetadata(fileKey, fileKey => + return CreateAssemblyMetadata(fullPath, fullPath => { - var metadata = TryCreateModuleMetadataFromMetadataImporter(fileKey); + var metadata = TryCreateModuleMetadataFromMetadataImporter(fullPath); + if (metadata != null) + return (metadata, storageHandle: null); // getting metadata didn't work out through importer. fallback to shadow copy one - if (metadata == null) - GetMetadataFromTemporaryStorage(fileKey, out _, out metadata); - - return metadata; + return GetMetadataFromTemporaryStorage(fullPath, _temporaryStorageService); }); - ModuleMetadata? TryCreateModuleMetadataFromMetadataImporter(FileKey moduleFileKey) + ModuleMetadata? TryCreateModuleMetadataFromMetadataImporter(string fullPath) { - if (!TryGetFileMappingFromMetadataImporter(moduleFileKey, out var info, out var pImage, out var length)) + if (!TryGetFileMappingFromMetadataImporter(fullPath, out var info, out var pImage, out var length)) { return null; } @@ -272,16 +279,12 @@ private AssemblyMetadata CreateAssemblyMetadataFromMetadataImporter(FileKey file return metadata; } - bool TryGetFileMappingFromMetadataImporter(FileKey fileKey, [NotNullWhen(true)] out IMetaDataInfo? info, out IntPtr pImage, out long length) + bool TryGetFileMappingFromMetadataImporter(string fullPath, [NotNullWhen(true)] out IMetaDataInfo? info, out IntPtr pImage, out long length) { // We might not be able to use COM services to get this if VS is shutting down. We'll synchronize to make sure this // doesn't race against - using (_readerWriterLock.DisposableRead()) + using (_smartOpenScopeLock.DisposableRead()) { - // here, we don't care about timestamp since all those bits should be part of Fx. and we assume that - // it won't be changed in the middle of VS running. - var fullPath = fileKey.FullPath; - info = null; pImage = default; length = default; @@ -309,33 +312,45 @@ bool TryGetFileMappingFromMetadataImporter(FileKey fileKey, [NotNullWhen(true)] /// /// - private static AssemblyMetadata CreateAssemblyMetadata( - FileKey fileKey, - Func moduleMetadataFactory) + private static (AssemblyMetadata assemblyMetadata, IReadOnlyList? handles) CreateAssemblyMetadata( + string fullPath, + Func moduleMetadataFactory) { - var manifestModule = moduleMetadataFactory(fileKey); + var (manifestModule, manifestHandle) = moduleMetadataFactory(fullPath); + var moduleNames = manifestModule.GetModuleNames(); + + var modules = new FixedSizeArrayBuilder(1 + moduleNames.Length); + var handles = new FixedSizeArrayBuilder(1 + moduleNames.Length); - using var _ = ArrayBuilder.GetInstance(out var moduleBuilder); + modules.Add(manifestModule); + handles.Add(manifestHandle); string? assemblyDir = null; - foreach (var moduleName in manifestModule.GetModuleNames()) + foreach (var moduleName in moduleNames) { - if (assemblyDir is null) - { - moduleBuilder.Add(manifestModule); - assemblyDir = Path.GetDirectoryName(fileKey.FullPath); - } + assemblyDir ??= Path.GetDirectoryName(fullPath); // Suppression should be removed or addressed https://github.com/dotnet/roslyn/issues/41636 - var moduleFileKey = FileKey.Create(PathUtilities.CombineAbsoluteAndRelativePaths(assemblyDir, moduleName)!); - var metadata = moduleMetadataFactory(moduleFileKey); + var moduleFileKey = PathUtilities.CombineAbsoluteAndRelativePaths(assemblyDir, moduleName)!; - moduleBuilder.Add(metadata); + var (moduleMetadata, moduleHandle) = moduleMetadataFactory(moduleFileKey); + modules.Add(moduleMetadata); + handles.Add(moduleHandle); } - if (moduleBuilder.Count == 0) - moduleBuilder.Add(manifestModule); + var assembly = AssemblyMetadata.Create(modules.MoveToImmutable()); - return AssemblyMetadata.Create(moduleBuilder.ToImmutable()); + // If we got any null handles, then we weren't able to map this whole assembly into memory mapped files. So we + // can't use those to transfer over the data efficiently to the OOP process. In that case, we don't store the + // handles at all. + var storageHandles = handles.MoveToImmutable(); + return (assembly, storageHandles.Any(h => h is null) ? null : storageHandles); + } + + public static class TestAccessor + { + public static (AssemblyMetadata assemblyMetadata, IReadOnlyList? handles) CreateAssemblyMetadata( + string fullPath, TemporaryStorageService temporaryStorageService) + => VisualStudioMetadataReferenceManager.CreateAssemblyMetadata(fullPath, fullPath => GetMetadataFromTemporaryStorage(fullPath, temporaryStorageService)); } } diff --git a/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.Factory.cs b/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManagerFactory.cs similarity index 74% rename from src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.Factory.cs rename to src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManagerFactory.cs index 28b98b418f0ca..373bc843d89f3 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.Factory.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManagerFactory.cs @@ -14,15 +14,11 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; // TODO: Remove this type. This factory is needed just to instantiate a singleton of VisualStudioMetadataReferenceProvider. // We should be able to MEF-instantiate a singleton of VisualStudioMetadataReferenceProvider without creating this factory. [ExportWorkspaceServiceFactory(typeof(VisualStudioMetadataReferenceManager), ServiceLayer.Host), Shared] -internal class VisualStudioMetadataReferenceManagerFactory : IWorkspaceServiceFactory +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal class VisualStudioMetadataReferenceManagerFactory(SVsServiceProvider serviceProvider) : IWorkspaceServiceFactory { private VisualStudioMetadataReferenceManager? _singleton; - private readonly IServiceProvider _serviceProvider; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public VisualStudioMetadataReferenceManagerFactory(SVsServiceProvider serviceProvider) - => _serviceProvider = serviceProvider; public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) { @@ -30,7 +26,7 @@ public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) { // If we're in VS we know we must be able to get a TemporaryStorageService var temporaryStorage = (TemporaryStorageService)workspaceServices.GetRequiredService(); - Interlocked.CompareExchange(ref _singleton, new VisualStudioMetadataReferenceManager(_serviceProvider, temporaryStorage), null); + Interlocked.CompareExchange(ref _singleton, new VisualStudioMetadataReferenceManager(serviceProvider, temporaryStorage), null); } return _singleton; diff --git a/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceProviderServiceFactory.cs b/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceProviderServiceFactory.cs index 1d513382ef67a..cb23cbbed58e6 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceProviderServiceFactory.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceProviderServiceFactory.cs @@ -2,11 +2,8 @@ // 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.Composition; -using System.Diagnostics; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; @@ -14,29 +11,20 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; [ExportWorkspaceServiceFactory(typeof(IMetadataService), ServiceLayer.Host), Shared] -internal sealed class VsMetadataServiceFactory : IWorkspaceServiceFactory +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class VisualStudioMetadataServiceFactory() : IWorkspaceServiceFactory { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public VsMetadataServiceFactory() - { - } - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) => new Service(workspaceServices); - private sealed class Service : IMetadataService + private sealed class Service(HostWorkspaceServices workspaceServices) : IMetadataService { - private readonly Lazy _manager; - - public Service(HostWorkspaceServices workspaceServices) - { - // We will defer creation of this reference manager until we have to to avoid it being constructed too - // early and potentially causing deadlocks. We do initialize it on the UI thread in the - // VisualStudioWorkspaceImpl.DeferredState constructor to ensure it gets created there. - _manager = new Lazy( - () => workspaceServices.GetRequiredService()); - } + // We will defer creation of this reference manager until we have to to avoid it being constructed too early + // and potentially causing deadlocks. We do initialize it on the UI thread in the + // VisualStudioWorkspaceImpl.DeferredState constructor to ensure it gets created there. + private readonly Lazy _manager = new( + () => workspaceServices.GetRequiredService()); public PortableExecutableReference GetReference(string resolvedPath, MetadataReferenceProperties properties) => _manager.Value.CreateMetadataReferenceSnapshot(resolvedPath, properties); diff --git a/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReference.Snapshot.cs b/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioPortableExecutableReference.cs similarity index 58% rename from src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReference.Snapshot.cs rename to src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioPortableExecutableReference.cs index 8b8d1b5f0cc5f..e84fed7d27bfe 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReference.Snapshot.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioPortableExecutableReference.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Collections.Generic; using System.Diagnostics; @@ -17,42 +15,43 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; -// TODO: This class is now an empty container just to hold onto the nested type. Renaming that is an invasive change that will be it's own commit. -internal static class VisualStudioMetadataReference +internal partial class VisualStudioMetadataReferenceManager { /// - /// Represents a metadata reference corresponding to a specific version of a file. - /// If a file changes in future this reference will still refer to the original version. + /// Represents a metadata reference corresponding to a specific version of a file. If a file changes in future this + /// reference will still refer to the original version. /// /// - /// The compiler observes the metadata content a reference refers to by calling - /// and the observed metadata is memoized by the compilation. However we drop compilations to decrease memory consumption. - /// When the compilation is recreated for a solution the compiler asks for metadata again and we need to provide the original content, - /// not read the file again. Therefore we need to save the timestamp on the . - /// - /// When the VS observes a change in a metadata reference file the project version is advanced and a new instance of - /// is created for the corresponding reference. + /// The compiler observes the metadata content a reference refers to by calling and the observed metadata is memoized by the compilation. + /// When the VS observes a change in a metadata reference file the project version is advanced and a new + /// instance of is created for the corresponding reference. /// [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] - internal sealed class Snapshot : PortableExecutableReference, ISupportTemporaryStorage + private sealed class VisualStudioPortableExecutableReference : PortableExecutableReference, ISupportTemporaryStorage { private readonly VisualStudioMetadataReferenceManager _provider; private readonly Lazy _timestamp; - private Exception _error; - private readonly FileChangeTracker _fileChangeTrackerOpt; + private readonly FileChangeTracker? _fileChangeTracker; + + private Exception? _error; - internal Snapshot(VisualStudioMetadataReferenceManager provider, MetadataReferenceProperties properties, string fullPath, FileChangeTracker fileChangeTrackerOpt) + internal VisualStudioPortableExecutableReference( + VisualStudioMetadataReferenceManager provider, + MetadataReferenceProperties properties, + string fullPath, + FileChangeTracker? fileChangeTracker) : base(properties, fullPath) { Debug.Assert(Properties.Kind == MetadataImageKind.Assembly); _provider = provider; - _fileChangeTrackerOpt = fileChangeTrackerOpt; + _fileChangeTracker = fileChangeTracker; _timestamp = new Lazy(() => { try { - _fileChangeTrackerOpt?.EnsureSubscription(); + _fileChangeTracker?.EnsureSubscription(); return FileUtilities.GetFileTimeStamp(this.FilePath); } @@ -69,15 +68,15 @@ internal Snapshot(VisualStudioMetadataReferenceManager provider, MetadataReferen }, LazyThreadSafetyMode.PublicationOnly); } + private new string FilePath => base.FilePath!; + protected override Metadata GetMetadataImpl() { // Fetch the timestamp first, so as to populate _error if needed var timestamp = _timestamp.Value; if (_error != null) - { throw _error; - } try { @@ -87,30 +86,28 @@ protected override Metadata GetMetadataImpl() { throw ExceptionUtilities.Unreachable(); } - } - private bool SaveMetadataReadingException(Exception e) - { - // Save metadata reading failure so that future compilations created - // with this reference snapshot fail consistently in the same way. - if (e is IOException or BadImageFormatException) + bool SaveMetadataReadingException(Exception e) { - _error = e; - } + // Save metadata reading failure so that future compilations created + // with this reference snapshot fail consistently in the same way. + if (e is IOException or BadImageFormatException) + _error = e; - return false; + return false; + } } protected override DocumentationProvider CreateDocumentationProvider() - => new VisualStudioDocumentationProvider(this.FilePath, _provider.XmlMemberIndexService); + => new VisualStudioDocumentationProvider(this.FilePath, _provider._xmlMemberIndexService); protected override PortableExecutableReference WithPropertiesImpl(MetadataReferenceProperties properties) - => new Snapshot(_provider, properties, this.FilePath, _fileChangeTrackerOpt); + => new VisualStudioPortableExecutableReference(_provider, properties, this.FilePath, _fileChangeTracker); private string GetDebuggerDisplay() => "Metadata File: " + FilePath; - public IReadOnlyList GetStorages() - => _provider.GetStorages(this.FilePath, _timestamp.Value); + public IReadOnlyList? StorageHandles + => _provider.GetStorageHandles(this.FilePath, _timestamp.Value); } } diff --git a/src/VisualStudio/Core/Def/RoslynPackage.cs b/src/VisualStudio/Core/Def/RoslynPackage.cs index 79a33a370c61e..76a77b536afee 100644 --- a/src/VisualStudio/Core/Def/RoslynPackage.cs +++ b/src/VisualStudio/Core/Def/RoslynPackage.cs @@ -232,8 +232,6 @@ protected override async Task LoadComponentsAsync(CancellationToken cancellation await this.ComponentModel.GetService().InitializeAsync(this, cancellationToken).ConfigureAwait(false); await this.ComponentModel.GetService().InitializeAsync(this, cancellationToken).ConfigureAwait(false); - await this.ComponentModel.GetService().InitializeAsync(this, cancellationToken).ConfigureAwait(false); - await this.ComponentModel.GetService().InitializeAsync(this, cancellationToken).ConfigureAwait(false); await this.ComponentModel.GetService().InitializeAsync(this, cancellationToken).ConfigureAwait(false); await this.ComponentModel.GetService().InitializeAsync(this, cancellationToken).ConfigureAwait(false); @@ -273,7 +271,7 @@ private async Task LoadComponentsBackgroundAsync(CancellationToken cancellationT await LoadStackTraceExplorerMenusAsync(cancellationToken).ConfigureAwait(true); // Initialize keybinding reset detector - await ComponentModel.DefaultExportProvider.GetExportedValue().InitializeAsync().ConfigureAwait(true); + await ComponentModel.DefaultExportProvider.GetExportedValue().InitializeAsync(cancellationToken).ConfigureAwait(true); } private async Task LoadStackTraceExplorerMenusAsync(CancellationToken cancellationToken) diff --git a/src/VisualStudio/Core/Def/Telemetry/Shared/AggregatingTelemetryLog.cs b/src/VisualStudio/Core/Def/Telemetry/Shared/AggregatingTelemetryLog.cs index 042a8bb5877a1..711cf06437e77 100644 --- a/src/VisualStudio/Core/Def/Telemetry/Shared/AggregatingTelemetryLog.cs +++ b/src/VisualStudio/Core/Def/Telemetry/Shared/AggregatingTelemetryLog.cs @@ -28,7 +28,6 @@ internal sealed class AggregatingTelemetryLog : ITelemetryLog private readonly HistogramConfiguration? _histogramConfiguration; private readonly string _eventName; private readonly FunctionId _functionId; - private readonly AggregatingTelemetryLogManager _aggregatingTelemetryLogManager; private readonly object _flushLock; private ImmutableDictionary Histogram, TelemetryEvent TelemetryEvent, object Lock)> _histograms = ImmutableDictionary, TelemetryEvent, object)>.Empty; @@ -40,7 +39,7 @@ internal sealed class AggregatingTelemetryLog : ITelemetryLog /// Used to derive meter name /// Optional values indicating bucket boundaries in milliseconds. If not specified, /// all histograms created will use the default histogram configuration - public AggregatingTelemetryLog(TelemetrySession session, FunctionId functionId, double[]? bucketBoundaries, AggregatingTelemetryLogManager aggregatingTelemetryLogManager) + public AggregatingTelemetryLog(TelemetrySession session, FunctionId functionId, double[]? bucketBoundaries) { var meterName = TelemetryLogger.GetPropertyName(functionId, "meter"); var meterProvider = new VSTelemetryMeterProvider(); @@ -49,7 +48,6 @@ public AggregatingTelemetryLog(TelemetrySession session, FunctionId functionId, _meter = meterProvider.CreateMeter(meterName, version: MeterVersion); _eventName = TelemetryLogger.GetEventName(functionId); _functionId = functionId; - _aggregatingTelemetryLogManager = aggregatingTelemetryLogManager; _flushLock = new(); if (bucketBoundaries != null) @@ -104,8 +102,6 @@ public void Log(KeyValueLogMessage logMessage) { histogram.Record(value); } - - _aggregatingTelemetryLogManager.EnsureTelemetryWorkQueued(); } public IDisposable? LogBlockTime(KeyValueLogMessage logMessage, int minThresholdMs) diff --git a/src/VisualStudio/Core/Def/Telemetry/Shared/AggregatingTelemetryLogManager.cs b/src/VisualStudio/Core/Def/Telemetry/Shared/AggregatingTelemetryLogManager.cs index 1188da45610ee..2b3042e607592 100644 --- a/src/VisualStudio/Core/Def/Telemetry/Shared/AggregatingTelemetryLogManager.cs +++ b/src/VisualStudio/Core/Def/Telemetry/Shared/AggregatingTelemetryLogManager.cs @@ -2,39 +2,24 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; using System.Collections.Immutable; -using System.Threading; -using System.Threading.Tasks; using Microsoft.CodeAnalysis.Internal.Log; -using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.VisualStudio.Telemetry; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Telemetry; /// -/// Manages creation and obtaining aggregated telemetry logs. Also, notifies logs to -/// send aggregated events every 30 minutes. +/// Manages creation and obtaining aggregated telemetry logs. /// internal sealed class AggregatingTelemetryLogManager { - private static readonly TimeSpan s_batchedTelemetryCollectionPeriod = TimeSpan.FromMinutes(30); - private readonly TelemetrySession _session; - private readonly AsyncBatchingWorkQueue _postTelemetryQueue; private ImmutableDictionary _aggregatingLogs = ImmutableDictionary.Empty; - public AggregatingTelemetryLogManager(TelemetrySession session, IAsynchronousOperationListener asyncListener) + public AggregatingTelemetryLogManager(TelemetrySession session) { _session = session; - - _postTelemetryQueue = new AsyncBatchingWorkQueue( - s_batchedTelemetryCollectionPeriod, - PostCollectedTelemetryAsync, - asyncListener, - CancellationToken.None); } public ITelemetryLog? GetLog(FunctionId functionId, double[]? bucketBoundaries) @@ -42,22 +27,11 @@ public AggregatingTelemetryLogManager(TelemetrySession session, IAsynchronousOpe if (!_session.IsOptedIn) return null; - return ImmutableInterlocked.GetOrAdd(ref _aggregatingLogs, functionId, functionId => new AggregatingTelemetryLog(_session, functionId, bucketBoundaries, this)); - } - - public void EnsureTelemetryWorkQueued() - { - // Ensure PostCollectedTelemetryAsync will get fired after the collection period. - _postTelemetryQueue.AddWork(); - } - - private ValueTask PostCollectedTelemetryAsync(CancellationToken token) - { - token.ThrowIfCancellationRequested(); - - Flush(); - - return ValueTaskFactory.CompletedTask; + return ImmutableInterlocked.GetOrAdd( + ref _aggregatingLogs, + functionId, + static (functionId, arg) => new AggregatingTelemetryLog(arg._session, functionId, arg.bucketBoundaries), + factoryArgument: (_session, bucketBoundaries)); } public void Flush() diff --git a/src/VisualStudio/Core/Def/Telemetry/Shared/TelemetryLogProvider.cs b/src/VisualStudio/Core/Def/Telemetry/Shared/TelemetryLogProvider.cs index 3bc330135068c..16e44077e4178 100644 --- a/src/VisualStudio/Core/Def/Telemetry/Shared/TelemetryLogProvider.cs +++ b/src/VisualStudio/Core/Def/Telemetry/Shared/TelemetryLogProvider.cs @@ -17,17 +17,17 @@ internal sealed class TelemetryLogProvider : ITelemetryLogProvider private readonly AggregatingTelemetryLogManager _aggregatingTelemetryLogManager; private readonly VisualStudioTelemetryLogManager _visualStudioTelemetryLogManager; - private TelemetryLogProvider(TelemetrySession session, ILogger telemetryLogger, IAsynchronousOperationListener asyncListener) + private TelemetryLogProvider(TelemetrySession session, ILogger telemetryLogger) { - _aggregatingTelemetryLogManager = new AggregatingTelemetryLogManager(session, asyncListener); + _aggregatingTelemetryLogManager = new AggregatingTelemetryLogManager(session); _visualStudioTelemetryLogManager = new VisualStudioTelemetryLogManager(session, telemetryLogger); } public static TelemetryLogProvider Create(TelemetrySession session, ILogger telemetryLogger, IAsynchronousOperationListener asyncListener) { - var logProvider = new TelemetryLogProvider(session, telemetryLogger, asyncListener); + var logProvider = new TelemetryLogProvider(session, telemetryLogger); - TelemetryLogging.SetLogProvider(logProvider); + TelemetryLogging.SetLogProvider(logProvider, asyncListener); return logProvider; } diff --git a/src/VisualStudio/Core/Def/UnusedReferences/Dialog/UnusedReferencesTableProvider.cs b/src/VisualStudio/Core/Def/UnusedReferences/Dialog/UnusedReferencesTableProvider.cs index ace4d7cfc5ea6..e15e7d56ecbc2 100644 --- a/src/VisualStudio/Core/Def/UnusedReferences/Dialog/UnusedReferencesTableProvider.cs +++ b/src/VisualStudio/Core/Def/UnusedReferences/Dialog/UnusedReferencesTableProvider.cs @@ -40,7 +40,7 @@ public IWpfTableControl4 CreateTableControl() _tableManager, autoSubscribe: true, BuildColumnStates(), - UnusedReferencesColumnDefinitions.ColumnNames.ToArray()); + [.. UnusedReferencesColumnDefinitions.ColumnNames]); tableControl.ShowGroupingLine = true; tableControl.DoColumnsAutoAdjust = true; tableControl.DoSortingAndGroupingWhileUnstable = true; diff --git a/src/VisualStudio/Core/Def/Workspace/VisualStudioSymbolNavigationService.cs b/src/VisualStudio/Core/Def/Workspace/VisualStudioSymbolNavigationService.cs index c703b64497112..8013cdac4f854 100644 --- a/src/VisualStudio/Core/Def/Workspace/VisualStudioSymbolNavigationService.cs +++ b/src/VisualStudio/Core/Def/Workspace/VisualStudioSymbolNavigationService.cs @@ -180,7 +180,6 @@ await navigationService.TryNavigateToSpanAsync( public async Task TrySymbolNavigationNotifyAsync(ISymbol symbol, Project project, CancellationToken cancellationToken) { await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - _threadingContext.ThrowIfNotOnUIThread(); var definitionItem = symbol.ToNonClassifiedDefinitionItem(project.Solution, includeHiddenLocations: true); definitionItem.Properties.TryGetValue(DefinitionItem.RQNameKey1, out var rqName); diff --git a/src/VisualStudio/Core/Impl/CodeModel/FileCodeModel_CodeGen.cs b/src/VisualStudio/Core/Impl/CodeModel/FileCodeModel_CodeGen.cs index 3a1213d0d2013..4a980f4e05115 100644 --- a/src/VisualStudio/Core/Impl/CodeModel/FileCodeModel_CodeGen.cs +++ b/src/VisualStudio/Core/Impl/CodeModel/FileCodeModel_CodeGen.cs @@ -141,7 +141,7 @@ private static object[] GetValidArray(object itemOrArray, bool allowMultipleElem } } - return result.ToArray(); + return [.. result]; } internal EnvDTE80.CodeAttributeArgument AddAttributeArgument(SyntaxNode containerNode, string name, string value, object position) diff --git a/src/VisualStudio/Core/Impl/CodeModel/ProjectCodeModelFactory.cs b/src/VisualStudio/Core/Impl/CodeModel/ProjectCodeModelFactory.cs index 75d7d1abb1dd7..3c1acd9e7c9aa 100644 --- a/src/VisualStudio/Core/Impl/CodeModel/ProjectCodeModelFactory.cs +++ b/src/VisualStudio/Core/Impl/CodeModel/ProjectCodeModelFactory.cs @@ -110,7 +110,7 @@ private async ValueTask ProcessNextDocumentBatchAsync( // Keep firing events for this doc, as long as we haven't exceeded the max amount // of waiting time, and there's no user input that should take precedence. - if (stopwatch.Elapsed.Ticks > MaxTimeSlice || IThreadingContextExtensions.IsInputPending()) + if (stopwatch.Elapsed.Ticks > MaxTimeSlice || IsInputPending()) { await this.Listener.Delay(delayBetweenProcessing, cancellationToken).ConfigureAwait(true); stopwatch = SharedStopwatch.StartNew(); @@ -140,6 +140,21 @@ void FireEventsForDocument(DocumentId documentId) codeModel.FireEvents(); return; } + + // Returns true if any keyboard or mouse button input is pending on the message queue. + static bool IsInputPending() + { + // The code below invokes into user32.dll, which is not available in non-Windows. + if (PlatformInformation.IsUnix) + return false; + + // The return value of GetQueueStatus is HIWORD:LOWORD. + // A non-zero value in HIWORD indicates some input message in the queue. + var result = NativeMethods.GetQueueStatus(NativeMethods.QS_INPUT); + + const uint InputMask = NativeMethods.QS_INPUT | (NativeMethods.QS_INPUT << 16); + return (result & InputMask) != 0; + } } private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) diff --git a/src/VisualStudio/Core/Impl/Options/AbstractOptionPreviewViewModel.cs b/src/VisualStudio/Core/Impl/Options/AbstractOptionPreviewViewModel.cs index 9ca3dba4d904b..94ff8208e4117 100644 --- a/src/VisualStudio/Core/Impl/Options/AbstractOptionPreviewViewModel.cs +++ b/src/VisualStudio/Core/Impl/Options/AbstractOptionPreviewViewModel.cs @@ -148,7 +148,7 @@ public void UpdatePreview(string text) _editorOptions.CreateOptions(), textBuffer.CurrentSnapshot, separator: "", - exposedLineSpans: GetExposedLineSpans(textBuffer.CurrentSnapshot).ToArray()); + exposedLineSpans: [.. GetExposedLineSpans(textBuffer.CurrentSnapshot)]); var textView = _textEditorFactoryService.CreateTextView(projection, _textEditorFactoryService.CreateTextViewRoleSet(PredefinedTextViewRoles.Interactive)); @@ -224,7 +224,7 @@ protected void AddParenthesesOption( isChecked: !defaultAddForClarity)); CodeStyleItems.Add(new EnumCodeStyleOptionViewModel( - languageOption, title, preferences.ToArray(), + languageOption, title, [.. preferences], examples, this, optionStore, ServicesVSResources.Parentheses_preferences_colon, codeStylePreferences)); } diff --git a/src/VisualStudio/Core/Impl/Options/Style/NamingPreferences/NamingStyleOptionPageControl.xaml.cs b/src/VisualStudio/Core/Impl/Options/Style/NamingPreferences/NamingStyleOptionPageControl.xaml.cs index 983d1474bfea5..75f2401bcaaf0 100644 --- a/src/VisualStudio/Core/Impl/Options/Style/NamingPreferences/NamingStyleOptionPageControl.xaml.cs +++ b/src/VisualStudio/Core/Impl/Options/Style/NamingPreferences/NamingStyleOptionPageControl.xaml.cs @@ -72,7 +72,7 @@ private void AddButton_Click(object sender, RoutedEventArgs e) private void ManageSpecificationsButton_Click(object sender, RoutedEventArgs e) { - var viewModel = new ManageSymbolSpecificationsDialogViewModel(_viewModel.Specifications, _viewModel.CodeStyleItems.ToList(), _languageName, _notificationService); + var viewModel = new ManageSymbolSpecificationsDialogViewModel(_viewModel.Specifications, [.. _viewModel.CodeStyleItems], _languageName, _notificationService); var dialog = new ManageNamingStylesInfoDialog(viewModel); if (dialog.ShowModal().Value == true) { @@ -82,7 +82,7 @@ private void ManageSpecificationsButton_Click(object sender, RoutedEventArgs e) private void ManageStylesButton_Click(object sender, RoutedEventArgs e) { - var viewModel = new ManageNamingStylesDialogViewModel(_viewModel.NamingStyles, _viewModel.CodeStyleItems.ToList(), _notificationService); + var viewModel = new ManageNamingStylesDialogViewModel(_viewModel.NamingStyles, [.. _viewModel.CodeStyleItems], _notificationService); var dialog = new ManageNamingStylesInfoDialog(viewModel); if (dialog.ShowModal().Value == true) { diff --git a/src/VisualStudio/Core/Test.Next/Options/VisualStudioSettingsOptionPersisterTests.cs b/src/VisualStudio/Core/Test.Next/Options/VisualStudioSettingsOptionPersisterTests.cs index 7381a03351a4f..be85443f41957 100644 --- a/src/VisualStudio/Core/Test.Next/Options/VisualStudioSettingsOptionPersisterTests.cs +++ b/src/VisualStudio/Core/Test.Next/Options/VisualStudioSettingsOptionPersisterTests.cs @@ -155,8 +155,7 @@ public void SettingsChangeEvent() refreshedOptions.Clear(); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public void SettingsManagerReadOptionValue_Success( [CombinatorialValues( typeof(bool), @@ -189,8 +188,7 @@ public void SettingsManagerReadOptionValue_Success( Assert.Equal(optionValue, result.Value); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public void SettingsManagerReadOptionValue_Error( [CombinatorialValues( GetValueResult.Missing, diff --git a/src/VisualStudio/Core/Test.Next/Remote/SerializationValidator.cs b/src/VisualStudio/Core/Test.Next/Remote/SerializationValidator.cs index 77cd8df2ccb65..632755a0b1a82 100644 --- a/src/VisualStudio/Core/Test.Next/Remote/SerializationValidator.cs +++ b/src/VisualStudio/Core/Test.Next/Remote/SerializationValidator.cs @@ -94,11 +94,10 @@ public async Task GetValueAsync(Checksum checksum) var data = await GetRequiredAssetAsync(checksum).ConfigureAwait(false); Contract.ThrowIfNull(data.Value); - using var context = new SolutionReplicationContext(); using var stream = SerializableBytes.CreateWritableStream(); using (var writer = new ObjectWriter(stream, leaveOpen: true)) { - Serializer.Serialize(data.Value, writer, context, CancellationToken.None); + Serializer.Serialize(data.Value, writer, CancellationToken.None); } stream.Position = 0; @@ -189,7 +188,7 @@ internal async Task VerifyAssetAsync(Checksum attributeChecksum, Checksum textCh await VerifyAssetSerializationAsync( textChecksum, WellKnownSynchronizationKind.SerializableSourceText, - (v, k, s) => new SolutionAsset(s.CreateChecksum(v, CancellationToken.None), v)); + (v, k, s) => new SolutionAsset(v.ContentChecksum, v)); } internal async Task VerifyAssetSerializationAsync( diff --git a/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs b/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs index bbb5f577a5433..d638391df2977 100644 --- a/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs +++ b/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs @@ -536,7 +536,7 @@ public async Task EmptyAssetChecksumTest() var serializer = document.Project.Solution.Services.GetService(); var text = await document.GetTextAsync().ConfigureAwait(false); - var source = serializer.CreateChecksum(new SerializableSourceText(text, text.GetContentHash()), CancellationToken.None); + var source = new SerializableSourceText(text, text.GetContentHash()).ContentChecksum; var metadata = serializer.CreateChecksum(new MissingMetadataReference(), CancellationToken.None); var analyzer = serializer.CreateChecksum(new AnalyzerFileReference(Path.Combine(TempRoot.Root, "missing"), new MissingAnalyzerLoader()), CancellationToken.None); @@ -611,11 +611,9 @@ public void TestEncodingSerialization() var serializableSourceText = new SerializableSourceText(sourceText, sourceText.GetContentHash()); using (var stream = SerializableBytes.CreateWritableStream()) { - using var context = new SolutionReplicationContext(); - using (var objectWriter = new ObjectWriter(stream, leaveOpen: true)) { - serializer.Serialize(serializableSourceText, objectWriter, context, CancellationToken.None); + serializer.Serialize(serializableSourceText, objectWriter, CancellationToken.None); } stream.Position = 0; @@ -631,11 +629,9 @@ public void TestEncodingSerialization() serializableSourceText = new SerializableSourceText(sourceText, sourceText.GetContentHash()); using (var stream = SerializableBytes.CreateWritableStream()) { - using var context = new SolutionReplicationContext(); - using (var objectWriter = new ObjectWriter(stream, leaveOpen: true)) { - serializer.Serialize(serializableSourceText, objectWriter, context, CancellationToken.None); + serializer.Serialize(serializableSourceText, objectWriter, CancellationToken.None); } stream.Position = 0; @@ -662,11 +658,9 @@ public void TestCompilationOptions_NullableAndImport() void VerifyOptions(CompilationOptions originalOptions) { using var stream = SerializableBytes.CreateWritableStream(); - using var context = new SolutionReplicationContext(); - using (var objectWriter = new ObjectWriter(stream, leaveOpen: true)) { - serializer.Serialize(originalOptions, objectWriter, context, CancellationToken.None); + serializer.Serialize(originalOptions, objectWriter, CancellationToken.None); } stream.Position = 0; @@ -683,17 +677,17 @@ void VerifyOptions(CompilationOptions originalOptions) private static SolutionAsset CloneAsset(ISerializerService serializer, SolutionAsset asset) { using var stream = SerializableBytes.CreateWritableStream(); - using var context = new SolutionReplicationContext(); - using (var writer = new ObjectWriter(stream, leaveOpen: true)) { - serializer.Serialize(asset.Value, writer, context, CancellationToken.None); + serializer.Serialize(asset.Value, writer, CancellationToken.None); } stream.Position = 0; using var reader = ObjectReader.TryGetReader(stream); var recovered = serializer.Deserialize(asset.Kind, reader, CancellationToken.None); - var assetFromStorage = new SolutionAsset(serializer.CreateChecksum(recovered, CancellationToken.None), recovered); + var checksum = recovered is SerializableSourceText text ? text.ContentChecksum : serializer.CreateChecksum(recovered, CancellationToken.None); + + var assetFromStorage = new SolutionAsset(checksum, recovered); Assert.Equal(asset.Checksum, assetFromStorage.Checksum); return assetFromStorage; diff --git a/src/VisualStudio/Core/Test.Next/Services/AssetProviderTests.cs b/src/VisualStudio/Core/Test.Next/Services/AssetProviderTests.cs index 30732fa083aa0..f9826d7e8aafe 100644 --- a/src/VisualStudio/Core/Test.Next/Services/AssetProviderTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/AssetProviderTests.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -76,7 +77,7 @@ public async Task TestAssetSynchronization() // build checksum await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var map = await solution.GetAssetMapAsync(CancellationToken.None); + var map = await solution.GetAssetMapAsync(projectConeId: null, CancellationToken.None); using var remoteWorkspace = CreateRemoteWorkspace(); @@ -104,7 +105,7 @@ public async Task TestSolutionSynchronization() // build checksum await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var map = await solution.GetAssetMapAsync(CancellationToken.None); + var map = await solution.GetAssetMapAsync(projectConeId: null, CancellationToken.None); using var remoteWorkspace = CreateRemoteWorkspace(); @@ -146,5 +147,79 @@ public async Task TestProjectSynchronization() TestUtils.VerifyAssetStorage(map, storage); } + + [Fact] + public async Task TestAssetArrayOrdering() + { + var code1 = @"class Test1 { void Method() { } }"; + var code2 = @"class Test2 { void Method() { } }"; + + using var workspace = TestWorkspace.CreateCSharp([code1, code2]); + var project = workspace.CurrentSolution.Projects.First(); + + await project.State.GetChecksumAsync(CancellationToken.None); + + var map = await project.GetAssetMapAsync(CancellationToken.None); + + using var remoteWorkspace = CreateRemoteWorkspace(); + + var sessionId = Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())); + var storage = new SolutionAssetCache(); + var assetSource = new OrderedAssetSource(workspace.Services.GetService(), map); + + var service = new AssetProvider(sessionId, storage, assetSource, remoteWorkspace.Services.GetService()); + + using var _ = ArrayBuilder.GetInstance(out var allProjectChecksums); + var stateChecksums = await project.State.GetStateChecksumsAsync(CancellationToken.None); + + var textChecksums = stateChecksums.Documents.TextChecksums; + var textChecksumsReversed = new ChecksumCollection(textChecksums.Children.Reverse().ToImmutableArray()); + + var documents = await service.GetAssetsArrayAsync( + AssetPath.FullLookupForTesting, textChecksums, CancellationToken.None); + Assert.True(documents.Length == 2); + + storage.GetTestAccessor().Clear(); + var documentsReversed = await service.GetAssetsArrayAsync( + AssetPath.FullLookupForTesting, textChecksumsReversed, CancellationToken.None); + Assert.True(documentsReversed.Length == 2); + + Assert.True(documents.Select(d => d.ContentChecksum).SequenceEqual(documentsReversed.Reverse().Select(d => d.ContentChecksum))); + } + + private sealed class OrderedAssetSource( + ISerializerService serializerService, + IReadOnlyDictionary map) : IAssetSource + { + public ValueTask GetAssetsAsync( + Checksum solutionChecksum, + AssetPath assetPath, + ReadOnlyMemory checksums, + ISerializerService deserializerService, + Action callback, + TArg arg, + CancellationToken cancellationToken) + { + foreach (var (checksum, asset) in map) + { + if (checksums.Span.IndexOf(checksum) >= 0) + { + using var stream = new MemoryStream(); + using (var writer = new ObjectWriter(stream, leaveOpen: true)) + { + serializerService.Serialize(asset, writer, cancellationToken); + } + + stream.Position = 0; + using var reader = ObjectReader.GetReader(stream, leaveOpen: true); + var deserialized = deserializerService.Deserialize(asset.GetWellKnownSynchronizationKind(), reader, cancellationToken); + Contract.ThrowIfNull(deserialized); + callback(checksum, (T)deserialized, arg); + } + } + + return ValueTaskFactory.CompletedTask; + } + } } } diff --git a/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs b/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs index 893cc20f5e94f..9ea28f64a6d06 100644 --- a/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs @@ -241,8 +241,7 @@ await solution.CompilationState.GetChecksumAsync(CancellationToken.None), await remoteWorkspace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1365014")] public async Task TestRemoteHostSynchronizeIncrementalUpdate(bool applyInBatch) { @@ -1450,7 +1449,7 @@ private static void VerifyStates(Solution solution1, Solution solution2, string private static async Task VerifyAssetStorageAsync(InProcRemoteHostClient client, Solution solution) { - var map = await solution.GetAssetMapAsync(CancellationToken.None); + var map = await solution.GetAssetMapAsync(projectConeId: null, CancellationToken.None); var storage = client.TestData.WorkspaceManager.SolutionAssetCache; @@ -1525,7 +1524,7 @@ private static Solution Populate(Solution solution) ], [ "cs additional file content" - ], solution.ProjectIds.ToArray()); + ], [.. solution.ProjectIds]); solution = AddProject(solution, LanguageNames.CSharp, [ diff --git a/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs b/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs index 93583058aead3..7d162f9bed1a9 100644 --- a/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs @@ -56,8 +56,7 @@ public async Task TestCreation() Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public async Task TestGetSolutionWithPrimaryFlag(bool updatePrimaryBranch) { var code1 = @"class Test1 { void Method() { } }"; diff --git a/src/VisualStudio/Core/Test.Next/Services/VisualStudioDiagnosticAnalyzerExecutorTests.cs b/src/VisualStudio/Core/Test.Next/Services/VisualStudioDiagnosticAnalyzerExecutorTests.cs index 51c11425410a8..4eaec828a85b0 100644 --- a/src/VisualStudio/Core/Test.Next/Services/VisualStudioDiagnosticAnalyzerExecutorTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/VisualStudioDiagnosticAnalyzerExecutorTests.cs @@ -192,11 +192,7 @@ void Method() analyzerReference.GetAnalyzers(project.Language).Where(a => a.GetType() == analyzerType).ToImmutableArray(), new WorkspaceAnalyzerOptions(project.AnalyzerOptions, ideAnalyzerOptions)); - // no result for open file only analyzer unless forced - var result = await runner.AnalyzeProjectAsync(project, compilationWithAnalyzers, forceExecuteAllAnalyzers: false, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: CancellationToken.None); - Assert.Empty(result.AnalysisResult); - - result = await runner.AnalyzeProjectAsync(project, compilationWithAnalyzers, forceExecuteAllAnalyzers: true, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: CancellationToken.None); + var result = await runner.AnalyzeProjectAsync(project, compilationWithAnalyzers, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: CancellationToken.None); var analyzerResult = result.AnalysisResult[compilationWithAnalyzers.Analyzers[0]]; // check result @@ -234,7 +230,7 @@ void Method() var compilationWithAnalyzers = (await project.GetCompilationAsync()) .WithAnalyzers(analyzers, new WorkspaceAnalyzerOptions(project.AnalyzerOptions, ideAnalyzerOptions)); - var result = await runner.AnalyzeProjectAsync(project, compilationWithAnalyzers, forceExecuteAllAnalyzers: false, + var result = await runner.AnalyzeProjectAsync(project, compilationWithAnalyzers, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: CancellationToken.None); var analyzerResult = result.AnalysisResult[compilationWithAnalyzers.Analyzers[0]]; @@ -258,8 +254,8 @@ private static async Task AnalyzeAsync(TestWorkspace w analyzerReference.GetAnalyzers(project.Language).Where(a => a.GetType() == analyzerType).ToImmutableArray(), new WorkspaceAnalyzerOptions(project.AnalyzerOptions, ideOptions)); - var result = await executor.AnalyzeProjectAsync(project, analyzerDriver, forceExecuteAllAnalyzers: true, logPerformanceInfo: false, - getTelemetryInfo: false, cancellationToken); + var result = await executor.AnalyzeProjectAsync( + project, analyzerDriver, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken); return result.AnalysisResult[analyzerDriver.Analyzers[0]]; } diff --git a/src/VisualStudio/Core/Test/CallHierarchy/CallHierarchyTests.vb b/src/VisualStudio/Core/Test/CallHierarchy/CallHierarchyTests.vb index 024656e352818..e7f6cfe009ee8 100644 --- a/src/VisualStudio/Core/Test/CallHierarchy/CallHierarchyTests.vb +++ b/src/VisualStudio/Core/Test/CallHierarchy/CallHierarchyTests.vb @@ -185,8 +185,7 @@ public class D : I End Using End Function - - + Public Async Function TestCallHierarchyCrossProjectForImplements() As Task Dim input = @@ -225,8 +224,7 @@ class CSharpIt : IChangeSignatureOptionsService End Using End Function - - + Public Async Function TestCallHierarchyCrossProjectForCallsTo() As Task Dim input = @@ -264,8 +262,7 @@ class D End Using End Function - - + Public Async Function TestMustInheritMethodInclusionToOverrides() As Task Dim input = @@ -292,8 +289,7 @@ End Class End Using End Function - - + Public Async Function TestNavigateCrossProject() As Task Dim input = @@ -327,8 +323,7 @@ class D : C End Using End Function - - + Public Async Function TestUseDocumentIdWhenNavigating() As Task Dim input = @@ -367,8 +362,7 @@ namespace N End Using End Function - - + Public Async Function TestDisplayErrorWhenNotOnMemberCS() As Task Dim input = @@ -390,8 +384,7 @@ cla$$ss C End Using End Function - - + Public Async Function TestDisplayErrorWhenNotOnMemberCS2() As Task Dim input = @@ -414,8 +407,7 @@ class CC End Using End Function - - + Public Async Function TestDisplayErrorWhenNotOnMemberCS3() As Task Dim input = @@ -438,8 +430,7 @@ class CC End Using End Function - - + Public Async Function TestDisplayErrorWhenNotOnMemberVB() As Task Dim input = diff --git a/src/VisualStudio/Core/Test/ChangeSignature/AddParameterViewModelTests.vb b/src/VisualStudio/Core/Test/ChangeSignature/AddParameterViewModelTests.vb index 6910f9af386e0..b167207e190d1 100644 --- a/src/VisualStudio/Core/Test/ChangeSignature/AddParameterViewModelTests.vb +++ b/src/VisualStudio/Core/Test/ChangeSignature/AddParameterViewModelTests.vb @@ -308,8 +308,7 @@ class MyClass End Using End Function - - + Public Sub AddParameter_SubmittingTypeWithModifiersIsInvalid() Dim markup = - + Public Async Function ClassDesigner1() As Task Dim text = @@ -445,8 +444,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.DebuggerIntelliSense End Using End Function - - + Public Async Function ClassDesigner2() As Task Dim text = @@ -470,8 +468,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.DebuggerIntelliSense End Using End Function - - + Public Async Function CompletionUsesContextBufferPositions() As Task Dim text = @@ -735,8 +732,7 @@ $$ End Using End Function - - + Public Async Function TestItemDescription() As Task Dim text = diff --git a/src/VisualStudio/Core/Test/DebuggerIntelliSense/VisualBasicDebuggerIntellisenseTests.vb b/src/VisualStudio/Core/Test/DebuggerIntelliSense/VisualBasicDebuggerIntellisenseTests.vb index 9aa0ce192fccf..8adb3a0b82751 100644 --- a/src/VisualStudio/Core/Test/DebuggerIntelliSense/VisualBasicDebuggerIntellisenseTests.vb +++ b/src/VisualStudio/Core/Test/DebuggerIntelliSense/VisualBasicDebuggerIntellisenseTests.vb @@ -336,8 +336,7 @@ End Module End Using End Function - - + Public Async Function StoppedOnEndSub() As Task Dim text = @@ -352,8 +351,7 @@ End Module End Using End Function - - + Public Async Function StoppedOnEndProperty() As Task Dim text = diff --git a/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb b/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb index 0c278d44d7ece..edbf03f93fae6 100644 --- a/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb +++ b/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb @@ -430,11 +430,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics Return SpecializedTasks.EmptyImmutableArray(Of DiagnosticData)() End Function - Public Function GetDiagnosticsAsync(solution As Solution, projectId As ProjectId, documentId As DocumentId, includeSuppressedDiagnostics As Boolean, includeNonLocalDocumentDiagnostics As Boolean, cancellationToken As CancellationToken) As Task(Of ImmutableArray(Of DiagnosticData)) Implements IDiagnosticAnalyzerService.GetDiagnosticsAsync - Return SpecializedTasks.EmptyImmutableArray(Of DiagnosticData)() - End Function - - Public Function GetDiagnosticsForIdsAsync(solution As Solution, projectId As ProjectId, documentId As DocumentId, diagnosticIds As ImmutableHashSet(Of String), shouldIncludeAnalyzer As Func(Of DiagnosticAnalyzer, Boolean), includeSuppressedDiagnostics As Boolean, includeLocalDocumentDiagnostics As Boolean, includeNonLocalDocumentDiagnostics As Boolean, cancellationToken As CancellationToken) As Task(Of ImmutableArray(Of DiagnosticData)) Implements IDiagnosticAnalyzerService.GetDiagnosticsForIdsAsync + Public Function GetDiagnosticsForIdsAsync(solution As Solution, projectId As ProjectId, documentId As DocumentId, diagnosticIds As ImmutableHashSet(Of String), shouldIncludeAnalyzer As Func(Of DiagnosticAnalyzer, Boolean), getDocuments As Func(Of Project, DocumentId, IReadOnlyList(Of DocumentId)), includeSuppressedDiagnostics As Boolean, includeLocalDocumentDiagnostics As Boolean, includeNonLocalDocumentDiagnostics As Boolean, cancellationToken As CancellationToken) As Task(Of ImmutableArray(Of DiagnosticData)) Implements IDiagnosticAnalyzerService.GetDiagnosticsForIdsAsync Return SpecializedTasks.EmptyImmutableArray(Of DiagnosticData)() End Function diff --git a/src/VisualStudio/Core/Test/Microsoft.VisualStudio.LanguageServices.UnitTests.vbproj b/src/VisualStudio/Core/Test/Microsoft.VisualStudio.LanguageServices.UnitTests.vbproj index 7e8b130d74ac7..15fc95f510bd7 100644 --- a/src/VisualStudio/Core/Test/Microsoft.VisualStudio.LanguageServices.UnitTests.vbproj +++ b/src/VisualStudio/Core/Test/Microsoft.VisualStudio.LanguageServices.UnitTests.vbproj @@ -62,4 +62,8 @@ + + + + \ No newline at end of file diff --git a/src/VisualStudio/Core/Test/ObjectBrowser/CSharp/ObjectBrowerTests.vb b/src/VisualStudio/Core/Test/ObjectBrowser/CSharp/ObjectBrowerTests.vb index 3e1e88ab21818..3decfbbb50f12 100644 --- a/src/VisualStudio/Core/Test/ObjectBrowser/CSharp/ObjectBrowerTests.vb +++ b/src/VisualStudio/Core/Test/ObjectBrowser/CSharp/ObjectBrowerTests.vb @@ -149,8 +149,7 @@ namespace N End Using End Sub - - + Public Sub TestContent_InheritedMembers1() Dim code = @@ -202,8 +201,7 @@ class C : B End Using End Sub - - + Public Sub TestContent_InheritedMembers2() Dim code = @@ -256,8 +254,7 @@ class C : B End Using End Sub - - + Public Sub TestContent_InheritedMembers3() Dim code = @@ -310,8 +307,7 @@ class C : B End Using End Sub - - + Public Sub TestContent_HelpKeyword_Ctor() Dim code = @@ -503,8 +499,7 @@ $" {String.Format(ServicesVSResources.Member_of_0, "C")}") End Using End Sub - - + Public Sub TestDescription_MethodInInterface() Dim code = @@ -1472,8 +1467,7 @@ $" {String.Format(ServicesVSResources.Member_of_0, "C")}") End Using End Sub - - + Public Sub TestNavInfo_Class() Dim code = @@ -1498,8 +1492,7 @@ namespace EditorFunctionalityHelper End Using End Sub - - + Public Sub TestNavInfo_NestedEnum() Dim code = @@ -1531,8 +1524,7 @@ namespace EditorFunctionalityHelper End Using End Sub - - + Public Sub TestCheckedBinaryOperator() Dim code = @@ -1558,8 +1550,7 @@ class C End Using End Sub - - + Public Sub TestCheckedUnaryOperator() Dim code = @@ -1585,8 +1576,7 @@ class C End Using End Sub - - + Public Sub TestCheckedCastOperator() Dim code = diff --git a/src/VisualStudio/Core/Test/ObjectBrowser/VisualBasic/ObjectBrowerTests.vb b/src/VisualStudio/Core/Test/ObjectBrowser/VisualBasic/ObjectBrowerTests.vb index c32984afce2c0..033fe1aec30a8 100644 --- a/src/VisualStudio/Core/Test/ObjectBrowser/VisualBasic/ObjectBrowerTests.vb +++ b/src/VisualStudio/Core/Test/ObjectBrowser/VisualBasic/ObjectBrowerTests.vb @@ -142,8 +142,7 @@ End Namespace End Using End Sub - - + Public Sub TestContent_InheritedMembers1() Dim code = @@ -197,8 +196,7 @@ End Class End Using End Sub - - + Public Sub TestContent_InheritedMembers2() Dim code = @@ -253,8 +251,7 @@ End Class End Using End Sub - - + Public Sub TestContent_InheritedMembers3() Dim code = @@ -309,8 +306,7 @@ End Class End Using End Sub - - + Public Sub TestContent_HelpKeyword_Ctor() Dim code = @@ -887,8 +883,7 @@ $" {String.Format(ServicesVSResources.Member_of_0, "N.C")}") End Using End Sub - - + Public Sub TestDescription_SubInInterface() Dim code = @@ -2286,8 +2281,7 @@ ServicesVSResources.Value_colon & vbCrLf & End Using End Sub - - + Public Sub TestNavInfo_Class() Dim code = @@ -2310,8 +2304,7 @@ End Namespace End Using End Sub - - + Public Sub TestNavInfo_NestedEnum() Dim code = diff --git a/src/VisualStudio/Core/Test/Progression/ContainsChildrenGraphQueryTests.vb b/src/VisualStudio/Core/Test/Progression/ContainsChildrenGraphQueryTests.vb index 6a0b1c7681dd2..bc9061ce5293d 100644 --- a/src/VisualStudio/Core/Test/Progression/ContainsChildrenGraphQueryTests.vb +++ b/src/VisualStudio/Core/Test/Progression/ContainsChildrenGraphQueryTests.vb @@ -72,8 +72,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Progression End Function - - + Public Async Function ContainsChildrenForFileWithIllegalPath() As Task Using testState = ProgressionTestState.Create() Dim graph = New Graph @@ -88,8 +87,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Progression End Function - - + Public Async Function ContainsChildrenForNotYetLoadedSolution() As Task Using testState = ProgressionTestState.Create( @@ -127,8 +125,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Progression End Using End Function - - + Public Async Function ContainsChildrenForNodeWithRelativeUriPath() As Task Using testState = ProgressionTestState.Create( diff --git a/src/VisualStudio/Core/Test/Progression/InheritsFromGraphQueryTests.vb b/src/VisualStudio/Core/Test/Progression/InheritsFromGraphQueryTests.vb index 33b1f45e68347..37c17c1fecc2d 100644 --- a/src/VisualStudio/Core/Test/Progression/InheritsFromGraphQueryTests.vb +++ b/src/VisualStudio/Core/Test/Progression/InheritsFromGraphQueryTests.vb @@ -43,8 +43,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Progression End Using End Function - - + Public Async Function TestErrorBaseType() As Task Using testState = ProgressionTestState.Create( diff --git a/src/VisualStudio/Core/Test/ReferenceManager/VisualStudioMetadataReferenceManagerTests.vb b/src/VisualStudio/Core/Test/ReferenceManager/VisualStudioMetadataReferenceManagerTests.vb new file mode 100644 index 0000000000000..58b838656219b --- /dev/null +++ b/src/VisualStudio/Core/Test/ReferenceManager/VisualStudioMetadataReferenceManagerTests.vb @@ -0,0 +1,86 @@ +' 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. + +Imports System.IO +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Host +Imports Microsoft.CodeAnalysis.Serialization +Imports Microsoft.CodeAnalysis.Test.Utilities +Imports Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem +Imports Roslyn.Utilities + +Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ReferenceManager + + Public Class VisualStudioMetadataReferenceManagerTests + + Public Sub TestReferenceAssemblyWithMultipleModules() + Using workspace = EditorTestWorkspace.CreateCSharp("") + Dim assemblyDir = Path.GetDirectoryName(GetType(Object).Assembly.Location) + Dim enterprisePath = Path.Combine(assemblyDir, "System.EnterpriseServices.dll") + + Dim tempStorageService = DirectCast(workspace.Services.GetRequiredService(Of ITemporaryStorageServiceInternal), TemporaryStorageService) + Dim serializerService = DirectCast(workspace.Services.GetRequiredService(Of ISerializerService), SerializerService) + + Dim tuple = VisualStudioMetadataReferenceManager.TestAccessor.CreateAssemblyMetadata( + enterprisePath, tempStorageService) + Assert.NotNull(tuple.assemblyMetadata) + Assert.NotNull(tuple.handles) + + ' We should have two handles as this assembly has two modules (itself, and one submodule for + ' System.EnterpriseServices.Wrapper.dll) + Assert.Equal(2, tuple.handles.Count) + + Dim testReference = New TestPEReference( + enterprisePath, tuple.assemblyMetadata, tuple.handles) + + Dim stream = New MemoryStream() + Dim writer = New ObjectWriter(stream, leaveOpen:=True) + serializerService.Serialize(testReference, writer, cancellationToken:=Nothing) + + stream.Position = 0 + Dim reader = ObjectReader.GetReader(stream, leaveOpen:=True) + Dim deserialized = DirectCast(serializerService.Deserialize( + WellKnownSynchronizationKind.MetadataReference, reader, cancellationToken:=Nothing), MetadataReference) + + Dim checksum1 = SerializerService.CreateChecksum(testReference, cancellationToken:=Nothing) + Dim checksum2 = SerializerService.CreateChecksum(deserialized, cancellationToken:=Nothing) + + ' Serializing the original reference and the deserialized reference should produce the same checksum + Assert.Equal(checksum1, checksum2) + End Using + End Sub + + Private Class TestPEReference + Inherits PortableExecutableReference + Implements ISupportTemporaryStorage + + Private ReadOnly _metadata As Microsoft.CodeAnalysis.Metadata + Private ReadOnly _storageHandles As IReadOnlyList(Of ITemporaryStorageStreamHandle) + + Public Sub New(fullPath As String, metadata As Microsoft.CodeAnalysis.Metadata, storageHandles As IReadOnlyList(Of ITemporaryStorageStreamHandle)) + MyBase.New(New MetadataReferenceProperties(), fullPath) + _metadata = metadata + _storageHandles = storageHandles + End Sub + + Public ReadOnly Property StorageHandles As IReadOnlyList(Of ITemporaryStorageStreamHandle) Implements ISupportTemporaryStorage.StorageHandles + Get + Return _storageHandles + End Get + End Property + + Protected Overrides Function CreateDocumentationProvider() As DocumentationProvider + Throw New NotImplementedException() + End Function + + Protected Overrides Function WithPropertiesImpl(properties As MetadataReferenceProperties) As PortableExecutableReference + Throw New NotImplementedException() + End Function + + Protected Overrides Function GetMetadataImpl() As Microsoft.CodeAnalysis.Metadata + Return _metadata + End Function + End Class + End Class +End Namespace diff --git a/src/VisualStudio/Core/Test/Snippets/CSharpSnippetExpansionClientTests.vb b/src/VisualStudio/Core/Test/Snippets/CSharpSnippetExpansionClientTests.vb index 17ff5c571085e..40116e888fd13 100644 --- a/src/VisualStudio/Core/Test/Snippets/CSharpSnippetExpansionClientTests.vb +++ b/src/VisualStudio/Core/Test/Snippets/CSharpSnippetExpansionClientTests.vb @@ -67,8 +67,7 @@ using G.H.I; Await TestSnippetAddImportsAsync(originalCode, namespacesToAdd, placeSystemNamespaceFirst:=True, expectedUpdatedCode:=expectedUpdatedCode) End Function - - + Public Async Function TestAddImport_InsideNamespace() As Task Dim originalCode = " using A; diff --git a/src/VisualStudio/Core/Test/Snippets/SnippetCompletionProviderTests.vb b/src/VisualStudio/Core/Test/Snippets/SnippetCompletionProviderTests.vb index 9f54e4b6ded16..403f1480ad5a8 100644 --- a/src/VisualStudio/Core/Test/Snippets/SnippetCompletionProviderTests.vb +++ b/src/VisualStudio/Core/Test/Snippets/SnippetCompletionProviderTests.vb @@ -72,8 +72,7 @@ End Class.Value End Using End Function - - + Public Async Function SnippetNotOfferedInComments() As Task Dim markup = Class C @@ -89,8 +88,7 @@ End Class.Value End Using End Function - - + Public Async Function SnippetsNotOfferedInDocComments() As Task Dim markup = Class C diff --git a/src/VisualStudio/Core/Test/UnifiedSettings/VisualBasicUnifiedSettingsTests.vb b/src/VisualStudio/Core/Test/UnifiedSettings/VisualBasicUnifiedSettingsTests.vb new file mode 100644 index 0000000000000..7f42c3704baf2 --- /dev/null +++ b/src/VisualStudio/Core/Test/UnifiedSettings/VisualBasicUnifiedSettingsTests.vb @@ -0,0 +1,97 @@ +' 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. + +Imports System.Collections.Immutable +Imports System.IO +Imports System.Reflection +Imports Microsoft.CodeAnalysis +Imports Microsoft.CodeAnalysis.Completion +Imports Microsoft.CodeAnalysis.Options +Imports Microsoft.VisualStudio.LanguageServices +Imports Microsoft.VisualStudio.LanguageServices.UnitTests.UnifiedSettings +Imports Newtonsoft.Json.Linq + +Namespace Roslyn.VisualStudio.VisualBasic.UnitTests.UnifiedSettings + Public Class VisualBasicUnifiedSettingsTests + Inherits UnifiedSettingsTests + + Friend Overrides ReadOnly Property OnboardedOptions As ImmutableArray(Of IOption2) + Get + Return ImmutableArray.Create(Of IOption2)( + CompletionOptionsStorage.TriggerOnTypingLetters, + CompletionOptionsStorage.TriggerOnDeletion, + CompletionViewOptionsStorage.HighlightMatchingPortionsOfCompletionListItems, + CompletionViewOptionsStorage.ShowCompletionItemFilters, + CompletionOptionsStorage.SnippetsBehavior, + CompletionOptionsStorage.EnterKeyBehavior, + CompletionOptionsStorage.ShowItemsFromUnimportedNamespaces, + CompletionViewOptionsStorage.EnableArgumentCompletionSnippets + ) + End Get + End Property + + Friend Overrides Function GetEnumOptionValues([option] As IOption2) As Object() + Dim allValues = [Enum].GetValues([option].Type).Cast(Of Object) + If [option].Equals(CompletionOptionsStorage.SnippetsBehavior) Then + 'SnippetsRule.Default is used as a stub value, overridden per language at runtime. + ' It is not shown in the option page + Return allValues.Where(Function(value) Not value.Equals(SnippetsRule.Default)).ToArray() + ElseIf [option].Equals(CompletionOptionsStorage.EnterKeyBehavior) Then + ' EnterKeyRule.Default is used as a stub value, overridden per language at runtime. + ' It Is Not shown in the option page + Return allValues.Where(Function(value) Not value.Equals(EnterKeyRule.Default)).ToArray() + End If + + Return MyBase.GetEnumOptionValues([option]) + End Function + + Friend Overrides Function GetOptionsDefaultValue([option] As IOption2) As Object + ' The default values of some options are set at runtime. option.defaultValue is just a dummy value in this case. + ' However, in unified settings we always set the correct value in registration.json. + If [option].Equals(CompletionOptionsStorage.SnippetsBehavior) Then + ' CompletionOptionsStorage.SnippetsBehavior's default value is SnippetsRule.Default. + ' It's overridden differently per-language at runtime. + Return SnippetsRule.IncludeAfterTypingIdentifierQuestionTab + ElseIf [option].Equals(CompletionOptionsStorage.EnterKeyBehavior) Then + ' CompletionOptionsStorage.EnterKeyBehavior's default value is EnterKeyBehavior.Default. + ' It's overridden differently per-language at runtime. + Return EnterKeyRule.Always + ElseIf [option].Equals(CompletionOptionsStorage.TriggerOnDeletion) Then + ' CompletionOptionsStorage.TriggerOnDeletion's default value is null. + ' It's enabled by default for Visual Basic + Return True + ElseIf [option].Equals(CompletionOptionsStorage.ShowItemsFromUnimportedNamespaces) Then + ' CompletionOptionsStorage.ShowItemsFromUnimportedNamespaces's default value is null + ' It's enabled by default for Visual Basic + Return True + ElseIf [option].Equals(CompletionViewOptionsStorage.EnableArgumentCompletionSnippets) Then + ' CompletionViewOptionsStorage.EnableArgumentCompletionSnippets' default value is null + ' It's disabled by default for Visual Basic + Return False + End If + + Return MyBase.GetOptionsDefaultValue([option]) + End Function + + + Public Async Function IntelliSensePageTests() As Task + Using registrationFileStream = GetType(VisualBasicUnifiedSettingsTests).GetTypeInfo().Assembly.GetManifestResourceStream("visualBasicSettings.registration.json") + Using pkgDefFileStream = GetType(VisualBasicUnifiedSettingsTests).GetTypeInfo().Assembly.GetManifestResourceStream("PackageRegistration.pkgdef") + Using pkgDefFileReader = New StreamReader(pkgDefFileStream) + Using reader = New StreamReader(registrationFileStream) + Dim registrationFile = Await reader.ReadToEndAsync().ConfigureAwait(False) + Dim pkgDefFile = Await pkgDefFileReader.ReadToEndAsync().ConfigureAwait(False) + Dim registrationJsonObject = JObject.Parse(registrationFile, New JsonLoadSettings()) + Dim categoriesTitle = registrationJsonObject.SelectToken("$.categories['textEditor.basic'].title") + Assert.Equal("Visual Basic", categoriesTitle) + Dim optionPageId = registrationJsonObject.SelectToken("$.categories['textEditor.basic.intellisense'].legacyOptionPageId") + Assert.Equal(Guids.VisualBasicOptionPageIntelliSenseIdString, optionPageId.ToString()) + TestUnifiedSettingsCategory(registrationJsonObject, categoryBasePath:="textEditor.basic.intellisense", languageName:=LanguageNames.VisualBasic, pkgDefFile) + End Using + End Using + End Using + End Using + End Function + End Class +End Namespace diff --git a/src/VisualStudio/Core/Test/Venus/DocumentService_IntegrationTests.vb b/src/VisualStudio/Core/Test/Venus/DocumentService_IntegrationTests.vb index 0f6d62a5a75da..25ca0b50529f7 100644 --- a/src/VisualStudio/Core/Test/Venus/DocumentService_IntegrationTests.vb +++ b/src/VisualStudio/Core/Test/Venus/DocumentService_IntegrationTests.vb @@ -238,9 +238,9 @@ class { } diagnosticService.CreateIncrementalAnalyzer(workspace) ' confirm that IDE doesn't report the diagnostics - Dim diagnostics = Await diagnosticService.GetDiagnosticsAsync(workspace.CurrentSolution, projectId:=Nothing, documentId:=document.Id, - includeSuppressedDiagnostics:=False, includeNonLocalDocumentDiagnostics:=True, - CancellationToken.None) + Dim diagnostics = Await diagnosticService.GetDiagnosticsForIdsAsync( + workspace.CurrentSolution, projectId:=Nothing, documentId:=document.Id, diagnosticIds:=Nothing, shouldIncludeAnalyzer:=Nothing, + includeSuppressedDiagnostics:=False, includeLocalDocumentDiagnostics:=True, includeNonLocalDocumentDiagnostics:=True, CancellationToken.None) Assert.False(diagnostics.Any()) End Using End Function diff --git a/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/EditorInProcess.cs b/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/EditorInProcess.cs index 8af34dd060cae..d3da6ef64f484 100644 --- a/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/EditorInProcess.cs +++ b/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/EditorInProcess.cs @@ -309,7 +309,7 @@ public async Task GetLightBulbPreviewClassificationsAsync( activeSession.Collapse(); var classifier = classifierAggregatorService.GetClassifier(preview); var classifiedSpans = classifier.GetClassificationSpans(new SnapshotSpan(preview.TextBuffer.CurrentSnapshot, 0, preview.TextBuffer.CurrentSnapshot.Length)); - return classifiedSpans.ToArray(); + return [.. classifiedSpans]; } activeSession.Collapse(); diff --git a/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/InteractiveWindowInProcess.cs b/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/InteractiveWindowInProcess.cs index bf450cd2d9fb9..dd48c32854087 100644 --- a/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/InteractiveWindowInProcess.cs +++ b/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/InteractiveWindowInProcess.cs @@ -259,7 +259,7 @@ public async Task VerifyTagsAsync(int expectedCount, CancellationToken can var view = await GetActiveTextViewAsync(cancellationToken); - bool filterTag(IMappingTagSpan tag) + static bool filterTag(IMappingTagSpan tag) { return tag.Tag.GetType().Equals(typeof(TTag)); } diff --git a/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/ScreenshotInProcess.cs b/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/ScreenshotInProcess.cs index 517c21d606ad1..c9ca70e510dc0 100644 --- a/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/ScreenshotInProcess.cs +++ b/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/ScreenshotInProcess.cs @@ -157,7 +157,7 @@ static ScreenshotInProcess() return; } - frames = s_frames.ToArray(); + frames = [.. s_frames]; } // Make sure the frames are processed in order of their timestamps @@ -303,7 +303,7 @@ private static (TimeSpan elapsed, BitmapSource image, Size offset)[] DetectChang Marshal.FreeHGlobal(imageBuffer); } - return resultFrames.ToArray(); + return [.. resultFrames]; } private static void WritePngSignature(Stream stream, byte[] buffer) diff --git a/src/VisualStudio/IntegrationTest/New.IntegrationTests/InfrastructureTests.cs b/src/VisualStudio/IntegrationTest/New.IntegrationTests/InfrastructureTests.cs index c11174300d08a..f520004406aed 100644 --- a/src/VisualStudio/IntegrationTest/New.IntegrationTests/InfrastructureTests.cs +++ b/src/VisualStudio/IntegrationTest/New.IntegrationTests/InfrastructureTests.cs @@ -22,7 +22,7 @@ public InfrastructureTests() protected override string LanguageName => LanguageNames.CSharp; - [IdeFact] + [IdeFact(Skip = "https://github.com/dotnet/roslyn/issues/73099")] public async Task CanCloseSaveDialog() { await SetUpEditorAsync( diff --git a/src/VisualStudio/LiveShare/Impl/Client/LanguageServiceUtils.cs b/src/VisualStudio/LiveShare/Impl/Client/LanguageServiceUtils.cs index e922257ddd994..38c1fb7179dc0 100644 --- a/src/VisualStudio/LiveShare/Impl/Client/LanguageServiceUtils.cs +++ b/src/VisualStudio/LiveShare/Impl/Client/LanguageServiceUtils.cs @@ -23,7 +23,7 @@ public static string GetLanguageServerProviderServiceName(string[] contentTypes) public static string GetLanguageServerProviderServiceName(string lspServiceName) => LanguageServerProviderServiceName + "-" + lspServiceName; - public static string GetContentTypesName(string[] contentTypes) => string.Join("-", contentTypes.OrderBy(c => c).ToArray()); + public static string GetContentTypesName(string[] contentTypes) => string.Join("-", [.. contentTypes.OrderBy(c => c)]); public static bool IsContentTypeRemote(string contentType) => contentType.EndsWith("-remote"); diff --git a/src/VisualStudio/Setup/Roslyn.VisualStudio.Setup.csproj b/src/VisualStudio/Setup/Roslyn.VisualStudio.Setup.csproj index 5d2ff4a4c435a..d739438f4952f 100644 --- a/src/VisualStudio/Setup/Roslyn.VisualStudio.Setup.csproj +++ b/src/VisualStudio/Setup/Roslyn.VisualStudio.Setup.csproj @@ -135,12 +135,6 @@ true BindingRedirect - - Microsoft.CommonLanguageServerProtocolFramework - BuiltProjectOutputGroup;SatelliteDllsProjectOutputGroup - true - BindingRedirect - LiveShareLanguageServices BuiltProjectOutputGroup;SatelliteDllsProjectOutputGroup diff --git a/src/VisualStudio/TestUtilities2/ProjectSystemShim/Framework/TestEnvironment.vb b/src/VisualStudio/TestUtilities2/ProjectSystemShim/Framework/TestEnvironment.vb index 8a25462f33b34..c0261ecc1802c 100644 --- a/src/VisualStudio/TestUtilities2/ProjectSystemShim/Framework/TestEnvironment.vb +++ b/src/VisualStudio/TestUtilities2/ProjectSystemShim/Framework/TestEnvironment.vb @@ -62,7 +62,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.ProjectSystemShim.Fr GetType(ProjectCodeModelFactory), GetType(CPSProjectFactory), GetType(VisualStudioRuleSetManagerFactory), - GetType(VsMetadataServiceFactory), + GetType(VisualStudioMetadataServiceFactory), GetType(VisualStudioMetadataReferenceManagerFactory), GetType(MockWorkspaceEventListenerProvider), GetType(HierarchyItemToProjectIdMap), diff --git a/src/VisualStudio/TestUtilities2/UnifiedSettings/UnifiedSettingsTests.vb b/src/VisualStudio/TestUtilities2/UnifiedSettings/UnifiedSettingsTests.vb index 48abfad77a558..ecf8ba700f9bf 100644 --- a/src/VisualStudio/TestUtilities2/UnifiedSettings/UnifiedSettingsTests.vb +++ b/src/VisualStudio/TestUtilities2/UnifiedSettings/UnifiedSettingsTests.vb @@ -3,6 +3,9 @@ ' See the LICENSE file in the project root for more information. Imports System.Collections.Immutable +Imports System.IO.Hashing +Imports System.Text +Imports System.Text.RegularExpressions Imports Microsoft.CodeAnalysis Imports Microsoft.CodeAnalysis.Options Imports Microsoft.VisualStudio.LanguageServices.Options @@ -28,7 +31,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.UnifiedSettings Return [Enum].GetValues(type).Cast(Of Object).AsArray() End Function - Protected Sub TestUnifiedSettingsCategory(registrationJsonObject As JObject, categoryBasePath As String, languageName As String) + Protected Sub TestUnifiedSettingsCategory(registrationJsonObject As JObject, categoryBasePath As String, languageName As String, pkdDefFile As String) Dim actualAllSettings = registrationJsonObject.SelectToken($"$.properties").Children.OfType(Of JProperty). Where(Function(setting) setting.Name.StartsWith(categoryBasePath)). Select(Function(setting) setting.Name). @@ -62,6 +65,17 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.UnifiedSettings Throw ExceptionUtilities.UnexpectedValue(optionName) End If Next + + Dim registrationFileBytes = ASCIIEncoding.ASCII.GetBytes(registrationJsonObject.ToString()) + Dim hash = XxHash128.Hash(registrationFileBytes) + Dim tagBytes = hash.Take(8).ToArray() + Dim expectedCacheTagValue = BitConverter.ToInt64(tagBytes, 0).ToString("X16") + + Dim regexExp = New Regex("""CacheTag""=qword:\w{16}") + Dim match = regexExp.Match(pkdDefFile, 0).Value + Dim actual = match.Substring(match.Length - 16) + ' Please change the CacheTag value in pkddef if you modify the unified settings regirstration file + Assert.Equal(expectedCacheTagValue, actual) End Sub Private Shared Sub VerifySettings(registrationJsonObject As JObject, unifiedSettingPath As String, [option] As IOption2, languageName As String) diff --git a/src/VisualStudio/VisualBasic/Impl/BasicVSResources.resx b/src/VisualStudio/VisualBasic/Impl/BasicVSResources.resx index feb1994aec76a..b0aabb98df28e 100644 --- a/src/VisualStudio/VisualBasic/Impl/BasicVSResources.resx +++ b/src/VisualStudio/VisualBasic/Impl/BasicVSResources.resx @@ -244,15 +244,6 @@ _Never add new line on enter - - Always include snippets - - - Include snippets when ?-Tab is typed after an identifier - - - Never include snippets - Snippets behavior diff --git a/src/VisualStudio/VisualBasic/Impl/LanguageService/VisualBasicPackage.vb b/src/VisualStudio/VisualBasic/Impl/LanguageService/VisualBasicPackage.vb index 046f984628ff0..f03d6a481434d 100644 --- a/src/VisualStudio/VisualBasic/Impl/LanguageService/VisualBasicPackage.vb +++ b/src/VisualStudio/VisualBasic/Impl/LanguageService/VisualBasicPackage.vb @@ -38,6 +38,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic + Friend NotInheritable Class VisualBasicPackage Inherits AbstractPackage(Of VisualBasicPackage, VisualBasicLanguageService) diff --git a/src/VisualStudio/VisualBasic/Impl/Microsoft.VisualStudio.LanguageServices.VisualBasic.vbproj b/src/VisualStudio/VisualBasic/Impl/Microsoft.VisualStudio.LanguageServices.VisualBasic.vbproj index b9cc5973ed78e..b94764b606b87 100644 --- a/src/VisualStudio/VisualBasic/Impl/Microsoft.VisualStudio.LanguageServices.VisualBasic.vbproj +++ b/src/VisualStudio/VisualBasic/Impl/Microsoft.VisualStudio.LanguageServices.VisualBasic.vbproj @@ -14,6 +14,12 @@ true false + + + PreserveNewest + true + + @@ -44,12 +50,12 @@ - + - + true VSPackage Designer diff --git a/src/VisualStudio/VisualBasic/Impl/Options/IntelliSenseOptionPageControl.xaml.vb b/src/VisualStudio/VisualBasic/Impl/Options/IntelliSenseOptionPageControl.xaml.vb index 26f9bfa584311..7a28520e808f1 100644 --- a/src/VisualStudio/VisualBasic/Impl/Options/IntelliSenseOptionPageControl.xaml.vb +++ b/src/VisualStudio/VisualBasic/Impl/Options/IntelliSenseOptionPageControl.xaml.vb @@ -42,8 +42,8 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.Options End Sub Private Sub SetEnterKeyDefaultBehavior() - Dim snippetValue = Me.OptionStore.GetOption(CompletionOptionsStorage.EnterKeyBehavior, LanguageNames.VisualBasic) - If snippetValue = SnippetsRule.Default Then + Dim enterKeyRule = Me.OptionStore.GetOption(CompletionOptionsStorage.EnterKeyBehavior, LanguageNames.VisualBasic) + If enterKeyRule = EnterKeyRule.Default Then Always_add_new_line_on_enter.IsChecked = True End If End Sub diff --git a/src/VisualStudio/VisualBasic/Impl/Options/IntelliSenseOptionPageStrings.vb b/src/VisualStudio/VisualBasic/Impl/Options/IntelliSenseOptionPageStrings.vb index e3b00fcebfd41..39b3bf040f610 100644 --- a/src/VisualStudio/VisualBasic/Impl/Options/IntelliSenseOptionPageStrings.vb +++ b/src/VisualStudio/VisualBasic/Impl/Options/IntelliSenseOptionPageStrings.vb @@ -35,13 +35,13 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic.Options BasicVSResources.Snippets_behavior Public ReadOnly Property Option_Never_include_snippets As String = - BasicVSResources.Never_include_snippets + VSPackage.Never_include_snippets Public ReadOnly Property Option_Always_include_snippets As String = - BasicVSResources.Always_include_snippets + VSPackage.Always_include_snippets Public ReadOnly Property Option_Include_snippets_when_question_Tab_is_typed_after_an_identifier As String = - BasicVSResources.Include_snippets_when_Tab_is_typed_after_an_identifier + VSPackage.Include_snippets_when_Tab_is_typed_after_an_identifier Public ReadOnly Property Option_Show_items_from_unimported_namespaces As String = BasicVSResources.Show_items_from_unimported_namespaces diff --git a/src/VisualStudio/VisualBasic/Impl/PackageRegistration.pkgdef b/src/VisualStudio/VisualBasic/Impl/PackageRegistration.pkgdef index bfb3a04c6856c..088ecdbfeb727 100644 --- a/src/VisualStudio/VisualBasic/Impl/PackageRegistration.pkgdef +++ b/src/VisualStudio/VisualBasic/Impl/PackageRegistration.pkgdef @@ -188,4 +188,11 @@ "Indent Style"=dword:00000002 [$RootKey$\VB Editor\Roslyn] -"DisplayLineSeparators"=dword:00000001 \ No newline at end of file +"DisplayLineSeparators"=dword:00000001 + +// CacheTag value should be changed when registration file changes +// See https://devdiv.visualstudio.com/DevDiv/_wiki/wikis/DevDiv.wiki/39345/Manifest-Build-Deployment-and-Setup-Authoring-In-Depth?anchor=example-pkgdef-key for more infomation +[$RootKey$\SettingsManifests\{574fc912-f74f-4b4e-92c3-f695c208a2bb}] +@="Microsoft.VisualStudio.LanguageServices.VisualBasic.VisualBasicPackage" +"ManifestPath"="$PackageFolder$\UnifiedSettings\visualBasicSettings.registration.json" +"CacheTag"=qword:5DE8496A8900B809 diff --git a/src/VisualStudio/VisualBasic/Impl/UnifiedSettings/visualBasicSettings.registration.json b/src/VisualStudio/VisualBasic/Impl/UnifiedSettings/visualBasicSettings.registration.json new file mode 100644 index 0000000000000..c744cc9dc0357 --- /dev/null +++ b/src/VisualStudio/VisualBasic/Impl/UnifiedSettings/visualBasicSettings.registration.json @@ -0,0 +1,185 @@ +// NOTE: +// When this file is changed. Please also update the cache tag under settings entry in src/VisualStudio/VisualBasic/Impl/PackageRegistration.pkgdef +// Otherwise your change might be ignored. +// See https://devdiv.visualstudio.com/DevDiv/_wiki/wikis/DevDiv.wiki/39345/Manifest-Build-Deployment-and-Setup-Authoring-In-Depth?anchor=example-pkgdef-key for more details +{ + "properties": { + // CompletionOptionsStorage.TriggerOnTypingLetters + "textEditor.basic.intellisense.triggerCompletionOnTypingLetters": { + "title": "@Show_completion_list_after_a_character_is_typed;..\\Microsoft.VisualStudio.LanguageServices.VisualBasic.dll", + "type": "boolean", + "default": true, + "order": 0, + "migration": { + "pass": { + "input": { + "store": "SettingsManager", + "path": "TextEditor.VisualBasic.Specific.TriggerOnTypingLetters" + } + } + } + }, + // CompletionOptionsStorage.TriggerOnDeletion + "textEditor.basic.intellisense.triggerCompletionOnDeletion": { + "title": "@Show_completion_list_after_a_character_is_deleted;..\\Microsoft.VisualStudio.LanguageServices.VisualBasic.dll", + "type": "boolean", + "default": true, + "order": 1, + "migration": { + "pass": { + "input": { + "store": "SettingsManager", + "path": "TextEditor.VisualBasic.Specific.TriggerOnDeletion" + } + } + } + }, + // CompletionViewOptionsStorage.HighlightMatchingPortionsOfCompletionListItems + "textEditor.basic.intellisense.highlightMatchingPortionsOfCompletionListItems": { + "title": "@Highlight_matching_portions_of_completion_list_items;..\\Microsoft.VisualStudio.LanguageServices.VisualBasic.dll", + "type": "boolean", + "default": true, + "order": 10, + "migration": { + "pass": { + "input": { + "store": "SettingsManager", + "path": "TextEditor.VisualBasic.Specific.HighlightMatchingPortionsOfCompletionListItems" + } + } + } + }, + // CompletionViewOptionsStorage.ShowCompletionItemFilters + "textEditor.basic.intellisense.showCompletionItemFilters": { + "title": "@Show_completion_item_filters;..\\Microsoft.VisualStudio.LanguageServices.VisualBasic.dll", + "type": "boolean", + "default": true, + "order": 20, + "migration": { + "pass": { + "input": { + "store": "SettingsManager", + "path": "TextEditor.VisualBasic.Specific.ShowCompletionItemFilters" + } + } + } + }, + // CompletionOptionsStorage.SnippetsBehavior + "textEditor.basic.intellisense.snippetsBehavior": { + "title": "@Snippets_behavior;..\\Microsoft.VisualStudio.LanguageServices.VisualBasic.dll", + "type": "string", + "enum": [ "neverInclude", "alwaysInclude", "includeAfterTypingIdentifierQuestionTab" ], + "enumItemLabels": [ "@Never_include_snippets;{574fc912-f74f-4b4e-92c3-f695c208a2bb}", "@Always_include_snippets;{574fc912-f74f-4b4e-92c3-f695c208a2bb}", "@Include_snippets_when_Tab_is_typed_after_an_identifier;{574fc912-f74f-4b4e-92c3-f695c208a2bb}" ], + "default": "includeAfterTypingIdentifierQuestionTab", + "order": 30, + "migration": { + "enumIntegerToString": { + "input": { + "store": "SettingsManager", + "path": "TextEditor.VisualBasic.Specific.SnippetsBehavior" + }, + "map": [ + { + "result": "neverInclude", + "match": 1 + }, + // '0' matches to SnippetsRule.Default. Means the behavior is decided by language. + // '3' matches to SnippetsRule.IncludeAfterTypingIdentifierQuestionTab. It's the default behavior for Visual Basic + // Put both mapping here, so it's possible for unified setting to load '0' from the storage. + // Put '3' in front, so unifed settings would persist '3' to storage when 'includeAfterTypingIdentifierQuestionTab' is selected. + { + "result": "alwaysInclude", + "match": 2 + }, + { + "result": "includeAfterTypingIdentifierQuestionTab", + "match": 3 + }, + { + "result": "includeAfterTypingIdentifierQuestionTab", + "match": 0 + } + ] + } + } + }, + // CompletionOptionsStorage.EnterKeyBehavior + "textEditor.basic.intellisense.returnKeyCompletionBehavior": { + "title": "@Enter_key_behavior_colon;..\\Microsoft.VisualStudio.LanguageServices.VisualBasic.dll", + "type": "string", + "enum": [ "never", "afterFullyTypedWord", "always" ], + "enumItemLabels": [ "@Never_add_new_line_on_enter;{574fc912-f74f-4b4e-92c3-f695c208a2bb}", "@Only_add_new_line_on_enter_after_end_of_fully_typed_word;{574fc912-f74f-4b4e-92c3-f695c208a2bb}", "@Always_add_new_line_on_enter;{574fc912-f74f-4b4e-92c3-f695c208a2bb}" ], + "default": "always", + "order": 40, + "migration": { + "enumIntegerToString": { + "input": { + "store": "SettingsManager", + "path": "TextEditor.VisualBasic.Specific.EnterKeyBehavior" + }, + "map": [ + // '0' matches to EnterKeyRule.Default. Means the behavior is decided by langauge. + // '2' matches to EnterKeyRule.Alwasys. It's the default behavior for Visual Basic + // Put both mapping here, so it's possible for unified setting to load '0' from the storage. + // Put '2' in front, so unifed settings would persist '2' to storage when 'always' is selected. + { + "result": "never", + "match": 1 + }, + { + "result": "always", + "match": 2 + }, + { + "result": "always", + "match": 0 + }, + { + "result": "afterFullyTypedWord", + "match": 3 + } + ] + } + } + }, + // CompletionOptionsStorage.ShowItemsFromUnimportedNamespaces + "textEditor.basic.intellisense.showCompletionItemsFromUnimportedNamespaces": { + "title": "@Show_items_from_unimported_namespaces;..\\Microsoft.VisualStudio.LanguageServices.VisualBasic.dll", + "type": "boolean", + "default": true, + "order": 50, + "migration": { + "pass": { + "input": { + "store": "SettingsManager", + "path": "TextEditor.VisualBasic.Specific.ShowItemsFromUnimportedNamespaces" + } + } + } + }, + // CompletionViewOptionsStorage.EnableArgumentCompletionSnippets + "textEditor.basic.intellisense.enableArgumentCompletionSnippets": { + "title": "@Tab_twice_to_insert_arguments;..\\Microsoft.VisualStudio.LanguageServices.dll", + "type": "boolean", + "default": false, + "order": 60, + "migration": { + "pass": { + "input": { + "store": "SettingsManager", + "path": "TextEditor.VisualBasic.Specific.EnableArgumentCompletionSnippets" + } + } + } + } + }, + "categories": { + "textEditor.basic":{ + "title": "Visual Basic" + }, + "textEditor.basic.intellisense": { + "title": "@112;{574fc912-f74f-4b4e-92c3-f695c208a2bb}", + "legacyOptionPageId": "04460A3B-1B5F-4402-BC6D-89A4F6F0A8D7" + } + } +} diff --git a/src/VisualStudio/VisualBasic/Impl/VSPackage.resx b/src/VisualStudio/VisualBasic/Impl/VSPackage.resx index a77f0471bc1f1..00e7dc9bbac3c 100644 --- a/src/VisualStudio/VisualBasic/Impl/VSPackage.resx +++ b/src/VisualStudio/VisualBasic/Impl/VSPackage.resx @@ -208,4 +208,22 @@ Use enhanced colors;Editor Color Scheme;Inheritance Margin;Import Directives;Visual Basic Tools Help > About + + Always add new line on enter + + + Always include snippets + + + Include snippets when ?-Tab is typed after an identifier + + + Never add new line on enter + + + Never include snippets + + + Only add new line on enter after end of fully typed word + \ No newline at end of file diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.cs.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.cs.xlf index 84a1def04bec8..a095b351cce37 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.cs.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.cs.xlf @@ -272,21 +272,6 @@ _Při stisku Enter nikdy nepřidávat nový řádek - - Always include snippets - Vždy zahrnovat fragmenty - - - - Include snippets when ?-Tab is typed after an identifier - Zahrnovat fragmenty po zadání ?-Tab za identifikátor - - - - Never include snippets - Nikdy nezahrnovat fragmenty - - Snippets behavior Chování fragmentů diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.de.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.de.xlf index 25dd17f7e540f..63b9d71ab0553 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.de.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.de.xlf @@ -272,21 +272,6 @@ _Nie neue Zeile beim Drücken der EINGABETASTE einfügen - - Always include snippets - Schnipsel immer einschließen - - - - Include snippets when ?-Tab is typed after an identifier - Schnipsel einschließen, wenn ?-TAB nach einem Bezeichner eingegeben wird - - - - Never include snippets - Schnipsel nie einschließen - - Snippets behavior Schnipselverhalten diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.es.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.es.xlf index b9fac78124ec8..ecad7b68a9f27 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.es.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.es.xlf @@ -272,21 +272,6 @@ _No agregar nunca una nueva línea al presionar Entrar - - Always include snippets - Incluir siempre fragmentos de código - - - - Include snippets when ?-Tab is typed after an identifier - Incluir fragmentos de código cuando ?-Tab se escriba después de un identificador - - - - Never include snippets - No incluir nunca fragmentos de código - - Snippets behavior Comportamiento de los fragmentos de código diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.fr.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.fr.xlf index bc7b5d5411ccb..244dafeaadc9e 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.fr.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.fr.xlf @@ -272,21 +272,6 @@ _Ne jamais ajouter de nouvelle ligne après Entrée - - Always include snippets - Toujours inclure les extraits de code - - - - Include snippets when ?-Tab is typed after an identifier - Inclure les extraits de code quand ?-Tab est typé après un identificateur - - - - Never include snippets - Ne jamais inclure d'extrait de code - - Snippets behavior Comportement des extraits de code diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.it.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.it.xlf index 2875c1089924e..b8cbfe8759d4c 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.it.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.it.xlf @@ -272,21 +272,6 @@ _Non aggiungere mai una nuova riga dopo INVIO - - Always include snippets - Includi sempre i frammenti - - - - Include snippets when ?-Tab is typed after an identifier - Includi i frammenti quando si digita ?+TAB dopo un identificatore - - - - Never include snippets - Non includere mai i frammenti - - Snippets behavior Comportamento dei frammenti diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.ja.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.ja.xlf index 0d5f609496cd6..b12555f31c426 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.ja.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.ja.xlf @@ -272,21 +272,6 @@ Enter キーで新しい行を追加しない(_N) - - Always include snippets - 常にスニペットを含める - - - - Include snippets when ?-Tab is typed after an identifier - 識別子の後に ? Tab を入力したときにスニペットを含める - - - - Never include snippets - スニペットを含めない - - Snippets behavior スニペットの動作 diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.ko.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.ko.xlf index 44a0191e1b8d8..19878d608dcdf 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.ko.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.ko.xlf @@ -272,21 +272,6 @@ <Enter> 키를 누르면 새 줄 추가 안 함(_N) - - Always include snippets - 코드 조각 항상 포함 - - - - Include snippets when ?-Tab is typed after an identifier - 식별자 뒤에 ?-Tab을 입력하면 코드 조각 포함 - - - - Never include snippets - 코드 조각 포함 안 함 - - Snippets behavior 코드 조각 동작 diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.pl.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.pl.xlf index be4079612f862..821817918bd48 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.pl.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.pl.xlf @@ -272,21 +272,6 @@ _Nigdy nie dodawaj nowego wiersza po naciśnięciu klawisza Enter - - Always include snippets - Zawsze dołączaj fragmenty kodu - - - - Include snippets when ?-Tab is typed after an identifier - Dołącz fragmenty kodu po wpisaniu znaku ? po identyfikatorze i naciśnięciu klawisza Tab - - - - Never include snippets - Nigdy nie dołączaj fragmentów kodu - - Snippets behavior Zachowanie fragmentów kodu diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.pt-BR.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.pt-BR.xlf index 12bbbe50c8e6f..71f7adbfcc571 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.pt-BR.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.pt-BR.xlf @@ -272,21 +272,6 @@ _Nunca adicionar nova linha ao inserir - - Always include snippets - Sempre incluir snippets - - - - Include snippets when ?-Tab is typed after an identifier - Incluir snippets quando ?-Tab for digitado após um identificador - - - - Never include snippets - Nunca incluir snippets - - Snippets behavior Comportamento de snippets diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.ru.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.ru.xlf index 18e80b678e73e..2f19095174378 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.ru.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.ru.xlf @@ -272,21 +272,6 @@ _Никогда не добавлять новую строку при нажатии клавиши ВВОД - - Always include snippets - Всегда включать фрагменты кода - - - - Include snippets when ?-Tab is typed after an identifier - Включать фрагменты кода, когда после идентификатора указывается "?-Tab" - - - - Never include snippets - Никогда не включать фрагменты кода - - Snippets behavior Поведение фрагментов кода diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.tr.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.tr.xlf index d5186e12e001b..fe333e73dae87 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.tr.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.tr.xlf @@ -272,21 +272,6 @@ Enter tuşuna basıldığında _hiçbir zaman yeni satır ekleme - - Always include snippets - Kod parçacıklarını her zaman dahil et - - - - Include snippets when ?-Tab is typed after an identifier - Bir tanımlayıcıdan sonra ?-Tab yazılırsa kod parçacıklarını dahil et - - - - Never include snippets - Kod parçacıklarını hiçbir zaman dahil etme - - Snippets behavior Kod parçacığı davranışı diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.zh-Hans.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.zh-Hans.xlf index 5d3359de43850..4532d2fa79a4a 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.zh-Hans.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.zh-Hans.xlf @@ -272,21 +272,6 @@ 按下 Enter 时不添加新行(_N) - - Always include snippets - 始终包含片段 - - - - Include snippets when ?-Tab is typed after an identifier - 在标识符后键入 ?-Tab 时包含片段 - - - - Never include snippets - 从不包含片段 - - Snippets behavior 片段行为 diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.zh-Hant.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.zh-Hant.xlf index b912f0c3e7c4b..b1a8e9c40547a 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.zh-Hant.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/BasicVSResources.zh-Hant.xlf @@ -272,21 +272,6 @@ 永不在按下 Enter 鍵時加入新行(_N) - - Always include snippets - 一律包含程式碼片段 - - - - Include snippets when ?-Tab is typed after an identifier - 在識別碼後輸入 ?-Tab 時包含程式碼片段 - - - - Never include snippets - 一律不包含程式碼片段 - - Snippets behavior 程式碼片段行為 diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.cs.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.cs.xlf index 89b8f916f76d4..276c0e8c18faf 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.cs.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.cs.xlf @@ -130,6 +130,36 @@ Používat rozšířené barvy;Barevné schéma editoru;Okraj dědičnosti;Direk Nástroje Visual Basicu Help > About + + Always add new line on enter + Always add new line on enter + + + + Always include snippets + Always include snippets + + + + Include snippets when ?-Tab is typed after an identifier + Include snippets when ?-Tab is typed after an identifier + + + + Never add new line on enter + Never add new line on enter + + + + Never include snippets + Never include snippets + + + + Only add new line on enter after end of fully typed word + Only add new line on enter after end of fully typed word + + \ No newline at end of file diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.de.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.de.xlf index 5f0cd72bc7208..a3e9414b14f48 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.de.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.de.xlf @@ -130,6 +130,36 @@ Erweiterte Farben verwenden;Editor-Farbschema;Vererbungsspielraum;Richtlinien im Visual Basic-Tools Help > About + + Always add new line on enter + Always add new line on enter + + + + Always include snippets + Always include snippets + + + + Include snippets when ?-Tab is typed after an identifier + Include snippets when ?-Tab is typed after an identifier + + + + Never add new line on enter + Never add new line on enter + + + + Never include snippets + Never include snippets + + + + Only add new line on enter after end of fully typed word + Only add new line on enter after end of fully typed word + + \ No newline at end of file diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.es.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.es.xlf index 8a092b2a86686..a318fc614937a 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.es.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.es.xlf @@ -130,6 +130,36 @@ Usar colores mejorados;Combinación de colores del editor;Margen de herencia;Dir Herramientas de Visual Basic Help > About + + Always add new line on enter + Always add new line on enter + + + + Always include snippets + Always include snippets + + + + Include snippets when ?-Tab is typed after an identifier + Include snippets when ?-Tab is typed after an identifier + + + + Never add new line on enter + Never add new line on enter + + + + Never include snippets + Never include snippets + + + + Only add new line on enter after end of fully typed word + Only add new line on enter after end of fully typed word + + \ No newline at end of file diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.fr.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.fr.xlf index 6f9d5251b0861..d202187102890 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.fr.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.fr.xlf @@ -130,6 +130,36 @@ Utiliser des couleurs améliorées ; Modèle de couleurs de l’éditeur;Marge d Outils Visual Basic Help > About + + Always add new line on enter + Always add new line on enter + + + + Always include snippets + Always include snippets + + + + Include snippets when ?-Tab is typed after an identifier + Include snippets when ?-Tab is typed after an identifier + + + + Never add new line on enter + Never add new line on enter + + + + Never include snippets + Never include snippets + + + + Only add new line on enter after end of fully typed word + Only add new line on enter after end of fully typed word + + \ No newline at end of file diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.it.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.it.xlf index 05ead449eda01..0ffbe854456b1 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.it.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.it.xlf @@ -130,6 +130,36 @@ Usare colori avanzati; Combinazione colori editor;Margine di ereditarietà;Diret Strumenti di Visual Basic Help > About + + Always add new line on enter + Always add new line on enter + + + + Always include snippets + Always include snippets + + + + Include snippets when ?-Tab is typed after an identifier + Include snippets when ?-Tab is typed after an identifier + + + + Never add new line on enter + Never add new line on enter + + + + Never include snippets + Never include snippets + + + + Only add new line on enter after end of fully typed word + Only add new line on enter after end of fully typed word + + \ No newline at end of file diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.ja.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.ja.xlf index c4cb8c5e5ff15..8b06a20801ee9 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.ja.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.ja.xlf @@ -130,6 +130,36 @@ JSON 文字列のエディター機能の検出と提供; Visual Basic ツール Help > About + + Always add new line on enter + Always add new line on enter + + + + Always include snippets + Always include snippets + + + + Include snippets when ?-Tab is typed after an identifier + Include snippets when ?-Tab is typed after an identifier + + + + Never add new line on enter + Never add new line on enter + + + + Never include snippets + Never include snippets + + + + Only add new line on enter after end of fully typed word + Only add new line on enter after end of fully typed word + + \ No newline at end of file diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.ko.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.ko.xlf index b43840a0f901b..efb4258efa09a 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.ko.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.ko.xlf @@ -130,6 +130,36 @@ JSON 문자열 색상 지정, Visual Basic 도구 Help > About + + Always add new line on enter + Always add new line on enter + + + + Always include snippets + Always include snippets + + + + Include snippets when ?-Tab is typed after an identifier + Include snippets when ?-Tab is typed after an identifier + + + + Never add new line on enter + Never add new line on enter + + + + Never include snippets + Never include snippets + + + + Only add new line on enter after end of fully typed word + Only add new line on enter after end of fully typed word + + \ No newline at end of file diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.pl.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.pl.xlf index dfc04b5da1e7f..2a996c13f65dd 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.pl.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.pl.xlf @@ -130,6 +130,36 @@ Używanie rozszerzonych kolorów; Schemat kolorów edytora;Margines dziedziczeni Narzędzia języka Visual Basic Help > About + + Always add new line on enter + Always add new line on enter + + + + Always include snippets + Always include snippets + + + + Include snippets when ?-Tab is typed after an identifier + Include snippets when ?-Tab is typed after an identifier + + + + Never add new line on enter + Never add new line on enter + + + + Never include snippets + Never include snippets + + + + Only add new line on enter after end of fully typed word + Only add new line on enter after end of fully typed word + + \ No newline at end of file diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.pt-BR.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.pt-BR.xlf index 7a64fb3e6120b..88970da2f6d56 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.pt-BR.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.pt-BR.xlf @@ -130,6 +130,36 @@ Usar cores aprimoradas;Esquema de Cores do Editor;Margem de Herança;Importar Di Ferramentas do Visual Basic Help > About + + Always add new line on enter + Always add new line on enter + + + + Always include snippets + Always include snippets + + + + Include snippets when ?-Tab is typed after an identifier + Include snippets when ?-Tab is typed after an identifier + + + + Never add new line on enter + Never add new line on enter + + + + Never include snippets + Never include snippets + + + + Only add new line on enter after end of fully typed word + Only add new line on enter after end of fully typed word + + \ No newline at end of file diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.ru.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.ru.xlf index b0315632359ad..63dbfc68c0bb5 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.ru.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.ru.xlf @@ -130,6 +130,36 @@ JSON; Инструменты Visual Basic Help > About + + Always add new line on enter + Always add new line on enter + + + + Always include snippets + Always include snippets + + + + Include snippets when ?-Tab is typed after an identifier + Include snippets when ?-Tab is typed after an identifier + + + + Never add new line on enter + Never add new line on enter + + + + Never include snippets + Never include snippets + + + + Only add new line on enter after end of fully typed word + Only add new line on enter after end of fully typed word + + \ No newline at end of file diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.tr.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.tr.xlf index 7c8826d805575..10bf015c805aa 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.tr.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.tr.xlf @@ -130,6 +130,36 @@ Gelişmiş renkleri kullan;Düzenleyici Renk Düzeni;Devralma Kenar Boşluğu;İ Visual Basic Araçları Help > About + + Always add new line on enter + Always add new line on enter + + + + Always include snippets + Always include snippets + + + + Include snippets when ?-Tab is typed after an identifier + Include snippets when ?-Tab is typed after an identifier + + + + Never add new line on enter + Never add new line on enter + + + + Never include snippets + Never include snippets + + + + Only add new line on enter after end of fully typed word + Only add new line on enter after end of fully typed word + + \ No newline at end of file diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.zh-Hans.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.zh-Hans.xlf index 8e6b9e052b60d..9dc70f453cd84 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.zh-Hans.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.zh-Hans.xlf @@ -130,6 +130,36 @@ JSON; Visual Basic 工具 Help > About + + Always add new line on enter + Always add new line on enter + + + + Always include snippets + Always include snippets + + + + Include snippets when ?-Tab is typed after an identifier + Include snippets when ?-Tab is typed after an identifier + + + + Never add new line on enter + Never add new line on enter + + + + Never include snippets + Never include snippets + + + + Only add new line on enter after end of fully typed word + Only add new line on enter after end of fully typed word + + \ No newline at end of file diff --git a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.zh-Hant.xlf b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.zh-Hant.xlf index 5784a3d19b718..06fd1d217bfde 100644 --- a/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.zh-Hant.xlf +++ b/src/VisualStudio/VisualBasic/Impl/xlf/VSPackage.zh-Hant.xlf @@ -130,6 +130,36 @@ JSON; Visual Basic 工具 Help > About + + Always add new line on enter + Always add new line on enter + + + + Always include snippets + Always include snippets + + + + Include snippets when ?-Tab is typed after an identifier + Include snippets when ?-Tab is typed after an identifier + + + + Never add new line on enter + Never add new line on enter + + + + Never include snippets + Never include snippets + + + + Only add new line on enter after end of fully typed word + Only add new line on enter after end of fully typed word + + \ No newline at end of file diff --git a/src/VisualStudio/VisualStudioDiagnosticsToolWindow/Panels/WorkspacePanel.xaml.cs b/src/VisualStudio/VisualStudioDiagnosticsToolWindow/Panels/WorkspacePanel.xaml.cs index 38fd06245c515..e949ef523ae1c 100644 --- a/src/VisualStudio/VisualStudioDiagnosticsToolWindow/Panels/WorkspacePanel.xaml.cs +++ b/src/VisualStudio/VisualStudioDiagnosticsToolWindow/Panels/WorkspacePanel.xaml.cs @@ -99,7 +99,7 @@ async Task CompareDocumentAsync(Document document) { lock (gate) { - output.AppendLine($"{document.FilePath}: {BitConverter.ToString(snapshotChecksum.ToArray())} : {BitConverter.ToString(fileChecksum.ToArray())}"); + output.AppendLine($"{document.FilePath}: {BitConverter.ToString([.. snapshotChecksum])} : {BitConverter.ToString([.. fileChecksum])}"); outOfDateCount++; } } diff --git a/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/Handler/Definitions/GoToDefinitionHandler.cs b/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/Handler/Definitions/GoToDefinitionHandler.cs index e7c14eb07a3eb..a1ee575ad5aed 100644 --- a/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/Handler/Definitions/GoToDefinitionHandler.cs +++ b/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/Handler/Definitions/GoToDefinitionHandler.cs @@ -55,13 +55,13 @@ public GoToDefinitionHandler(IMetadataAsSourceFileService metadataAsSourceFileSe var document = context.Document; if (document == null) { - return locations.ToArray(); + return [.. locations]; } var xamlGoToDefinitionService = document.Project.Services.GetService(); if (xamlGoToDefinitionService == null) { - return locations.ToArray(); + return [.. locations]; } var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(false); @@ -83,7 +83,7 @@ public GoToDefinitionHandler(IMetadataAsSourceFileService metadataAsSourceFileSe await Task.WhenAll(tasks).ConfigureAwait(false); - return locations.ToArray(); + return [.. locations]; } private async Task GetLocationsAsync(XamlDefinition definition, RequestContext context, CancellationToken cancellationToken) diff --git a/src/Workspaces/CSharp/Portable/Formatting/CSharpSyntaxFormattingService.cs b/src/Workspaces/CSharp/Portable/Formatting/CSharpSyntaxFormattingService.cs index c45a418d8c073..5df82749fae48 100644 --- a/src/Workspaces/CSharp/Portable/Formatting/CSharpSyntaxFormattingService.cs +++ b/src/Workspaces/CSharp/Portable/Formatting/CSharpSyntaxFormattingService.cs @@ -148,7 +148,7 @@ public ImmutableArray GetFormattingChangesOnTypedCharacter( return changes; } - return FormatToken(document, indentationOptions, token, formattingRules, cancellationToken).ToImmutableArray(); + return [.. FormatToken(document, indentationOptions, token, formattingRules, cancellationToken)]; } private static bool OnlySmartIndentCloseBrace(in AutoFormattingOptions options) @@ -199,7 +199,7 @@ private static ImmutableArray FormatRange( var formatter = new CSharpSmartTokenFormatter(options, formattingRules, (CompilationUnitSyntax)document.Root, document.Text); var changes = formatter.FormatRange(tokenRange.Value.Item1, tokenRange.Value.Item2, cancellationToken); - return changes.ToImmutableArray(); + return [.. changes]; } private static IEnumerable GetTypingRules(SyntaxToken tokenBeforeCaret) @@ -318,9 +318,12 @@ or SyntaxKind.EndOfDirectiveToken private ImmutableArray GetFormattingRules(ParsedDocument document, int position, SyntaxToken tokenBeforeCaret) { var formattingRuleFactory = _services.SolutionServices.GetRequiredService(); - return ImmutableArray.Create(formattingRuleFactory.CreateRule(document, position)) - .AddRange(GetTypingRules(tokenBeforeCaret)) - .AddRange(Formatter.GetDefaultFormattingRules(_services)); + return + [ + formattingRuleFactory.CreateRule(document, position), + .. GetTypingRules(tokenBeforeCaret), + .. Formatter.GetDefaultFormattingRules(_services), + ]; } public ImmutableArray GetFormattingChangesOnPaste(ParsedDocument document, TextSpan textSpan, SyntaxFormattingOptions options, CancellationToken cancellationToken) @@ -332,7 +335,7 @@ public ImmutableArray GetFormattingChangesOnPaste(ParsedDocument doc rules.AddRange(service.GetDefaultFormattingRules()); var result = service.GetFormattingResult(document.Root, [formattingSpan], options, rules, cancellationToken); - return result.GetTextChanges(cancellationToken).ToImmutableArray(); + return [.. result.GetTextChanges(cancellationToken)]; } internal sealed class PasteFormattingRule : AbstractFormattingRule diff --git a/src/Workspaces/CSharp/Portable/Simplification/CSharpSimplificationService.NodesAndTokensToReduceComputer.cs b/src/Workspaces/CSharp/Portable/Simplification/CSharpSimplificationService.NodesAndTokensToReduceComputer.cs index 127cfd73e6a56..55bdb955984c6 100644 --- a/src/Workspaces/CSharp/Portable/Simplification/CSharpSimplificationService.NodesAndTokensToReduceComputer.cs +++ b/src/Workspaces/CSharp/Portable/Simplification/CSharpSimplificationService.NodesAndTokensToReduceComputer.cs @@ -31,7 +31,7 @@ public static ImmutableArray Compute(SyntaxNode root, Func< { var reduceNodeComputer = new NodesAndTokensToReduceComputer(isNodeOrTokenOutsideSimplifySpans); reduceNodeComputer.Visit(root); - return reduceNodeComputer._nodesAndTokensToReduce.ToImmutableArray(); + return [.. reduceNodeComputer._nodesAndTokensToReduce]; } private NodesAndTokensToReduceComputer(Func isNodeOrTokenOutsideSimplifySpans) diff --git a/src/Workspaces/CSharpTest/Formatting/FormattingTests_Patterns.cs b/src/Workspaces/CSharpTest/Formatting/FormattingTests_Patterns.cs index b1159ead211fd..4caeafdd8856c 100644 --- a/src/Workspaces/CSharpTest/Formatting/FormattingTests_Patterns.cs +++ b/src/Workspaces/CSharpTest/Formatting/FormattingTests_Patterns.cs @@ -17,8 +17,7 @@ namespace Microsoft.CodeAnalysis.CSharp.UnitTests.Formatting [Trait(Traits.Feature, Traits.Features.Formatting)] public class FormattingTests_Patterns : CSharpFormattingTestBase { - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public async Task FormatRelationalPatterns1( [CombinatorialValues("<", "<=", ">", ">=")] string operatorText, BinaryOperatorSpacingOptions spacing) @@ -76,8 +75,7 @@ bool Method(int value) await AssertFormatAsync(expected, content, changedOptionSet: changingOptions); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public async Task FormatRelationalPatterns2( [CombinatorialValues("<", "<=", ">", ">=")] string operatorText, BinaryOperatorSpacingOptions spacing, @@ -167,8 +165,7 @@ bool Method(int value) await AssertFormatAsync(expected, content, changedOptionSet: changingOptions); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public async Task FormatNotPatterns1(BinaryOperatorSpacingOptions spacing) { var content = $@" @@ -224,8 +221,7 @@ bool Method(int value) await AssertFormatAsync(expected, content, changedOptionSet: changingOptions); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public async Task FormatNotPatterns2( BinaryOperatorSpacingOptions spacing, bool spaceWithinExpressionParentheses) diff --git a/src/Workspaces/Core/MSBuild.BuildHost/Build/ProjectBuildManager.cs b/src/Workspaces/Core/MSBuild.BuildHost/Build/ProjectBuildManager.cs index 9a331882f074f..0cba06c47234c 100644 --- a/src/Workspaces/Core/MSBuild.BuildHost/Build/ProjectBuildManager.cs +++ b/src/Workspaces/Core/MSBuild.BuildHost/Build/ProjectBuildManager.cs @@ -260,7 +260,7 @@ public void EndBatchBuild() _batchBuildLogger?.SetProjectAndLog(projectInstance.FullPath, log); - var buildRequestData = new MSB.Execution.BuildRequestData(projectInstance, targets.ToArray()); + var buildRequestData = new MSB.Execution.BuildRequestData(projectInstance, [.. targets]); var result = await BuildAsync(buildRequestData, cancellationToken).ConfigureAwait(false); diff --git a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/Extensions.cs b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/Extensions.cs index ef9950a71ca1b..13e65ac97f128 100644 --- a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/Extensions.cs +++ b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/Extensions.cs @@ -32,7 +32,6 @@ internal static class Extensions public static IEnumerable GetProjectReferences(this MSB.Execution.ProjectInstance executedProject) => executedProject .GetItems(ItemNames.ProjectReference) - .Where(i => i.ReferenceOutputAssemblyIsTrue()) .Select(CreateProjectFileReference); public static ImmutableArray GetPackageReferences(this MSB.Execution.ProjectInstance executedProject) @@ -48,14 +47,14 @@ public static ImmutableArray GetPackageReferences(this MSB.Exe references.Add(packageReference); } - return references.ToImmutableArray(); + return [.. references]; } /// /// Create a from a ProjectReference node in the MSBuild file. /// private static ProjectFileReference CreateProjectFileReference(MSB.Execution.ProjectItemInstance reference) - => new(reference.EvaluatedInclude, reference.GetAliases()); + => new(reference.EvaluatedInclude, reference.GetAliases(), reference.ReferenceOutputAssemblyIsTrue()); public static ImmutableArray GetAliases(this MSB.Framework.ITaskItem item) { diff --git a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFile.cs b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFile.cs index 4fa6436771ed5..92fb55d25e1b1 100644 --- a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFile.cs +++ b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFile.cs @@ -39,7 +39,7 @@ protected ProjectFile(ProjectFileLoader loader, MSB.Evaluation.Project? loadedPr Log = log; } - public ImmutableArray GetDiagnosticLogItems() => Log.ToImmutableArray(); + public ImmutableArray GetDiagnosticLogItems() => [.. Log]; protected abstract SourceCodeKind GetSourceCodeKind(string documentFileName); public abstract string GetDocumentExtension(SourceCodeKind kind); @@ -252,7 +252,7 @@ private ImmutableArray GetRelativeFolders(MSB.Framework.ITaskItem docume var linkPath = documentItem.GetMetadata(MetadataNames.Link); if (!RoslynString.IsNullOrEmpty(linkPath)) { - return PathUtilities.GetDirectoryName(linkPath).Split(PathUtilities.DirectorySeparatorChar, PathUtilities.AltDirectorySeparatorChar).ToImmutableArray(); + return [.. PathUtilities.GetDirectoryName(linkPath).Split(PathUtilities.DirectorySeparatorChar, PathUtilities.AltDirectorySeparatorChar)]; } else { diff --git a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFileReference.cs b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFileReference.cs index 6ed6024473d6a..fb8d3a216e32a 100644 --- a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFileReference.cs +++ b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFileReference.cs @@ -27,12 +27,19 @@ internal sealed class ProjectFileReference [DataMember(Order = 1)] public ImmutableArray Aliases { get; } - public ProjectFileReference(string path, ImmutableArray aliases) + /// + /// The value of . + /// + [DataMember(Order = 2)] + public bool ReferenceOutputAssembly { get; } + + public ProjectFileReference(string path, ImmutableArray aliases, bool referenceOutputAssembly) { Debug.Assert(!aliases.IsDefault); - this.Path = path; - this.Aliases = aliases; + Path = path; + Aliases = aliases; + ReferenceOutputAssembly = referenceOutputAssembly; } } } diff --git a/src/Workspaces/Core/MSBuild/MSBuild/BuildHostProcessManager.cs b/src/Workspaces/Core/MSBuild/MSBuild/BuildHostProcessManager.cs index e50bf7d57852b..0bdbcd428bba0 100644 --- a/src/Workspaces/Core/MSBuild/MSBuild/BuildHostProcessManager.cs +++ b/src/Workspaces/Core/MSBuild/MSBuild/BuildHostProcessManager.cs @@ -149,7 +149,7 @@ public async ValueTask DisposeAsync() // may try to mutate the list while we're enumerating. using (await _gate.DisposableWaitAsync().ConfigureAwait(false)) { - processesToDispose = _processes.Values.ToList(); + processesToDispose = [.. _processes.Values]; _processes.Clear(); } diff --git a/src/Workspaces/Core/MSBuild/MSBuild/MSBuildProjectLoader.Worker_ResolveReferences.cs b/src/Workspaces/Core/MSBuild/MSBuild/MSBuildProjectLoader.Worker_ResolveReferences.cs index d2fc3f9fe896d..26e000e02c6c5 100644 --- a/src/Workspaces/Core/MSBuild/MSBuild/MSBuildProjectLoader.Worker_ResolveReferences.cs +++ b/src/Workspaces/Core/MSBuild/MSBuild/MSBuildProjectLoader.Worker_ResolveReferences.cs @@ -209,26 +209,35 @@ private async Task ResolveReferencesAsync(ProjectId id, Proj continue; } - // If we don't know how to load a project (that is, it's not a language we support), we can still - // attempt to verify that its output exists on disk and is included in our set of metadata references. - // If it is, we'll just leave it in place. - if (!IsProjectLoadable(projectReferencePath) && - await VerifyUnloadableProjectOutputExistsAsync(projectReferencePath, builder, cancellationToken).ConfigureAwait(false)) + if (projectFileReference.ReferenceOutputAssembly) { - continue; - } - - // If metadata is preferred, see if the project reference's output exists on disk and is included - // in our metadata references. If it is, don't create a project reference; we'll just use the metadata. - if (_preferMetadataForReferencesOfDiscoveredProjects && - await VerifyProjectOutputExistsAsync(projectReferencePath, builder, cancellationToken).ConfigureAwait(false)) - { - continue; + // If we don't know how to load a project (that is, it's not a language we support), we can still + // attempt to verify that its output exists on disk and is included in our set of metadata references. + // If it is, we'll just leave it in place. + if (!IsProjectLoadable(projectReferencePath) && + await VerifyUnloadableProjectOutputExistsAsync(projectReferencePath, builder, cancellationToken).ConfigureAwait(false)) + { + continue; + } + + // If metadata is preferred, see if the project reference's output exists on disk and is included + // in our metadata references. If it is, don't create a project reference; we'll just use the metadata. + if (_preferMetadataForReferencesOfDiscoveredProjects && + await VerifyProjectOutputExistsAsync(projectReferencePath, builder, cancellationToken).ConfigureAwait(false)) + { + continue; + } + + // Finally, we'll try to load and reference the project. + if (await TryLoadAndAddReferenceAsync(id, projectReferencePath, aliases, builder, cancellationToken).ConfigureAwait(false)) + { + continue; + } } - - // Finally, we'll try to load and reference the project. - if (await TryLoadAndAddReferenceAsync(id, projectReferencePath, aliases, builder, cancellationToken).ConfigureAwait(false)) + else { + // Load the project but do not add a reference: + _ = await LoadProjectInfosFromPathAsync(projectReferencePath, _discoveredProjectOptions, cancellationToken).ConfigureAwait(false); continue; } } diff --git a/src/Workspaces/Core/MSBuild/MSBuild/MSBuildWorkspace.cs b/src/Workspaces/Core/MSBuild/MSBuild/MSBuildWorkspace.cs index d22ddc0d0a33e..35e8f99bf2704 100644 --- a/src/Workspaces/Core/MSBuild/MSBuild/MSBuildWorkspace.cs +++ b/src/Workspaces/Core/MSBuild/MSBuild/MSBuildWorkspace.cs @@ -457,7 +457,7 @@ protected override void ApplyDocumentAdded(DocumentInfo info, SourceText text) var fileName = Path.ChangeExtension(info.Name, extension); var relativePath = (info.Folders != null && info.Folders.Count > 0) - ? Path.Combine(Path.Combine(info.Folders.ToArray()), fileName) + ? Path.Combine(Path.Combine([.. info.Folders]), fileName) : fileName; var fullPath = GetAbsolutePath(relativePath, Path.GetDirectoryName(project.FilePath)!); @@ -647,7 +647,9 @@ protected override void ApplyProjectReferenceAdded(ProjectId projectId, ProjectR var project = this.CurrentSolution.GetProject(projectReference.ProjectId); if (project?.FilePath is not null) { - _applyChangesProjectFile.AddProjectReferenceAsync(project.Name, new ProjectFileReference(project.FilePath, projectReference.Aliases), CancellationToken.None).Wait(); + // Only "ReferenceOutputAssembly=true" project references are represented in the workspace: + var reference = new ProjectFileReference(project.FilePath, projectReference.Aliases, referenceOutputAssembly: true); + _applyChangesProjectFile.AddProjectReferenceAsync(project.Name, reference, CancellationToken.None).Wait(); } this.OnProjectReferenceAdded(projectId, projectReference); diff --git a/src/Workspaces/Core/Portable/CaseCorrection/AbstractCaseCorrectionService.cs b/src/Workspaces/Core/Portable/CaseCorrection/AbstractCaseCorrectionService.cs index f8178c6c5933f..a40000b3ff50b 100644 --- a/src/Workspaces/Core/Portable/CaseCorrection/AbstractCaseCorrectionService.cs +++ b/src/Workspaces/Core/Portable/CaseCorrection/AbstractCaseCorrectionService.cs @@ -52,7 +52,7 @@ private SyntaxNode CaseCorrect(SemanticModel? semanticModel, SyntaxNode root, Im using (Logger.LogBlock(FunctionId.CaseCorrection_AddReplacements, cancellationToken)) { - AddReplacements(semanticModel, root, normalizedSpanCollection.ToImmutableArray(), replacements, cancellationToken); + AddReplacements(semanticModel, root, [.. normalizedSpanCollection], replacements, cancellationToken); } using (Logger.LogBlock(FunctionId.CaseCorrection_ReplaceTokens, cancellationToken)) diff --git a/src/Workspaces/Core/Portable/Classification/ClassifierHelper.cs b/src/Workspaces/Core/Portable/Classification/ClassifierHelper.cs index 1d45eb4e6ab7b..dd8e71e8f7906 100644 --- a/src/Workspaces/Core/Portable/Classification/ClassifierHelper.cs +++ b/src/Workspaces/Core/Portable/Classification/ClassifierHelper.cs @@ -139,7 +139,7 @@ private static ImmutableArray MergeClassifiedSpans( // be gaps in what it produces. Fill in those gaps so we have *all* parts of the span classified properly. using var _2 = Classifier.GetPooledList(out var filledInSpans); FillInClassifiedSpanGaps(widenedSpan.Start, mergedSpans, filledInSpans); - return filledInSpans.ToImmutableArray(); + return [.. filledInSpans]; } private static readonly Comparison s_spanComparison = static (s1, s2) => s1.TextSpan.Start - s2.TextSpan.Start; diff --git a/src/Workspaces/Core/Portable/CodeCleanup/AbstractCodeCleanerService.cs b/src/Workspaces/Core/Portable/CodeCleanup/AbstractCodeCleanerService.cs index a7bec9d9175e8..2a65deb55737d 100644 --- a/src/Workspaces/Core/Portable/CodeCleanup/AbstractCodeCleanerService.cs +++ b/src/Workspaces/Core/Portable/CodeCleanup/AbstractCodeCleanerService.cs @@ -530,7 +530,7 @@ private ImmutableArray GetSpans( // Remove the spans we should not touch from the requested spans and return that final set. var result = NormalizedTextSpanCollection.Difference(requestedSpans, spansToAvoid); - return result.ToImmutableArray(); + return [.. result]; } private async Task IterateAllCodeCleanupProvidersAsync( @@ -594,7 +594,7 @@ private string GetCodeCleanerTypeName(ICodeCleanupProvider codeCleaner) private static SyntaxNode InjectAnnotations(SyntaxNode node, Dictionary> map) { var tokenMap = map.ToDictionary(p => p.Key, p => p.Value); - return node.ReplaceTokens(tokenMap.Keys, (o, n) => o.WithAdditionalAnnotations(tokenMap[o].ToArray())); + return node.ReplaceTokens(tokenMap.Keys, (o, n) => o.WithAdditionalAnnotations([.. tokenMap[o]])); } private static bool TryCreateTextSpan(int start, int end, out TextSpan span) diff --git a/src/Workspaces/Core/Portable/CodeFixes/FixAllOccurrences/DocumentBasedFixAllProvider.cs b/src/Workspaces/Core/Portable/CodeFixes/FixAllOccurrences/DocumentBasedFixAllProvider.cs index 4f2f90530fff3..823220fb6a546 100644 --- a/src/Workspaces/Core/Portable/CodeFixes/FixAllOccurrences/DocumentBasedFixAllProvider.cs +++ b/src/Workspaces/Core/Portable/CodeFixes/FixAllOccurrences/DocumentBasedFixAllProvider.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixesAndRefactorings; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; @@ -27,20 +26,15 @@ namespace Microsoft.CodeAnalysis.CodeFixes; /// project and then appropriately bucketed by document. These are then passed to for implementors to process. /// -public abstract class DocumentBasedFixAllProvider : FixAllProvider +public abstract class DocumentBasedFixAllProvider(ImmutableArray supportedFixAllScopes) : FixAllProvider { - private readonly ImmutableArray _supportedFixAllScopes; + private readonly ImmutableArray _supportedFixAllScopes = supportedFixAllScopes; protected DocumentBasedFixAllProvider() : this(DefaultSupportedFixAllScopes) { } - protected DocumentBasedFixAllProvider(ImmutableArray supportedFixAllScopes) - { - _supportedFixAllScopes = supportedFixAllScopes; - } - /// /// Produce a suitable title for the fix-all this type creates in . Override this if customizing that title is desired. @@ -72,79 +66,42 @@ public sealed override IEnumerable GetSupportedFixAllScopes() fixAllContext.GetDefaultFixAllTitle(), fixAllContext, FixAllContextsHelperAsync); private Task FixAllContextsHelperAsync(FixAllContext originalFixAllContext, ImmutableArray fixAllContexts) - => DocumentBasedFixAllProviderHelpers.FixAllContextsAsync(originalFixAllContext, fixAllContexts, - originalFixAllContext.Progress, - this.GetFixAllTitle(originalFixAllContext), - DetermineDiagnosticsAndGetFixedDocumentsAsync); - - private async Task> DetermineDiagnosticsAndGetFixedDocumentsAsync( - FixAllContext fixAllContext, - IProgress progressTracker) - { - // First, determine the diagnostics to fix. - var diagnostics = await DetermineDiagnosticsAsync(fixAllContext, progressTracker).ConfigureAwait(false); - - // Second, get the fixes for all the diagnostics, and apply them to determine the new root/text for each doc. - return await GetFixedDocumentsAsync(fixAllContext, progressTracker, diagnostics).ConfigureAwait(false); - } - - /// - /// Determines all the diagnostics we should be fixing for the given . - /// - private static async Task>> DetermineDiagnosticsAsync(FixAllContext fixAllContext, IProgress progressTracker) - { - using var _ = progressTracker.ItemCompletedScope(); - return await FixAllContextHelper.GetDocumentDiagnosticsToFixAsync(fixAllContext).ConfigureAwait(false); - } - - /// - /// Attempts to fix all the provided returning, for each updated document, either - /// the new syntax root for that document or its new text. Syntax roots are returned for documents that support - /// them, and are used to perform a final cleanup pass for formatting/simplication/etc. Text is returned for - /// documents that don't support syntax. - /// - private async Task> GetFixedDocumentsAsync( - FixAllContext fixAllContext, IProgress progressTracker, ImmutableDictionary> diagnostics) + => DocumentBasedFixAllProviderHelpers.FixAllContextsAsync( + originalFixAllContext, + fixAllContexts, + originalFixAllContext.Progress, + this.GetFixAllTitle(originalFixAllContext), + DetermineDiagnosticsAndGetFixedDocumentsAsync); + + private async Task DetermineDiagnosticsAndGetFixedDocumentsAsync( + FixAllContext fixAllContext, Action<(DocumentId documentId, (SyntaxNode? node, SourceText? text))> callback) { var cancellationToken = fixAllContext.CancellationToken; - using var _1 = progressTracker.ItemCompletedScope(); - using var _2 = ArrayBuilder>.GetInstance(out var tasks); - - var docIdToNewRootOrText = new Dictionary(); - if (!diagnostics.IsEmpty) - { - // Then, process all documents in parallel to get the change for each doc. - foreach (var (document, documentDiagnostics) in diagnostics) + // First, determine the diagnostics to fix. + var documentToDiagnostics = await FixAllContextHelper.GetDocumentDiagnosticsToFixAsync(fixAllContext).ConfigureAwait(false); + + // Second, get the fixes for each document+diagnostics pair in parallel, and apply them to determine the new + // root/text for each doc. + await RoslynParallel.ForEachAsync( + source: documentToDiagnostics, + cancellationToken, + async (kvp, cancellationToken) => { + var (document, documentDiagnostics) = kvp; if (documentDiagnostics.IsDefaultOrEmpty) - continue; - - tasks.Add(Task.Run(async () => - { - var newDocument = await this.FixAllAsync(fixAllContext, document, documentDiagnostics).ConfigureAwait(false); - if (newDocument == null || newDocument == document) - return default; - - // For documents that support syntax, grab the tree so that we can clean it up later. If it's a - // language that doesn't support that, then just grab the text. - var node = newDocument.SupportsSyntaxTree ? await newDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false) : null; - var text = newDocument.SupportsSyntaxTree ? null : await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + return; - return (document.Id, (node, text)); - }, cancellationToken)); - } + var newDocument = await this.FixAllAsync(fixAllContext, document, documentDiagnostics).ConfigureAwait(false); + if (newDocument == null || newDocument == document) + return; - await Task.WhenAll(tasks).ConfigureAwait(false); - - foreach (var task in tasks) - { - var (docId, nodeOrText) = await task.ConfigureAwait(false); - if (docId != null) - docIdToNewRootOrText[docId] = nodeOrText; - } - } + // For documents that support syntax, grab the tree so that we can clean it up later. If it's a + // language that doesn't support that, then just grab the text. + var node = newDocument.SupportsSyntaxTree ? await newDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false) : null; + var text = newDocument.SupportsSyntaxTree ? null : await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - return docIdToNewRootOrText; + callback((document.Id, (node, text))); + }).ConfigureAwait(false); } } diff --git a/src/Workspaces/Core/Portable/CodeFixes/FixAllOccurrences/FixAllContext.DiagnosticProvider.cs b/src/Workspaces/Core/Portable/CodeFixes/FixAllOccurrences/FixAllContext.DiagnosticProvider.cs index 2c7b4ed56d743..391678a382115 100644 --- a/src/Workspaces/Core/Portable/CodeFixes/FixAllOccurrences/FixAllContext.DiagnosticProvider.cs +++ b/src/Workspaces/Core/Portable/CodeFixes/FixAllOccurrences/FixAllContext.DiagnosticProvider.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.CodeFixesAndRefactorings; using Microsoft.CodeAnalysis.Internal.Log; +using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CodeFixes; @@ -81,26 +82,27 @@ internal static async Task>(); - - var tasks = project.Solution.Projects.Select(async p => new - { - Project = p, - Diagnostics = await fixAllContext.GetProjectDiagnosticsAsync(p).ConfigureAwait(false) - }).ToArray(); - - await Task.WhenAll(tasks).ConfigureAwait(false); - - foreach (var task in tasks) - { - var projectAndDiagnostics = await task.ConfigureAwait(false); - if (projectAndDiagnostics.Diagnostics.Any()) + return await ProducerConsumer<(Project project, ImmutableArray diagnostics)>.RunParallelAsync( + source: project.Solution.Projects, + produceItems: static async (project, callback, fixAllContext, cancellationToken) => { - projectsAndDiagnostics[projectAndDiagnostics.Project] = projectAndDiagnostics.Diagnostics; - } - } - - return projectsAndDiagnostics.ToImmutable(); + var diagnostics = await fixAllContext.GetProjectDiagnosticsAsync(project).ConfigureAwait(false); + callback((project, diagnostics)); + }, + consumeItems: static async (results, args, cancellationToken) => + { + var projectsAndDiagnostics = ImmutableDictionary.CreateBuilder>(); + + await foreach (var (project, diagnostics) in results) + { + if (diagnostics.Any()) + projectsAndDiagnostics.Add(project, diagnostics); + } + + return projectsAndDiagnostics.ToImmutable(); + }, + args: fixAllContext, + fixAllContext.CancellationToken).ConfigureAwait(false); } } diff --git a/src/Workspaces/Core/Portable/CodeFixes/FixAllOccurrences/FixAllContextHelper.cs b/src/Workspaces/Core/Portable/CodeFixes/FixAllOccurrences/FixAllContextHelper.cs index 97a51241386aa..834c0d8f05cdb 100644 --- a/src/Workspaces/Core/Portable/CodeFixes/FixAllOccurrences/FixAllContextHelper.cs +++ b/src/Workspaces/Core/Portable/CodeFixes/FixAllOccurrences/FixAllContextHelper.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.CodeFixesAndRefactorings; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; @@ -64,23 +65,34 @@ public static async Task p.Language == project.Language) - .ToImmutableArray(); - - // Update the progress dialog with the count of projects to actually fix. We'll update the progress - // bar as we get all the documents in AddDocumentDiagnosticsAsync. - - progressTracker.AddItems(projectsToFix.Length); - - var diagnostics = new ConcurrentDictionary>(); - using (var _ = ArrayBuilder.GetInstance(projectsToFix.Length, out var tasks)) { - foreach (var projectToFix in projectsToFix) - tasks.Add(Task.Run(async () => await AddDocumentDiagnosticsAsync(diagnostics, projectToFix).ConfigureAwait(false), cancellationToken)); - - await Task.WhenAll(tasks).ConfigureAwait(false); - allDiagnostics = allDiagnostics.AddRange(diagnostics.SelectMany(i => i.Value)); + var projectsToFix = project.Solution.Projects + .Where(p => p.Language == project.Language) + .ToImmutableArray(); + + // Update the progress dialog with the count of projects to actually fix. We'll update the progress + // bar as we get all the documents in AddDocumentDiagnosticsAsync. + + progressTracker.AddItems(projectsToFix.Length); + + allDiagnostics = await ProducerConsumer>.RunParallelAsync( + source: projectsToFix, + produceItems: static async (projectToFix, callback, args, cancellationToken) => + { + using var _ = args.progressTracker.ItemCompletedScope(); + callback(await args.fixAllContext.GetAllDiagnosticsAsync(projectToFix).ConfigureAwait(false)); + }, + consumeItems: static async (results, args, cancellationToken) => + { + using var _ = ArrayBuilder.GetInstance(out var builder); + + await foreach (var diagnostics in results) + builder.AddRange(diagnostics); + + return builder.ToImmutableAndClear(); + }, + args: (fixAllContext, progressTracker), + cancellationToken).ConfigureAwait(false); } break; @@ -94,19 +106,6 @@ public static async Task> diagnostics, Project projectToFix) - { - try - { - var projectDiagnostics = await fixAllContext.GetAllDiagnosticsAsync(projectToFix).ConfigureAwait(false); - diagnostics.TryAdd(projectToFix.Id, projectDiagnostics); - } - finally - { - progressTracker.ItemCompleted(); - } - } - static async Task>> GetSpanDiagnosticsAsync( FixAllContext fixAllContext, IEnumerable>> documentsAndSpans) diff --git a/src/Workspaces/Core/Portable/CodeFixesAndRefactorings/DocumentBasedFixAllProviderHelpers.cs b/src/Workspaces/Core/Portable/CodeFixesAndRefactorings/DocumentBasedFixAllProviderHelpers.cs index 37248a1e7031f..1035dcc812517 100644 --- a/src/Workspaces/Core/Portable/CodeFixesAndRefactorings/DocumentBasedFixAllProviderHelpers.cs +++ b/src/Workspaces/Core/Portable/CodeFixesAndRefactorings/DocumentBasedFixAllProviderHelpers.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; @@ -12,7 +11,6 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Remote; using Microsoft.CodeAnalysis.Shared.Extensions; -using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; @@ -29,121 +27,111 @@ internal static class DocumentBasedFixAllProviderHelpers ImmutableArray fixAllContexts, IProgress progressTracker, string progressTrackerDescription, - Func, Task>> getFixedDocumentsAsync) + Func, Task> getFixedDocumentsAsync) where TFixAllContext : IFixAllContext { + var cancellationToken = originalFixAllContext.CancellationToken; + progressTracker.Report(CodeAnalysisProgress.Description(progressTrackerDescription)); var solution = originalFixAllContext.Solution; - // For code fixes, we have 3 pieces of work per project. Computing diagnostics, computing fixes, and applying fixes. - // For refactorings, we have 2 pieces of work per project. Computing refactorings, and applying refactorings. - var fixAllKind = originalFixAllContext.State.FixAllKind; - var workItemCount = fixAllKind == FixAllKind.CodeFix ? 3 : 2; - progressTracker.AddItems(fixAllContexts.Length * workItemCount); + // One work item for each context. + progressTracker.AddItems(fixAllContexts.Length); + + var (dirtySolution, changedRootDocumentIds) = await GetInitialUncleanedSolutionAsync().ConfigureAwait(false); + return await CleanSolutionAsync(dirtySolution, changedRootDocumentIds).ConfigureAwait(false); - using var _1 = PooledDictionary.GetInstance(out var allContextsDocIdToNewRootOrText); + async Task<(Solution dirtySolution, ImmutableArray changedRootDocumentIds)> GetInitialUncleanedSolutionAsync() { // First, iterate over all contexts, and collect all the changes for each of them. We'll be making a lot of // calls to the remote server to compute diagnostics and changes. So keep a single connection alive to it // so we never resync or recompute anything. - using var _2 = await RemoteKeepAliveSession.CreateAsync(solution, originalFixAllContext.CancellationToken).ConfigureAwait(false); - - foreach (var fixAllContext in fixAllContexts) - { - Contract.ThrowIfFalse( - fixAllContext.Scope is FixAllScope.Document or FixAllScope.Project or FixAllScope.ContainingMember or FixAllScope.ContainingType); - - // TODO: consider computing this in parallel. - var singleContextDocIdToNewRootOrText = await getFixedDocumentsAsync(fixAllContext, progressTracker).ConfigureAwait(false); - - // Note: it is safe to blindly add the dictionary for a particular context to the full dictionary. Each - // dictionary will only update documents within that context, and each context represents a distinct - // project, so these should all be distinct without collisions. However, to be very safe, we use an - // overwriting policy here to ensure nothing causes any problems here. - foreach (var kvp in singleContextDocIdToNewRootOrText) - allContextsDocIdToNewRootOrText[kvp.Key] = kvp.Value; - } + using var _ = await RemoteKeepAliveSession.CreateAsync(solution, cancellationToken).ConfigureAwait(false); + + return await ProducerConsumer<(DocumentId documentId, (SyntaxNode? node, SourceText? text))>.RunParallelAsync( + source: fixAllContexts, + produceItems: static async (fixAllContext, callback, args, cancellationToken) => + { + // Update our progress for each fixAllContext we process. + using var _ = args.progressTracker.ItemCompletedScope(); + + Contract.ThrowIfFalse( + fixAllContext.Scope is FixAllScope.Document or FixAllScope.Project or FixAllScope.ContainingMember or FixAllScope.ContainingType); + + await args.getFixedDocumentsAsync(fixAllContext, callback).ConfigureAwait(false); + }, + consumeItems: static async (stream, args, cancellationToken) => + { + var currentSolution = args.solution; + using var _ = ArrayBuilder.GetInstance(out var changedRootDocumentIds); + + // Next, go and insert those all into the solution so all the docs in this particular project + // point at the new trees (or text). At this point though, the trees have not been cleaned up. + // We don't cleanup the documents as they are created, or one at a time as we add them, as that + // would cause us to run cleanup on N different solution forks (which would be very expensive). + // Instead, by adding all the changed documents to one solution, and then cleaning *those* we + // only perform cleanup semantics on one forked solution. + await foreach (var (docId, (newRoot, newText)) in stream) + { + // If we produced a new root (as opposed to new text), keep track of that doc-id so that we + // can clean this doc later. + if (newRoot != null) + changedRootDocumentIds.Add(docId); + + currentSolution = newRoot != null + ? currentSolution.WithDocumentSyntaxRoot(docId, newRoot) + : currentSolution.WithDocumentText(docId, newText!); + } + + return (currentSolution, changedRootDocumentIds.ToImmutableAndClear()); + }, + args: (getFixedDocumentsAsync, progressTracker, solution), + cancellationToken).ConfigureAwait(false); } - // Next, go and insert those all into the solution so all the docs in this particular project point at - // the new trees (or text). At this point though, the trees have not been cleaned up. We don't cleanup - // the documents as they are created, or one at a time as we add them, as that would cause us to run - // cleanup on N different solution forks (which would be very expensive). Instead, by adding all the - // changed documents to one solution, and then cleaning *those* we only perform cleanup semantics on one - // forked solution. - var currentSolution = solution; - foreach (var (docId, (newRoot, newText)) in allContextsDocIdToNewRootOrText) + async Task CleanSolutionAsync(Solution dirtySolution, ImmutableArray changedRootDocumentIds) { - currentSolution = newRoot != null - ? currentSolution.WithDocumentSyntaxRoot(docId, newRoot) - : currentSolution.WithDocumentText(docId, newText!); - } + if (changedRootDocumentIds.IsEmpty) + return dirtySolution; + + // Clear out the progress so far. We're starting a new progress pass for the final cleanup. + progressTracker.Report(CodeAnalysisProgress.Clear()); + progressTracker.Report(CodeAnalysisProgress.AddIncompleteItems(changedRootDocumentIds.Length, WorkspacesResources.Running_code_cleanup_on_fixed_documents)); - { // We're about to making a ton of calls to this new solution, including expensive oop calls to get up to // date compilations, skeletons and SG docs. Create and pin this solution so that all remote calls operate // on the same fork and do not cause the forked solution to be created and dropped repeatedly. - using var _2 = await RemoteKeepAliveSession.CreateAsync(currentSolution, originalFixAllContext.CancellationToken).ConfigureAwait(false); - - var finalSolution = await CleanupAndApplyChangesAsync( - progressTracker, - currentSolution, - allContextsDocIdToNewRootOrText, - originalFixAllContext.CancellationToken).ConfigureAwait(false); + using var _ = await RemoteKeepAliveSession.CreateAsync(dirtySolution, cancellationToken).ConfigureAwait(false); - return finalSolution; - } - } - - /// - /// Take all the fixed documents and format/simplify/clean them up (if the language supports that), and take the - /// resultant text and apply it to the solution. If the language doesn't support cleanup, then just take the - /// given text and apply that instead. - /// - private static async Task CleanupAndApplyChangesAsync( - IProgress progressTracker, - Solution currentSolution, - Dictionary docIdToNewRootOrText, - CancellationToken cancellationToken) - { - using var _1 = progressTracker.ItemCompletedScope(); - - if (docIdToNewRootOrText.Count > 0) - { - - // Next, go and cleanup any trees we inserted. Once we clean the document, we get the text of it and insert - // that back into the final solution. This way we can release both the original fixed tree, and the cleaned - // tree (both of which can be much more expensive than just text). + // Next, go and cleanup any trees we inserted. Once we clean the document, we get the text of it and insert that + // back into the final solution. This way we can release both the original fixed tree, and the cleaned tree + // (both of which can be much more expensive than just text). // - // Do this in parallel across all the documents that were fixed. - - using var _2 = ArrayBuilder>.GetInstance(out var tasks); - foreach (var (docId, (newRoot, _)) in docIdToNewRootOrText) - { - if (newRoot != null) - tasks.Add(GetCleanedDocumentAsync(currentSolution.GetRequiredDocument(docId), cancellationToken)); - } - - await Task.WhenAll(tasks).ConfigureAwait(false); - - // Finally, apply the cleaned documents to the solution. - foreach (var task in tasks) - { - var (docId, cleanedText) = await task.ConfigureAwait(false); - currentSolution = currentSolution.WithDocumentText(docId, cleanedText); - } - } - - return currentSolution; - - static async Task<(DocumentId docId, SourceText sourceText)> GetCleanedDocumentAsync(Document dirtyDocument, CancellationToken cancellationToken) - { - await Task.Yield(); - - var cleanedDocument = await PostProcessCodeAction.Instance.PostProcessChangesAsync(dirtyDocument, cancellationToken).ConfigureAwait(false); - var cleanedText = await cleanedDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - return (dirtyDocument.Id, cleanedText); + // Do this in parallel across all the documents that were fixed and resulted in a new tree (as opposed to new + // text). + return await ProducerConsumer<(DocumentId docId, SourceText sourceText)>.RunParallelAsync( + source: changedRootDocumentIds, + produceItems: static async (documentId, callback, args, cancellationToken) => + { + using var _ = args.progressTracker.ItemCompletedScope(); + + var dirtyDocument = args.dirtySolution.GetRequiredDocument(documentId); + var cleanedDocument = await PostProcessCodeAction.Instance.PostProcessChangesAsync(dirtyDocument, cancellationToken).ConfigureAwait(false); + var cleanedText = await cleanedDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + callback((dirtyDocument.Id, cleanedText)); + }, + consumeItems: static async (results, args, cancellationToken) => + { + // Finally, apply the cleaned documents to the solution. + var finalSolution = args.dirtySolution; + await foreach (var (docId, cleanedText) in results) + finalSolution = finalSolution.WithDocumentText(docId, cleanedText); + + return finalSolution; + }, + args: (dirtySolution, progressTracker), + cancellationToken).ConfigureAwait(false); } } diff --git a/src/Workspaces/Core/Portable/CodeRefactorings/FixAllOccurences/DocumentBasedFixAllProvider.cs b/src/Workspaces/Core/Portable/CodeRefactorings/FixAllOccurences/DocumentBasedFixAllProvider.cs index 6fd6c060e18ff..7bb3a4c6c2526 100644 --- a/src/Workspaces/Core/Portable/CodeRefactorings/FixAllOccurences/DocumentBasedFixAllProvider.cs +++ b/src/Workspaces/Core/Portable/CodeRefactorings/FixAllOccurences/DocumentBasedFixAllProvider.cs @@ -73,10 +73,12 @@ public sealed override IEnumerable GetSupportedFixAllScopes() fixAllContext.GetDefaultFixAllTitle(), fixAllContext, FixAllContextsHelperAsync); private Task FixAllContextsHelperAsync(FixAllContext originalFixAllContext, ImmutableArray fixAllContexts) - => DocumentBasedFixAllProviderHelpers.FixAllContextsAsync(originalFixAllContext, fixAllContexts, - originalFixAllContext.Progress, - this.GetFixAllTitle(originalFixAllContext), - GetFixedDocumentsAsync); + => DocumentBasedFixAllProviderHelpers.FixAllContextsAsync( + originalFixAllContext, + fixAllContexts, + originalFixAllContext.Progress, + this.GetFixAllTitle(originalFixAllContext), + GetFixedDocumentsAsync); /// /// Attempts to apply fix all operations returning, for each updated document, either @@ -84,48 +86,33 @@ public sealed override IEnumerable GetSupportedFixAllScopes() /// them, and are used to perform a final cleanup pass for formatting/simplication/etc. Text is returned for /// documents that don't support syntax. /// - private async Task> GetFixedDocumentsAsync( - FixAllContext fixAllContext, IProgress progressTracker) + private async Task GetFixedDocumentsAsync( + FixAllContext fixAllContext, Action<(DocumentId documentId, (SyntaxNode? node, SourceText? text))> callback) { Contract.ThrowIfFalse(fixAllContext.Scope is FixAllScope.Document or FixAllScope.Project or FixAllScope.ContainingMember or FixAllScope.ContainingType); var cancellationToken = fixAllContext.CancellationToken; - using var _1 = progressTracker.ItemCompletedScope(); - using var _2 = ArrayBuilder>.GetInstance(out var tasks); - - var docIdToNewRootOrText = new Dictionary(); - // Process all documents in parallel to get the change for each doc. var documentsAndSpansToFix = await fixAllContext.GetFixAllSpansAsync(cancellationToken).ConfigureAwait(false); - foreach (var (document, spans) in documentsAndSpansToFix) - { - tasks.Add(Task.Run(async () => + await RoslynParallel.ForEachAsync( + source: documentsAndSpansToFix, + cancellationToken, + async (tuple, cancellationToken) => { + var (document, spans) = tuple; var newDocument = await this.FixAllAsync(fixAllContext, document, spans).ConfigureAwait(false); if (newDocument == null || newDocument == document) - return default; + return; // For documents that support syntax, grab the tree so that we can clean it up later. If it's a // language that doesn't support that, then just grab the text. var node = newDocument.SupportsSyntaxTree ? await newDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false) : null; var text = newDocument.SupportsSyntaxTree ? null : await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - return (document.Id, (node, text)); - }, cancellationToken)); - } - - await Task.WhenAll(tasks).ConfigureAwait(false); - - foreach (var task in tasks) - { - var (docId, nodeOrText) = await task.ConfigureAwait(false); - if (docId != null) - docIdToNewRootOrText[docId] = nodeOrText; - } - - return docIdToNewRootOrText; + callback((document.Id, (node, text))); + }).ConfigureAwait(false); } } diff --git a/src/Workspaces/Core/Portable/Diagnostics/DiagnosticAnalysisResultBuilder.cs b/src/Workspaces/Core/Portable/Diagnostics/DiagnosticAnalysisResultBuilder.cs index 680b935e03483..883c28be7be1a 100644 --- a/src/Workspaces/Core/Portable/Diagnostics/DiagnosticAnalysisResultBuilder.cs +++ b/src/Workspaces/Core/Portable/Diagnostics/DiagnosticAnalysisResultBuilder.cs @@ -30,11 +30,11 @@ internal struct DiagnosticAnalysisResultBuilder(Project project, VersionStamp ve private List? _lazyOthers = null; - public readonly ImmutableHashSet DocumentIds => _lazyDocumentsWithDiagnostics == null ? [] : _lazyDocumentsWithDiagnostics.ToImmutableHashSet(); + public readonly ImmutableHashSet DocumentIds => _lazyDocumentsWithDiagnostics == null ? [] : [.. _lazyDocumentsWithDiagnostics]; public readonly ImmutableDictionary> SyntaxLocals => Convert(_lazySyntaxLocals); public readonly ImmutableDictionary> SemanticLocals => Convert(_lazySemanticLocals); public readonly ImmutableDictionary> NonLocals => Convert(_lazyNonLocals); - public readonly ImmutableArray Others => _lazyOthers == null ? [] : _lazyOthers.ToImmutableArray(); + public readonly ImmutableArray Others => _lazyOthers == null ? [] : [.. _lazyOthers]; public void AddExternalSyntaxDiagnostics(DocumentId documentId, IEnumerable diagnostics) { diff --git a/src/Workspaces/Core/Portable/Diagnostics/Extensions.cs b/src/Workspaces/Core/Portable/Diagnostics/Extensions.cs index 6128338a0a43c..148b1280ab343 100644 --- a/src/Workspaces/Core/Portable/Diagnostics/Extensions.cs +++ b/src/Workspaces/Core/Portable/Diagnostics/Extensions.cs @@ -15,6 +15,7 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Workspaces.Diagnostics; using Roslyn.Utilities; @@ -372,62 +373,60 @@ private static async Task> GetPragmaSuppressionAnalyz var analyzers = documentAnalysisScope?.Analyzers ?? compilationWithAnalyzers.Analyzers; var suppressionAnalyzer = analyzers.OfType().FirstOrDefault(); if (suppressionAnalyzer == null) - { return []; - } if (documentAnalysisScope != null) { if (documentAnalysisScope.TextDocument is not Document document) - { return []; - } using var _ = ArrayBuilder.GetInstance(out var diagnosticsBuilder); - await AnalyzeDocumentAsync(suppressionAnalyzer, document, documentAnalysisScope.Span, diagnosticsBuilder.Add).ConfigureAwait(false); + await AnalyzeDocumentAsync( + compilationWithAnalyzers, analyzerInfoCache, suppressionAnalyzer, + document, documentAnalysisScope.Span, diagnosticsBuilder.Add, cancellationToken).ConfigureAwait(false); return diagnosticsBuilder.ToImmutableAndClear(); } else { if (compilationWithAnalyzers.AnalysisOptions.ConcurrentAnalysis) { - var bag = new ConcurrentBag(); - using var _ = ArrayBuilder.GetInstance(project.DocumentIds.Count, out var tasks); - foreach (var document in project.Documents) - { - tasks.Add(AnalyzeDocumentAsync(suppressionAnalyzer, document, span: null, bag.Add)); - } - - foreach (var document in await project.GetSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false)) - { - tasks.Add(AnalyzeDocumentAsync(suppressionAnalyzer, document, span: null, bag.Add)); - } - - await Task.WhenAll(tasks).ConfigureAwait(false); - return bag.ToImmutableArray(); + return await ProducerConsumer.RunParallelAsync( + source: project.GetAllRegularAndSourceGeneratedDocumentsAsync(cancellationToken), + produceItems: static async (document, callback, args, cancellationToken) => + { + await AnalyzeDocumentAsync( + args.compilationWithAnalyzers, args.analyzerInfoCache, args.suppressionAnalyzer, + document, span: null, callback, cancellationToken).ConfigureAwait(false); + }, + args: (compilationWithAnalyzers, analyzerInfoCache, suppressionAnalyzer), + cancellationToken).ConfigureAwait(false); } else { using var _ = ArrayBuilder.GetInstance(out var diagnosticsBuilder); - foreach (var document in project.Documents) + await foreach (var document in project.GetAllRegularAndSourceGeneratedDocumentsAsync(cancellationToken)) { - await AnalyzeDocumentAsync(suppressionAnalyzer, document, span: null, diagnosticsBuilder.Add).ConfigureAwait(false); - } - - foreach (var document in await project.GetSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false)) - { - await AnalyzeDocumentAsync(suppressionAnalyzer, document, span: null, diagnosticsBuilder.Add).ConfigureAwait(false); + await AnalyzeDocumentAsync( + compilationWithAnalyzers, analyzerInfoCache, suppressionAnalyzer, + document, span: null, diagnosticsBuilder.Add, cancellationToken).ConfigureAwait(false); } return diagnosticsBuilder.ToImmutableAndClear(); } } - async Task AnalyzeDocumentAsync(IPragmaSuppressionsAnalyzer suppressionAnalyzer, Document document, TextSpan? span, Action reportDiagnostic) + static async Task AnalyzeDocumentAsync( + CompilationWithAnalyzers compilationWithAnalyzers, + DiagnosticAnalyzerInfoCache analyzerInfoCache, + IPragmaSuppressionsAnalyzer suppressionAnalyzer, + Document document, + TextSpan? span, + Action reportDiagnostic, + CancellationToken cancellationToken) { var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - await suppressionAnalyzer.AnalyzeAsync(semanticModel, span, compilationWithAnalyzers, - analyzerInfoCache.GetDiagnosticDescriptors, reportDiagnostic, cancellationToken).ConfigureAwait(false); + await suppressionAnalyzer.AnalyzeAsync( + semanticModel, span, compilationWithAnalyzers, analyzerInfoCache.GetDiagnosticDescriptors, reportDiagnostic, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/Workspaces/Core/Portable/Editing/ImportAdderService.cs b/src/Workspaces/Core/Portable/Editing/ImportAdderService.cs index 555dfa7b6826f..6615ecbd563fe 100644 --- a/src/Workspaces/Core/Portable/Editing/ImportAdderService.cs +++ b/src/Workspaces/Core/Portable/Editing/ImportAdderService.cs @@ -229,7 +229,7 @@ private async Task AddImportDirectivesFromSymbolAnnotationsAsync( var importContainer = addImportsService.GetImportContainer(root, context, importToSyntax.First().Value, options); // Now remove any imports we think can cause conflicts in that container. - var safeImportsToAdd = GetSafeToAddImports(importToSyntax.Keys.ToImmutableArray(), importContainer, model, cancellationToken); + var safeImportsToAdd = GetSafeToAddImports([.. importToSyntax.Keys], importContainer, model, cancellationToken); var importsToAdd = importToSyntax.Where(kvp => safeImportsToAdd.Contains(kvp.Key)).Select(kvp => kvp.Value).ToImmutableArray(); if (importsToAdd.Length == 0) diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindLiterals/FindLiteralsSearchEngine.cs b/src/Workspaces/Core/Portable/FindSymbols/FindLiterals/FindLiteralsSearchEngine.cs index d489d306ed371..edc98a9a92721 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindLiterals/FindLiteralsSearchEngine.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindLiterals/FindLiteralsSearchEngine.cs @@ -87,21 +87,22 @@ private async Task FindReferencesWorkerAsync(CancellationToken cancellationToken var count = _solution.Projects.SelectMany(p => p.DocumentIds).Count(); await _progressTracker.AddItemsAsync(count, cancellationToken).ConfigureAwait(false); - foreach (var project in _solution.Projects) - { - cancellationToken.ThrowIfCancellationRequested(); + await RoslynParallel.ForEachAsync( + source: SelectManyAsync(_solution.Projects, p => p.GetAllRegularAndSourceGeneratedDocumentsAsync(cancellationToken)), + cancellationToken, + ProcessDocumentAsync).ConfigureAwait(false); - var documentTasks = new List(); - foreach (var document in await project.GetAllRegularAndSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false)) + static async IAsyncEnumerable SelectManyAsync(IEnumerable source, Func> selector) + { + foreach (var item in source) { - documentTasks.Add(ProcessDocumentAsync(document, cancellationToken)); + await foreach (var result in selector(item)) + yield return result; } - - await Task.WhenAll(documentTasks).ConfigureAwait(false); } } - private async Task ProcessDocumentAsync(Document document, CancellationToken cancellationToken) + private async ValueTask ProcessDocumentAsync(Document document, CancellationToken cancellationToken) { try { diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/BaseTypeFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/BaseTypeFinder.cs index 5ac66337bfae1..6ece9e7edac9e 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/BaseTypeFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/BaseTypeFinder.cs @@ -15,10 +15,10 @@ internal static partial class BaseTypeFinder public static ImmutableArray FindBaseTypesAndInterfaces(INamedTypeSymbol type) => FindBaseTypes(type).AddRange(type.AllInterfaces); - public static async ValueTask> FindOverriddenAndImplementedMembersAsync( + public static ImmutableArray FindOverriddenAndImplementedMembers( ISymbol symbol, Solution solution, CancellationToken cancellationToken) { - var results = ArrayBuilder.GetInstance(); + using var _ = ArrayBuilder.GetInstance(out var results); // This is called for all: class, struct or interface member. results.AddRange(symbol.ExplicitOrImplicitInterfaceImplementations()); @@ -31,7 +31,7 @@ public static async ValueTask> FindOverriddenAndImplemen cancellationToken.ThrowIfCancellationRequested(); // Add to results overridden members only. Do not add hidden members. - if (await SymbolFinder.IsOverrideAsync(solution, symbol, member, cancellationToken).ConfigureAwait(false)) + if (SymbolFinder.IsOverride(solution, symbol, member)) { results.Add(member); @@ -64,7 +64,8 @@ public static async ValueTask> FindOverriddenAndImplemen } // Remove duplicates from interface implementations before adding their projects. - return results.ToImmutableAndFree().Distinct(); + results.RemoveDuplicates(); + return results.ToImmutableAndClear(); } private static ImmutableArray FindBaseTypes(INamedTypeSymbol type) diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentProjectsFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentProjectsFinder.cs index 225da746a1431..380a3c3bf55f0 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentProjectsFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentProjectsFinder.cs @@ -35,7 +35,7 @@ public static async Task> GetDependentProjectsAsync( { // namespaces are visible in all projects. if (symbols.Any(static s => s.Kind == SymbolKind.Namespace)) - return projects.ToImmutableArray(); + return [.. projects]; var dependentProjects = await GetDependentProjectsWorkerAsync(solution, symbols, cancellationToken).ConfigureAwait(false); return dependentProjects.WhereAsArray(projects.Contains); @@ -84,7 +84,7 @@ private static async Task> GetDependentProjectsWorkerAsy result.AddRange(filteredProjects.Select(p => p.project)); } - return result.ToImmutableArray(); + return [.. result]; } /// @@ -145,7 +145,7 @@ private static async Task> GetDependentProjectsWorkerAsy // further submissions can bind to them. await AddSubmissionDependentProjectsAsync(solution, symbolOrigination.sourceProject, dependentProjects, cancellationToken).ConfigureAwait(false); - return dependentProjects.ToImmutableArray(); + return [.. dependentProjects]; } private static async Task AddSubmissionDependentProjectsAsync( diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentTypeFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentTypeFinder.cs index 79f7ef5d7893a..9e360bd3f47cb 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentTypeFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentTypeFinder.cs @@ -136,7 +136,7 @@ private static async Task> DescendInheritanceTr await DescendInheritanceTreeInProjectAsync(project).ConfigureAwait(false); } - return result.ToImmutableArray(); + return [.. result]; async Task DescendInheritanceTreeInProjectAsync(Project project) { @@ -472,7 +472,7 @@ private static ImmutableArray OrderTopologically( index++; } - return projectsToExamine.OrderBy((p1, p2) => order[p1.Id] - order[p2.Id]).ToImmutableArray(); + return [.. projectsToExamine.OrderBy((p1, p2) => order[p1.Id] - order[p2.Id])]; } private static ImmutableArray GetProjectsToExamineWorker( diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferenceCache.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferenceCache.cs index aaad258c3e4e3..ba3cdc5889a6e 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferenceCache.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferenceCache.cs @@ -32,17 +32,27 @@ public static async ValueTask GetCacheAsync(Document documen static async Task ComputeCacheAsync(Document document, CancellationToken cancellationToken) { + var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + // Find-Refs is not impacted by nullable types at all. So get a nullable-disabled semantic model to avoid // unnecessary costs while binding. var model = await document.GetRequiredNullableDisabledSemanticModelAsync(cancellationToken).ConfigureAwait(false); var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - return new(document, model, root); + + // It's very costly to walk an entire tree. So if the tree is simple and doesn't contain + // any unicode escapes in it, then we do simple string matching to find the tokens. + var index = await SyntaxTreeIndex.GetRequiredIndexAsync(document, cancellationToken).ConfigureAwait(false); + + return new(document, text, model, root, index); } } public readonly Document Document; + public readonly SourceText Text; public readonly SemanticModel SemanticModel; public readonly SyntaxNode Root; + public readonly ISyntaxFactsService SyntaxFacts; + public readonly SyntaxTreeIndex SyntaxTreeIndex; private readonly ConcurrentDictionary _symbolInfoCache = []; private readonly ConcurrentDictionary> _identifierCache; @@ -50,11 +60,16 @@ static async Task ComputeCacheAsync(Document document, Cance private ImmutableHashSet? _aliasNameSet; private ImmutableArray _constructorInitializerCache; - private FindReferenceCache(Document document, SemanticModel semanticModel, SyntaxNode root) + private FindReferenceCache( + Document document, SourceText text, SemanticModel semanticModel, SyntaxNode root, SyntaxTreeIndex syntaxTreeIndex) { Document = document; + Text = text; SemanticModel = semanticModel; Root = root; + SyntaxTreeIndex = syntaxTreeIndex; + SyntaxFacts = document.GetRequiredLanguageService(); + _identifierCache = new(comparer: semanticModel.Language switch { LanguageNames.VisualBasic => StringComparer.OrdinalIgnoreCase, @@ -81,10 +96,8 @@ public SymbolInfo GetSymbolInfo(SyntaxNode node, CancellationToken cancellationT return null; } - public async ValueTask> FindMatchingIdentifierTokensAsync( - Document document, - string identifier, - CancellationToken cancellationToken) + public ImmutableArray FindMatchingIdentifierTokens( + string identifier, CancellationToken cancellationToken) { if (identifier == "") { @@ -99,103 +112,82 @@ public async ValueTask> FindMatchingIdentifierTokens if (_identifierCache.TryGetValue(identifier, out var result)) return result; - // It's very costly to walk an entire tree. So if the tree is simple and doesn't contain - // any unicode escapes in it, then we do simple string matching to find the tokens. - var info = await SyntaxTreeIndex.GetRequiredIndexAsync(document, cancellationToken).ConfigureAwait(false); - // If this document doesn't even contain this identifier (escaped or non-escaped) we don't have to search it at all. - if (!info.ProbablyContainsIdentifier(identifier)) + if (!this.SyntaxTreeIndex.ProbablyContainsIdentifier(identifier)) return []; - return await ComputeAndCacheTokensAsync(this, document, identifier, info, cancellationToken).ConfigureAwait(false); + // If the identifier was escaped in the file then we'll have to do a more involved search that actually + // walks the root and checks all identifier tokens. + // + // otherwise, we can use the text of the document to quickly find candidates and test those directly. + return this.SyntaxTreeIndex.ProbablyContainsEscapedIdentifier(identifier) + ? _identifierCache.GetOrAdd(identifier, identifier => FindMatchingIdentifierTokensFromTree(identifier, cancellationToken)) + : _identifierCache.GetOrAdd(identifier, _ => FindMatchingIdentifierTokensFromText(identifier, cancellationToken)); + } + + private bool IsMatch(string identifier, SyntaxToken token) + => !token.IsMissing && this.SyntaxFacts.IsIdentifier(token) && this.SyntaxFacts.TextMatch(token.ValueText, identifier); + + private ImmutableArray FindMatchingIdentifierTokensFromTree( + string identifier, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var result); + using var obj = SharedPools.Default>().GetPooledObject(); + + var stack = obj.Object; + stack.Push(this.Root); - static async ValueTask> ComputeAndCacheTokensAsync( - FindReferenceCache cache, Document document, string identifier, SyntaxTreeIndex info, CancellationToken cancellationToken) + while (stack.TryPop(out var current)) { - var syntaxFacts = document.GetRequiredLanguageService(); - var root = await cache.SemanticModel.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - - // If the identifier was escaped in the file then we'll have to do a more involved search that actually - // walks the root and checks all identifier tokens. - // - // otherwise, we can use the text of the document to quickly find candidates and test those directly. - if (info.ProbablyContainsEscapedIdentifier(identifier)) + cancellationToken.ThrowIfCancellationRequested(); + if (current.IsNode) { - return cache._identifierCache.GetOrAdd( - identifier, _ => FindMatchingIdentifierTokensFromTree(syntaxFacts, identifier, root)); + foreach (var child in current.AsNode()!.ChildNodesAndTokens().Reverse()) + stack.Push(child); } - else + else if (current.IsToken) { - var text = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - return cache._identifierCache.GetOrAdd( - identifier, _ => FindMatchingIdentifierTokensFromText(syntaxFacts, identifier, root, text, cancellationToken)); - } - } - - static bool IsMatch(ISyntaxFactsService syntaxFacts, string identifier, SyntaxToken token) - => !token.IsMissing && syntaxFacts.IsIdentifier(token) && syntaxFacts.TextMatch(token.ValueText, identifier); - - static ImmutableArray FindMatchingIdentifierTokensFromTree( - ISyntaxFactsService syntaxFacts, string identifier, SyntaxNode root) - { - using var _ = ArrayBuilder.GetInstance(out var result); - using var obj = SharedPools.Default>().GetPooledObject(); - - var stack = obj.Object; - stack.Push(root); + var token = current.AsToken(); + if (IsMatch(identifier, token)) + result.Add(token); - while (stack.TryPop(out var current)) - { - if (current.IsNode) - { - foreach (var child in current.AsNode()!.ChildNodesAndTokens().Reverse()) - stack.Push(child); - } - else if (current.IsToken) + if (token.HasStructuredTrivia) { - var token = current.AsToken(); - if (IsMatch(syntaxFacts, identifier, token)) - result.Add(token); - - if (token.HasStructuredTrivia) + // structured trivia can only be leading trivia + foreach (var trivia in token.LeadingTrivia) { - // structured trivia can only be leading trivia - foreach (var trivia in token.LeadingTrivia) - { - if (trivia.HasStructure) - stack.Push(trivia.GetStructure()!); - } + if (trivia.HasStructure) + stack.Push(trivia.GetStructure()!); } } } - - return result.ToImmutableAndClear(); } - static ImmutableArray FindMatchingIdentifierTokensFromText( - ISyntaxFactsService syntaxFacts, string identifier, SyntaxNode root, SourceText sourceText, CancellationToken cancellationToken) - { - using var _ = ArrayBuilder.GetInstance(out var result); + return result.ToImmutableAndClear(); + } - var index = 0; - while ((index = sourceText.IndexOf(identifier, index, syntaxFacts.IsCaseSensitive)) >= 0) - { - cancellationToken.ThrowIfCancellationRequested(); + private ImmutableArray FindMatchingIdentifierTokensFromText( + string identifier, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var result); - var token = root.FindToken(index, findInsideTrivia: true); - var span = token.Span; - if (span.Start == index && span.Length == identifier.Length && IsMatch(syntaxFacts, identifier, token)) - result.Add(token); + var index = 0; + while ((index = this.Text.IndexOf(identifier, index, this.SyntaxFacts.IsCaseSensitive)) >= 0) + { + cancellationToken.ThrowIfCancellationRequested(); - var nextIndex = index + identifier.Length; - nextIndex = Math.Max(nextIndex, token.SpanStart); - index = nextIndex; - } + var token = this.Root.FindToken(index, findInsideTrivia: true); + var span = token.Span; + if (span.Start == index && span.Length == identifier.Length && IsMatch(identifier, token)) + result.Add(token); - return result.ToImmutableAndClear(); + var nextIndex = index + identifier.Length; + nextIndex = Math.Max(nextIndex, token.SpanStart); + index = nextIndex; } - } + return result.ToImmutableAndClear(); + } public IEnumerable GetConstructorInitializerTokens( ISyntaxFactsService syntaxFacts, SyntaxNode root, CancellationToken cancellationToken) { diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.BidirectionalSymbolSet.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.BidirectionalSymbolSet.cs index c877f21a1ee4f..cf11c3110e238 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.BidirectionalSymbolSet.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.BidirectionalSymbolSet.cs @@ -42,7 +42,7 @@ public BidirectionalSymbolSet( } public override ImmutableArray GetAllSymbols() - => _allSymbols.ToImmutableArray(); + => [.. _allSymbols]; public override async Task InheritanceCascadeAsync(Project project, CancellationToken cancellationToken) { diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.NonCascadingSymbolSet.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.NonCascadingSymbolSet.cs index a6778eb0608a7..2463a1d7033dc 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.NonCascadingSymbolSet.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.NonCascadingSymbolSet.cs @@ -16,7 +16,7 @@ internal partial class FindReferencesSearchEngine /// private sealed class NonCascadingSymbolSet(FindReferencesSearchEngine engine, MetadataUnifyingSymbolHashSet searchSymbols) : SymbolSet(engine) { - private readonly ImmutableArray _symbols = searchSymbols.ToImmutableArray(); + private readonly ImmutableArray _symbols = [.. searchSymbols]; public override ImmutableArray GetAllSymbols() => _symbols; diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.UnidirectionalSymbolSet.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.UnidirectionalSymbolSet.cs index 9e87a98d6648e..9e15a2d7db461 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.UnidirectionalSymbolSet.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.UnidirectionalSymbolSet.cs @@ -40,7 +40,7 @@ public override ImmutableArray GetAllSymbols() var result = new MetadataUnifyingSymbolHashSet(); result.AddRange(_upSymbols); result.AddRange(initialSymbols); - return result.ToImmutableArray(); + return [.. result]; } public override async Task InheritanceCascadeAsync(Project project, CancellationToken cancellationToken) diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.cs index 8d788e83130bb..a80669feb1155 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.FindSymbols.Finders; @@ -18,10 +19,10 @@ namespace Microsoft.CodeAnalysis.FindSymbols; +using Reference = (SymbolGroup group, ISymbol symbol, ReferenceLocation location); + internal partial class FindReferencesSearchEngine { - private static readonly ObjectPool s_metadataUnifyingSymbolHashSetPool = new(() => []); - private readonly Solution _solution; private readonly IImmutableSet? _documents; private readonly ImmutableArray _finders; @@ -29,11 +30,6 @@ internal partial class FindReferencesSearchEngine private readonly IStreamingFindReferencesProgress _progress; private readonly FindReferencesSearchOptions _options; - /// - /// Scheduler to run our tasks on. If we're in mode, we'll - /// run all our tasks concurrently. Otherwise, we will run them serially using - /// - private readonly TaskScheduler _scheduler; private static readonly TaskScheduler s_exclusiveScheduler = new ConcurrentExclusiveSchedulerPair().ExclusiveScheduler; /// @@ -57,83 +53,108 @@ public FindReferencesSearchEngine( _options = options; _progressTracker = progress.ProgressTracker; - - // If we're an explicit invocation, just defer to the threadpool to execute all our work in parallel to get - // things done as quickly as possible. If we're running implicitly, then use a - // ConcurrentExclusiveSchedulerPair's exclusive scheduler as that's the most built-in way in the TPL to get - // will run things serially. - _scheduler = _options.Explicit ? TaskScheduler.Default : s_exclusiveScheduler; } + /// + /// Options to control the parallelism of the search. If we're in mode, we'll run all our tasks concurrently. Otherwise, we will + /// run them serially using + /// + private ParallelOptions GetParallelOptions(CancellationToken cancellationToken) + => new() + { + CancellationToken = cancellationToken, + // If we're an explicit invocation, just defer to the threadpool to execute all our work in parallel to get + // things done as quickly as possible. If we're running implicitly, then use a exclusive scheduler as + // that's the most built-in way in the TPL to get will run things serially. + TaskScheduler = _options.Explicit ? TaskScheduler.Default : s_exclusiveScheduler, + }; + public Task FindReferencesAsync(ISymbol symbol, CancellationToken cancellationToken) => FindReferencesAsync([symbol], cancellationToken); public async Task FindReferencesAsync( ImmutableArray symbols, CancellationToken cancellationToken) { - var unifiedSymbols = new MetadataUnifyingSymbolHashSet(); - unifiedSymbols.AddRange(symbols); - await _progress.OnStartedAsync(cancellationToken).ConfigureAwait(false); try { - var disposable = await _progressTracker.AddSingleItemAsync(cancellationToken).ConfigureAwait(false); - await using var _ = disposable.ConfigureAwait(false); + await ProducerConsumer.RunAsync( + ProducerConsumerOptions.SingleReaderOptions, + produceItems: static (onItemFound, args, cancellationToken) => args.@this.PerformSearchAsync(args.symbols, onItemFound, cancellationToken), + consumeItems: static async (references, args, cancellationToken) => await args.@this._progress.OnReferencesFoundAsync(references, cancellationToken).ConfigureAwait(false), + (@this: this, symbols), + cancellationToken).ConfigureAwait(false); + } + finally + { + await _progress.OnCompletedAsync(cancellationToken).ConfigureAwait(false); + } + } - // Create the initial set of symbols to search for. As we walk the appropriate projects in the solution - // we'll expand this set as we discover new symbols to search for in each project. - var symbolSet = await SymbolSet.CreateAsync( - this, unifiedSymbols, includeImplementationsThroughDerivedTypes: true, cancellationToken).ConfigureAwait(false); + private async Task PerformSearchAsync( + ImmutableArray symbols, Action onReferenceFound, CancellationToken cancellationToken) + { + var unifiedSymbols = new MetadataUnifyingSymbolHashSet(); + unifiedSymbols.AddRange(symbols); - // Report the initial set of symbols to the caller. - var allSymbols = symbolSet.GetAllSymbols(); - await ReportGroupsAsync(allSymbols, cancellationToken).ConfigureAwait(false); + var disposable = await _progressTracker.AddSingleItemAsync(cancellationToken).ConfigureAwait(false); + await using var _ = disposable.ConfigureAwait(false); - // Determine the set of projects we actually have to walk to find results in. If the caller provided a - // set of documents to search, we only bother with those. - var projectsToSearch = await GetProjectsToSearchAsync(allSymbols, cancellationToken).ConfigureAwait(false); + // Create the initial set of symbols to search for. As we walk the appropriate projects in the solution + // we'll expand this set as we discover new symbols to search for in each project. + var symbolSet = await SymbolSet.CreateAsync( + this, unifiedSymbols, includeImplementationsThroughDerivedTypes: true, cancellationToken).ConfigureAwait(false); - // We need to process projects in order when updating our symbol set. Say we have three projects (A, B - // and C), we cannot necessarily find inherited symbols in C until we have searched B. Importantly, - // while we're processing each project linearly to update the symbol set we're searching for, we still - // then process the projects in parallel once we know the set of symbols we're searching for in that - // project. - var dependencyGraph = _solution.GetProjectDependencyGraph(); - await _progressTracker.AddItemsAsync(projectsToSearch.Length, cancellationToken).ConfigureAwait(false); + // Report the initial set of symbols to the caller. + var allSymbols = symbolSet.GetAllSymbols(); + await ReportGroupsAsync(allSymbols, cancellationToken).ConfigureAwait(false); - using var _1 = ArrayBuilder.GetInstance(out var tasks); + // Determine the set of projects we actually have to walk to find results in. If the caller provided a + // set of documents to search, we only bother with those. + var projectsToSearch = await GetProjectsToSearchAsync(allSymbols, cancellationToken).ConfigureAwait(false); - foreach (var projectId in dependencyGraph.GetTopologicallySortedProjects(cancellationToken)) - { - var currentProject = _solution.GetRequiredProject(projectId); - if (!projectsToSearch.Contains(currentProject)) - continue; + await _progressTracker.AddItemsAsync(projectsToSearch.Length, cancellationToken).ConfigureAwait(false); - // As we walk each project, attempt to grow the search set appropriately up and down the inheritance - // hierarchy and grab a copy of the symbols to be processed. Note: this has to happen serially - // which is why we do it in this loop and not inside the concurrent project processing that happens - // below. - await symbolSet.InheritanceCascadeAsync(currentProject, cancellationToken).ConfigureAwait(false); - allSymbols = symbolSet.GetAllSymbols(); + // Pull off and start searching each project as soon as we can once we've done the inheritance cascade into it. + await RoslynParallel.ForEachAsync( + GetProjectsAndSymbolsToSearchAsync(symbolSet, projectsToSearch, cancellationToken), + GetParallelOptions(cancellationToken), + async (tuple, cancellationToken) => await ProcessProjectAsync( + tuple.project, tuple.allSymbols, onReferenceFound, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); + } - // Report any new symbols we've cascaded to to our caller. - await ReportGroupsAsync(allSymbols, cancellationToken).ConfigureAwait(false); + private async IAsyncEnumerable<(Project project, ImmutableArray allSymbols)> GetProjectsAndSymbolsToSearchAsync( + SymbolSet symbolSet, + ImmutableArray projectsToSearch, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + // We need to process projects in order when updating our symbol set. Say we have three projects (A, B + // and C), we cannot necessarily find inherited symbols in C until we have searched B. Importantly, + // while we're processing each project linearly to update the symbol set we're searching for, we still + // then process the projects in parallel once we know the set of symbols we're searching for in that + // project. + var dependencyGraph = _solution.GetProjectDependencyGraph(); + foreach (var projectId in dependencyGraph.GetTopologicallySortedProjects(cancellationToken)) + { + var currentProject = _solution.GetRequiredProject(projectId); + if (!projectsToSearch.Contains(currentProject)) + continue; + + // As we walk each project, attempt to grow the search set appropriately up and down the inheritance + // hierarchy and grab a copy of the symbols to be processed. Note: this has to happen serially + // which is why we do it in this loop and not inside the concurrent project processing that happens + // below. + await symbolSet.InheritanceCascadeAsync(currentProject, cancellationToken).ConfigureAwait(false); + var allSymbols = symbolSet.GetAllSymbols(); - tasks.Add(CreateWorkAsync(() => ProcessProjectAsync(currentProject, allSymbols, cancellationToken), cancellationToken)); - } + // Report any new symbols we've cascaded to to our caller. + await ReportGroupsAsync(allSymbols, cancellationToken).ConfigureAwait(false); - // Now, wait for all projects to complete. - await Task.WhenAll(tasks).ConfigureAwait(false); - } - finally - { - await _progress.OnCompletedAsync(cancellationToken).ConfigureAwait(false); + yield return (currentProject, allSymbols); } } - public Task CreateWorkAsync(Func createWorkAsync, CancellationToken cancellationToken) - => Task.Factory.StartNew(createWorkAsync, cancellationToken, TaskCreationOptions.None, _scheduler).Unwrap(); - /// /// Notify the caller of the engine about the definitions we've found that we're looking for. We'll only notify /// them once per symbol group, but we may have to notify about new symbols each time we expand our symbol set @@ -184,12 +205,16 @@ private Task> GetProjectsToSearchAsync( return DependentProjectsFinder.GetDependentProjectsAsync(_solution, symbols, projects, cancellationToken); } - private async Task ProcessProjectAsync(Project project, ImmutableArray allSymbols, CancellationToken cancellationToken) + private async ValueTask ProcessProjectAsync( + Project project, ImmutableArray allSymbols, Action onReferenceFound, CancellationToken cancellationToken) { using var _1 = PooledDictionary>.GetInstance(out var symbolToGlobalAliases); using var _2 = PooledDictionary.GetInstance(out var documentToSymbols); try { + // scratch hashset to place results in. Populated/inspected/cleared in inner loop. + using var _3 = PooledHashSet.GetInstance(out var foundDocuments); + await AddGlobalAliasesAsync(project, allSymbols, symbolToGlobalAliases, cancellationToken).ConfigureAwait(false); foreach (var symbol in allSymbols) @@ -198,33 +223,29 @@ private async Task ProcessProjectAsync(Project project, ImmutableArray foreach (var finder in _finders) { - var documents = await finder.DetermineDocumentsToSearchAsync( - symbol, globalAliases, project, _documents, _options, cancellationToken).ConfigureAwait(false); - - foreach (var document in documents) - { - var docSymbols = GetSymbolSet(documentToSymbols, document); - docSymbols.Add(symbol); - } - } - } + await finder.DetermineDocumentsToSearchAsync( + symbol, globalAliases, project, _documents, + StandardCallbacks.AddToHashSet, + foundDocuments, + _options, cancellationToken).ConfigureAwait(false); - using var _3 = ArrayBuilder.GetInstance(out var tasks); - foreach (var (document, docSymbols) in documentToSymbols) - { - tasks.Add(CreateWorkAsync(() => ProcessDocumentAsync( - document, docSymbols, symbolToGlobalAliases, cancellationToken), cancellationToken)); + foreach (var document in foundDocuments) + GetSymbolSet(documentToSymbols, document).Add(symbol); + + foundDocuments.Clear(); + } } - await Task.WhenAll(tasks).ConfigureAwait(false); + await RoslynParallel.ForEachAsync( + documentToSymbols, + GetParallelOptions(cancellationToken), + (kvp, cancellationToken) => + ProcessDocumentAsync(kvp.Key, kvp.Value, symbolToGlobalAliases, onReferenceFound, cancellationToken)).ConfigureAwait(false); } finally { foreach (var (_, symbols) in documentToSymbols) - { - symbols.Clear(); - s_metadataUnifyingSymbolHashSetPool.Free(symbols); - } + MetadataUnifyingSymbolHashSet.ClearAndFree(symbols); FreeGlobalAliases(symbolToGlobalAliases); @@ -232,53 +253,57 @@ private async Task ProcessProjectAsync(Project project, ImmutableArray } static MetadataUnifyingSymbolHashSet GetSymbolSet(PooledDictionary dictionary, T key) where T : notnull - { - if (!dictionary.TryGetValue(key, out var set)) - { - set = s_metadataUnifyingSymbolHashSetPool.Allocate(); - dictionary.Add(key, set); - } - - return set; - } + => dictionary.GetOrAdd(key, static _ => MetadataUnifyingSymbolHashSet.AllocateFromPool()); } private static PooledHashSet? TryGet(Dictionary> dictionary, T key) where T : notnull => dictionary.TryGetValue(key, out var set) ? set : null; - private async Task ProcessDocumentAsync( + private async ValueTask ProcessDocumentAsync( Document document, MetadataUnifyingSymbolHashSet symbols, Dictionary> symbolToGlobalAliases, + Action onReferenceFound, CancellationToken cancellationToken) { - await _progress.OnFindInDocumentStartedAsync(document, cancellationToken).ConfigureAwait(false); - - try + // We're doing to do all of our processing of this document at once. This will necessitate all the + // appropriate finders checking this document for hits. We know that in the initial pass to determine + // documents, this document was already considered a strong match (e.g. we know it contains the name of + // the symbol being searched for). As such, we're almost certainly going to have to do semantic checks + // to now see if the candidate actually matches the symbol. This will require syntax and semantics. So + // just grab those once here and hold onto them for the lifetime of this call. + var cache = await FindReferenceCache.GetCacheAsync(document, cancellationToken).ConfigureAwait(false); + + // This search almost always involves trying to find the tokens matching the name of the symbol we're looking + // for. Get the cache ready with those tokens so that kicking of N searches to search for each symbol in + // parallel doesn't cause us to compute and cache the same thing concurrently. + + // Note: cascaded symbols will normally have the same name. That's ok. The second call to + // FindMatchingIdentifierTokens with the same name will short circuit since it will already see the result of + // the prior call. + foreach (var symbol in symbols) { - // We're doing to do all of our processing of this document at once. This will necessitate all the - // appropriate finders checking this document for hits. We know that in the initial pass to determine - // documents, this document was already considered a strong match (e.g. we know it contains the name of - // the symbol being searched for). As such, we're almost certainly going to have to do semantic checks - // to now see if the candidate actually matches the symbol. This will require syntax and semantics. So - // just grab those once here and hold onto them for the lifetime of this call. - var cache = await FindReferenceCache.GetCacheAsync(document, cancellationToken).ConfigureAwait(false); - - foreach (var symbol in symbols) + if (symbol.CanBeReferencedByName) + cache.FindMatchingIdentifierTokens(symbol.Name, cancellationToken); + } + + await RoslynParallel.ForEachAsync( + symbols, + GetParallelOptions(cancellationToken), + async (symbol, cancellationToken) => { - var globalAliases = TryGet(symbolToGlobalAliases, symbol); - var state = new FindReferencesDocumentState(cache, globalAliases); + // symbolToGlobalAliases is safe to read in parallel. It is created fully before this point and is no + // longer mutated. + var state = new FindReferencesDocumentState( + cache, TryGet(symbolToGlobalAliases, symbol)); - await ProcessDocumentAsync(symbol, state).ConfigureAwait(false); - } - } - finally - { - await _progress.OnFindInDocumentCompletedAsync(document, cancellationToken).ConfigureAwait(false); - } + await ProcessDocumentAsync(symbol, state, onReferenceFound).ConfigureAwait(false); + }).ConfigureAwait(false); + + return; async Task ProcessDocumentAsync( - ISymbol symbol, FindReferencesDocumentState state) + ISymbol symbol, FindReferencesDocumentState state, Action onReferenceFound) { cancellationToken.ThrowIfCancellationRequested(); @@ -287,12 +312,18 @@ async Task ProcessDocumentAsync( // This is safe to just blindly read. We can only ever get here after the call to ReportGroupsAsync // happened. So there must be a group for this symbol in our map. var group = _symbolToGroup[symbol]; + + // Note: nearly every finder will no-op when passed a in a symbol it's not applicable to. So it's + // simple to just iterate over all of them, knowing that will quickly skip all the irrelevant ones, + // and only do interesting work on the single relevant one. foreach (var finder in _finders) { - var references = await finder.FindReferencesInDocumentAsync( - symbol, state, _options, cancellationToken).ConfigureAwait(false); - foreach (var (_, location) in references) - await _progress.OnReferenceFoundAsync(group, symbol, location, cancellationToken).ConfigureAwait(false); + await finder.FindReferencesInDocumentAsync( + symbol, state, + static (loc, tuple) => tuple.onReferenceFound((tuple.group, tuple.symbol, loc.Location)), + (group, symbol, onReferenceFound), + _options, + cancellationToken).ConfigureAwait(false); } } } @@ -311,24 +342,13 @@ private async Task AddGlobalAliasesAsync( var aliases = await finder.DetermineGlobalAliasesAsync( symbol, project, cancellationToken).ConfigureAwait(false); if (aliases.Length > 0) - { - var globalAliases = GetGlobalAliasesSet(symbolToGlobalAliases, symbol); - globalAliases.AddRange(aliases); - } + GetGlobalAliasesSet(symbolToGlobalAliases, symbol).AddRange(aliases); } } } private static PooledHashSet GetGlobalAliasesSet(PooledDictionary> dictionary, T key) where T : notnull - { - if (!dictionary.TryGetValue(key, out var set)) - { - set = PooledHashSet.GetInstance(); - dictionary.Add(key, set); - } - - return set; - } + => dictionary.GetOrAdd(key, static _ => PooledHashSet.GetInstance()); private static void FreeGlobalAliases(PooledDictionary> symbolToGlobalAliases) { diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine_FindReferencesInDocuments.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine_FindReferencesInDocuments.cs index 0e982257b278e..0666734493488 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine_FindReferencesInDocuments.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine_FindReferencesInDocuments.cs @@ -91,35 +91,46 @@ async ValueTask PerformSearchInDocumentAsync( foreach (var symbol in symbols) { - var globalAliases = TryGet(symbolToGlobalAliases, symbol); - var state = new FindReferencesDocumentState(cache, globalAliases); + var state = new FindReferencesDocumentState( + cache, TryGet(symbolToGlobalAliases, symbol)); await PerformSearchInDocumentWorkerAsync(symbol, state).ConfigureAwait(false); } } - async ValueTask PerformSearchInDocumentWorkerAsync( - ISymbol symbol, FindReferencesDocumentState state) + async ValueTask PerformSearchInDocumentWorkerAsync(ISymbol symbol, FindReferencesDocumentState state) { // Always perform a normal search, looking for direct references to exactly that symbol. + await DirectSymbolSearchAsync(symbol, state).ConfigureAwait(false); + + // Now, for symbols that could involve inheritance, look for references to the same named entity, and + // see if it's a reference to a symbol that shares an inheritance relationship with that symbol. + await InheritanceSymbolSearchAsync(symbol, state).ConfigureAwait(false); + } + + async ValueTask DirectSymbolSearchAsync(ISymbol symbol, FindReferencesDocumentState state) + { + using var _ = ArrayBuilder.GetInstance(out var referencesForFinder); foreach (var finder in _finders) { - var references = await finder.FindReferencesInDocumentAsync( - symbol, state, _options, cancellationToken).ConfigureAwait(false); - foreach (var (_, location) in references) - { - var group = await ReportGroupAsync(symbol, cancellationToken).ConfigureAwait(false); - await _progress.OnReferenceFoundAsync(group, symbol, location, cancellationToken).ConfigureAwait(false); - } + await finder.FindReferencesInDocumentAsync( + symbol, state, StandardCallbacks.AddToArrayBuilder, referencesForFinder, _options, cancellationToken).ConfigureAwait(false); } - // Now, for symbols that could involve inheritance, look for references to the same named entity, and - // see if it's a reference to a symbol that shares an inheritance relationship with that symbol. + if (referencesForFinder.Count > 0) + { + var group = await ReportGroupAsync(symbol, cancellationToken).ConfigureAwait(false); + var references = referencesForFinder.SelectAsArray(r => (group, symbol, r.Location)); + await _progress.OnReferencesFoundAsync(references, cancellationToken).ConfigureAwait(false); + } + } + + async ValueTask InheritanceSymbolSearchAsync(ISymbol symbol, FindReferencesDocumentState state) + { if (InvolvesInheritance(symbol)) { - var tokens = await AbstractReferenceFinder.FindMatchingIdentifierTokensAsync( - state, symbol.Name, cancellationToken).ConfigureAwait(false); + var tokens = AbstractReferenceFinder.FindMatchingIdentifierTokens(state, symbol.Name, cancellationToken); foreach (var token in tokens) { @@ -133,7 +144,7 @@ async ValueTask PerformSearchInDocumentWorkerAsync( var candidateGroup = await ReportGroupAsync(candidate, cancellationToken).ConfigureAwait(false); var location = AbstractReferenceFinder.CreateReferenceLocation(state, token, candidateReason, cancellationToken); - await _progress.OnReferenceFoundAsync(candidateGroup, candidate, location, cancellationToken).ConfigureAwait(false); + await _progress.OnReferencesFoundAsync([(candidateGroup, candidate, location)], cancellationToken).ConfigureAwait(false); } } } @@ -175,7 +186,7 @@ async Task ComputeInheritanceRelationshipAsync( // Counter-intuitive, but if these are matching symbols, they do *not* have an inheritance relationship. // We do *not* want to report these as they would have been found in the original call to the finders in // PerformSearchInTextSpanAsync. - if (await SymbolFinder.OriginalSymbolsMatchAsync(_solution, searchSymbol, candidate, cancellationToken).ConfigureAwait(false)) + if (SymbolFinder.OriginalSymbolsMatch(_solution, searchSymbol, candidate)) return false; // walk up the original symbol's inheritance hierarchy to see if we hit the candidate. Don't walk down @@ -184,7 +195,7 @@ async Task ComputeInheritanceRelationshipAsync( this, [searchSymbol], includeImplementationsThroughDerivedTypes: false, cancellationToken).ConfigureAwait(false); foreach (var symbolUp in searchSymbolUpSet.GetAllSymbols()) { - if (await SymbolFinder.OriginalSymbolsMatchAsync(_solution, symbolUp, candidate, cancellationToken).ConfigureAwait(false)) + if (SymbolFinder.OriginalSymbolsMatch(_solution, symbolUp, candidate)) return true; } @@ -194,7 +205,7 @@ async Task ComputeInheritanceRelationshipAsync( this, [candidate], includeImplementationsThroughDerivedTypes: false, cancellationToken).ConfigureAwait(false); foreach (var candidateUp in candidateSymbolUpSet.GetAllSymbols()) { - if (await SymbolFinder.OriginalSymbolsMatchAsync(_solution, searchSymbol, candidateUp, cancellationToken).ConfigureAwait(false)) + if (SymbolFinder.OriginalSymbolsMatch(_solution, searchSymbol, candidateUp)) return true; } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractMemberScopedReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractMemberScopedReferenceFinder.cs index 224ab915fc901..bc902b6dde014 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractMemberScopedReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractMemberScopedReferenceFinder.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.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -23,45 +24,51 @@ protected abstract bool TokensMatch( protected sealed override bool CanFind(TSymbol symbol) => true; - protected sealed override Task> DetermineDocumentsToSearchAsync( + protected sealed override Task DetermineDocumentsToSearchAsync( TSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { var location = symbol.Locations.FirstOrDefault(); if (location == null || !location.IsInSource) - return SpecializedTasks.EmptyImmutableArray(); + return Task.CompletedTask; var document = project.GetDocument(location.SourceTree); if (document == null) - return SpecializedTasks.EmptyImmutableArray(); + return Task.CompletedTask; if (documents != null && !documents.Contains(document)) - return SpecializedTasks.EmptyImmutableArray(); + return Task.CompletedTask; - return Task.FromResult(ImmutableArray.Create(document)); + processResult(document, processResultData); + return Task.CompletedTask; } - protected sealed override async ValueTask> FindReferencesInDocumentAsync( + protected sealed override ValueTask FindReferencesInDocumentAsync( TSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { var container = GetContainer(symbol); if (container != null) - return await FindReferencesInContainerAsync(symbol, container, state, cancellationToken).ConfigureAwait(false); - - if (symbol.ContainingType != null && symbol.ContainingType.IsScriptClass) { - var tokens = await FindMatchingIdentifierTokensAsync(state, symbol.Name, cancellationToken).ConfigureAwait(false); - return await FindReferencesInTokensAsync(symbol, state, tokens, cancellationToken).ConfigureAwait(false); + FindReferencesInContainer(symbol, container, state, processResult, processResultData, cancellationToken); + } + else if (symbol.ContainingType != null && symbol.ContainingType.IsScriptClass) + { + var tokens = FindMatchingIdentifierTokens(state, symbol.Name, cancellationToken); + FindReferencesInTokens(symbol, state, tokens, processResult, processResultData, cancellationToken); } - return []; + return ValueTaskFactory.CompletedTask; } private static ISymbol? GetContainer(ISymbol symbol) @@ -92,10 +99,12 @@ protected sealed override async ValueTask> FindRe return null; } - private ValueTask> FindReferencesInContainerAsync( + private void FindReferencesInContainer( TSymbol symbol, ISymbol container, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { var service = state.Document.GetRequiredLanguageService(); @@ -114,6 +123,7 @@ private ValueTask> FindReferencesInContainerAsync } } - return FindReferencesInTokensAsync(symbol, state, tokens.ToImmutable(), cancellationToken); + FindReferencesInTokens( + symbol, state, tokens.ToImmutable(), processResult, processResultData, cancellationToken); } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractMethodOrPropertyOrEventSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractMethodOrPropertyOrEventSymbolReferenceFinder.cs index 094c5db7de444..e9944e7ff30c0 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractMethodOrPropertyOrEventSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractMethodOrPropertyOrEventSymbolReferenceFinder.cs @@ -4,10 +4,8 @@ using System.Collections.Immutable; using System.Threading; -using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Shared.Extensions; namespace Microsoft.CodeAnalysis.FindSymbols.Finders; diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractReferenceFinder.cs index 1ac57d9d93596..99787ba6b637f 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractReferenceFinder.cs @@ -30,13 +30,13 @@ public abstract Task> DetermineGlobalAliasesAsync( public abstract ValueTask> DetermineCascadedSymbolsAsync( ISymbol symbol, Solution solution, FindReferencesSearchOptions options, CancellationToken cancellationToken); - public abstract Task> DetermineDocumentsToSearchAsync( - ISymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, FindReferencesSearchOptions options, CancellationToken cancellationToken); + public abstract Task DetermineDocumentsToSearchAsync( + ISymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, Action processResult, TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken); - public abstract ValueTask> FindReferencesInDocumentAsync( - ISymbol symbol, FindReferencesDocumentState state, FindReferencesSearchOptions options, CancellationToken cancellationToken); + public abstract ValueTask FindReferencesInDocumentAsync( + ISymbol symbol, FindReferencesDocumentState state, Action processResult, TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken); - private static ValueTask<(bool matched, CandidateReason reason)> SymbolsMatchAsync( + private static (bool matched, CandidateReason reason) SymbolsMatch( ISymbol symbol, FindReferencesDocumentState state, SyntaxToken token, CancellationToken cancellationToken) { // delegates don't have exposed symbols for their constructors. so when you do `new MyDel()`, that's only a @@ -47,26 +47,25 @@ public abstract ValueTask> FindReferencesInDocume : state.SyntaxFacts.TryGetBindableParent(token); parent ??= token.Parent!; - return SymbolsMatchAsync(symbol, state, parent, cancellationToken); + return SymbolsMatch(symbol, state, parent, cancellationToken); } - protected static ValueTask<(bool matched, CandidateReason reason)> SymbolsMatchAsync( + protected static (bool matched, CandidateReason reason) SymbolsMatch( ISymbol searchSymbol, FindReferencesDocumentState state, SyntaxNode node, CancellationToken cancellationToken) { var symbolInfo = state.Cache.GetSymbolInfo(node, cancellationToken); - - return MatchesAsync(searchSymbol, state, symbolInfo, cancellationToken); + return Matches(searchSymbol, state, symbolInfo); } - protected static async ValueTask<(bool matched, CandidateReason reason)> MatchesAsync( - ISymbol searchSymbol, FindReferencesDocumentState state, SymbolInfo symbolInfo, CancellationToken cancellationToken) + protected static (bool matched, CandidateReason reason) Matches( + ISymbol searchSymbol, FindReferencesDocumentState state, SymbolInfo symbolInfo) { - if (await SymbolFinder.OriginalSymbolsMatchAsync(state.Solution, searchSymbol, symbolInfo.Symbol, cancellationToken).ConfigureAwait(false)) + if (SymbolFinder.OriginalSymbolsMatch(state.Solution, searchSymbol, symbolInfo.Symbol)) return (matched: true, CandidateReason.None); foreach (var candidate in symbolInfo.CandidateSymbols) { - if (await SymbolFinder.OriginalSymbolsMatchAsync(state.Solution, searchSymbol, candidate, cancellationToken).ConfigureAwait(false)) + if (SymbolFinder.OriginalSymbolsMatch(state.Solution, searchSymbol, candidate)) return (matched: true, symbolInfo.CandidateReason); } @@ -81,11 +80,13 @@ protected static bool TryGetNameWithoutAttributeSuffix( return name.TryGetWithoutAttributeSuffix(syntaxFacts.IsCaseSensitive, out result); } - protected static async Task> FindDocumentsAsync( + protected static async Task FindDocumentsAsync( Project project, IImmutableSet? scope, Func> predicateAsync, T value, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { // special case for highlight references @@ -93,31 +94,30 @@ protected static async Task> FindDocumentsAsync( { var document = scope.First(); if (document.Project == project) - return scope.ToImmutableArray(); + processResult(document, processResultData); - return []; + return; } - using var _ = ArrayBuilder.GetInstance(out var documents); - foreach (var document in await project.GetAllRegularAndSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false)) + await foreach (var document in project.GetAllRegularAndSourceGeneratedDocumentsAsync(cancellationToken)) { if (scope != null && !scope.Contains(document)) continue; if (await predicateAsync(document, value, cancellationToken).ConfigureAwait(false)) - documents.Add(document); + processResult(document, processResultData); } - - return documents.ToImmutableAndClear(); } /// /// Finds all the documents in the provided project that contain the requested string /// values /// - protected static Task> FindDocumentsAsync( + protected static Task FindDocumentsAsync( Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, CancellationToken cancellationToken, params string[] values) { @@ -130,74 +130,74 @@ protected static Task> FindDocumentsAsync( } return true; - }, values, cancellationToken); + }, values, processResult, processResultData, cancellationToken); } /// /// Finds all the documents in the provided project that contain a global attribute in them. /// - protected static Task> FindDocumentsWithGlobalSuppressMessageAttributeAsync( - Project project, IImmutableSet? documents, CancellationToken cancellationToken) + protected static Task FindDocumentsWithGlobalSuppressMessageAttributeAsync( + Project project, IImmutableSet? documents, Action processResult, TData processResultData, CancellationToken cancellationToken) { return FindDocumentsWithPredicateAsync( - project, documents, static index => index.ContainsGlobalSuppressMessageAttribute, cancellationToken); + project, documents, static index => index.ContainsGlobalSuppressMessageAttribute, processResult, processResultData, cancellationToken); } - protected static Task> FindDocumentsAsync( + protected static Task FindDocumentsAsync( Project project, IImmutableSet? documents, PredefinedType predefinedType, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { if (predefinedType == PredefinedType.None) - return SpecializedTasks.EmptyImmutableArray(); + return Task.CompletedTask; return FindDocumentsWithPredicateAsync( - project, documents, static (index, predefinedType) => index.ContainsPredefinedType(predefinedType), predefinedType, cancellationToken); + project, documents, static (index, predefinedType) => index.ContainsPredefinedType(predefinedType), predefinedType, processResult, processResultData, cancellationToken); } protected static bool IdentifiersMatch(ISyntaxFactsService syntaxFacts, string name, SyntaxToken token) => syntaxFacts.IsIdentifier(token) && syntaxFacts.TextMatch(token.ValueText, name); - [PerformanceSensitive("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1224834", OftenCompletesSynchronously = true)] - protected static async ValueTask> FindReferencesInDocumentUsingIdentifierAsync( + protected static void FindReferencesInDocumentUsingIdentifier( ISymbol symbol, string identifier, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - var tokens = await FindMatchingIdentifierTokensAsync(state, identifier, cancellationToken).ConfigureAwait(false); - return await FindReferencesInTokensAsync(symbol, state, tokens, cancellationToken).ConfigureAwait(false); + var tokens = FindMatchingIdentifierTokens(state, identifier, cancellationToken); + FindReferencesInTokens(symbol, state, tokens, processResult, processResultData, cancellationToken); } - public static ValueTask> FindMatchingIdentifierTokensAsync(FindReferencesDocumentState state, string identifier, CancellationToken cancellationToken) - => state.Cache.FindMatchingIdentifierTokensAsync(state.Document, identifier, cancellationToken); + public static ImmutableArray FindMatchingIdentifierTokens(FindReferencesDocumentState state, string identifier, CancellationToken cancellationToken) + => state.Cache.FindMatchingIdentifierTokens(identifier, cancellationToken); - protected static async ValueTask> FindReferencesInTokensAsync( + protected static void FindReferencesInTokens( ISymbol symbol, FindReferencesDocumentState state, ImmutableArray tokens, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { if (tokens.IsEmpty) - return []; + return; - using var _ = ArrayBuilder.GetInstance(out var locations); foreach (var token in tokens) { cancellationToken.ThrowIfCancellationRequested(); - var (matched, reason) = await SymbolsMatchAsync( - symbol, state, token, cancellationToken).ConfigureAwait(false); + var (matched, reason) = SymbolsMatch(symbol, state, token, cancellationToken); if (matched) { var finderLocation = CreateFinderLocation(state, token, reason, cancellationToken); - - locations.Add(finderLocation); + processResult(finderLocation, processResultData); } } - - return locations.ToImmutableAndClear(); } protected static FinderLocation CreateFinderLocation(FindReferencesDocumentState state, SyntaxToken token, CandidateReason reason, CancellationToken cancellationToken) @@ -239,27 +239,29 @@ public static ReferenceLocation CreateReferenceLocation(FindReferencesDocumentSt return null; } - protected static async Task> FindLocalAliasReferencesAsync( + protected static void FindLocalAliasReferences( ArrayBuilder initialReferences, ISymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { var aliasSymbols = GetLocalAliasSymbols(state, initialReferences, cancellationToken); - return aliasSymbols.IsDefaultOrEmpty - ? [] - : await FindReferencesThroughLocalAliasSymbolsAsync(symbol, state, aliasSymbols, cancellationToken).ConfigureAwait(false); + if (!aliasSymbols.IsDefaultOrEmpty) + FindReferencesThroughLocalAliasSymbols(symbol, state, aliasSymbols, processResult, processResultData, cancellationToken); } - protected static async Task> FindLocalAliasReferencesAsync( + protected static void FindLocalAliasReferences( ArrayBuilder initialReferences, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { var aliasSymbols = GetLocalAliasSymbols(state, initialReferences, cancellationToken); - return aliasSymbols.IsDefaultOrEmpty - ? [] - : await FindReferencesThroughLocalAliasSymbolsAsync(state, aliasSymbols, cancellationToken).ConfigureAwait(false); + if (!aliasSymbols.IsDefaultOrEmpty) + FindReferencesThroughLocalAliasSymbols(state, aliasSymbols, processResult, processResultData, cancellationToken); } private static ImmutableArray GetLocalAliasSymbols( @@ -278,127 +280,127 @@ private static ImmutableArray GetLocalAliasSymbols( return aliasSymbols.ToImmutableAndClear(); } - private static async Task> FindReferencesThroughLocalAliasSymbolsAsync( + private static void FindReferencesThroughLocalAliasSymbols( ISymbol symbol, FindReferencesDocumentState state, ImmutableArray localAliasSymbols, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - using var _ = ArrayBuilder.GetInstance(out var allAliasReferences); foreach (var localAliasSymbol in localAliasSymbols) { - var aliasReferences = await FindReferencesInDocumentUsingIdentifierAsync( - symbol, localAliasSymbol.Name, state, cancellationToken).ConfigureAwait(false); - allAliasReferences.AddRange(aliasReferences); + FindReferencesInDocumentUsingIdentifier( + symbol, localAliasSymbol.Name, state, processResult, processResultData, cancellationToken); + // the alias may reference an attribute and the alias name may end with an "Attribute" suffix. In this case search for the // shortened name as well (e.g. using GooAttribute = MyNamespace.GooAttribute; [Goo] class C1 {}) if (TryGetNameWithoutAttributeSuffix(localAliasSymbol.Name, state.SyntaxFacts, out var simpleName)) { - aliasReferences = await FindReferencesInDocumentUsingIdentifierAsync( - symbol, simpleName, state, cancellationToken).ConfigureAwait(false); - allAliasReferences.AddRange(aliasReferences); + FindReferencesInDocumentUsingIdentifier( + symbol, simpleName, state, processResult, processResultData, cancellationToken); } } - - return allAliasReferences.ToImmutableAndClear(); } - private static async Task> FindReferencesThroughLocalAliasSymbolsAsync( + private static void FindReferencesThroughLocalAliasSymbols( FindReferencesDocumentState state, ImmutableArray localAliasSymbols, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - using var _ = ArrayBuilder.GetInstance(out var allAliasReferences); foreach (var aliasSymbol in localAliasSymbols) { - var aliasReferences = await FindReferencesInDocumentUsingIdentifierAsync( - aliasSymbol, aliasSymbol.Name, state, cancellationToken).ConfigureAwait(false); - allAliasReferences.AddRange(aliasReferences); + FindReferencesInDocumentUsingIdentifier( + aliasSymbol, aliasSymbol.Name, state, processResult, processResultData, cancellationToken); + // the alias may reference an attribute and the alias name may end with an "Attribute" suffix. In this case search for the // shortened name as well (e.g. using GooAttribute = MyNamespace.GooAttribute; [Goo] class C1 {}) if (TryGetNameWithoutAttributeSuffix(aliasSymbol.Name, state.SyntaxFacts, out var simpleName)) { - aliasReferences = await FindReferencesInDocumentUsingIdentifierAsync( - aliasSymbol, simpleName, state, cancellationToken).ConfigureAwait(false); - allAliasReferences.AddRange(aliasReferences); + FindReferencesInDocumentUsingIdentifier( + aliasSymbol, simpleName, state, processResult, processResultData, cancellationToken); } } - - return allAliasReferences.ToImmutableAndClear(); } - protected static Task> FindDocumentsWithPredicateAsync( + protected static Task FindDocumentsWithPredicateAsync( Project project, IImmutableSet? documents, Func predicate, T value, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { return FindDocumentsAsync(project, documents, static async (d, t, c) => { var info = await SyntaxTreeIndex.GetRequiredIndexAsync(d, c).ConfigureAwait(false); return t.predicate(info, t.value); - }, (predicate, value), cancellationToken); + }, (predicate, value), processResult, processResultData, cancellationToken); } - protected static Task> FindDocumentsWithPredicateAsync( + protected static Task FindDocumentsWithPredicateAsync( Project project, IImmutableSet? documents, Func predicate, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { return FindDocumentsWithPredicateAsync( project, documents, static (info, predicate) => predicate(info), predicate, + processResult, + processResultData, cancellationToken); } - protected static Task> FindDocumentsWithForEachStatementsAsync(Project project, IImmutableSet? documents, CancellationToken cancellationToken) - => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsForEachStatement, cancellationToken); + protected static Task FindDocumentsWithForEachStatementsAsync(Project project, IImmutableSet? documents, Action processResult, TData processResultData, CancellationToken cancellationToken) + => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsForEachStatement, processResult, processResultData, cancellationToken); /// /// If the `node` implicitly matches the `symbol`, then it will be added to `locations`. /// - protected delegate void CollectMatchingReferences( - SyntaxNode node, FindReferencesDocumentState state, ArrayBuilder locations); + protected delegate void CollectMatchingReferences( + SyntaxNode node, FindReferencesDocumentState state, Action processResult, TData processResultData); - protected static async Task> FindReferencesInDocumentAsync( + protected static void FindReferencesInDocument( FindReferencesDocumentState state, Func isRelevantDocument, - CollectMatchingReferences collectMatchingReferences, + CollectMatchingReferences collectMatchingReferences, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - var document = state.Document; - var syntaxTreeInfo = await SyntaxTreeIndex.GetRequiredIndexAsync(document, cancellationToken).ConfigureAwait(false); + var syntaxTreeInfo = state.Cache.SyntaxTreeIndex; if (isRelevantDocument(syntaxTreeInfo)) { - using var _ = ArrayBuilder.GetInstance(out var locations); - foreach (var node in state.Root.DescendantNodesAndSelf()) { cancellationToken.ThrowIfCancellationRequested(); - collectMatchingReferences(node, state, locations); + collectMatchingReferences(node, state, processResult, processResultData); } - - return locations.ToImmutableAndClear(); } - - return []; } - protected Task> FindReferencesInForEachStatementsAsync( + protected void FindReferencesInForEachStatements( ISymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, cancellationToken); + FindReferencesInDocument(state, IsRelevantDocument, CollectMatchingReferences, processResult, processResultData, cancellationToken); + return; static bool IsRelevantDocument(SyntaxTreeIndex syntaxTreeInfo) => syntaxTreeInfo.ContainsForEachStatement; void CollectMatchingReferences( - SyntaxNode node, FindReferencesDocumentState state, ArrayBuilder locations) + SyntaxNode node, FindReferencesDocumentState state, Action processResult, TData processResultData) { var info = state.SemanticFacts.GetForEachSymbols(state.SemanticModel, node); @@ -410,30 +412,34 @@ void CollectMatchingReferences( var location = node.GetFirstToken().GetLocation(); var symbolUsageInfo = GetSymbolUsageInfo(node, state, cancellationToken); - locations.Add(new FinderLocation(node, new ReferenceLocation( + var result = new FinderLocation(node, new ReferenceLocation( state.Document, alias: null, location: location, isImplicit: true, symbolUsageInfo, GetAdditionalFindUsagesProperties(node, state), - candidateReason: CandidateReason.None))); + candidateReason: CandidateReason.None)); + processResult(result, processResultData); } } } - protected Task> FindReferencesInCollectionInitializerAsync( + protected void FindReferencesInCollectionInitializer( ISymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, cancellationToken); + FindReferencesInDocument(state, IsRelevantDocument, CollectMatchingReferences, processResult, processResultData, cancellationToken); + return; static bool IsRelevantDocument(SyntaxTreeIndex syntaxTreeInfo) => syntaxTreeInfo.ContainsCollectionInitializer; void CollectMatchingReferences( - SyntaxNode node, FindReferencesDocumentState state, ArrayBuilder locations) + SyntaxNode node, FindReferencesDocumentState state, Action processResult, TData processResultData) { if (!state.SyntaxFacts.IsObjectCollectionInitializer(node)) return; @@ -448,31 +454,35 @@ void CollectMatchingReferences( var location = expression.GetFirstToken().GetLocation(); var symbolUsageInfo = GetSymbolUsageInfo(expression, state, cancellationToken); - locations.Add(new FinderLocation(expression, new ReferenceLocation( + var result = new FinderLocation(expression, new ReferenceLocation( state.Document, alias: null, location: location, isImplicit: true, symbolUsageInfo, GetAdditionalFindUsagesProperties(expression, state), - candidateReason: CandidateReason.None))); + candidateReason: CandidateReason.None)); + processResult(result, processResultData); } } } } - protected Task> FindReferencesInDeconstructionAsync( + protected void FindReferencesInDeconstruction( ISymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, cancellationToken); + FindReferencesInDocument(state, IsRelevantDocument, CollectMatchingReferences, processResult, processResultData, cancellationToken); + return; static bool IsRelevantDocument(SyntaxTreeIndex syntaxTreeInfo) => syntaxTreeInfo.ContainsDeconstruction; void CollectMatchingReferences( - SyntaxNode node, FindReferencesDocumentState state, ArrayBuilder locations) + SyntaxNode node, FindReferencesDocumentState state, Action processResult, TData processResultData) { var semanticModel = state.SemanticModel; var semanticFacts = state.SemanticFacts; @@ -488,25 +498,29 @@ void CollectMatchingReferences( var location = state.SyntaxFacts.GetDeconstructionReferenceLocation(node); var symbolUsageInfo = GetSymbolUsageInfo(node, state, cancellationToken); - locations.Add(new FinderLocation(node, new ReferenceLocation( + var result = new FinderLocation(node, new ReferenceLocation( state.Document, alias: null, location, isImplicit: true, symbolUsageInfo, - GetAdditionalFindUsagesProperties(node, state), CandidateReason.None))); + GetAdditionalFindUsagesProperties(node, state), CandidateReason.None)); + processResult(result, processResultData); } } } - protected Task> FindReferencesInAwaitExpressionAsync( + protected void FindReferencesInAwaitExpression( ISymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, cancellationToken); + FindReferencesInDocument(state, IsRelevantDocument, CollectMatchingReferences, processResult, processResultData, cancellationToken); + return; static bool IsRelevantDocument(SyntaxTreeIndex syntaxTreeInfo) => syntaxTreeInfo.ContainsAwait; void CollectMatchingReferences( - SyntaxNode node, FindReferencesDocumentState state, ArrayBuilder locations) + SyntaxNode node, FindReferencesDocumentState state, Action processResult, TData processResultData) { var awaitExpressionMethod = state.SemanticFacts.GetGetAwaiterMethod(state.SemanticModel, node); @@ -515,25 +529,29 @@ void CollectMatchingReferences( var location = node.GetFirstToken().GetLocation(); var symbolUsageInfo = GetSymbolUsageInfo(node, state, cancellationToken); - locations.Add(new FinderLocation(node, new ReferenceLocation( + var result = new FinderLocation(node, new ReferenceLocation( state.Document, alias: null, location, isImplicit: true, symbolUsageInfo, - GetAdditionalFindUsagesProperties(node, state), CandidateReason.None))); + GetAdditionalFindUsagesProperties(node, state), CandidateReason.None)); + processResult(result, processResultData); } } } - protected Task> FindReferencesInImplicitObjectCreationExpressionAsync( + protected void FindReferencesInImplicitObjectCreationExpression( ISymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, cancellationToken); + FindReferencesInDocument(state, IsRelevantDocument, CollectMatchingReferences, processResult, processResultData, cancellationToken); + return; static bool IsRelevantDocument(SyntaxTreeIndex syntaxTreeInfo) => syntaxTreeInfo.ContainsImplicitObjectCreation; void CollectMatchingReferences( - SyntaxNode node, FindReferencesDocumentState state, ArrayBuilder locations) + SyntaxNode node, FindReferencesDocumentState state, Action processResult, TData processResultData) { // Avoid binding unrelated nodes if (!state.SyntaxFacts.IsImplicitObjectCreationExpression(node)) @@ -546,9 +564,10 @@ void CollectMatchingReferences( var location = node.GetFirstToken().GetLocation(); var symbolUsageInfo = GetSymbolUsageInfo(node, state, cancellationToken); - locations.Add(new FinderLocation(node, new ReferenceLocation( + var result = new FinderLocation(node, new ReferenceLocation( state.Document, alias: null, location, isImplicit: true, symbolUsageInfo, - GetAdditionalFindUsagesProperties(node, state), CandidateReason.None))); + GetAdditionalFindUsagesProperties(node, state), CandidateReason.None)); + processResult(result, processResultData); } } } @@ -824,12 +843,15 @@ internal abstract partial class AbstractReferenceFinder : AbstractRefer { protected abstract bool CanFind(TSymbol symbol); - protected abstract Task> DetermineDocumentsToSearchAsync( + protected abstract Task DetermineDocumentsToSearchAsync( TSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken); - protected abstract ValueTask> FindReferencesInDocumentAsync( - TSymbol symbol, FindReferencesDocumentState state, FindReferencesSearchOptions options, CancellationToken cancellationToken); + protected abstract ValueTask FindReferencesInDocumentAsync( + TSymbol symbol, FindReferencesDocumentState state, + Action processResult, TData processResultData, + FindReferencesSearchOptions options, CancellationToken cancellationToken); protected virtual Task> DetermineGlobalAliasesAsync( TSymbol symbol, Project project, CancellationToken cancellationToken) @@ -845,21 +867,23 @@ public sealed override Task> DetermineGlobalAliasesAsync( : SpecializedTasks.EmptyImmutableArray(); } - public sealed override Task> DetermineDocumentsToSearchAsync( + public sealed override Task DetermineDocumentsToSearchAsync( ISymbol symbol, HashSet? globalAliases, Project project, - IImmutableSet? documents, FindReferencesSearchOptions options, CancellationToken cancellationToken) + IImmutableSet? documents, Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - return symbol is TSymbol typedSymbol && CanFind(typedSymbol) - ? DetermineDocumentsToSearchAsync(typedSymbol, globalAliases, project, documents, options, cancellationToken) - : SpecializedTasks.EmptyImmutableArray(); + if (symbol is TSymbol typedSymbol && CanFind(typedSymbol)) + return DetermineDocumentsToSearchAsync(typedSymbol, globalAliases, project, documents, processResult, processResultData, options, cancellationToken); + + return Task.CompletedTask; } - public sealed override ValueTask> FindReferencesInDocumentAsync( - ISymbol symbol, FindReferencesDocumentState state, FindReferencesSearchOptions options, CancellationToken cancellationToken) + public sealed override ValueTask FindReferencesInDocumentAsync( + ISymbol symbol, FindReferencesDocumentState state, Action processResult, TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { return symbol is TSymbol typedSymbol && CanFind(typedSymbol) - ? FindReferencesInDocumentAsync(typedSymbol, state, options, cancellationToken) - : new ValueTask>([]); + ? FindReferencesInDocumentAsync(typedSymbol, state, processResult, processResultData, options, cancellationToken) + : ValueTaskFactory.CompletedTask; } public sealed override ValueTask> DetermineCascadedSymbolsAsync( @@ -881,11 +905,11 @@ protected virtual ValueTask> DetermineCascadedSymbolsAsy return new([]); } - protected static ValueTask> FindReferencesInDocumentUsingSymbolNameAsync( - TSymbol symbol, FindReferencesDocumentState state, CancellationToken cancellationToken) + protected static void FindReferencesInDocumentUsingSymbolName( + TSymbol symbol, FindReferencesDocumentState state, Action processResult, TData processResultData, CancellationToken cancellationToken) { - return FindReferencesInDocumentUsingIdentifierAsync( - symbol, symbol.Name, state, cancellationToken); + FindReferencesInDocumentUsingIdentifier( + symbol, symbol.Name, state, processResult, processResultData, cancellationToken); } protected static async Task> GetAllMatchingGlobalAliasNamesAsync( @@ -893,7 +917,7 @@ protected static async Task> GetAllMatchingGlobalAliasNam { using var result = TemporaryArray.Empty; - foreach (var document in await project.GetAllRegularAndSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false)) + await foreach (var document in project.GetAllRegularAndSourceGeneratedDocumentsAsync(cancellationToken)) { var index = await SyntaxTreeIndex.GetRequiredIndexAsync(document, cancellationToken).ConfigureAwait(false); foreach (var alias in index.GetGlobalAliases(name, arity)) diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractReferenceFinder_GlobalSuppressions.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractReferenceFinder_GlobalSuppressions.cs index d3c9275698895..e03b3805db4a8 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractReferenceFinder_GlobalSuppressions.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractReferenceFinder_GlobalSuppressions.cs @@ -8,7 +8,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageService; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; @@ -51,46 +50,47 @@ static bool SupportsGlobalSuppression(ISymbol symbol) /// [assembly: SuppressMessage("RuleCategory", "RuleId', Scope = "member", Target = "~F:C.Field")] /// [PerformanceSensitive("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1224834", OftenCompletesSynchronously = true)] - protected static async ValueTask> FindReferencesInDocumentInsideGlobalSuppressionsAsync( + protected static void FindReferencesInDocumentInsideGlobalSuppressions( ISymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { if (!ShouldFindReferencesInGlobalSuppressions(symbol, out var docCommentId)) - return []; + return; // Check if we have any relevant global attributes in this document. - var info = await SyntaxTreeIndex.GetRequiredIndexAsync(state.Document, cancellationToken).ConfigureAwait(false); + var info = state.Cache.SyntaxTreeIndex; if (!info.ContainsGlobalSuppressMessageAttribute) - return []; + return; var semanticModel = state.SemanticModel; var suppressMessageAttribute = semanticModel.Compilation.SuppressMessageAttributeType(); if (suppressMessageAttribute == null) - return []; + return; // Check if we have any instances of the symbol documentation comment ID string literals within global attributes. // These string literals represent references to the symbol. if (!TryGetExpectedDocumentationCommentId(docCommentId, out var expectedDocCommentId)) - return []; + return; var syntaxFacts = state.SyntaxFacts; // We map the positions of documentation ID literals in tree to string literal tokens, // perform semantic checks to ensure these are valid references to the symbol // and if so, add these locations to the computed references. - var root = await semanticModel.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - using var _ = ArrayBuilder.GetInstance(out var locations); + var root = state.Root; foreach (var token in root.DescendantTokens()) { if (IsCandidate(state, token, expectedDocCommentId.Span, suppressMessageAttribute, cancellationToken, out var offsetOfReferenceInToken)) { var referenceLocation = CreateReferenceLocation(offsetOfReferenceInToken, token, root, state.Document, syntaxFacts); - locations.Add(new FinderLocation(token.GetRequiredParent(), referenceLocation)); + processResult(new FinderLocation(token.GetRequiredParent(), referenceLocation), processResultData); } } - return locations.ToImmutableAndClear(); + return; // Local functions static bool IsCandidate( diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractTypeParameterSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractTypeParameterSymbolReferenceFinder.cs index 756ce8036d1fa..b84e1bd7abaf1 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractTypeParameterSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractTypeParameterSymbolReferenceFinder.cs @@ -7,16 +7,17 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageService; -using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.FindSymbols.Finders; internal abstract class AbstractTypeParameterSymbolReferenceFinder : AbstractReferenceFinder { - protected sealed override async ValueTask> FindReferencesInDocumentAsync( + protected sealed override ValueTask FindReferencesInDocumentAsync( ITypeParameterSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -28,17 +29,21 @@ protected sealed override async ValueTask> FindRe // T()`). In the former case GetSymbolInfo can be used to bind the symbol and check if it matches this symbol. // in the latter though GetSymbolInfo will fail and we have to directly check if we have the right type info. - var tokens = await FindMatchingIdentifierTokensAsync(state, symbol.Name, cancellationToken).ConfigureAwait(false); + var tokens = FindMatchingIdentifierTokens(state, symbol.Name, cancellationToken); - var normalReferences = await FindReferencesInTokensAsync( + FindReferencesInTokens( symbol, state, tokens.WhereAsArray(static (token, state) => !IsObjectCreationToken(token, state), state), - cancellationToken).ConfigureAwait(false); + processResult, + processResultData, + cancellationToken); - var objectCreationReferences = GetObjectCreationReferences( - tokens.WhereAsArray(static (token, state) => IsObjectCreationToken(token, state), state)); + GetObjectCreationReferences( + tokens.WhereAsArray(static (token, state) => IsObjectCreationToken(token, state), state), + processResult, + processResultData); - return normalReferences.Concat(objectCreationReferences); + return ValueTaskFactory.CompletedTask; static bool IsObjectCreationToken(SyntaxToken token, FindReferencesDocumentState state) { @@ -47,19 +52,18 @@ static bool IsObjectCreationToken(SyntaxToken token, FindReferencesDocumentState syntaxFacts.IsObjectCreationExpression(token.Parent.Parent); } - ImmutableArray GetObjectCreationReferences(ImmutableArray objectCreationTokens) + void GetObjectCreationReferences( + ImmutableArray objectCreationTokens, + Action processResult, + TData processResultData) { - using var _ = ArrayBuilder.GetInstance(out var result); - foreach (var token in objectCreationTokens) { Contract.ThrowIfNull(token.Parent?.Parent); var typeInfo = state.SemanticModel.GetTypeInfo(token.Parent.Parent, cancellationToken); if (symbol.Equals(typeInfo.Type, SymbolEqualityComparer.Default)) - result.Add(CreateFinderLocation(state, token, CandidateReason.None, cancellationToken)); + processResult(CreateFinderLocation(state, token, CandidateReason.None, cancellationToken), processResultData); } - - return result.ToImmutableAndClear(); } } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ConstructorInitializerSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ConstructorInitializerSymbolReferenceFinder.cs index 88dc9fad96af2..3ee5b2f3a72e4 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ConstructorInitializerSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ConstructorInitializerSymbolReferenceFinder.cs @@ -2,13 +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.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; @@ -19,11 +18,13 @@ internal sealed class ConstructorInitializerSymbolReferenceFinder : AbstractRefe protected override bool CanFind(IMethodSymbol symbol) => symbol.MethodKind == MethodKind.Constructor; - protected override Task> DetermineDocumentsToSearchAsync( + protected override Task DetermineDocumentsToSearchAsync( IMethodSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -47,27 +48,27 @@ protected override Task> DetermineDocumentsToSearchAsyn } return false; - }, symbol.ContainingType.Name, cancellationToken); + }, symbol.ContainingType.Name, processResult, processResultData, cancellationToken); } - protected sealed override async ValueTask> FindReferencesInDocumentAsync( + protected sealed override ValueTask FindReferencesInDocumentAsync( IMethodSymbol methodSymbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { var tokens = state.Cache.GetConstructorInitializerTokens(state.SyntaxFacts, state.Root, cancellationToken); if (state.SemanticModel.Language == LanguageNames.VisualBasic) - { - tokens = tokens.Concat(await FindMatchingIdentifierTokensAsync( - state, "New", cancellationToken).ConfigureAwait(false)).Distinct(); - } + tokens = tokens.Concat(FindMatchingIdentifierTokens(state, "New", cancellationToken)).Distinct(); var totalTokens = tokens.WhereAsArray( static (token, tuple) => TokensMatch(tuple.state, token, tuple.methodSymbol.ContainingType.Name, tuple.cancellationToken), (state, methodSymbol, cancellationToken)); - return await FindReferencesInTokensAsync(methodSymbol, state, totalTokens, cancellationToken).ConfigureAwait(false); + FindReferencesInTokens(methodSymbol, state, totalTokens, processResult, processResultData, cancellationToken); + return ValueTaskFactory.CompletedTask; // local functions static bool TokensMatch( diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ConstructorSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ConstructorSymbolReferenceFinder.cs index f4e3e0828a5d5..9e0d20f947dfd 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ConstructorSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ConstructorSymbolReferenceFinder.cs @@ -9,7 +9,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageService; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; @@ -32,80 +31,77 @@ protected override Task> DetermineGlobalAliasesAsync(IMet return GetAllMatchingGlobalAliasNamesAsync(project, containingType.Name, containingType.Arity, cancellationToken); } - protected override async Task> DetermineDocumentsToSearchAsync( + protected override async Task DetermineDocumentsToSearchAsync( IMethodSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { var containingType = symbol.ContainingType; var typeName = symbol.ContainingType.Name; - using var _ = ArrayBuilder.GetInstance(out var result); - await AddDocumentsAsync( - project, documents, typeName, result, cancellationToken).ConfigureAwait(false); + project, documents, typeName, processResult, processResultData, cancellationToken).ConfigureAwait(false); if (globalAliases != null) { foreach (var globalAlias in globalAliases) { await AddDocumentsAsync( - project, documents, globalAlias, result, cancellationToken).ConfigureAwait(false); + project, documents, globalAlias, processResult, processResultData, cancellationToken).ConfigureAwait(false); } } - result.AddRange(await FindDocumentsAsync( - project, documents, containingType.SpecialType.ToPredefinedType(), cancellationToken).ConfigureAwait(false)); - - result.AddRange(await FindDocumentsWithGlobalSuppressMessageAttributeAsync( - project, documents, cancellationToken).ConfigureAwait(false)); + await FindDocumentsAsync( + project, documents, containingType.SpecialType.ToPredefinedType(), processResult, processResultData, cancellationToken).ConfigureAwait(false); - result.AddRange(symbol.MethodKind == MethodKind.Constructor - ? await FindDocumentsWithImplicitObjectCreationExpressionAsync(project, documents, cancellationToken).ConfigureAwait(false) - : []); + await FindDocumentsWithGlobalSuppressMessageAttributeAsync( + project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); - return result.ToImmutableAndClear(); + if (symbol.MethodKind == MethodKind.Constructor) + { + await FindDocumentsWithImplicitObjectCreationExpressionAsync( + project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); + } } - private static Task> FindDocumentsWithImplicitObjectCreationExpressionAsync(Project project, IImmutableSet? documents, CancellationToken cancellationToken) - => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsImplicitObjectCreation, cancellationToken); + private static Task FindDocumentsWithImplicitObjectCreationExpressionAsync(Project project, IImmutableSet? documents, Action processResult, TData processResultData, CancellationToken cancellationToken) + => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsImplicitObjectCreation, processResult, processResultData, cancellationToken); - private static async Task AddDocumentsAsync( + private static async Task AddDocumentsAsync( Project project, IImmutableSet? documents, string typeName, - ArrayBuilder result, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - var documentsWithName = await FindDocumentsAsync(project, documents, cancellationToken, typeName).ConfigureAwait(false); + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, typeName).ConfigureAwait(false); - var documentsWithAttribute = TryGetNameWithoutAttributeSuffix(typeName, project.Services.GetRequiredService(), out var simpleName) - ? await FindDocumentsAsync(project, documents, cancellationToken, simpleName).ConfigureAwait(false) - : []; - - result.AddRange(documentsWithName); - result.AddRange(documentsWithAttribute); + if (TryGetNameWithoutAttributeSuffix(typeName, project.Services.GetRequiredService(), out var simpleName)) + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, simpleName).ConfigureAwait(false); } private static bool IsPotentialReference(PredefinedType predefinedType, ISyntaxFactsService syntaxFacts, SyntaxToken token) => syntaxFacts.TryGetPredefinedType(token, out var actualType) && predefinedType == actualType; - protected override async ValueTask> FindReferencesInDocumentAsync( + protected override ValueTask FindReferencesInDocumentAsync( IMethodSymbol methodSymbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - using var _1 = ArrayBuilder.GetInstance(out var result); - // First just look for this normal constructor references using the name of it's containing type. var name = methodSymbol.ContainingType.Name; - await AddReferencesInDocumentWorkerAsync( - methodSymbol, name, state, result, cancellationToken).ConfigureAwait(false); + AddReferencesInDocumentWorker( + methodSymbol, name, state, processResult, processResultData, cancellationToken); // Next, look for constructor references through a global alias to our containing type. foreach (var globalAlias in state.GlobalAliases) @@ -116,68 +112,64 @@ await AddReferencesInDocumentWorkerAsync( if (state.SyntaxFacts.StringComparer.Equals(name, globalAlias)) continue; - await AddReferencesInDocumentWorkerAsync( - methodSymbol, globalAlias, state, result, cancellationToken).ConfigureAwait(false); + AddReferencesInDocumentWorker( + methodSymbol, globalAlias, state, processResult, processResultData, cancellationToken); } - // Nest, our containing type might itself have local aliases to it in this particular file. - // If so, see what the local aliases are and then search for constructor references to that. - using var _2 = ArrayBuilder.GetInstance(out var typeReferences); - await NamedTypeSymbolReferenceFinder.AddReferencesToTypeOrGlobalAliasToItAsync( - methodSymbol.ContainingType, state, typeReferences, cancellationToken).ConfigureAwait(false); - - var aliasReferences = await FindLocalAliasReferencesAsync( - typeReferences, methodSymbol, state, cancellationToken).ConfigureAwait(false); - // Finally, look for constructor references to predefined types (like `new int()`), // implicit object references, and inside global suppression attributes. - result.AddRange(await FindPredefinedTypeReferencesAsync( - methodSymbol, state, cancellationToken).ConfigureAwait(false)); + FindPredefinedTypeReferences( + methodSymbol, state, processResult, processResultData, cancellationToken); - result.AddRange(await FindReferencesInImplicitObjectCreationExpressionAsync( - methodSymbol, state, cancellationToken).ConfigureAwait(false)); + FindReferencesInImplicitObjectCreationExpression( + methodSymbol, state, processResult, processResultData, cancellationToken); - result.AddRange(await FindReferencesInDocumentInsideGlobalSuppressionsAsync( - methodSymbol, state, cancellationToken).ConfigureAwait(false)); + FindReferencesInDocumentInsideGlobalSuppressions( + methodSymbol, state, processResult, processResultData, cancellationToken); - return result.ToImmutableAndClear(); + return ValueTaskFactory.CompletedTask; } /// /// Finds references to in this , but only if it referenced /// though (which might be the actual name of the type, or a global alias to it). /// - private static async Task AddReferencesInDocumentWorkerAsync( + private static void AddReferencesInDocumentWorker( IMethodSymbol symbol, string name, FindReferencesDocumentState state, - ArrayBuilder result, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - result.AddRange(await FindOrdinaryReferencesAsync( - symbol, name, state, cancellationToken).ConfigureAwait(false)); - result.AddRange(await FindAttributeReferencesAsync( - symbol, name, state, cancellationToken).ConfigureAwait(false)); + FindOrdinaryReferences( + symbol, name, state, processResult, processResultData, cancellationToken); + FindAttributeReferences( + symbol, name, state, processResult, processResultData, cancellationToken); } - private static ValueTask> FindOrdinaryReferencesAsync( + private static void FindOrdinaryReferences( IMethodSymbol symbol, string name, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - return FindReferencesInDocumentUsingIdentifierAsync( - symbol, name, state, cancellationToken); + FindReferencesInDocumentUsingIdentifier( + symbol, name, state, processResult, processResultData, cancellationToken); } - private static ValueTask> FindPredefinedTypeReferencesAsync( + private static void FindPredefinedTypeReferences( IMethodSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { var predefinedType = symbol.ContainingType.SpecialType.ToPredefinedType(); if (predefinedType == PredefinedType.None) - return new([]); + return; var tokens = state.Root .DescendantTokens(descendIntoTrivia: true) @@ -185,23 +177,26 @@ private static ValueTask> FindPredefinedTypeRefer static (token, tuple) => IsPotentialReference(tuple.predefinedType, tuple.state.SyntaxFacts, token), (state, predefinedType)); - return FindReferencesInTokensAsync(symbol, state, tokens, cancellationToken); + FindReferencesInTokens(symbol, state, tokens, processResult, processResultData, cancellationToken); } - private static ValueTask> FindAttributeReferencesAsync( + private static void FindAttributeReferences( IMethodSymbol symbol, string name, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - return TryGetNameWithoutAttributeSuffix(name, state.SyntaxFacts, out var simpleName) - ? FindReferencesInDocumentUsingIdentifierAsync(symbol, simpleName, state, cancellationToken) - : new([]); + if (TryGetNameWithoutAttributeSuffix(name, state.SyntaxFacts, out var simpleName)) + FindReferencesInDocumentUsingIdentifier(symbol, simpleName, state, processResult, processResultData, cancellationToken); } - private Task> FindReferencesInImplicitObjectCreationExpressionAsync( + private void FindReferencesInImplicitObjectCreationExpression( IMethodSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { // Only check `new (...)` calls that supply enough arguments to match all the required parameters for the constructor. @@ -214,13 +209,14 @@ private Task> FindReferencesInImplicitObjectCreat ? -1 : symbol.Parameters.Length; - return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, cancellationToken); + FindReferencesInDocument(state, IsRelevantDocument, CollectMatchingReferences, processResult, processResultData, cancellationToken); + return; static bool IsRelevantDocument(SyntaxTreeIndex syntaxTreeInfo) => syntaxTreeInfo.ContainsImplicitObjectCreation; void CollectMatchingReferences( - SyntaxNode node, FindReferencesDocumentState state, ArrayBuilder locations) + SyntaxNode node, FindReferencesDocumentState state, Action processResult, TData processResultData) { var syntaxFacts = state.SyntaxFacts; if (!syntaxFacts.IsImplicitObjectCreationExpression(node)) @@ -241,9 +237,10 @@ void CollectMatchingReferences( var location = node.GetFirstToken().GetLocation(); var symbolUsageInfo = GetSymbolUsageInfo(node, state, cancellationToken); - locations.Add(new FinderLocation(node, new ReferenceLocation( + var result = new FinderLocation(node, new ReferenceLocation( state.Document, alias: null, location, isImplicit: true, symbolUsageInfo, - GetAdditionalFindUsagesProperties(node, state), CandidateReason.None))); + GetAdditionalFindUsagesProperties(node, state), CandidateReason.None)); + processResult(result, processResultData); } } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/DestructorSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/DestructorSymbolReferenceFinder.cs index 610db4da1aeac..a6c1623d5ba95 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/DestructorSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/DestructorSymbolReferenceFinder.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.Collections.Generic; using System.Collections.Immutable; using System.Threading; @@ -15,23 +16,27 @@ internal sealed class DestructorSymbolReferenceFinder : AbstractReferenceFinder< protected override bool CanFind(IMethodSymbol symbol) => symbol.MethodKind == MethodKind.Destructor; - protected override Task> DetermineDocumentsToSearchAsync( + protected override Task DetermineDocumentsToSearchAsync( IMethodSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - return SpecializedTasks.EmptyImmutableArray(); + return Task.CompletedTask; } - protected override ValueTask> FindReferencesInDocumentAsync( + protected override ValueTask FindReferencesInDocumentAsync( IMethodSymbol methodSymbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - return new ValueTask>([]); + return ValueTaskFactory.CompletedTask; } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/EventSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/EventSymbolReferenceFinder.cs index 756bffd4366c4..7b5870acdbde7 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/EventSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/EventSymbolReferenceFinder.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.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.FindSymbols.Finders; @@ -33,25 +35,29 @@ protected sealed override ValueTask> DetermineCascadedSy return new(backingFields.Concat(associatedNamedTypes)); } - protected sealed override async Task> DetermineDocumentsToSearchAsync( + protected sealed override async Task DetermineDocumentsToSearchAsync( IEventSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - var documentsWithName = await FindDocumentsAsync(project, documents, cancellationToken, symbol.Name).ConfigureAwait(false); - var documentsWithGlobalAttributes = await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, cancellationToken).ConfigureAwait(false); - return documentsWithName.Concat(documentsWithGlobalAttributes); + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, symbol.Name).ConfigureAwait(false); + await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - protected sealed override ValueTask> FindReferencesInDocumentAsync( + protected sealed override ValueTask FindReferencesInDocumentAsync( IEventSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - return FindReferencesInDocumentUsingSymbolNameAsync(symbol, state, cancellationToken); + FindReferencesInDocumentUsingSymbolName(symbol, state, processResult, processResultData, cancellationToken); + return ValueTaskFactory.CompletedTask; } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ExplicitConversionSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ExplicitConversionSymbolReferenceFinder.cs index 7059fefdf72ab..dfed302b16041 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ExplicitConversionSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ExplicitConversionSymbolReferenceFinder.cs @@ -2,9 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageService; @@ -23,11 +23,13 @@ protected override bool CanFind(IMethodSymbol symbol) private static INamedTypeSymbol? GetUnderlyingNamedType(ITypeSymbol symbol) => UnderlyingNamedTypeVisitor.Instance.Visit(symbol); - protected sealed override async Task> DetermineDocumentsToSearchAsync( + protected sealed override async Task DetermineDocumentsToSearchAsync( IMethodSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -43,25 +45,25 @@ protected sealed override async Task> DetermineDocument var underlyingNamedType = GetUnderlyingNamedType(symbol.ReturnType); Contract.ThrowIfNull(underlyingNamedType); - var documentsWithName = await FindDocumentsAsync(project, documents, cancellationToken, underlyingNamedType.Name).ConfigureAwait(false); - var documentsWithType = await FindDocumentsAsync(project, documents, underlyingNamedType.SpecialType.ToPredefinedType(), cancellationToken).ConfigureAwait(false); - using var _ = ArrayBuilder.GetInstance(out var result); + using var _ = PooledHashSet.GetInstance(out var result); + await FindDocumentsAsync(project, documents, StandardCallbacks.AddToHashSet, result, cancellationToken, underlyingNamedType.Name).ConfigureAwait(false); + await FindDocumentsAsync(project, documents, underlyingNamedType.SpecialType.ToPredefinedType(), StandardCallbacks.AddToHashSet, result, cancellationToken).ConfigureAwait(false); // Ignore any documents that don't also have an explicit cast in them. - foreach (var document in documentsWithName.Concat(documentsWithType).Distinct()) + foreach (var document in result) { var index = await SyntaxTreeIndex.GetRequiredIndexAsync(document, cancellationToken).ConfigureAwait(false); if (index.ContainsConversion) - result.Add(document); + processResult(document, processResultData); } - - return result.ToImmutableAndClear(); } - protected sealed override ValueTask> FindReferencesInDocumentAsync( + protected sealed override ValueTask FindReferencesInDocumentAsync( IMethodSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -71,7 +73,8 @@ protected sealed override ValueTask> FindReferenc static (token, state) => IsPotentialReference(state.SyntaxFacts, token), state); - return FindReferencesInTokensAsync(symbol, state, tokens, cancellationToken); + FindReferencesInTokens(symbol, state, tokens, processResult, processResultData, cancellationToken); + return ValueTaskFactory.CompletedTask; } private static bool IsPotentialReference( diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ExplicitInterfaceMethodReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ExplicitInterfaceMethodReferenceFinder.cs index 51256d807e4b0..a92e4f0ca1eba 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ExplicitInterfaceMethodReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ExplicitInterfaceMethodReferenceFinder.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.Collections.Generic; using System.Collections.Immutable; using System.Threading; @@ -15,25 +16,29 @@ internal sealed class ExplicitInterfaceMethodReferenceFinder : AbstractReference protected override bool CanFind(IMethodSymbol symbol) => symbol.MethodKind == MethodKind.ExplicitInterfaceImplementation; - protected sealed override Task> DetermineDocumentsToSearchAsync( + protected sealed override Task DetermineDocumentsToSearchAsync( IMethodSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { // An explicit method can't be referenced anywhere. - return SpecializedTasks.EmptyImmutableArray(); + return Task.CompletedTask; } - protected sealed override ValueTask> FindReferencesInDocumentAsync( + protected sealed override ValueTask FindReferencesInDocumentAsync( IMethodSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { // An explicit method can't be referenced anywhere. - return new([]); + return ValueTaskFactory.CompletedTask; } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/FieldSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/FieldSymbolReferenceFinder.cs index 43c3c03d7ccdc..4377ecd8bb5b5 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/FieldSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/FieldSymbolReferenceFinder.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.Collections.Generic; using System.Collections.Immutable; using System.Threading; @@ -26,29 +27,32 @@ protected override ValueTask> DetermineCascadedSymbolsAs : new(ImmutableArray.Empty); } - protected override async Task> DetermineDocumentsToSearchAsync( + protected override async Task DetermineDocumentsToSearchAsync( IFieldSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - var documentsWithName = await FindDocumentsAsync(project, documents, cancellationToken, symbol.Name).ConfigureAwait(false); - var documentsWithGlobalAttributes = await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, cancellationToken).ConfigureAwait(false); - return documentsWithName.Concat(documentsWithGlobalAttributes); + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, symbol.Name).ConfigureAwait(false); + await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - protected override async ValueTask> FindReferencesInDocumentAsync( + protected override ValueTask FindReferencesInDocumentAsync( IFieldSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - var nameReferences = await FindReferencesInDocumentUsingSymbolNameAsync( - symbol, state, cancellationToken).ConfigureAwait(false); - var suppressionReferences = await FindReferencesInDocumentInsideGlobalSuppressionsAsync( - symbol, state, cancellationToken).ConfigureAwait(false); - return nameReferences.Concat(suppressionReferences); + FindReferencesInDocumentUsingSymbolName( + symbol, state, processResult, processResultData, cancellationToken); + FindReferencesInDocumentInsideGlobalSuppressions( + symbol, state, processResult, processResultData, cancellationToken); + return ValueTaskFactory.CompletedTask; } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/IReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/IReferenceFinder.cs index 395fcb5402ca3..f5b717542dc20 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/IReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/IReferenceFinder.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.Collections.Generic; using System.Collections.Immutable; using System.Threading; @@ -49,9 +50,10 @@ ValueTask> DetermineCascadedSymbolsAsync( /// /// Implementations of this method must be thread-safe. /// - Task> DetermineDocumentsToSearchAsync( + Task DetermineDocumentsToSearchAsync( ISymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken); /// @@ -61,9 +63,11 @@ Task> DetermineDocumentsToSearchAsync( /// /// Implementations of this method must be thread-safe. /// - ValueTask> FindReferencesInDocumentAsync( + ValueTask FindReferencesInDocumentAsync( ISymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken); } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/MethodTypeParameterSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/MethodTypeParameterSymbolReferenceFinder.cs index 78d29392e7082..55150a8b42bf7 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/MethodTypeParameterSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/MethodTypeParameterSymbolReferenceFinder.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.Collections.Generic; using System.Collections.Immutable; using System.Threading; @@ -36,11 +37,13 @@ protected override ValueTask> DetermineCascadedSymbolsAs return new([]); } - protected sealed override Task> DetermineDocumentsToSearchAsync( + protected sealed override Task DetermineDocumentsToSearchAsync( ITypeParameterSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -55,7 +58,7 @@ protected sealed override Task> DetermineDocumentsToSea // Also, we only look for files that have the name of the owning type. This helps filter // down the set considerably. Contract.ThrowIfNull(symbol.DeclaringMethod); - return FindDocumentsAsync(project, documents, cancellationToken, symbol.Name, + return FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, symbol.Name, GetMemberNameWithoutInterfaceName(symbol.DeclaringMethod.Name), symbol.DeclaringMethod.ContainingType.Name); } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/NamedTypeSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/NamedTypeSymbolReferenceFinder.cs index 74537c547319e..0b2e5e08bb88c 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/NamedTypeSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/NamedTypeSymbolReferenceFinder.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.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -49,54 +50,49 @@ private static void Add(ArrayBuilder result, ImmutableArray()); } - protected override async Task> DetermineDocumentsToSearchAsync( + protected override async Task DetermineDocumentsToSearchAsync( INamedTypeSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - using var _ = ArrayBuilder.GetInstance(out var result); - - await AddDocumentsToSearchAsync(symbol.Name, project, documents, result, cancellationToken).ConfigureAwait(false); + await AddDocumentsToSearchAsync(symbol.Name, project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); if (globalAliases != null) { foreach (var alias in globalAliases) - await AddDocumentsToSearchAsync(alias, project, documents, result, cancellationToken).ConfigureAwait(false); + await AddDocumentsToSearchAsync(alias, project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - result.AddRange(await FindDocumentsAsync( - project, documents, symbol.SpecialType.ToPredefinedType(), cancellationToken).ConfigureAwait(false)); - - result.AddRange(await FindDocumentsWithGlobalSuppressMessageAttributeAsync( - project, documents, cancellationToken).ConfigureAwait(false)); + await FindDocumentsAsync( + project, documents, symbol.SpecialType.ToPredefinedType(), processResult, processResultData, cancellationToken).ConfigureAwait(false); - return result.ToImmutableAndClear(); + await FindDocumentsWithGlobalSuppressMessageAttributeAsync( + project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } /// /// Looks for documents likely containing in them. That name will either be the actual /// name of the named type we're looking for, or it might be a global alias to it. /// - private static async Task AddDocumentsToSearchAsync( + private static async Task AddDocumentsToSearchAsync( string throughName, Project project, IImmutableSet? documents, - ArrayBuilder result, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { var syntaxFacts = project.Services.GetRequiredService(); - var documentsWithName = await FindDocumentsAsync( - project, documents, cancellationToken, throughName).ConfigureAwait(false); + await FindDocumentsAsync( + project, documents, processResult, processResultData, cancellationToken, throughName).ConfigureAwait(false); - var documentsWithAttribute = TryGetNameWithoutAttributeSuffix(throughName, syntaxFacts, out var simpleName) - ? await FindDocumentsAsync(project, documents, cancellationToken, simpleName).ConfigureAwait(false) - : []; - - result.AddRange(documentsWithName); - result.AddRange(documentsWithAttribute); + if (TryGetNameWithoutAttributeSuffix(throughName, syntaxFacts, out var simpleName)) + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, simpleName).ConfigureAwait(false); } private static bool IsPotentialReference( @@ -109,9 +105,11 @@ private static bool IsPotentialReference( predefinedType == actualType; } - protected override async ValueTask> FindReferencesInDocumentAsync( + protected override ValueTask FindReferencesInDocumentAsync( INamedTypeSymbol namedType, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -119,32 +117,37 @@ protected override async ValueTask> FindReference // First find all references to this type, either with it's actual name, or through potential // global alises to it. - await AddReferencesToTypeOrGlobalAliasToItAsync( - namedType, state, initialReferences, cancellationToken).ConfigureAwait(false); + AddReferencesToTypeOrGlobalAliasToIt( + namedType, state, StandardCallbacks.AddToArrayBuilder, initialReferences, cancellationToken); + + // The items in initialReferences need to be both reported and used later to calculate additional results. + foreach (var location in initialReferences) + processResult(location, processResultData); // This named type may end up being locally aliased as well. If so, now find all the references // to the local alias. - initialReferences.AddRange(await FindLocalAliasReferencesAsync( - initialReferences, state, cancellationToken).ConfigureAwait(false)); + FindLocalAliasReferences( + initialReferences, state, processResult, processResultData, cancellationToken); - initialReferences.AddRange(await FindPredefinedTypeReferencesAsync( - namedType, state, cancellationToken).ConfigureAwait(false)); + FindPredefinedTypeReferences( + namedType, state, processResult, processResultData, cancellationToken); - initialReferences.AddRange(await FindReferencesInDocumentInsideGlobalSuppressionsAsync( - namedType, state, cancellationToken).ConfigureAwait(false)); + FindReferencesInDocumentInsideGlobalSuppressions( + namedType, state, processResult, processResultData, cancellationToken); - return initialReferences.ToImmutableAndClear(); + return ValueTaskFactory.CompletedTask; } - internal static async ValueTask AddReferencesToTypeOrGlobalAliasToItAsync( + internal static void AddReferencesToTypeOrGlobalAliasToIt( INamedTypeSymbol namedType, FindReferencesDocumentState state, - ArrayBuilder nonAliasReferences, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - await AddNonAliasReferencesAsync( - namedType, namedType.Name, state, nonAliasReferences, cancellationToken).ConfigureAwait(false); + AddNonAliasReferences( + namedType, namedType.Name, state, processResult, processResultData, cancellationToken); foreach (var globalAlias in state.GlobalAliases) { @@ -154,8 +157,8 @@ await AddNonAliasReferencesAsync( if (state.SyntaxFacts.StringComparer.Equals(namedType.Name, globalAlias)) continue; - await AddNonAliasReferencesAsync( - namedType, globalAlias, state, nonAliasReferences, cancellationToken).ConfigureAwait(false); + AddNonAliasReferences( + namedType, globalAlias, state, processResult, processResultData, cancellationToken); } } @@ -164,24 +167,27 @@ await AddNonAliasReferencesAsync( /// only if it referenced though (which might be the actual name /// of the type, or a global alias to it). /// - private static async ValueTask AddNonAliasReferencesAsync( + private static void AddNonAliasReferences( INamedTypeSymbol symbol, string name, FindReferencesDocumentState state, - ArrayBuilder nonAliasesReferences, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - nonAliasesReferences.AddRange(await FindOrdinaryReferencesAsync( - symbol, name, state, cancellationToken).ConfigureAwait(false)); + FindOrdinaryReferences( + symbol, name, state, processResult, processResultData, cancellationToken); - nonAliasesReferences.AddRange(await FindAttributeReferencesAsync( - symbol, name, state, cancellationToken).ConfigureAwait(false)); + FindAttributeReferences( + symbol, name, state, processResult, processResultData, cancellationToken); } - private static ValueTask> FindOrdinaryReferencesAsync( + private static void FindOrdinaryReferences( INamedTypeSymbol namedType, string name, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { // Get the parent node that best matches what this token represents. For example, if we have `new a.b()` @@ -189,18 +195,20 @@ private static ValueTask> FindOrdinaryReferencesA // to the constructor not the type. That's a good thing as we don't want these object-creations to // associate with the type, but rather with the constructor itself. - return FindReferencesInDocumentUsingIdentifierAsync( - namedType, name, state, cancellationToken); + FindReferencesInDocumentUsingIdentifier( + namedType, name, state, processResult, processResultData, cancellationToken); } - private static ValueTask> FindPredefinedTypeReferencesAsync( + private static void FindPredefinedTypeReferences( INamedTypeSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { var predefinedType = symbol.SpecialType.ToPredefinedType(); if (predefinedType == PredefinedType.None) - return new([]); + return; var tokens = state.Root .DescendantTokens(descendIntoTrivia: true) @@ -208,17 +216,18 @@ private static ValueTask> FindPredefinedTypeRefer static (token, tuple) => IsPotentialReference(tuple.predefinedType, tuple.state.SyntaxFacts, token), (state, predefinedType)); - return FindReferencesInTokensAsync(symbol, state, tokens, cancellationToken); + FindReferencesInTokens(symbol, state, tokens, processResult, processResultData, cancellationToken); } - private static ValueTask> FindAttributeReferencesAsync( + private static void FindAttributeReferences( INamedTypeSymbol namedType, string name, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - return TryGetNameWithoutAttributeSuffix(name, state.SyntaxFacts, out var nameWithoutSuffix) - ? FindReferencesInDocumentUsingIdentifierAsync(namedType, nameWithoutSuffix, state, cancellationToken) - : new([]); + if (TryGetNameWithoutAttributeSuffix(name, state.SyntaxFacts, out var nameWithoutSuffix)) + FindReferencesInDocumentUsingIdentifier(namedType, nameWithoutSuffix, state, processResult, processResultData, cancellationToken); } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/NamespaceSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/NamespaceSymbolReferenceFinder.cs index dc0f2a36f79a3..3ca848269ce6a 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/NamespaceSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/NamespaceSymbolReferenceFinder.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.Collections.Generic; using System.Collections.Immutable; using System.Threading; @@ -9,6 +10,7 @@ using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.FindSymbols.Finders; @@ -22,53 +24,53 @@ protected override Task> DetermineGlobalAliasesAsync(INam return GetAllMatchingGlobalAliasNamesAsync(project, symbol.Name, arity: 0, cancellationToken); } - protected override async Task> DetermineDocumentsToSearchAsync( + protected override async Task DetermineDocumentsToSearchAsync( INamespaceSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - using var _ = ArrayBuilder.GetInstance(out var result); - - result.AddRange(!symbol.IsGlobalNamespace - ? await FindDocumentsAsync(project, documents, cancellationToken, symbol.Name).ConfigureAwait(false) - : await FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsGlobalKeyword, cancellationToken).ConfigureAwait(false)); + if (!symbol.IsGlobalNamespace) + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, symbol.Name).ConfigureAwait(false); + else + await FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsGlobalKeyword, processResult, processResultData, cancellationToken).ConfigureAwait(false); if (globalAliases != null) { foreach (var globalAlias in globalAliases) { - result.AddRange(await FindDocumentsAsync( - project, documents, cancellationToken, globalAlias).ConfigureAwait(false)); + await FindDocumentsAsync( + project, documents, processResult, processResultData, cancellationToken, globalAlias).ConfigureAwait(false); } } - var documentsWithGlobalAttributes = await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, cancellationToken).ConfigureAwait(false); - result.AddRange(documentsWithGlobalAttributes); - - return result.ToImmutableAndClear(); + await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - protected override async ValueTask> FindReferencesInDocumentAsync( + protected override ValueTask FindReferencesInDocumentAsync( INamespaceSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - using var _ = ArrayBuilder.GetInstance(out var initialReferences); - if (symbol.IsGlobalNamespace) { - await AddGlobalNamespaceReferencesAsync( - symbol, state, initialReferences, cancellationToken).ConfigureAwait(false); + AddGlobalNamespaceReferences( + symbol, state, processResult, processResultData, cancellationToken); } else { + using var _ = ArrayBuilder.GetInstance(out var initialReferences); + var namespaceName = symbol.Name; - await AddNamedReferencesAsync( - symbol, namespaceName, state, initialReferences, cancellationToken).ConfigureAwait(false); + AddNamedReferences( + symbol, namespaceName, state, StandardCallbacks.AddToArrayBuilder, initialReferences, cancellationToken); foreach (var globalAlias in state.GlobalAliases) { @@ -78,42 +80,47 @@ await AddNamedReferencesAsync( if (state.SyntaxFacts.StringComparer.Equals(namespaceName, globalAlias)) continue; - await AddNamedReferencesAsync( - symbol, globalAlias, state, initialReferences, cancellationToken).ConfigureAwait(false); + AddNamedReferences( + symbol, globalAlias, state, StandardCallbacks.AddToArrayBuilder, initialReferences, cancellationToken); } - initialReferences.AddRange(await FindLocalAliasReferencesAsync( - initialReferences, symbol, state, cancellationToken).ConfigureAwait(false)); + // The items in initialReferences need to be both reported and used later to calculate additional results. + foreach (var location in initialReferences) + processResult(location, processResultData); + + FindLocalAliasReferences( + initialReferences, symbol, state, processResult, processResultData, cancellationToken); - initialReferences.AddRange(await FindReferencesInDocumentInsideGlobalSuppressionsAsync( - symbol, state, cancellationToken).ConfigureAwait(false)); + FindReferencesInDocumentInsideGlobalSuppressions( + symbol, state, processResult, processResultData, cancellationToken); } - return initialReferences.ToImmutableAndClear(); + return ValueTaskFactory.CompletedTask; } /// /// Finds references to in this , but only if it referenced /// though (which might be the actual name of the type, or a global alias to it). /// - private static async ValueTask AddNamedReferencesAsync( + private static void AddNamedReferences( INamespaceSymbol symbol, string name, FindReferencesDocumentState state, - ArrayBuilder initialReferences, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - var tokens = await FindMatchingIdentifierTokensAsync( - state, name, cancellationToken).ConfigureAwait(false); + var tokens = FindMatchingIdentifierTokens(state, name, cancellationToken); - initialReferences.AddRange(await FindReferencesInTokensAsync( - symbol, state, tokens, cancellationToken).ConfigureAwait(false)); + FindReferencesInTokens( + symbol, state, tokens, processResult, processResultData, cancellationToken); } - private static async Task AddGlobalNamespaceReferencesAsync( + private static void AddGlobalNamespaceReferences( INamespaceSymbol symbol, FindReferencesDocumentState state, - ArrayBuilder initialReferences, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { var tokens = state.Root @@ -122,7 +129,7 @@ private static async Task AddGlobalNamespaceReferencesAsync( static (token, state) => state.SyntaxFacts.IsGlobalNamespaceKeyword(token), state); - initialReferences.AddRange(await FindReferencesInTokensAsync( - symbol, state, tokens, cancellationToken).ConfigureAwait(false)); + FindReferencesInTokens( + symbol, state, tokens, processResult, processResultData, cancellationToken); } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OperatorSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OperatorSymbolReferenceFinder.cs index a4135e8d5f065..a6d5cb8b80c0c 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OperatorSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OperatorSymbolReferenceFinder.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.Collections.Generic; using System.Collections.Immutable; using System.Threading; @@ -17,36 +18,41 @@ internal sealed class OperatorSymbolReferenceFinder : AbstractMethodOrPropertyOr protected override bool CanFind(IMethodSymbol symbol) => symbol.MethodKind is MethodKind.UserDefinedOperator or MethodKind.BuiltinOperator; - protected sealed override async Task> DetermineDocumentsToSearchAsync( + protected sealed override async Task DetermineDocumentsToSearchAsync( IMethodSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { var op = symbol.GetPredefinedOperator(); - var documentsWithOp = await FindDocumentsAsync(project, documents, op, cancellationToken).ConfigureAwait(false); - var documentsWithGlobalAttributes = await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, cancellationToken).ConfigureAwait(false); - return documentsWithOp.Concat(documentsWithGlobalAttributes); + await FindDocumentsAsync(project, documents, op, processResult, processResultData, cancellationToken).ConfigureAwait(false); + await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - private static Task> FindDocumentsAsync( + private static Task FindDocumentsAsync( Project project, IImmutableSet? documents, PredefinedOperator op, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { if (op == PredefinedOperator.None) - return SpecializedTasks.EmptyImmutableArray(); + return Task.CompletedTask; return FindDocumentsWithPredicateAsync( - project, documents, static (index, op) => index.ContainsPredefinedOperator(op), op, cancellationToken); + project, documents, static (index, op) => index.ContainsPredefinedOperator(op), op, processResult, processResultData, cancellationToken); } - protected sealed override async ValueTask> FindReferencesInDocumentAsync( + protected sealed override ValueTask FindReferencesInDocumentAsync( IMethodSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -57,12 +63,11 @@ protected sealed override async ValueTask> FindRe static (token, tuple) => IsPotentialReference(tuple.state.SyntaxFacts, tuple.op, token), (state, op)); - var opReferences = await FindReferencesInTokensAsync( - symbol, state, tokens, cancellationToken).ConfigureAwait(false); - var suppressionReferences = await FindReferencesInDocumentInsideGlobalSuppressionsAsync( - symbol, state, cancellationToken).ConfigureAwait(false); - - return opReferences.Concat(suppressionReferences); + FindReferencesInTokens( + symbol, state, tokens, processResult, processResultData, cancellationToken); + FindReferencesInDocumentInsideGlobalSuppressions( + symbol, state, processResult, processResultData, cancellationToken); + return ValueTaskFactory.CompletedTask; } private static bool IsPotentialReference( diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OrdinaryMethodReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OrdinaryMethodReferenceFinder.cs index 91ab4963aa113..bb1dbb697400a 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OrdinaryMethodReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OrdinaryMethodReferenceFinder.cs @@ -2,10 +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.Collections.Generic; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.FindSymbols.Finders; @@ -42,11 +44,13 @@ private static ImmutableArray GetOtherPartsOfPartial(IMethodSymbol symb return []; } - protected override async Task> DetermineDocumentsToSearchAsync( + protected override async Task DetermineDocumentsToSearchAsync( IMethodSymbol methodSymbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -65,38 +69,32 @@ protected override async Task> DetermineDocumentsToSear // searches for these, then we should find usages of 'lock(goo)' or 'synclock(goo)' // since they implicitly call those methods. - var ordinaryDocuments = await FindDocumentsAsync(project, documents, cancellationToken, methodSymbol.Name).ConfigureAwait(false); - var forEachDocuments = IsForEachMethod(methodSymbol) - ? await FindDocumentsWithForEachStatementsAsync(project, documents, cancellationToken).ConfigureAwait(false) - : []; + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, methodSymbol.Name).ConfigureAwait(false); - var deconstructDocuments = IsDeconstructMethod(methodSymbol) - ? await FindDocumentsWithDeconstructionAsync(project, documents, cancellationToken).ConfigureAwait(false) - : []; + if (IsForEachMethod(methodSymbol)) + await FindDocumentsWithForEachStatementsAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var awaitExpressionDocuments = IsGetAwaiterMethod(methodSymbol) - ? await FindDocumentsWithAwaitExpressionAsync(project, documents, cancellationToken).ConfigureAwait(false) - : []; + if (IsDeconstructMethod(methodSymbol)) + await FindDocumentsWithDeconstructionAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var documentsWithGlobalAttributes = await FindDocumentsWithGlobalSuppressMessageAttributeAsync( - project, documents, cancellationToken).ConfigureAwait(false); + if (IsGetAwaiterMethod(methodSymbol)) + await FindDocumentsWithAwaitExpressionAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var documentsWithCollectionInitializers = IsAddMethod(methodSymbol) - ? await FindDocumentsWithCollectionInitializersAsync(project, documents, cancellationToken).ConfigureAwait(false) - : []; + await FindDocumentsWithGlobalSuppressMessageAttributeAsync( + project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); - return ordinaryDocuments.Concat( - forEachDocuments, deconstructDocuments, awaitExpressionDocuments, documentsWithGlobalAttributes, documentsWithCollectionInitializers); + if (IsAddMethod(methodSymbol)) + await FindDocumentsWithCollectionInitializersAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - private static Task> FindDocumentsWithDeconstructionAsync(Project project, IImmutableSet? documents, CancellationToken cancellationToken) - => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsDeconstruction, cancellationToken); + private static Task FindDocumentsWithDeconstructionAsync(Project project, IImmutableSet? documents, Action processResult, TData processResultData, CancellationToken cancellationToken) + => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsDeconstruction, processResult, processResultData, cancellationToken); - private static Task> FindDocumentsWithAwaitExpressionAsync(Project project, IImmutableSet? documents, CancellationToken cancellationToken) - => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsAwait, cancellationToken); + private static Task FindDocumentsWithAwaitExpressionAsync(Project project, IImmutableSet? documents, Action processResult, TData processResultData, CancellationToken cancellationToken) + => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsAwait, processResult, processResultData, cancellationToken); - private static Task> FindDocumentsWithCollectionInitializersAsync(Project project, IImmutableSet? documents, CancellationToken cancellationToken) - => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsCollectionInitializer, cancellationToken); + private static Task FindDocumentsWithCollectionInitializersAsync(Project project, IImmutableSet? documents, Action processResult, TData processResultData, CancellationToken cancellationToken) + => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsCollectionInitializer, processResult, processResultData, cancellationToken); private static bool IsForEachMethod(IMethodSymbol methodSymbol) => methodSymbol.Name is WellKnownMemberNames.GetEnumeratorMethodName or @@ -111,34 +109,32 @@ private static bool IsGetAwaiterMethod(IMethodSymbol methodSymbol) private static bool IsAddMethod(IMethodSymbol methodSymbol) => methodSymbol.Name == WellKnownMemberNames.CollectionInitializerAddMethodName; - protected sealed override async ValueTask> FindReferencesInDocumentAsync( + protected sealed override ValueTask FindReferencesInDocumentAsync( IMethodSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - var nameMatches = await FindReferencesInDocumentUsingSymbolNameAsync( - symbol, state, cancellationToken).ConfigureAwait(false); + FindReferencesInDocumentUsingSymbolName( + symbol, state, processResult, processResultData, cancellationToken); - var forEachMatches = IsForEachMethod(symbol) - ? await FindReferencesInForEachStatementsAsync(symbol, state, cancellationToken).ConfigureAwait(false) - : []; + if (IsForEachMethod(symbol)) + FindReferencesInForEachStatements(symbol, state, processResult, processResultData, cancellationToken); - var deconstructMatches = IsDeconstructMethod(symbol) - ? await FindReferencesInDeconstructionAsync(symbol, state, cancellationToken).ConfigureAwait(false) - : []; + if (IsDeconstructMethod(symbol)) + FindReferencesInDeconstruction(symbol, state, processResult, processResultData, cancellationToken); - var getAwaiterMatches = IsGetAwaiterMethod(symbol) - ? await FindReferencesInAwaitExpressionAsync(symbol, state, cancellationToken).ConfigureAwait(false) - : []; + if (IsGetAwaiterMethod(symbol)) + FindReferencesInAwaitExpression(symbol, state, processResult, processResultData, cancellationToken); - var suppressionReferences = await FindReferencesInDocumentInsideGlobalSuppressionsAsync( - symbol, state, cancellationToken).ConfigureAwait(false); + FindReferencesInDocumentInsideGlobalSuppressions( + symbol, state, processResult, processResultData, cancellationToken); - var addMatches = IsAddMethod(symbol) - ? await FindReferencesInCollectionInitializerAsync(symbol, state, cancellationToken).ConfigureAwait(false) - : []; + if (IsAddMethod(symbol)) + FindReferencesInCollectionInitializer(symbol, state, processResult, processResultData, cancellationToken); - return nameMatches.Concat(forEachMatches, deconstructMatches, getAwaiterMatches, suppressionReferences, addMatches); + return ValueTaskFactory.CompletedTask; } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ParameterSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ParameterSymbolReferenceFinder.cs index c15af3ce4355e..adef39983674b 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ParameterSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ParameterSymbolReferenceFinder.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.FindSymbols.Finders; @@ -20,11 +21,13 @@ internal sealed class ParameterSymbolReferenceFinder : AbstractReferenceFinder true; - protected override Task> DetermineDocumentsToSearchAsync( + protected override Task DetermineDocumentsToSearchAsync( IParameterSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -33,16 +36,19 @@ protected override Task> DetermineDocumentsToSearchAsyn // elsewhere as "paramName:" or "paramName:=". We can narrow the search by // filtering down to matches of that form. For now we just return any document // that references something with this name. - return FindDocumentsAsync(project, documents, cancellationToken, symbol.Name); + return FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, symbol.Name); } - protected override ValueTask> FindReferencesInDocumentAsync( + protected override ValueTask FindReferencesInDocumentAsync( IParameterSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - return FindReferencesInDocumentUsingIdentifierAsync(symbol, symbol.Name, state, cancellationToken); + FindReferencesInDocumentUsingIdentifier(symbol, symbol.Name, state, processResult, processResultData, cancellationToken); + return ValueTaskFactory.CompletedTask; } protected override async ValueTask> DetermineCascadedSymbolsAsync( diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertyAccessorSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertyAccessorSymbolReferenceFinder.cs index 383c82ad44096..cf0a657769c89 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertyAccessorSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertyAccessorSymbolReferenceFinder.cs @@ -2,13 +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.Collections.Generic; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.FindSymbols.Finders; @@ -30,64 +29,66 @@ protected override ValueTask> DetermineCascadedSymbolsAs : new(ImmutableArray.Create(symbol.AssociatedSymbol)); } - protected override async Task> DetermineDocumentsToSearchAsync( + protected override async Task DetermineDocumentsToSearchAsync( IMethodSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { // First, find any documents with the full name of the accessor (i.e. get_Goo). // This will find explicit calls to the method (which can happen when C# references // a VB parameterized property). - var documentsWithName = await FindDocumentsAsync(project, documents, cancellationToken, symbol.Name).ConfigureAwait(false); + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, symbol.Name).ConfigureAwait(false); - var propertyDocuments = ImmutableArray.Empty; if (symbol.AssociatedSymbol is IPropertySymbol property && options.AssociatePropertyReferencesWithSpecificAccessor) { // we want to associate normal property references with the specific accessor being // referenced. So we also need to include documents with our property's name. Just // defer to the Property finder to find these docs and combine them with the result. - propertyDocuments = await ReferenceFinders.Property.DetermineDocumentsToSearchAsync( + await ReferenceFinders.Property.DetermineDocumentsToSearchAsync( property, globalAliases, project, documents, + processResult, processResultData, options with { AssociatePropertyReferencesWithSpecificAccessor = false }, cancellationToken).ConfigureAwait(false); } - var documentsWithGlobalAttributes = await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, cancellationToken).ConfigureAwait(false); - return documentsWithName.Concat(propertyDocuments, documentsWithGlobalAttributes); + await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - protected override async ValueTask> FindReferencesInDocumentAsync( + protected override async ValueTask FindReferencesInDocumentAsync( IMethodSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - var references = await FindReferencesInDocumentUsingSymbolNameAsync( - symbol, state, cancellationToken).ConfigureAwait(false); + FindReferencesInDocumentUsingSymbolName( + symbol, state, processResult, processResultData, cancellationToken); if (symbol.AssociatedSymbol is not IPropertySymbol property || !options.AssociatePropertyReferencesWithSpecificAccessor) { - return references; + return; } - var propertyReferences = await ReferenceFinders.Property.FindReferencesInDocumentAsync( - property, state, - options with { AssociatePropertyReferencesWithSpecificAccessor = false }, - cancellationToken).ConfigureAwait(false); - - var accessorReferences = propertyReferences.WhereAsArray( - loc => + await ReferenceFinders.Property.FindReferencesInDocumentAsync( + property, + state, + static (loc, data) => { var accessors = GetReferencedAccessorSymbols( - state, property, loc.Node, cancellationToken); - return accessors.Contains(symbol); - }); - - return references.Concat(accessorReferences); + data.state, data.property, loc.Node, data.cancellationToken); + if (accessors.Contains(data.symbol)) + data.processResult(loc, data.processResultData); + }, + (property, symbol, state, processResult, processResultData, cancellationToken), + options with { AssociatePropertyReferencesWithSpecificAccessor = false }, + cancellationToken).ConfigureAwait(false); } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertySymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertySymbolReferenceFinder.cs index 6d2acf8ebeb77..d5fed2c94c5aa 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertySymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertySymbolReferenceFinder.cs @@ -13,6 +13,7 @@ using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.FindSymbols.Finders; @@ -91,87 +92,93 @@ private static void CascadeToPrimaryConstructorParameters(IPropertySymbol proper } } - protected sealed override async Task> DetermineDocumentsToSearchAsync( + protected sealed override async Task DetermineDocumentsToSearchAsync( IPropertySymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - var ordinaryDocuments = await FindDocumentsAsync(project, documents, cancellationToken, symbol.Name).ConfigureAwait(false); + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, symbol.Name).ConfigureAwait(false); - var forEachDocuments = IsForEachProperty(symbol) - ? await FindDocumentsWithForEachStatementsAsync(project, documents, cancellationToken).ConfigureAwait(false) - : []; + if (IsForEachProperty(symbol)) + await FindDocumentsWithForEachStatementsAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var elementAccessDocument = symbol.IsIndexer - ? await FindDocumentWithExplicitOrImplicitElementAccessExpressionsAsync(project, documents, cancellationToken).ConfigureAwait(false) - : []; + if (symbol.IsIndexer) + await FindDocumentWithExplicitOrImplicitElementAccessExpressionsAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var indexerMemberCrefDocument = symbol.IsIndexer - ? await FindDocumentWithIndexerMemberCrefAsync(project, documents, cancellationToken).ConfigureAwait(false) - : []; + if (symbol.IsIndexer) + await FindDocumentWithIndexerMemberCrefAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var documentsWithGlobalAttributes = await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, cancellationToken).ConfigureAwait(false); - return ordinaryDocuments.Concat(forEachDocuments, elementAccessDocument, indexerMemberCrefDocument, documentsWithGlobalAttributes); + await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } private static bool IsForEachProperty(IPropertySymbol symbol) => symbol.Name == WellKnownMemberNames.CurrentPropertyName; - protected sealed override async ValueTask> FindReferencesInDocumentAsync( + protected sealed override ValueTask FindReferencesInDocumentAsync( IPropertySymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - var nameReferences = await FindReferencesInDocumentUsingSymbolNameAsync( - symbol, state, cancellationToken).ConfigureAwait(false); - - if (options.AssociatePropertyReferencesWithSpecificAccessor) - { - // We want to associate property references to a specific accessor (if an accessor - // is being referenced). Check if this reference would match an accessor. If so, do - // not add it. It will be added by PropertyAccessorSymbolReferenceFinder. - nameReferences = nameReferences.WhereAsArray(loc => + FindReferencesInDocumentUsingSymbolName( + symbol, + state, + static (loc, data) => { - var accessors = GetReferencedAccessorSymbols( - state, symbol, loc.Node, cancellationToken); - return accessors.IsEmpty; - }); - } + var useResult = true; + if (data.options.AssociatePropertyReferencesWithSpecificAccessor) + { + // We want to associate property references to a specific accessor (if an accessor + // is being referenced). Check if this reference would match an accessor. If so, do + // not add it. It will be added by PropertyAccessorSymbolReferenceFinder. + var accessors = GetReferencedAccessorSymbols( + data.state, data.symbol, loc.Node, data.cancellationToken); + useResult = accessors.IsEmpty; + } - var forEachReferences = IsForEachProperty(symbol) - ? await FindReferencesInForEachStatementsAsync(symbol, state, cancellationToken).ConfigureAwait(false) - : []; + if (useResult) + data.processResult(loc, data.processResultData); + }, + processResultData: (self: this, symbol, state, processResult, processResultData, options, cancellationToken), + cancellationToken); - var indexerReferences = symbol.IsIndexer - ? await FindIndexerReferencesAsync(symbol, state, options, cancellationToken).ConfigureAwait(false) - : []; + if (IsForEachProperty(symbol)) + FindReferencesInForEachStatements(symbol, state, processResult, processResultData, cancellationToken); - var suppressionReferences = await FindReferencesInDocumentInsideGlobalSuppressionsAsync( - symbol, state, cancellationToken).ConfigureAwait(false); - return nameReferences.Concat(forEachReferences, indexerReferences, suppressionReferences); + if (symbol.IsIndexer) + FindIndexerReferences(symbol, state, processResult, processResultData, options, cancellationToken); + + FindReferencesInDocumentInsideGlobalSuppressions( + symbol, state, processResult, processResultData, cancellationToken); + return ValueTaskFactory.CompletedTask; } - private static Task> FindDocumentWithExplicitOrImplicitElementAccessExpressionsAsync( - Project project, IImmutableSet? documents, CancellationToken cancellationToken) + private static Task FindDocumentWithExplicitOrImplicitElementAccessExpressionsAsync( + Project project, IImmutableSet? documents, Action processResult, TData processResultData, CancellationToken cancellationToken) { return FindDocumentsWithPredicateAsync( - project, documents, static index => index.ContainsExplicitOrImplicitElementAccessExpression, cancellationToken); + project, documents, static index => index.ContainsExplicitOrImplicitElementAccessExpression, processResult, processResultData, cancellationToken); } - private static Task> FindDocumentWithIndexerMemberCrefAsync( - Project project, IImmutableSet? documents, CancellationToken cancellationToken) + private static Task FindDocumentWithIndexerMemberCrefAsync( + Project project, IImmutableSet? documents, Action processResult, TData processResultData, CancellationToken cancellationToken) { return FindDocumentsWithPredicateAsync( - project, documents, static index => index.ContainsIndexerMemberCref, cancellationToken); + project, documents, static index => index.ContainsIndexerMemberCref, processResult, processResultData, cancellationToken); } - private static async Task> FindIndexerReferencesAsync( + private static void FindIndexerReferences( IPropertySymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -179,7 +186,7 @@ private static async Task> FindIndexerReferencesA { // Looking for individual get/set references. Don't find anything here. // these results will be provided by the PropertyAccessorSymbolReferenceFinder - return []; + return; } var syntaxFacts = state.SyntaxFacts; @@ -190,31 +197,28 @@ private static async Task> FindIndexerReferencesA syntaxFacts.IsImplicitElementAccess(node) || syntaxFacts.IsConditionalAccessExpression(node) || syntaxFacts.IsIndexerMemberCref(node)); - using var _ = ArrayBuilder.GetInstance(out var locations); foreach (var node in indexerReferenceExpressions) { cancellationToken.ThrowIfCancellationRequested(); - var (matched, candidateReason, indexerReference) = await ComputeIndexerInformationAsync( - symbol, state, node, cancellationToken).ConfigureAwait(false); + var (matched, candidateReason, indexerReference) = ComputeIndexerInformation(symbol, state, node, cancellationToken); if (!matched) continue; var location = state.SyntaxTree.GetLocation(new TextSpan(indexerReference.SpanStart, 0)); var symbolUsageInfo = GetSymbolUsageInfo(node, state, cancellationToken); - locations.Add(new FinderLocation(node, + var result = new FinderLocation(node, new ReferenceLocation( state.Document, alias: null, location, isImplicit: false, symbolUsageInfo, GetAdditionalFindUsagesProperties(node, state), - candidateReason))); + candidateReason)); + processResult(result, processResultData); } - - return locations.ToImmutableAndClear(); } - private static ValueTask<(bool matched, CandidateReason reason, SyntaxNode indexerReference)> ComputeIndexerInformationAsync( + private static (bool matched, CandidateReason reason, SyntaxNode indexerReference) ComputeIndexerInformation( IPropertySymbol symbol, FindReferencesDocumentState state, SyntaxNode node, @@ -225,33 +229,33 @@ private static async Task> FindIndexerReferencesA if (syntaxFacts.IsElementAccessExpression(node)) { // The indexerReference for an element access expression will not be null - return ComputeElementAccessInformationAsync(symbol, node, state, cancellationToken)!; + return ComputeElementAccessInformation(symbol, node, state, cancellationToken)!; } else if (syntaxFacts.IsImplicitElementAccess(node)) { - return ComputeImplicitElementAccessInformationAsync(symbol, node, state, cancellationToken)!; + return ComputeImplicitElementAccessInformation(symbol, node, state, cancellationToken)!; } else if (syntaxFacts.IsConditionalAccessExpression(node)) { - return ComputeConditionalAccessInformationAsync(symbol, node, state, cancellationToken); + return ComputeConditionalAccessInformation(symbol, node, state, cancellationToken); } else { Debug.Assert(syntaxFacts.IsIndexerMemberCref(node)); - return ComputeIndexerMemberCRefInformationAsync(symbol, state, node, cancellationToken); + return ComputeIndexerMemberCRefInformation(symbol, state, node, cancellationToken); } } - private static async ValueTask<(bool matched, CandidateReason reason, SyntaxNode indexerReference)> ComputeIndexerMemberCRefInformationAsync( + private static (bool matched, CandidateReason reason, SyntaxNode indexerReference) ComputeIndexerMemberCRefInformation( IPropertySymbol symbol, FindReferencesDocumentState state, SyntaxNode node, CancellationToken cancellationToken) { - var (matched, reason) = await SymbolsMatchAsync(symbol, state, node, cancellationToken).ConfigureAwait(false); + var (matched, reason) = SymbolsMatch(symbol, state, node, cancellationToken); // For an IndexerMemberCRef the node itself is the indexer we are looking for. return (matched, reason, node); } - private static async ValueTask<(bool matched, CandidateReason reason, SyntaxNode indexerReference)> ComputeConditionalAccessInformationAsync( + private static (bool matched, CandidateReason reason, SyntaxNode indexerReference) ComputeConditionalAccessInformation( IPropertySymbol symbol, SyntaxNode node, FindReferencesDocumentState state, CancellationToken cancellationToken) { // For a ConditionalAccessExpression the whenNotNull component is the indexer reference we are looking for @@ -266,16 +270,16 @@ private static async Task> FindIndexerReferencesA return default; } - var (matched, reason) = await SymbolsMatchAsync(symbol, state, indexerReference, cancellationToken).ConfigureAwait(false); + var (matched, reason) = SymbolsMatch(symbol, state, indexerReference, cancellationToken); return (matched, reason, indexerReference); } - private static async ValueTask<(bool matched, CandidateReason reason, SyntaxNode? indexerReference)> ComputeElementAccessInformationAsync( + private static (bool matched, CandidateReason reason, SyntaxNode? indexerReference) ComputeElementAccessInformation( IPropertySymbol symbol, SyntaxNode node, FindReferencesDocumentState state, CancellationToken cancellationToken) { // For an ElementAccessExpression the indexer we are looking for is the argumentList component. state.SyntaxFacts.GetPartsOfElementAccessExpression(node, out var expression, out var indexerReference); - if (expression != null && (await SymbolsMatchAsync(symbol, state, expression, cancellationToken).ConfigureAwait(false)).matched) + if (expression != null && SymbolsMatch(symbol, state, expression, cancellationToken).matched) { // Element access with explicit member name (allowed in VB). We will have // already added a reference location for the member name identifier, so skip @@ -283,15 +287,15 @@ private static async Task> FindIndexerReferencesA return default; } - var (matched, reason) = await SymbolsMatchAsync(symbol, state, node, cancellationToken).ConfigureAwait(false); + var (matched, reason) = SymbolsMatch(symbol, state, node, cancellationToken); return (matched, reason, indexerReference); } - private static async ValueTask<(bool matched, CandidateReason reason, SyntaxNode indexerReference)> ComputeImplicitElementAccessInformationAsync( + private static (bool matched, CandidateReason reason, SyntaxNode indexerReference) ComputeImplicitElementAccessInformation( IPropertySymbol symbol, SyntaxNode node, FindReferencesDocumentState state, CancellationToken cancellationToken) { var argumentList = state.SyntaxFacts.GetArgumentListOfImplicitElementAccess(node); - var (matched, reason) = await SymbolsMatchAsync(symbol, state, node, cancellationToken).ConfigureAwait(false); + var (matched, reason) = SymbolsMatch(symbol, state, node, cancellationToken); return (matched, reason, argumentList); } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/TypeParameterSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/TypeParameterSymbolReferenceFinder.cs index 32587f95c601e..6b1ffc51734be 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/TypeParameterSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/TypeParameterSymbolReferenceFinder.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.Collections.Generic; using System.Collections.Immutable; using System.Threading; @@ -14,11 +15,13 @@ internal sealed class TypeParameterSymbolReferenceFinder : AbstractTypeParameter protected override bool CanFind(ITypeParameterSymbol symbol) => symbol.TypeParameterKind != TypeParameterKind.Method; - protected override Task> DetermineDocumentsToSearchAsync( + protected override Task DetermineDocumentsToSearchAsync( ITypeParameterSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -29,6 +32,6 @@ protected override Task> DetermineDocumentsToSearchAsyn // parameter has a different name in different parts that we won't find it. However, // this only happens in error situations. It is not legal in C# to use a different // name for a type parameter in different parts. - return FindDocumentsAsync(project, documents, cancellationToken, symbol.Name, symbol.ContainingType.Name); + return FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, symbol.Name, symbol.ContainingType.Name); } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/MetadataUnifyingSymbolHashSet.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/MetadataUnifyingSymbolHashSet.cs index 906b70075c4fb..7f31eb6cc5306 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/MetadataUnifyingSymbolHashSet.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/MetadataUnifyingSymbolHashSet.cs @@ -3,12 +3,24 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using Microsoft.CodeAnalysis.PooledObjects; namespace Microsoft.CodeAnalysis.FindSymbols; internal sealed class MetadataUnifyingSymbolHashSet : HashSet { + private static readonly ObjectPool s_metadataUnifyingSymbolHashSetPool = new(() => []); + public MetadataUnifyingSymbolHashSet() : base(MetadataUnifyingEquivalenceComparer.Instance) { } + + public static MetadataUnifyingSymbolHashSet AllocateFromPool() + => s_metadataUnifyingSymbolHashSetPool.Allocate(); + + public static void ClearAndFree(MetadataUnifyingSymbolHashSet set) + { + set.Clear(); + s_metadataUnifyingSymbolHashSetPool.Free(set); + } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/NoOpStreamingFindReferencesProgress.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/NoOpStreamingFindReferencesProgress.cs index 511fd7fb42fd7..b10cd55dfc0e9 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/NoOpStreamingFindReferencesProgress.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/NoOpStreamingFindReferencesProgress.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.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Shared.Utilities; @@ -26,9 +27,7 @@ private NoOpStreamingFindReferencesProgress() public ValueTask OnCompletedAsync(CancellationToken cancellationToken) => default; public ValueTask OnStartedAsync(CancellationToken cancellationToken) => default; public ValueTask OnDefinitionFoundAsync(SymbolGroup group, CancellationToken cancellationToken) => default; - public ValueTask OnReferenceFoundAsync(SymbolGroup group, ISymbol symbol, ReferenceLocation location, CancellationToken cancellationToken) => default; - public ValueTask OnFindInDocumentStartedAsync(Document document, CancellationToken cancellationToken) => default; - public ValueTask OnFindInDocumentCompletedAsync(Document document, CancellationToken cancellationToken) => default; + public ValueTask OnReferencesFoundAsync(ImmutableArray<(SymbolGroup group, ISymbol symbol, ReferenceLocation location)> references, CancellationToken cancellationToken) => default; private class NoOpProgressTracker : IStreamingProgressTracker { diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/StandardCallbacks.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/StandardCallbacks.cs new file mode 100644 index 0000000000000..5696c32d2e8b9 --- /dev/null +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/StandardCallbacks.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using Microsoft.CodeAnalysis.PooledObjects; + +namespace Microsoft.CodeAnalysis.FindSymbols; + +internal static class StandardCallbacks +{ + public static readonly Action> AddToHashSet = + static (data, set) => set.Add(data); + + public static readonly Action> AddToArrayBuilder = + static (data, builder) => builder.Add(data); +} diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/StreamingFindReferencesProgress.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/StreamingFindReferencesProgress.cs index 0a13aa5929644..32a4ce2080823 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/StreamingFindReferencesProgress.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/StreamingFindReferencesProgress.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.ErrorReporting; @@ -37,18 +38,6 @@ public ValueTask OnCompletedAsync(CancellationToken cancellationToken) return default; } - public ValueTask OnFindInDocumentCompletedAsync(Document document, CancellationToken cancellationToken) - { - _progress.OnFindInDocumentCompleted(document); - return default; - } - - public ValueTask OnFindInDocumentStartedAsync(Document document, CancellationToken cancellationToken) - { - _progress.OnFindInDocumentStarted(document); - return default; - } - public ValueTask OnDefinitionFoundAsync(SymbolGroup group, CancellationToken cancellationToken) { try @@ -64,9 +53,11 @@ public ValueTask OnDefinitionFoundAsync(SymbolGroup group, CancellationToken can } } - public ValueTask OnReferenceFoundAsync(SymbolGroup group, ISymbol symbol, ReferenceLocation location, CancellationToken cancellationToken) + public ValueTask OnReferencesFoundAsync(ImmutableArray<(SymbolGroup group, ISymbol symbol, ReferenceLocation location)> references, CancellationToken cancellationToken) { - _progress.OnReferenceFound(symbol, location); + foreach (var (_, symbol, location) in references) + _progress.OnReferenceFound(symbol, location); + return default; } diff --git a/src/Workspaces/Core/Portable/FindSymbols/IRemoteSymbolFinderService.cs b/src/Workspaces/Core/Portable/FindSymbols/IRemoteSymbolFinderService.cs index 1f3b3736c5090..2c95215a67b28 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/IRemoteSymbolFinderService.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/IRemoteSymbolFinderService.cs @@ -19,10 +19,8 @@ internal interface ICallback ValueTask ReferenceItemsCompletedAsync(RemoteServiceCallbackId callbackId, int count, CancellationToken cancellationToken); ValueTask OnStartedAsync(RemoteServiceCallbackId callbackId, CancellationToken cancellationToken); ValueTask OnCompletedAsync(RemoteServiceCallbackId callbackId, CancellationToken cancellationToken); - ValueTask OnFindInDocumentStartedAsync(RemoteServiceCallbackId callbackId, DocumentId documentId, CancellationToken cancellationToken); - ValueTask OnFindInDocumentCompletedAsync(RemoteServiceCallbackId callbackId, DocumentId documentId, CancellationToken cancellationToken); ValueTask OnDefinitionFoundAsync(RemoteServiceCallbackId callbackId, SerializableSymbolGroup group, CancellationToken cancellationToken); - ValueTask OnReferenceFoundAsync(RemoteServiceCallbackId callbackId, SerializableSymbolGroup group, SerializableSymbolAndProjectId definition, SerializableReferenceLocation reference, CancellationToken cancellationToken); + ValueTask OnReferencesFoundAsync(RemoteServiceCallbackId callbackId, ImmutableArray<(SerializableSymbolGroup group, SerializableSymbolAndProjectId definition, SerializableReferenceLocation reference)> references, CancellationToken cancellationToken); ValueTask AddLiteralItemsAsync(RemoteServiceCallbackId callbackId, int count, CancellationToken cancellationToken); ValueTask LiteralItemsCompletedAsync(RemoteServiceCallbackId callbackId, int count, CancellationToken cancellationToken); diff --git a/src/Workspaces/Core/Portable/FindSymbols/IStreamingFindReferencesProgress.cs b/src/Workspaces/Core/Portable/FindSymbols/IStreamingFindReferencesProgress.cs index fa7a34043bc62..5a84f3f20655e 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/IStreamingFindReferencesProgress.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/IStreamingFindReferencesProgress.cs @@ -72,11 +72,8 @@ internal interface IStreamingFindReferencesProgress ValueTask OnStartedAsync(CancellationToken cancellationToken); ValueTask OnCompletedAsync(CancellationToken cancellationToken); - ValueTask OnFindInDocumentStartedAsync(Document document, CancellationToken cancellationToken); - ValueTask OnFindInDocumentCompletedAsync(Document document, CancellationToken cancellationToken); - ValueTask OnDefinitionFoundAsync(SymbolGroup group, CancellationToken cancellationToken); - ValueTask OnReferenceFoundAsync(SymbolGroup group, ISymbol symbol, ReferenceLocation location, CancellationToken cancellationToken); + ValueTask OnReferencesFoundAsync(ImmutableArray<(SymbolGroup group, ISymbol symbol, ReferenceLocation location)> references, CancellationToken cancellationToken); } internal interface IStreamingFindLiteralReferencesProgress diff --git a/src/Workspaces/Core/Portable/FindSymbols/Shared/AbstractSyntaxIndex_Persistence.cs b/src/Workspaces/Core/Portable/FindSymbols/Shared/AbstractSyntaxIndex_Persistence.cs index e0a83cd9bd456..dd58ce4082086 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/Shared/AbstractSyntaxIndex_Persistence.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/Shared/AbstractSyntaxIndex_Persistence.cs @@ -64,7 +64,6 @@ internal partial class AbstractSyntaxIndex try { var storage = await storageService.GetStorageAsync(documentKey.Project.Solution, cancellationToken).ConfigureAwait(false); - await using var _ = storage.ConfigureAwait(false); // attempt to load from persisted state using var stream = await storage.ReadStreamAsync(documentKey, s_persistenceName, checksum, cancellationToken).ConfigureAwait(false); @@ -156,7 +155,6 @@ private async Task SaveAsync( try { var storage = await persistentStorageService.GetStorageAsync(solutionKey, cancellationToken).ConfigureAwait(false); - await using var _ = storage.ConfigureAwait(false); using (var stream = SerializableBytes.CreateWritableStream()) { diff --git a/src/Workspaces/Core/Portable/FindSymbols/StreamingProgressCollector.cs b/src/Workspaces/Core/Portable/FindSymbols/StreamingProgressCollector.cs index 9c62cd2c19139..912ae358bea4b 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/StreamingProgressCollector.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/StreamingProgressCollector.cs @@ -42,7 +42,7 @@ public ImmutableArray GetReferencedSymbols() { var result = new FixedSizeArrayBuilder(_symbolToLocations.Count); foreach (var (symbol, locations) in _symbolToLocations) - result.Add(new ReferencedSymbol(symbol, locations.ToImmutableArray())); + result.Add(new ReferencedSymbol(symbol, [.. locations])); return result.MoveToImmutable(); } @@ -51,9 +51,6 @@ public ImmutableArray GetReferencedSymbols() public ValueTask OnStartedAsync(CancellationToken cancellationToken) => underlyingProgress.OnStartedAsync(cancellationToken); public ValueTask OnCompletedAsync(CancellationToken cancellationToken) => underlyingProgress.OnCompletedAsync(cancellationToken); - public ValueTask OnFindInDocumentCompletedAsync(Document document, CancellationToken cancellationToken) => underlyingProgress.OnFindInDocumentCompletedAsync(document, cancellationToken); - public ValueTask OnFindInDocumentStartedAsync(Document document, CancellationToken cancellationToken) => underlyingProgress.OnFindInDocumentStartedAsync(document, cancellationToken); - public ValueTask OnDefinitionFoundAsync(SymbolGroup group, CancellationToken cancellationToken) { try @@ -72,13 +69,15 @@ public ValueTask OnDefinitionFoundAsync(SymbolGroup group, CancellationToken can } } - public ValueTask OnReferenceFoundAsync(SymbolGroup group, ISymbol definition, ReferenceLocation location, CancellationToken cancellationToken) + public ValueTask OnReferencesFoundAsync( + ImmutableArray<(SymbolGroup group, ISymbol symbol, ReferenceLocation location)> references, CancellationToken cancellationToken) { lock (_gate) { - _symbolToLocations[definition].Add(location); + foreach (var (_, definition, location) in references) + _symbolToLocations[definition].Add(location); } - return underlyingProgress.OnReferenceFoundAsync(group, definition, location, cancellationToken); + return underlyingProgress.OnReferencesFoundAsync(references, cancellationToken); } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.CallbackDispatcher.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.CallbackDispatcher.cs index 1c4eaad54f7a0..84d6270bf7145 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.CallbackDispatcher.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.CallbackDispatcher.cs @@ -3,6 +3,7 @@ // 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; @@ -43,14 +44,8 @@ public ValueTask OnCompletedAsync(RemoteServiceCallbackId callbackId, Cancellati public ValueTask OnDefinitionFoundAsync(RemoteServiceCallbackId callbackId, SerializableSymbolGroup symbolGroup, CancellationToken cancellationToken) => GetFindReferencesCallback(callbackId).OnDefinitionFoundAsync(symbolGroup, cancellationToken); - public ValueTask OnFindInDocumentCompletedAsync(RemoteServiceCallbackId callbackId, DocumentId documentId, CancellationToken cancellationToken) - => GetFindReferencesCallback(callbackId).OnFindInDocumentCompletedAsync(documentId, cancellationToken); - - public ValueTask OnFindInDocumentStartedAsync(RemoteServiceCallbackId callbackId, DocumentId documentId, CancellationToken cancellationToken) - => GetFindReferencesCallback(callbackId).OnFindInDocumentStartedAsync(documentId, cancellationToken); - - public ValueTask OnReferenceFoundAsync(RemoteServiceCallbackId callbackId, SerializableSymbolGroup symbolGroup, SerializableSymbolAndProjectId definition, SerializableReferenceLocation reference, CancellationToken cancellationToken) - => GetFindReferencesCallback(callbackId).OnReferenceFoundAsync(symbolGroup, definition, reference, cancellationToken); + public ValueTask OnReferencesFoundAsync(RemoteServiceCallbackId callbackId, ImmutableArray<(SerializableSymbolGroup group, SerializableSymbolAndProjectId definition, SerializableReferenceLocation reference)> references, CancellationToken cancellationToken) + => GetFindReferencesCallback(callbackId).OnReferencesFoundAsync(references, cancellationToken); public ValueTask OnStartedAsync(RemoteServiceCallbackId callbackId, CancellationToken cancellationToken) => GetFindReferencesCallback(callbackId).OnStartedAsync(cancellationToken); diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.FindReferencesServerCallback.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.FindReferencesServerCallback.cs index 89be7cdadb127..2830187083028 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.FindReferencesServerCallback.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.FindReferencesServerCallback.cs @@ -8,8 +8,6 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Remote; -using Microsoft.CodeAnalysis.Shared.Extensions; -using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.FindSymbols; @@ -40,18 +38,6 @@ public ValueTask OnStartedAsync(CancellationToken cancellationToken) public ValueTask OnCompletedAsync(CancellationToken cancellationToken) => progress.OnCompletedAsync(cancellationToken); - public async ValueTask OnFindInDocumentStartedAsync(DocumentId documentId, CancellationToken cancellationToken) - { - var document = await solution.GetRequiredDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); - await progress.OnFindInDocumentStartedAsync(document, cancellationToken).ConfigureAwait(false); - } - - public async ValueTask OnFindInDocumentCompletedAsync(DocumentId documentId, CancellationToken cancellationToken) - { - var document = await solution.GetRequiredDocumentAsync(documentId, includeSourceGenerated: true, cancellationToken).ConfigureAwait(false); - await progress.OnFindInDocumentCompletedAsync(document, cancellationToken).ConfigureAwait(false); - } - public async ValueTask OnDefinitionFoundAsync(SerializableSymbolGroup dehydrated, CancellationToken cancellationToken) { Contract.ThrowIfTrue(dehydrated.Symbols.Count == 0); @@ -67,7 +53,7 @@ public async ValueTask OnDefinitionFoundAsync(SerializableSymbolGroup dehydrated map[symbolAndProjectId] = symbol; } - var symbolGroup = new SymbolGroup(map.Values.ToImmutableArray()); + var symbolGroup = new SymbolGroup([.. map.Values]); lock (_gate) { _groupMap[dehydrated] = symbolGroup; @@ -78,34 +64,38 @@ public async ValueTask OnDefinitionFoundAsync(SerializableSymbolGroup dehydrated await progress.OnDefinitionFoundAsync(symbolGroup, cancellationToken).ConfigureAwait(false); } - public async ValueTask OnReferenceFoundAsync( - SerializableSymbolGroup serializableSymbolGroup, - SerializableSymbolAndProjectId serializableSymbol, - SerializableReferenceLocation reference, + public async ValueTask OnReferencesFoundAsync( + ImmutableArray<(SerializableSymbolGroup serializableSymbolGroup, SerializableSymbolAndProjectId serializableSymbol, SerializableReferenceLocation reference)> references, CancellationToken cancellationToken) { - SymbolGroup? symbolGroup; - ISymbol? symbol; - lock (_gate) + using var _ = ArrayBuilder<(SymbolGroup group, ISymbol symbol, ReferenceLocation location)>.GetInstance(references.Length, out var rehydrated); + foreach (var (serializableSymbolGroup, serializableSymbol, reference) in references) { - // The definition may not be in the map if we failed to map it over using TryRehydrateAsync in OnDefinitionFoundAsync. - // Just ignore this reference. Note: while this is a degraded experience: - // - // 1. TryRehydrateAsync logs an NFE so we can track down while we're failing to roundtrip the - // definition so we can track down that issue. - // 2. NFE'ing and failing to show a result, is much better than NFE'ing and then crashing - // immediately afterwards. - if (!_groupMap.TryGetValue(serializableSymbolGroup, out symbolGroup) || - !_definitionMap.TryGetValue(serializableSymbol, out symbol)) + SymbolGroup? symbolGroup; + ISymbol? symbol; + lock (_gate) { - return; + // The definition may not be in the map if we failed to map it over using TryRehydrateAsync in OnDefinitionFoundAsync. + // Just ignore this reference. Note: while this is a degraded experience: + // + // 1. TryRehydrateAsync logs an NFE so we can track down while we're failing to roundtrip the + // definition so we can track down that issue. + // 2. NFE'ing and failing to show a result, is much better than NFE'ing and then crashing + // immediately afterwards. + if (!_groupMap.TryGetValue(serializableSymbolGroup, out symbolGroup) || + !_definitionMap.TryGetValue(serializableSymbol, out symbol)) + { + continue; + } } - } - var referenceLocation = await reference.RehydrateAsync( - solution, cancellationToken).ConfigureAwait(false); + var referenceLocation = await reference.RehydrateAsync( + solution, cancellationToken).ConfigureAwait(false); + rehydrated.Add((symbolGroup, symbol, referenceLocation)); + } - await progress.OnReferenceFoundAsync(symbolGroup, symbol, referenceLocation, cancellationToken).ConfigureAwait(false); + if (rehydrated.Count > 0) + await progress.OnReferencesFoundAsync(rehydrated.ToImmutableAndClear(), cancellationToken).ConfigureAwait(false); } } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.cs index f69e50e9a3529..89ac7f1794849 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.cs @@ -308,6 +308,6 @@ internal static async Task> FindLinkedSymbolsAsync( } } - return linkedSymbols.ToImmutableArray(); + return [.. linkedSymbols]; } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder_Helpers.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder_Helpers.cs index 76d59fc83456d..c0d596fa2f7ed 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder_Helpers.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder_Helpers.cs @@ -30,11 +30,10 @@ Accessibility.Protected or return true; } - internal static async Task OriginalSymbolsMatchAsync( + internal static bool OriginalSymbolsMatch( Solution solution, ISymbol? searchSymbol, - ISymbol? symbolToMatch, - CancellationToken cancellationToken) + ISymbol? symbolToMatch) { if (ReferenceEquals(searchSymbol, symbolToMatch)) return true; @@ -48,7 +47,7 @@ internal static async Task OriginalSymbolsMatchAsync( if (searchSymbol.Equals(symbolToMatch)) return true; - if (await OriginalSymbolsMatchCoreAsync(solution, searchSymbol, symbolToMatch, cancellationToken).ConfigureAwait(false)) + if (OriginalSymbolsMatchCore(solution, searchSymbol, symbolToMatch)) return true; if (searchSymbol.Kind == SymbolKind.Namespace && symbolToMatch.Kind == SymbolKind.Namespace) @@ -60,8 +59,8 @@ internal static async Task OriginalSymbolsMatchAsync( var namespace2Count = namespace2.ConstituentNamespaces.Length; if (namespace1Count != namespace2Count) { - if ((namespace1Count > 1 && await namespace1.ConstituentNamespaces.AnyAsync(static (n, arg) => NamespaceSymbolsMatchAsync(arg.solution, n, arg.namespace2, arg.cancellationToken), (solution, namespace2, cancellationToken)).ConfigureAwait(false)) || - (namespace2Count > 1 && await namespace2.ConstituentNamespaces.AnyAsync(static (n2, arg) => NamespaceSymbolsMatchAsync(arg.solution, arg.namespace1, n2, arg.cancellationToken), (solution, namespace1, cancellationToken)).ConfigureAwait(false))) + if ((namespace1Count > 1 && namespace1.ConstituentNamespaces.Any(static (n, arg) => OriginalSymbolsMatch(arg.solution, n, arg.namespace2), (solution, namespace2))) || + (namespace2Count > 1 && namespace2.ConstituentNamespaces.Any(static (n2, arg) => OriginalSymbolsMatch(arg.solution, arg.namespace1, n2), (solution, namespace1)))) { return true; } @@ -71,11 +70,8 @@ internal static async Task OriginalSymbolsMatchAsync( return false; } - private static async Task OriginalSymbolsMatchCoreAsync( - Solution solution, - ISymbol searchSymbol, - ISymbol symbolToMatch, - CancellationToken cancellationToken) + private static bool OriginalSymbolsMatchCore( + Solution solution, ISymbol searchSymbol, ISymbol symbolToMatch) { if (searchSymbol == null || symbolToMatch == null) return false; @@ -121,32 +117,22 @@ private static async Task OriginalSymbolsMatchCoreAsync( if (equivalentTypesWithDifferingAssemblies.Count > 0) { // Step 3a) Ensure that all pairs of named types in equivalentTypesWithDifferingAssemblies are indeed equivalent types. - return await VerifyForwardedTypesAsync(solution, equivalentTypesWithDifferingAssemblies, cancellationToken).ConfigureAwait(false); + return VerifyForwardedTypes(solution, equivalentTypesWithDifferingAssemblies); } // 3b) If no such named type pairs were encountered, symbols ARE equivalent. return true; } - private static Task NamespaceSymbolsMatchAsync( - Solution solution, - INamespaceSymbol namespace1, - INamespaceSymbol namespace2, - CancellationToken cancellationToken) - { - return OriginalSymbolsMatchAsync(solution, namespace1, namespace2, cancellationToken); - } - /// /// Verifies that all pairs of named types in equivalentTypesWithDifferingAssemblies are equivalent forwarded types. /// - private static async Task VerifyForwardedTypesAsync( + private static bool VerifyForwardedTypes( Solution solution, - Dictionary equivalentTypesWithDifferingAssemblies, - CancellationToken cancellationToken) + Dictionary equivalentTypesWithDifferingAssemblies) { Contract.ThrowIfNull(equivalentTypesWithDifferingAssemblies); - Contract.ThrowIfTrue(!equivalentTypesWithDifferingAssemblies.Any()); + Contract.ThrowIfTrue(equivalentTypesWithDifferingAssemblies.Count == 0); // Must contain equivalents named types residing in different assemblies. Contract.ThrowIfFalse(equivalentTypesWithDifferingAssemblies.All(kvp => !SymbolEquivalenceComparer.Instance.Equals(kvp.Key.ContainingAssembly, kvp.Value.ContainingAssembly))); @@ -155,16 +141,13 @@ private static async Task VerifyForwardedTypesAsync( Contract.ThrowIfFalse(equivalentTypesWithDifferingAssemblies.All(kvp => kvp.Key.ContainingType == null)); Contract.ThrowIfFalse(equivalentTypesWithDifferingAssemblies.All(kvp => kvp.Value.ContainingType == null)); - // Cache compilations so we avoid recreating any as we walk the pairs of types. - using var _ = PooledHashSet.GetInstance(out var compilationSet); - foreach (var (type1, type2) in equivalentTypesWithDifferingAssemblies) { // Check if type1 was forwarded to type2 in type2's compilation, or if type2 was forwarded to type1 in // type1's compilation. We check both direction as this API is called from higher level comparison APIs // that are unordered. - if (!await VerifyForwardedTypeAsync(solution, candidate: type1, forwardedTo: type2, compilationSet, cancellationToken).ConfigureAwait(false) && - !await VerifyForwardedTypeAsync(solution, candidate: type2, forwardedTo: type1, compilationSet, cancellationToken).ConfigureAwait(false)) + if (!VerifyForwardedType(solution, candidate: type1, forwardedTo: type2) && + !VerifyForwardedType(solution, candidate: type2, forwardedTo: type1)) { return false; } @@ -177,30 +160,20 @@ private static async Task VerifyForwardedTypesAsync( /// Returns if was forwarded to in /// 's . /// - private static async Task VerifyForwardedTypeAsync( + private static bool VerifyForwardedType( Solution solution, INamedTypeSymbol candidate, - INamedTypeSymbol forwardedTo, - HashSet compilationSet, - CancellationToken cancellationToken) + INamedTypeSymbol forwardedTo) { // Only need to operate on original definitions. i.e. List is the type that is forwarded, // not List. candidate = GetOridinalUnderlyingType(candidate); forwardedTo = GetOridinalUnderlyingType(forwardedTo); - var forwardedToOriginatingProject = solution.GetOriginatingProject(forwardedTo); - if (forwardedToOriginatingProject == null) - return false; - - var forwardedToCompilation = await forwardedToOriginatingProject.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var forwardedToCompilation = solution.GetOriginatingCompilation(forwardedTo); if (forwardedToCompilation == null) return false; - // Cache the compilation so that if we need it while checking another set of forwarded types, we don't - // expensively throw it away and recreate it. - compilationSet.Add(forwardedToCompilation); - var candidateFullMetadataName = candidate.ContainingNamespace?.IsGlobalNamespace != false ? candidate.MetadataName : $"{candidate.ContainingNamespace.ToDisplayString(SymbolDisplayFormats.SignatureFormat)}.{candidate.MetadataName}"; diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder_Hierarchy.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder_Hierarchy.cs index 7bfe7ed73548e..ad70664b74512 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder_Hierarchy.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder_Hierarchy.cs @@ -54,10 +54,8 @@ internal static async Task> FindOverridesArrayAsync( var sourceMember = await FindSourceDefinitionAsync(m, solution, cancellationToken).ConfigureAwait(false); var bestMember = sourceMember ?? m; - if (await IsOverrideAsync(solution, bestMember, symbol, cancellationToken).ConfigureAwait(false)) - { + if (IsOverride(solution, bestMember, symbol)) results.Add(bestMember); - } } } } @@ -65,11 +63,11 @@ internal static async Task> FindOverridesArrayAsync( return results.ToImmutableAndFree(); } - internal static async Task IsOverrideAsync(Solution solution, ISymbol member, ISymbol symbol, CancellationToken cancellationToken) + internal static bool IsOverride(Solution solution, ISymbol member, ISymbol symbol) { for (var current = member; current != null; current = current.GetOverriddenMember()) { - if (await OriginalSymbolsMatchAsync(solution, current.GetOverriddenMember(), symbol.OriginalDefinition, cancellationToken).ConfigureAwait(false)) + if (OriginalSymbolsMatch(solution, current.GetOverriddenMember(), symbol.OriginalDefinition)) return true; } @@ -159,8 +157,7 @@ internal static async Task> FindImplementedInterfaceMemb var sourceMethod = await FindSourceDefinitionAsync(interfaceMember, solution, cancellationToken).ConfigureAwait(false); var bestMethod = sourceMethod ?? interfaceMember; - var implementations = await type.FindImplementationsForInterfaceMemberAsync( - bestMethod, solution, cancellationToken).ConfigureAwait(false); + var implementations = type.FindImplementationsForInterfaceMember(bestMethod, solution, cancellationToken); foreach (var implementation in implementations) { if (implementation != null && @@ -361,7 +358,7 @@ internal static async Task> FindMemberImplementationsArr using var _ = ArrayBuilder.GetInstance(out var results); foreach (var t in allTypes) { - var implementations = await t.FindImplementationsForInterfaceMemberAsync(symbol, solution, cancellationToken).ConfigureAwait(false); + var implementations = t.FindImplementationsForInterfaceMember(symbol, solution, cancellationToken); foreach (var implementation in implementations) { var sourceDef = await FindSourceDefinitionAsync(implementation, solution, cancellationToken).ConfigureAwait(false); diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Serialization.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Serialization.cs index 22ccb6dcc399f..07564d99b7c1d 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Serialization.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolTree/SymbolTreeInfo_Serialization.cs @@ -61,7 +61,6 @@ private static async Task LoadOrCreateAsync( var persistentStorageService = services.GetPersistentStorageService(); var storage = await persistentStorageService.GetStorageAsync(solutionKey, cancellationToken).ConfigureAwait(false); - await using var _ = storage.ConfigureAwait(false); using (var stream = SerializableBytes.CreateWritableStream()) { @@ -91,7 +90,6 @@ private static async Task LoadOrCreateAsync( var persistentStorageService = services.GetPersistentStorageService(); var storage = await persistentStorageService.GetStorageAsync(solutionKey, cancellationToken).ConfigureAwait(false); - await using var _ = storage.ConfigureAwait(false); // Get the unique key to identify our data. var key = PrefixSymbolTreeInfo + keySuffix; diff --git a/src/Workspaces/Core/Portable/LinkedFileDiffMerging/LinkedFileDiffMergingSession.cs b/src/Workspaces/Core/Portable/LinkedFileDiffMerging/LinkedFileDiffMergingSession.cs index 6603464b13a8e..1f7fb23a853f3 100644 --- a/src/Workspaces/Core/Portable/LinkedFileDiffMerging/LinkedFileDiffMergingSession.cs +++ b/src/Workspaces/Core/Portable/LinkedFileDiffMerging/LinkedFileDiffMergingSession.cs @@ -83,7 +83,7 @@ private async Task MergeLinkedDocumentGroupAsync( appliedChanges = await AddDocumentMergeChangesAsync( oldSolution.GetDocument(documentId), newSolution.GetDocument(documentId), - appliedChanges.ToList(), + [.. appliedChanges], unmergedChanges, groupSessionInfo, textDifferencingService, diff --git a/src/Workspaces/Core/Portable/Log/AggregateLogger.cs b/src/Workspaces/Core/Portable/Log/AggregateLogger.cs index 19a74f1a4972d..6f20609433576 100644 --- a/src/Workspaces/Core/Portable/Log/AggregateLogger.cs +++ b/src/Workspaces/Core/Portable/Log/AggregateLogger.cs @@ -36,7 +36,7 @@ public static AggregateLogger Create(params ILogger[] loggers) set.Add(logger); } - return new AggregateLogger(set.ToImmutableArray()); + return new AggregateLogger([.. set]); } public static ILogger AddOrReplace(ILogger newLogger, ILogger oldLogger, Func predicate) @@ -81,7 +81,7 @@ public static ILogger AddOrReplace(ILogger newLogger, ILogger oldLogger, Func predicate) @@ -105,7 +105,7 @@ public static ILogger Remove(ILogger logger, Func predicate) return set.Single(); } - return new AggregateLogger(set.ToImmutableArray()); + return new AggregateLogger([.. set]); } private AggregateLogger(ImmutableArray loggers) diff --git a/src/Workspaces/Core/Portable/Remote/ISerializerService.cs b/src/Workspaces/Core/Portable/Remote/ISerializerService.cs index f6f74f5bae648..f56f5f0a89417 100644 --- a/src/Workspaces/Core/Portable/Remote/ISerializerService.cs +++ b/src/Workspaces/Core/Portable/Remote/ISerializerService.cs @@ -10,7 +10,7 @@ namespace Microsoft.CodeAnalysis.Serialization; internal interface ISerializerService : IWorkspaceService { - void Serialize(object value, ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken); + void Serialize(object value, ObjectWriter writer, CancellationToken cancellationToken); void SerializeParseOptions(ParseOptions options, ObjectWriter writer); diff --git a/src/Workspaces/Core/Portable/Rename/ConflictEngine/ConflictResolver.Session.cs b/src/Workspaces/Core/Portable/Rename/ConflictEngine/ConflictResolver.Session.cs index 34190d5261e2c..0a0b5e3c63411 100644 --- a/src/Workspaces/Core/Portable/Rename/ConflictEngine/ConflictResolver.Session.cs +++ b/src/Workspaces/Core/Portable/Rename/ConflictEngine/ConflictResolver.Session.cs @@ -177,10 +177,13 @@ public async Task ResolveConflictsAsync() if (phase == 1) { - conflictLocations = conflictLocations.Concat(conflictResolution.RelatedLocations - .Where(loc => documentIdsThatGetsAnnotatedAndRenamed.Contains(loc.DocumentId) && loc.Type == RelatedLocationType.PossiblyResolvableConflict) - .Select(loc => new ConflictLocationInfo(loc))) - .ToImmutableHashSet(); + conflictLocations = + [ + .. conflictLocations, + .. conflictResolution.RelatedLocations + .Where(loc => documentIdsThatGetsAnnotatedAndRenamed.Contains(loc.DocumentId) && loc.Type == RelatedLocationType.PossiblyResolvableConflict) + .Select(loc => new ConflictLocationInfo(loc)), + ]; } // Set the documents with conflicts that need to be processed in the next phase. diff --git a/src/Workspaces/Core/Portable/Rename/ConflictEngine/RenamedSpansTracker.cs b/src/Workspaces/Core/Portable/Rename/ConflictEngine/RenamedSpansTracker.cs index 6c6546e908b97..7a71e5dd2b9a3 100644 --- a/src/Workspaces/Core/Portable/Rename/ConflictEngine/RenamedSpansTracker.cs +++ b/src/Workspaces/Core/Portable/Rename/ConflictEngine/RenamedSpansTracker.cs @@ -226,7 +226,7 @@ internal async Task SimplifyAsync(Solution solution, IEnumerable>(); foreach (var (docId, spans) in _documentToModifiedSpansMap) - builder.Add(docId, spans.ToImmutableArray()); + builder.Add(docId, [.. spans]); return builder.ToImmutable(); } @@ -238,7 +238,7 @@ public ImmutableDictionary> GetDocu foreach (var (docId, spans) in _documentToComplexifiedSpansMap) { builder.Add(docId, spans.SelectAsArray( - s => new ComplexifiedSpan(s.OriginalSpan, s.NewSpan, s.ModifiedSubSpans.ToImmutableArray()))); + s => new ComplexifiedSpan(s.OriginalSpan, s.NewSpan, [.. s.ModifiedSubSpans]))); } return builder.ToImmutable(); diff --git a/src/Workspaces/Core/Portable/Rename/Renamer.cs b/src/Workspaces/Core/Portable/Rename/Renamer.cs index 741bab2bd9cfa..cc3c90299437b 100644 --- a/src/Workspaces/Core/Portable/Rename/Renamer.cs +++ b/src/Workspaces/Core/Portable/Rename/Renamer.cs @@ -119,7 +119,7 @@ internal static async Task RenameDocumentAsync( if (document.Services.GetService() != null) { // Don't advertise that we can file rename generated documents that map to a different file. - return new RenameDocumentActionSet([], document.Id, document.Name, document.Folders.ToImmutableArray(), options); + return new RenameDocumentActionSet([], document.Id, document.Name, [.. document.Folders], options); } using var _ = ArrayBuilder.GetInstance(out var actions); @@ -143,7 +143,7 @@ internal static async Task RenameDocumentAsync( actions.ToImmutable(), document.Id, newDocumentName, - newDocumentFolders.ToImmutableArray(), + [.. newDocumentFolders], options); } diff --git a/src/Workspaces/Core/Portable/Serialization/ISupportTemporaryStorage.cs b/src/Workspaces/Core/Portable/Serialization/ISupportTemporaryStorage.cs index e17bcb1a26d4f..a7c02bba2bfc5 100644 --- a/src/Workspaces/Core/Portable/Serialization/ISupportTemporaryStorage.cs +++ b/src/Workspaces/Core/Portable/Serialization/ISupportTemporaryStorage.cs @@ -8,10 +8,11 @@ namespace Microsoft.CodeAnalysis.Serialization; /// -/// This lets consumer to get to inner temporary storage that references use -/// as its shadow copy storage +/// Interface for services that support dumping their contents to memory-mapped-files (generally speaking, our assembly +/// reference objects). This allows those objects to expose the memory-mapped-file info needed to read that data back +/// in in any process. /// internal interface ISupportTemporaryStorage { - IReadOnlyList? GetStorages(); + IReadOnlyList? StorageHandles { get; } } diff --git a/src/Workspaces/Core/Portable/Serialization/PooledList.cs b/src/Workspaces/Core/Portable/Serialization/PooledList.cs deleted file mode 100644 index 3eb2680bb6702..0000000000000 --- a/src/Workspaces/Core/Portable/Serialization/PooledList.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -#nullable disable - -using System; -using System.Collections.Generic; - -namespace Microsoft.CodeAnalysis.Serialization; - -/// -/// This is just internal utility type to reduce allocations and redundant code -/// -internal static class Creator -{ - public static PooledObject> CreateList() - => SharedPools.Default>().GetPooledObject(); -} diff --git a/src/Workspaces/Core/Portable/Serialization/SerializableSourceText.cs b/src/Workspaces/Core/Portable/Serialization/SerializableSourceText.cs index 00c816d4c0e31..06e2b900e3035 100644 --- a/src/Workspaces/Core/Portable/Serialization/SerializableSourceText.cs +++ b/src/Workspaces/Core/Portable/Serialization/SerializableSourceText.cs @@ -5,9 +5,7 @@ using System; using System.Collections.Immutable; using System.Diagnostics; -using System.Linq; using System.Runtime.InteropServices; -using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; @@ -16,6 +14,10 @@ using Roslyn.Utilities; using static Microsoft.CodeAnalysis.Host.TemporaryStorageService; +#if DEBUG +using System.Linq; +#endif + namespace Microsoft.CodeAnalysis.Serialization; #pragma warning disable CA1416 // Validate platform compatibility @@ -31,50 +33,49 @@ internal sealed class SerializableSourceText /// The storage location for . /// /// - /// Exactly one of or will be non-. + /// Exactly one of or will be non-. /// - private readonly TemporaryTextStorage? _storage; + private readonly TemporaryStorageTextHandle? _storageHandle; /// /// The in the current process. /// /// - /// + /// /// private readonly SourceText? _text; /// - /// The hash that would be produced by calling on . Can be passed in when already known to avoid unnecessary computation costs. + /// Weak reference to a SourceText computed from . Useful so that if multiple requests + /// come in for the source text, the same one can be returned as long as something is holding it alive. /// - public readonly ImmutableArray ContentHash; + private readonly WeakReference _computedText = new(target: null); /// - /// Weak reference to a SourceText computed from . Useful so that if multiple requests - /// come in for the source text, the same one can be returned as long as something is holding it alive. + /// Checksum of the contents (see ) of the text. /// - private readonly WeakReference _computedText = new(target: null); + public readonly Checksum ContentChecksum; - public SerializableSourceText(TemporaryTextStorage storage, ImmutableArray contentHash) - : this(storage, text: null, contentHash) + public SerializableSourceText(TemporaryStorageTextHandle storageHandle) + : this(storageHandle, text: null, storageHandle.ContentHash) { } public SerializableSourceText(SourceText text, ImmutableArray contentHash) - : this(storage: null, text, contentHash) + : this(storageHandle: null, text, contentHash) { } - private SerializableSourceText(TemporaryTextStorage? storage, SourceText? text, ImmutableArray contentHash) + private SerializableSourceText(TemporaryStorageTextHandle? storageHandle, SourceText? text, ImmutableArray contentHash) { - Debug.Assert(storage is null != text is null); + Debug.Assert(storageHandle is null != text is null); - _storage = storage; + _storageHandle = storageHandle; _text = text; - ContentHash = contentHash; + ContentChecksum = Checksum.Create(contentHash); #if DEBUG - var computedContentHash = TryGetText()?.GetContentHash() ?? _storage!.ContentHash; + var computedContentHash = TryGetText()?.GetContentHash() ?? _storageHandle!.ContentHash; Debug.Assert(contentHash.SequenceEqual(computedContentHash)); #endif } @@ -94,7 +95,7 @@ public async ValueTask GetTextAsync(CancellationToken cancellationTo return text; // Read and cache the text from the storage object so that other requests may see it if still kept alive by something. - text = await _storage!.ReadTextAsync(cancellationToken).ConfigureAwait(false); + text = await _storageHandle!.ReadFromTemporaryStorageAsync(cancellationToken).ConfigureAwait(false); _computedText.SetTarget(text); return text; } @@ -106,7 +107,7 @@ public SourceText GetText(CancellationToken cancellationToken) return text; // Read and cache the text from the storage object so that other requests may see it if still kept alive by something. - text = _storage!.ReadText(cancellationToken); + text = _storageHandle!.ReadFromTemporaryStorage(cancellationToken); _computedText.SetTarget(text); return text; } @@ -114,12 +115,22 @@ public SourceText GetText(CancellationToken cancellationToken) public static ValueTask FromTextDocumentStateAsync( TextDocumentState state, CancellationToken cancellationToken) { - if (state.Storage is TemporaryTextStorage storage) + if (state.TextAndVersionSource.TextLoader is SerializableSourceTextLoader serializableLoader) { - return new ValueTask(new SerializableSourceText(storage, storage.ContentHash)); + // If we're already pointing at a serializable loader, we can just use that directly. + return new(serializableLoader.SerializableSourceText); + } + else if (state.StorageHandle is TemporaryStorageTextHandle storageHandle) + { + // Otherwise, if we're pointing at a memory mapped storage location, we can create the source text that directly wraps that. + return new(new SerializableSourceText(storageHandle)); } else { + // Otherwise, the state object has reified the text into some other form, and dumped any original + // information on how it got it. In that case, we create a new text instance to represent the serializable + // source text out of. + return SpecializedTasks.TransformWithoutIntermediateCancellationExceptionAsync( static (state, cancellationToken) => state.GetTextAsync(cancellationToken), static (text, _) => new SerializableSourceText(text, text.GetContentHash()), @@ -128,66 +139,104 @@ public static ValueTask FromTextDocumentStateAsync( } } - public void Serialize(ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken) + public void Serialize(ObjectWriter writer, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - if (_storage is not null) - { - context.AddResource(_storage); - - writer.WriteInt32((int)_storage.ChecksumAlgorithm); - writer.WriteEncoding(_storage.Encoding); - writer.WriteByteArray(ImmutableCollectionsMarshal.AsArray(_storage.ContentHash)!); + if (_storageHandle is not null) + { writer.WriteInt32((int)SerializationKinds.MemoryMapFile); - writer.WriteString(_storage.Name); - writer.WriteInt64(_storage.Offset); - writer.WriteInt64(_storage.Size); + _storageHandle.Identifier.WriteTo(writer); + writer.WriteInt32((int)_storageHandle.ChecksumAlgorithm); + writer.WriteEncoding(_storageHandle.Encoding); + writer.WriteByteArray(ImmutableCollectionsMarshal.AsArray(_storageHandle.ContentHash)!); } else { RoslynDebug.AssertNotNull(_text); + writer.WriteInt32((int)SerializationKinds.Bits); writer.WriteInt32((int)_text.ChecksumAlgorithm); writer.WriteEncoding(_text.Encoding); writer.WriteByteArray(ImmutableCollectionsMarshal.AsArray(_text.GetContentHash())!); - writer.WriteInt32((int)SerializationKinds.Bits); _text.WriteTo(writer, cancellationToken); } } public static SerializableSourceText Deserialize( ObjectReader reader, - ITemporaryStorageServiceInternal storageService, + TemporaryStorageService storageService, ITextFactoryService textService, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var checksumAlgorithm = (SourceHashAlgorithm)reader.ReadInt32(); - var encoding = reader.ReadEncoding(); - var contentHash = ImmutableCollectionsMarshal.AsImmutableArray(reader.ReadByteArray()); - var kind = (SerializationKinds)reader.ReadInt32(); Contract.ThrowIfFalse(kind is SerializationKinds.Bits or SerializationKinds.MemoryMapFile); if (kind == SerializationKinds.MemoryMapFile) { - var storage2 = (TemporaryStorageService)storageService; - - var name = reader.ReadRequiredString(); - var offset = reader.ReadInt64(); - var size = reader.ReadInt64(); + var identifier = TemporaryStorageIdentifier.ReadFrom(reader); + var checksumAlgorithm = (SourceHashAlgorithm)reader.ReadInt32(); + var encoding = reader.ReadEncoding(); + var contentHash = ImmutableCollectionsMarshal.AsImmutableArray(reader.ReadByteArray()); + var storageHandle = storageService.GetTextHandle(identifier, checksumAlgorithm, encoding, contentHash); - var storage = storage2.AttachTemporaryTextStorage(name, offset, size, checksumAlgorithm, encoding, contentHash); - return new SerializableSourceText(storage, contentHash); + return new SerializableSourceText(storageHandle); } else { + var checksumAlgorithm = (SourceHashAlgorithm)reader.ReadInt32(); + var encoding = reader.ReadEncoding(); + var contentHash = ImmutableCollectionsMarshal.AsImmutableArray(reader.ReadByteArray()); + return new SerializableSourceText( SourceTextExtensions.ReadFrom(textService, reader, encoding, checksumAlgorithm, cancellationToken), contentHash); } } + + public TextLoader ToTextLoader(string? filePath) + => new SerializableSourceTextLoader(this, filePath); + + /// + /// A that wraps a and provides access to the text in + /// a deferred fashion. In practice, during a host and OOP sync, while all the documents will be 'serialized' over + /// to OOP, the actual contents of the documents will only need to be loaded depending on which files are open, and + /// thus what compilations and trees are needed. As such, we want to be able to lazily defer actually getting the + /// contents of the text until it's actually needed. This loader allows us to do that, allowing the OOP side to + /// simply point to the segments in the memory-mapped-file the host has dumped its text into, and only actually + /// realizing the real text values when they're needed. + /// + private sealed class SerializableSourceTextLoader : TextLoader + { + public readonly SerializableSourceText SerializableSourceText; + private readonly VersionStamp _version = VersionStamp.Create(); + + public SerializableSourceTextLoader( + SerializableSourceText serializableSourceText, + string? filePath) + { + SerializableSourceText = serializableSourceText; + FilePath = filePath; + } + + internal override string? FilePath { get; } + + /// + /// Documents should always hold onto instances of this text loader strongly. In other words, they should load + /// from this, and then dump the contents into a RecoverableText object that then dumps the contents to a memory + /// mapped file within this process. Doing that is pointless as the contents of this text are already in a + /// memory mapped file on the host side. + /// + internal override bool AlwaysHoldStrongly + => true; + + public override async Task LoadTextAndVersionAsync(LoadTextOptions options, CancellationToken cancellationToken) + => TextAndVersion.Create(await this.SerializableSourceText.GetTextAsync(cancellationToken).ConfigureAwait(false), _version); + + internal override TextAndVersion LoadTextAndVersionSynchronously(LoadTextOptions options, CancellationToken cancellationToken) + => TextAndVersion.Create(this.SerializableSourceText.GetText(cancellationToken), _version); + } } diff --git a/src/Workspaces/Core/Portable/Serialization/SerializerService.cs b/src/Workspaces/Core/Portable/Serialization/SerializerService.cs index 9d94bccc485e8..e786280b8b51c 100644 --- a/src/Workspaces/Core/Portable/Serialization/SerializerService.cs +++ b/src/Workspaces/Core/Portable/Serialization/SerializerService.cs @@ -6,16 +6,19 @@ using System.Collections.Concurrent; using System.Composition; using System.Linq; +using System.Runtime.Versioning; using System.Threading; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Internal.Log; -using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Serialization; +#if NETCOREAPP +[SupportedOSPlatform("windows")] +#endif internal partial class SerializerService : ISerializerService { [ExportWorkspaceServiceFactory(typeof(ISerializerService), layer: ServiceLayer.Default), Shared] @@ -36,7 +39,7 @@ public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) private readonly SolutionServices _workspaceServices; - private readonly ITemporaryStorageServiceInternal _storageService; + private readonly TemporaryStorageService _storageService; private readonly ITextFactoryService _textService; private readonly IDocumentationProviderService? _documentationService; private readonly IAnalyzerAssemblyLoaderProvider _analyzerLoaderProvider; @@ -48,7 +51,9 @@ private protected SerializerService(SolutionServices workspaceServices) { _workspaceServices = workspaceServices; - _storageService = workspaceServices.GetRequiredService(); + // Serialization is only involved when we have a remote process. Which is only in VS. So the type of the + // storage service here is well known. + _storageService = (TemporaryStorageService)workspaceServices.GetRequiredService(); _textService = workspaceServices.GetRequiredService(); _analyzerLoaderProvider = workspaceServices.GetRequiredService(); _documentationService = workspaceServices.GetService(); @@ -75,7 +80,7 @@ public Checksum CreateChecksum(object value, CancellationToken cancellationToken case WellKnownSynchronizationKind.ParseOptions: case WellKnownSynchronizationKind.ProjectReference: case WellKnownSynchronizationKind.SourceGeneratedDocumentIdentity: - return Checksum.Create(value, this); + return Checksum.Create(value, this, cancellationToken); case WellKnownSynchronizationKind.MetadataReference: return CreateChecksum((MetadataReference)value, cancellationToken); @@ -84,7 +89,7 @@ public Checksum CreateChecksum(object value, CancellationToken cancellationToken return CreateChecksum((AnalyzerReference)value, cancellationToken); case WellKnownSynchronizationKind.SerializableSourceText: - return Checksum.Create(((SerializableSourceText)value).ContentHash); + throw new InvalidOperationException("Clients can already get a checksum directly from a SerializableSourceText"); default: // object that is not part of solution is not supported since we don't know what inputs are required to @@ -94,7 +99,7 @@ public Checksum CreateChecksum(object value, CancellationToken cancellationToken } } - public void Serialize(object value, ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken) + public void Serialize(object value, ObjectWriter writer, CancellationToken cancellationToken) { var kind = value.GetWellKnownSynchronizationKind(); @@ -134,7 +139,7 @@ public void Serialize(object value, ObjectWriter writer, SolutionReplicationCont return; case WellKnownSynchronizationKind.MetadataReference: - SerializeMetadataReference((MetadataReference)value, writer, context, cancellationToken); + SerializeMetadataReference((MetadataReference)value, writer, cancellationToken); return; case WellKnownSynchronizationKind.AnalyzerReference: @@ -142,7 +147,7 @@ public void Serialize(object value, ObjectWriter writer, SolutionReplicationCont return; case WellKnownSynchronizationKind.SerializableSourceText: - SerializeSourceText((SerializableSourceText)value, writer, context, cancellationToken); + SerializeSourceText((SerializableSourceText)value, writer, cancellationToken); return; case WellKnownSynchronizationKind.SolutionCompilationState: diff --git a/src/Workspaces/Core/Portable/Serialization/SerializerService_Asset.cs b/src/Workspaces/Core/Portable/Serialization/SerializerService_Asset.cs index c3842c9ff3870..d7bbeba322659 100644 --- a/src/Workspaces/Core/Portable/Serialization/SerializerService_Asset.cs +++ b/src/Workspaces/Core/Portable/Serialization/SerializerService_Asset.cs @@ -17,9 +17,9 @@ namespace Microsoft.CodeAnalysis.Serialization; /// internal partial class SerializerService { - private static void SerializeSourceText(SerializableSourceText text, ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken) + private static void SerializeSourceText(SerializableSourceText text, ObjectWriter writer, CancellationToken cancellationToken) { - text.Serialize(writer, context, cancellationToken); + text.Serialize(writer, cancellationToken); } private void SerializeCompilationOptions(CompilationOptions options, ObjectWriter writer, CancellationToken cancellationToken) @@ -86,10 +86,10 @@ private static ProjectReference DeserializeProjectReference(ObjectReader reader, return new ProjectReference(projectId, aliases.ToImmutableArrayOrEmpty(), embedInteropTypes); } - private void SerializeMetadataReference(MetadataReference reference, ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken) + private void SerializeMetadataReference(MetadataReference reference, ObjectWriter writer, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - WriteMetadataReferenceTo(reference, writer, context, cancellationToken); + WriteMetadataReferenceTo(reference, writer, cancellationToken); } private MetadataReference DeserializeMetadataReference(ObjectReader reader, CancellationToken cancellationToken) diff --git a/src/Workspaces/Core/Portable/Serialization/SerializerService_Reference.cs b/src/Workspaces/Core/Portable/Serialization/SerializerService_Reference.cs index f39c0d444450b..67102657d9132 100644 --- a/src/Workspaces/Core/Portable/Serialization/SerializerService_Reference.cs +++ b/src/Workspaces/Core/Portable/Serialization/SerializerService_Reference.cs @@ -9,8 +9,6 @@ using System.IO; using System.Linq; using System.Reflection.Metadata; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Threading; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host; @@ -19,12 +17,12 @@ namespace Microsoft.CodeAnalysis.Serialization; +using static TemporaryStorageService; + internal partial class SerializerService { private const int MetadataFailed = int.MaxValue; - private static readonly ConditionalWeakTable s_lifetimeMap = new(); - public static Checksum CreateChecksum(MetadataReference reference, CancellationToken cancellationToken) { if (reference is PortableExecutableReference portable) @@ -62,16 +60,15 @@ public static Checksum CreateChecksum(AnalyzerReference reference, CancellationT return Checksum.Create(stream); } - public virtual void WriteMetadataReferenceTo(MetadataReference reference, ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken) + public virtual void WriteMetadataReferenceTo(MetadataReference reference, ObjectWriter writer, CancellationToken cancellationToken) { if (reference is PortableExecutableReference portable) { - if (portable is ISupportTemporaryStorage supportTemporaryStorage) + if (portable is ISupportTemporaryStorage { StorageHandles: { Count: > 0 } handles } && + TryWritePortableExecutableReferenceBackedByTemporaryStorageTo( + portable, handles, writer, cancellationToken)) { - if (TryWritePortableExecutableReferenceBackedByTemporaryStorageTo(supportTemporaryStorage, writer, context, cancellationToken)) - { - return; - } + return; } WritePortableExecutableReferenceTo(portable, writer, cancellationToken); @@ -207,13 +204,15 @@ private static void WriteMvidTo(ModuleMetadata metadata, ObjectWriter writer, Ca cancellationToken.ThrowIfCancellationRequested(); writer.WriteInt32((int)metadata.Kind); + writer.WriteGuid(GetMetadataGuid(metadata)); + } + private static Guid GetMetadataGuid(ModuleMetadata metadata) + { var metadataReader = metadata.GetMetadataReader(); - var mvidHandle = metadataReader.GetModuleDefinition().Mvid; var guid = metadataReader.GetGuid(mvidHandle); - - writer.WriteGuid(guid); + return guid; } private static void WritePortableExecutableReferenceTo( @@ -235,8 +234,7 @@ private PortableExecutableReference ReadPortableExecutableReferenceFrom(ObjectRe var filePath = reader.ReadString(); - var tuple = TryReadMetadataFrom(reader, kind, cancellationToken); - if (tuple == null) + if (TryReadMetadataFrom(reader, kind, cancellationToken) is not (var metadata, var storageHandles)) { // TODO: deal with xml document provider properly // should we shadow copy xml doc comment? @@ -256,7 +254,7 @@ private PortableExecutableReference ReadPortableExecutableReferenceFrom(ObjectRe _documentationService.GetDocumentationProvider(filePath) : XmlDocumentationProvider.Default; return new SerializedMetadataReference( - properties, filePath, tuple.Value.metadata, tuple.Value.storages, documentProvider); + properties, filePath, metadata, storageHandles, documentProvider); } private static void WriteTo(MetadataReferenceProperties properties, ObjectWriter writer, CancellationToken cancellationToken) @@ -313,46 +311,28 @@ private static void WriteTo(Metadata? metadata, ObjectWriter writer, Cancellatio } private static bool TryWritePortableExecutableReferenceBackedByTemporaryStorageTo( - ISupportTemporaryStorage reference, ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken) + PortableExecutableReference reference, + IReadOnlyList handles, + ObjectWriter writer, + CancellationToken cancellationToken) { - var storages = reference.GetStorages(); - if (storages == null) - { - return false; - } - - // Not clear if name should be allowed to be null here (https://github.com/dotnet/roslyn/issues/43037) - using var pooled = Creator.CreateList<(string? name, long offset, long size)>(); + Contract.ThrowIfTrue(handles.Count == 0); - foreach (var storage in storages) - { - if (storage is not ITemporaryStorageWithName storage2) - { - return false; - } - - context.AddResource(storage); - - pooled.Object.Add((storage2.Name, storage2.Offset, storage2.Size)); - } - - WritePortableExecutableReferenceHeaderTo((PortableExecutableReference)reference, SerializationKinds.MemoryMapFile, writer, cancellationToken); + WritePortableExecutableReferenceHeaderTo(reference, SerializationKinds.MemoryMapFile, writer, cancellationToken); writer.WriteInt32((int)MetadataImageKind.Assembly); - writer.WriteInt32(pooled.Object.Count); + writer.WriteInt32(handles.Count); - foreach (var (name, offset, size) in pooled.Object) + foreach (var handle in handles) { writer.WriteInt32((int)MetadataImageKind.Module); - writer.WriteString(name); - writer.WriteInt64(offset); - writer.WriteInt64(size); + handle.Identifier.WriteTo(writer); } return true; } - private (Metadata metadata, ImmutableArray storages)? TryReadMetadataFrom( + private (Metadata metadata, ImmutableArray storageHandles)? TryReadMetadataFrom( ObjectReader reader, SerializationKinds kind, CancellationToken cancellationToken) { var imageKind = reader.ReadInt32(); @@ -363,160 +343,87 @@ private static bool TryWritePortableExecutableReferenceBackedByTemporaryStorageT } var metadataKind = (MetadataImageKind)imageKind; - if (_storageService == null) - { - if (metadataKind == MetadataImageKind.Assembly) - { - using var pooledMetadata = Creator.CreateList(); - - var count = reader.ReadInt32(); - for (var i = 0; i < count; i++) - { - metadataKind = (MetadataImageKind)reader.ReadInt32(); - Contract.ThrowIfFalse(metadataKind == MetadataImageKind.Module); - -#pragma warning disable CA2016 // https://github.com/dotnet/roslyn-analyzers/issues/4985 - pooledMetadata.Object.Add(ReadModuleMetadataFrom(reader, kind)); -#pragma warning restore CA2016 - } - - return (AssemblyMetadata.Create(pooledMetadata.Object), storages: default); - } - - Contract.ThrowIfFalse(metadataKind == MetadataImageKind.Module); -#pragma warning disable CA2016 // https://github.com/dotnet/roslyn-analyzers/issues/4985 - return (ReadModuleMetadataFrom(reader, kind), storages: default); -#pragma warning restore CA2016 - } - if (metadataKind == MetadataImageKind.Assembly) { - using var pooledMetadata = Creator.CreateList(); - using var pooledStorage = Creator.CreateList(); - var count = reader.ReadInt32(); + + var allMetadata = new FixedSizeArrayBuilder(count); + var allHandles = new FixedSizeArrayBuilder(count); + for (var i = 0; i < count; i++) { metadataKind = (MetadataImageKind)reader.ReadInt32(); Contract.ThrowIfFalse(metadataKind == MetadataImageKind.Module); - var (metadata, storage) = ReadModuleMetadataFrom(reader, kind, cancellationToken); + var (metadata, storageHandle) = ReadModuleMetadataFrom(reader, kind, cancellationToken); - pooledMetadata.Object.Add(metadata); - pooledStorage.Object.Add(storage); + allMetadata.Add(metadata); + allHandles.Add(storageHandle); } - return (AssemblyMetadata.Create(pooledMetadata.Object), pooledStorage.Object.ToImmutableArrayOrEmpty()); + return (AssemblyMetadata.Create(allMetadata.MoveToImmutable()), allHandles.MoveToImmutable()); } + else + { + Contract.ThrowIfFalse(metadataKind == MetadataImageKind.Module); - Contract.ThrowIfFalse(metadataKind == MetadataImageKind.Module); - - var moduleInfo = ReadModuleMetadataFrom(reader, kind, cancellationToken); - return (moduleInfo.metadata, ImmutableArray.Create(moduleInfo.storage)); + var moduleInfo = ReadModuleMetadataFrom(reader, kind, cancellationToken); + return (moduleInfo.metadata, [moduleInfo.storageHandle]); + } } - private (ModuleMetadata metadata, ITemporaryStreamStorageInternal storage) ReadModuleMetadataFrom( + private (ModuleMetadata metadata, TemporaryStorageStreamHandle storageHandle) ReadModuleMetadataFrom( ObjectReader reader, SerializationKinds kind, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var (storage, length) = GetTemporaryStorage(reader, kind, cancellationToken); - - var storageStream = storage.ReadStream(cancellationToken); - Contract.ThrowIfFalse(length == storageStream.Length); - - GetMetadata(storageStream, length, out var metadata, out var lifeTimeObject); - - // make sure we keep storageStream alive while Metadata is alive - // we use conditional weak table since we can't control metadata liftetime - if (lifeTimeObject != null) - s_lifetimeMap.Add(metadata, lifeTimeObject); - - return (metadata, storage); - } - - private static ModuleMetadata ReadModuleMetadataFrom(ObjectReader reader, SerializationKinds kind) - { - Contract.ThrowIfFalse(SerializationKinds.Bits == kind); - - var array = reader.ReadByteArray(); - var pinnedObject = new PinnedObject(array); - - var metadata = ModuleMetadata.CreateFromMetadata(pinnedObject.GetPointer(), array.Length); - - // make sure we keep storageStream alive while Metadata is alive - // we use conditional weak table since we can't control metadata liftetime - s_lifetimeMap.Add(metadata, pinnedObject); + Contract.ThrowIfFalse(kind is SerializationKinds.Bits or SerializationKinds.MemoryMapFile); - return metadata; - } + return kind == SerializationKinds.Bits + ? ReadModuleMetadataFromBits() + : ReadModuleMetadataFromMemoryMappedFile(); - private (ITemporaryStreamStorageInternal storage, long length) GetTemporaryStorage( - ObjectReader reader, SerializationKinds kind, CancellationToken cancellationToken) - { - Contract.ThrowIfFalse(kind is SerializationKinds.Bits or SerializationKinds.MemoryMapFile); + (ModuleMetadata metadata, TemporaryStorageStreamHandle storageHandle) ReadModuleMetadataFromMemoryMappedFile() + { + // Host passed us a segment of its own memory mapped file. We can just refer to that segment directly as it + // will not be released by the host. + var storageIdentifier = TemporaryStorageIdentifier.ReadFrom(reader); + var storageHandle = TemporaryStorageService.GetStreamHandle(storageIdentifier); + return ReadModuleMetadataFromStorage(storageHandle); + } - if (kind == SerializationKinds.Bits) + (ModuleMetadata metadata, TemporaryStorageStreamHandle storageHandle) ReadModuleMetadataFromBits() { - var storage = _storageService.CreateTemporaryStreamStorage(); + // Host is sending us all the data as bytes. Take that and write that out to a memory mapped file on the + // server side so that we can refer to this data uniformly. using var stream = SerializableBytes.CreateWritableStream(); - CopyByteArrayToStream(reader, stream, cancellationToken); var length = stream.Length; - - stream.Position = 0; - storage.WriteStream(stream, cancellationToken); - - return (storage, length); - } - else - { - var service2 = (TemporaryStorageService)_storageService; - - var name = reader.ReadRequiredString(); - var offset = reader.ReadInt64(); - var size = reader.ReadInt64(); - -#pragma warning disable CA1416 // Validate platform compatibility - var storage = service2.AttachTemporaryStreamStorage(name, offset, size); -#pragma warning restore CA1416 // Validate platform compatibility - var length = size; - - return (storage, length); + var storageHandle = _storageService.WriteToTemporaryStorage(stream, cancellationToken); + Contract.ThrowIfTrue(length != storageHandle.Identifier.Size); + return ReadModuleMetadataFromStorage(storageHandle); } - } - private static void GetMetadata(Stream stream, long length, out ModuleMetadata metadata, out object? lifeTimeObject) - { - if (stream is UnmanagedMemoryStream unmanagedStream) + (ModuleMetadata metadata, TemporaryStorageStreamHandle storageHandle) ReadModuleMetadataFromStorage( + TemporaryStorageStreamHandle storageHandle) { - // For an unmanaged memory stream, ModuleMetadata can take ownership directly. + // Now read in the module data using that identifier. This will either be reading from the host's memory if + // they passed us the information about that memory segment. Or it will be reading from our own memory if they + // sent us the full contents. + var unmanagedStream = storageHandle.ReadFromTemporaryStorage(cancellationToken); + Contract.ThrowIfFalse(storageHandle.Identifier.Size == unmanagedStream.Length); + + // For an unmanaged memory stream, ModuleMetadata can take ownership directly. Stream will be kept alive as + // long as the ModuleMetadata is alive due to passing its .Dispose method in as the onDispose callback of + // the metadata. unsafe { - metadata = ModuleMetadata.CreateFromMetadata( + var metadata = ModuleMetadata.CreateFromMetadata( (IntPtr)unmanagedStream.PositionPointer, (int)unmanagedStream.Length, unmanagedStream.Dispose); - lifeTimeObject = null; - return; + return (metadata, storageHandle); } } - - PinnedObject pinnedObject; - if (stream is MemoryStream memory && - memory.TryGetBuffer(out var buffer) && - buffer.Offset == 0) - { - pinnedObject = new PinnedObject(buffer.Array!); - } - else - { - var array = new byte[length]; - stream.Read(array, 0, (int)length); - pinnedObject = new PinnedObject(array); - } - - metadata = ModuleMetadata.CreateFromMetadata(pinnedObject.GetPointer(), (int)length); - lifeTimeObject = pinnedObject; } private static void CopyByteArrayToStream(ObjectReader reader, Stream stream, CancellationToken cancellationToken) @@ -563,35 +470,6 @@ private static void WriteUnresolvedAnalyzerReferenceTo(AnalyzerReference referen } } - private sealed class PinnedObject : IDisposable - { - // shouldn't be read-only since GCHandle is a mutable struct - private GCHandle _gcHandle; - - public PinnedObject(byte[] array) - => _gcHandle = GCHandle.Alloc(array, GCHandleType.Pinned); - - internal IntPtr GetPointer() - => _gcHandle.AddrOfPinnedObject(); - - private void OnDispose() - { - if (_gcHandle.IsAllocated) - { - _gcHandle.Free(); - } - } - - ~PinnedObject() - => OnDispose(); - - public void Dispose() - { - GC.SuppressFinalize(this); - OnDispose(); - } - } - private sealed class MissingMetadataReference : PortableExecutableReference { private readonly DocumentationProvider _provider; @@ -629,16 +507,22 @@ protected override PortableExecutableReference WithPropertiesImpl(MetadataRefere private sealed class SerializedMetadataReference : PortableExecutableReference, ISupportTemporaryStorage { private readonly Metadata _metadata; - private readonly ImmutableArray _storagesOpt; + private readonly ImmutableArray _storageHandles; private readonly DocumentationProvider _provider; + public IReadOnlyList StorageHandles => _storageHandles; + public SerializedMetadataReference( - MetadataReferenceProperties properties, string? fullPath, - Metadata metadata, ImmutableArray storagesOpt, DocumentationProvider initialDocumentation) + MetadataReferenceProperties properties, + string? fullPath, + Metadata metadata, + ImmutableArray storageHandles, + DocumentationProvider initialDocumentation) : base(properties, fullPath, initialDocumentation) { + Contract.ThrowIfTrue(storageHandles.IsDefault); _metadata = metadata; - _storagesOpt = storagesOpt; + _storageHandles = storageHandles; _provider = initialDocumentation; } @@ -653,9 +537,37 @@ protected override Metadata GetMetadataImpl() => _metadata; protected override PortableExecutableReference WithPropertiesImpl(MetadataReferenceProperties properties) - => new SerializedMetadataReference(properties, FilePath, _metadata, _storagesOpt, _provider); + => new SerializedMetadataReference(properties, FilePath, _metadata, _storageHandles, _provider); + + public override string ToString() + { + var metadata = TryGetMetadata(this); + var modules = GetModules(metadata); + + return $""" + {nameof(SerializedMetadataReference)} + FilePath={this.FilePath} + Kind={this.Properties.Kind} + Aliases={this.Properties.Aliases.Join(",")} + EmbedInteropTypes={this.Properties.EmbedInteropTypes} + MetadataKind={metadata switch { null => "null", AssemblyMetadata => "assembly", ModuleMetadata => "module", _ => metadata.GetType().Name }} + Guids={modules.Select(m => GetMetadataGuid(m).ToString()).Join(",")} + """; + + static ImmutableArray GetModules(Metadata? metadata) + { + if (metadata is AssemblyMetadata assemblyMetadata) + { + if (TryGetModules(assemblyMetadata, out var modules)) + return modules; + } + else if (metadata is ModuleMetadata moduleMetadata) + { + return [moduleMetadata]; + } - public IReadOnlyList? GetStorages() - => _storagesOpt.IsDefault ? null : _storagesOpt; + return []; + } + } } } diff --git a/src/Workspaces/Core/Portable/Serialization/SolutionReplicationContext.cs b/src/Workspaces/Core/Portable/Serialization/SolutionReplicationContext.cs deleted file mode 100644 index e1cb61be7218f..0000000000000 --- a/src/Workspaces/Core/Portable/Serialization/SolutionReplicationContext.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using Microsoft.CodeAnalysis.PooledObjects; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.Serialization; - -internal readonly struct SolutionReplicationContext : IDisposable -{ - private static readonly ObjectPool> s_pool = new(() => []); - - private readonly ConcurrentSet _resources; - - public SolutionReplicationContext() - => _resources = s_pool.Allocate(); - - public void AddResource(IDisposable resource) - => _resources.Add(resource); - - public void Dispose() - { - // TODO: https://github.com/dotnet/roslyn/issues/49973 - // Currently we don't dispose resources, only keep them alive. - // Shouldn't we dispose them? - // _resources.All(resource => resource.Dispose()); - s_pool.ClearAndFree(_resources); - } -} diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/IAsyncEnumerableExtensions.cs b/src/Workspaces/Core/Portable/Shared/Extensions/IAsyncEnumerableExtensions.cs index 3a390a2769457..a41b629c3f889 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/IAsyncEnumerableExtensions.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/IAsyncEnumerableExtensions.cs @@ -93,4 +93,14 @@ public static void CompletesChannel(this Task task, Channel channel) static (task, channel) => ((Channel)channel!).Writer.Complete(task.Exception), channel, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); } + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously +#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods + public static async IAsyncEnumerable AsAsyncEnumerable(this IEnumerable source) +#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + { + foreach (var item in source) + yield return item; + } } diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/ISymbolExtensions.cs b/src/Workspaces/Core/Portable/Shared/Extensions/ISymbolExtensions.cs index bf841432cddf8..aba239ec1cb92 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/ISymbolExtensions.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/ISymbolExtensions.cs @@ -344,7 +344,7 @@ private static XNode[] RewriteMany(ISymbol symbol, HashSet? visitedSymb result.AddRange(RewriteInheritdocElements(symbol, visitedSymbols, compilation, child, cancellationToken)); } - return result.ToArray(); + return [.. result]; } private static XNode[]? RewriteInheritdocElement(ISymbol memberSymbol, HashSet? visitedSymbols, Compilation compilation, XElement element, CancellationToken cancellationToken) diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.AnonymousTypeRemover.cs b/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.AnonymousTypeRemover.cs index b45c69cfd0dc4..f929abc39b74d 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.AnonymousTypeRemover.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.AnonymousTypeRemover.cs @@ -47,7 +47,7 @@ public override ITypeSymbol VisitNamedType(INamedTypeSymbol symbol) if (arguments.SequenceEqual(symbol.TypeArguments)) return symbol; - return symbol.ConstructedFrom.Construct(arguments.ToArray()); + return symbol.ConstructedFrom.Construct([.. arguments]); } public override ITypeSymbol VisitPointerType(IPointerTypeSymbol symbol) diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.UnavailableTypeParameterRemover.cs b/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.UnavailableTypeParameterRemover.cs index 24f728ff9b703..b591d1c9d2818 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.UnavailableTypeParameterRemover.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.UnavailableTypeParameterRemover.cs @@ -45,7 +45,7 @@ public override ITypeSymbol VisitNamedType(INamedTypeSymbol symbol) return symbol; } - return symbol.ConstructedFrom.Construct(arguments.ToArray()); + return symbol.ConstructedFrom.Construct([.. arguments]); } public override ITypeSymbol VisitPointerType(IPointerTypeSymbol symbol) diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.UnnamedErrorTypeRemover.cs b/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.UnnamedErrorTypeRemover.cs index 31510f8004fae..93bde82d583ef 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.UnnamedErrorTypeRemover.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.UnnamedErrorTypeRemover.cs @@ -49,7 +49,7 @@ public override ITypeSymbol VisitNamedType(INamedTypeSymbol symbol) return symbol; } - return symbol.ConstructedFrom.Construct(arguments.ToArray()); + return symbol.ConstructedFrom.Construct([.. arguments]); } public override ITypeSymbol VisitPointerType(IPointerTypeSymbol symbol) diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.cs b/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.cs index 6bef4e0d8324e..4153fcdbc4fa3 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.cs @@ -27,7 +27,7 @@ internal static partial class ITypeSymbolExtensions /// interfaceMember, or this type doesn't supply a member that successfully implements /// interfaceMember). /// - public static async Task> FindImplementationsForInterfaceMemberAsync( + public static ImmutableArray FindImplementationsForInterfaceMember( this ITypeSymbol typeSymbol, ISymbol interfaceMember, Solution solution, @@ -97,13 +97,11 @@ not SymbolKind.Method and // OriginalSymbolMatch allows types to be matched across different assemblies if they are considered to // be the same type, which provides a more accurate implementations list for interfaces. var constructedInterfaceMember = - await constructedInterface.GetMembers(interfaceMember.Name).FirstOrDefaultAsync( - typeSymbol => SymbolFinder.OriginalSymbolsMatchAsync(solution, typeSymbol, interfaceMember, cancellationToken)).ConfigureAwait(false); + constructedInterface.GetMembers(interfaceMember.Name).FirstOrDefault( + typeSymbol => SymbolFinder.OriginalSymbolsMatch(solution, typeSymbol, interfaceMember)); if (constructedInterfaceMember == null) - { continue; - } // Now we need to walk the base type chain, but we start at the first type that actually // has the interface directly in its interface hierarchy. diff --git a/src/Workspaces/Core/Portable/Shared/TestHooks/AsynchronousOperationListener.cs b/src/Workspaces/Core/Portable/Shared/TestHooks/AsynchronousOperationListener.cs index f9de85f7493c1..174d252614bbc 100644 --- a/src/Workspaces/Core/Portable/Shared/TestHooks/AsynchronousOperationListener.cs +++ b/src/Workspaces/Core/Portable/Shared/TestHooks/AsynchronousOperationListener.cs @@ -235,7 +235,7 @@ public ImmutableArray ActiveDiagnosticTokens return []; } - return _diagnosticTokenList.ToImmutableArray(); + return [.. _diagnosticTokenList]; } } } diff --git a/src/Workspaces/Core/Portable/Shared/TestHooks/AsynchronousOperationListenerProvider.cs b/src/Workspaces/Core/Portable/Shared/TestHooks/AsynchronousOperationListenerProvider.cs index 2fb062340a4c3..ed5be74296ee3 100644 --- a/src/Workspaces/Core/Portable/Shared/TestHooks/AsynchronousOperationListenerProvider.cs +++ b/src/Workspaces/Core/Portable/Shared/TestHooks/AsynchronousOperationListenerProvider.cs @@ -151,7 +151,7 @@ public async Task WaitAllAsync(Workspace? workspace, string[]? featureNames = nu do { // wait for all current tasks to be done for the time given - if (Task.WaitAll(tasks.ToArray(), smallTimeout)) + if (Task.WaitAll([.. tasks], smallTimeout)) { // current set of tasks are done. // see whether there are new tasks added while we were waiting diff --git a/src/Workspaces/Core/Portable/Shared/TestHooks/FeatureAttribute.cs b/src/Workspaces/Core/Portable/Shared/TestHooks/FeatureAttribute.cs index 8cc93a4bf90a0..4e7cd40576ebd 100644 --- a/src/Workspaces/Core/Portable/Shared/TestHooks/FeatureAttribute.cs +++ b/src/Workspaces/Core/Portable/Shared/TestHooks/FeatureAttribute.cs @@ -48,6 +48,7 @@ internal static class FeatureAttribute public const string NavigableSymbols = nameof(NavigableSymbols); public const string NavigateTo = nameof(NavigateTo); public const string NavigationBar = nameof(NavigationBar); + public const string OnTheFlyDocs = nameof(OnTheFlyDocs); public const string Outlining = nameof(Outlining); public const string OrganizeDocument = nameof(OrganizeDocument); public const string PackageInstaller = nameof(PackageInstaller); diff --git a/src/Workspaces/Core/Portable/Shared/Utilities/BloomFilter.cs b/src/Workspaces/Core/Portable/Shared/Utilities/BloomFilter.cs index 6cc6cad8d4b1a..4d740090dc8a4 100644 --- a/src/Workspaces/Core/Portable/Shared/Utilities/BloomFilter.cs +++ b/src/Workspaces/Core/Portable/Shared/Utilities/BloomFilter.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; namespace Microsoft.CodeAnalysis.Shared.Utilities; @@ -113,7 +114,7 @@ private static int ComputeK(int expectedCount, double falsePositiveProbability) /// /// Murmur hash is public domain. Actual code is included below as reference. /// - private int ComputeHash(string key, int seed) + private static int ComputeHash(string key, int seed, bool isCaseSensitive) { unchecked { @@ -127,8 +128,8 @@ private int ComputeHash(string key, int seed) var index = 0; while (numberOfCharsLeft >= 2) { - var c1 = GetCharacter(key, index); - var c2 = GetCharacter(key, index + 1); + var c1 = GetCharacter(key, index, isCaseSensitive); + var c2 = GetCharacter(key, index + 1, isCaseSensitive); h = CombineTwoCharacters(h, c1, c2); @@ -140,7 +141,7 @@ private int ComputeHash(string key, int seed) // odd length. if (numberOfCharsLeft == 1) { - var c = GetCharacter(key, index); + var c = GetCharacter(key, index, isCaseSensitive); h = CombineLastCharacter(h, c); } @@ -225,10 +226,10 @@ private static uint CombineTwoCharacters(uint h, uint c1, uint c2) } } - private char GetCharacter(string key, int index) + private static char GetCharacter(string key, int index, bool isCaseSensitive) { var c = key[index]; - return _isCaseSensitive ? c : char.ToLowerInvariant(c); + return isCaseSensitive ? c : char.ToLowerInvariant(c); } private static char GetCharacter(long key, int index) @@ -319,13 +320,13 @@ public void Add(string value) { for (var i = 0; i < _hashFunctionCount; i++) { - _bitArray[GetBitArrayIndex(value, i)] = true; + var hash = ComputeHash(value, i, _isCaseSensitive); + _bitArray[GetBitArrayIndexFromHash(hash)] = true; } } - private int GetBitArrayIndex(string value, int i) + private int GetBitArrayIndexFromHash(int hash) { - var hash = ComputeHash(value, i); hash %= _bitArray.Length; return Math.Abs(hash); } @@ -334,22 +335,24 @@ public void Add(long value) { for (var i = 0; i < _hashFunctionCount; i++) { - _bitArray[GetBitArrayIndex(value, i)] = true; + var hash = ComputeHash(value, i); + _bitArray[GetBitArrayIndexFromHash(hash)] = true; } } - private int GetBitArrayIndex(long value, int i) - { - var hash = ComputeHash(value, i); - hash %= _bitArray.Length; - return Math.Abs(hash); - } - public bool ProbablyContains(string value) { + // Request an array of immutable hashes for this input. Note that it's possible + // that the returned array might return a cached entry calculated by a different + // bloom filter and thus might have more entries than we need, but it's ok as + // it's guaranteed that the first _hashFunctionCount of those values are the values + // we would have computed had we not used the cache. + var hashes = BloomFilterHash.GetOrCreateHashArray(value, _isCaseSensitive, _hashFunctionCount); + for (var i = 0; i < _hashFunctionCount; i++) { - if (!_bitArray[GetBitArrayIndex(value, i)]) + var hash = hashes[i]; + if (!_bitArray[GetBitArrayIndexFromHash(hash)]) { return false; } @@ -362,7 +365,8 @@ public bool ProbablyContains(long value) { for (var i = 0; i < _hashFunctionCount; i++) { - if (!_bitArray[GetBitArrayIndex(value, i)]) + var hash = ComputeHash(value, i); + if (!_bitArray[GetBitArrayIndexFromHash(hash)]) { return false; } @@ -395,4 +399,86 @@ private static bool IsEquivalent(BitArray array1, BitArray array2) return true; } + + /// + /// Provides mechanism to efficiently obtain bloom filter hash for a value. Backed by a single element cache. + /// + internal sealed class BloomFilterHash + { + private static BloomFilterHash? s_cachedHash; + + private readonly string _value; + private readonly bool _isCaseSensitive; + private readonly ImmutableArray _hashes; + + private BloomFilterHash(string value, bool isCaseSensitive, int hashFunctionCount) + { + _value = value; + _isCaseSensitive = isCaseSensitive; + + var hashBuilder = new FixedSizeArrayBuilder(hashFunctionCount); + + for (var i = 0; i < hashFunctionCount; i++) + hashBuilder.Add(BloomFilter.ComputeHash(value, i, _isCaseSensitive)); + + _hashes = hashBuilder.MoveToImmutable(); + } + + /// + /// Although calculating this hash isn't terribly expensive, it does involve multiple + /// (usually around 13) hashings of the string (the actual count is ). + /// The typical usage pattern of bloom filters is that some operation (eg: find references) + /// requires asking a multitude of bloom filters whether a particular value is likely contained. + /// The vast majority of those bloom filters will end up hashing that string to the same values, so + /// we put those values into a simple cache and see if it can be used before calculating. + /// Local testing has put the hit rate of this at around 99%. + /// + /// Note that it's possible for this method to return an array from the cache longer than hashFunctionCount, + /// but if so, it's guaranteed that the values returned in the first hashFunctionCount entries are + /// the same as if the cache hadn't been used. + /// + public static ImmutableArray GetOrCreateHashArray(string value, bool isCaseSensitive, int hashFunctionCount) + { + var cachedHash = s_cachedHash; + + // Not an equivalency check on the hashFunctionCount as a longer array is ok. This is because the + // values in the array are determined by value and isCaseSensitive and hashFunctionCount is simply + // used to determine the length of the returned array. As long as the cached entry matches the value + // and isCaseSensitive and is at least as long as we need, then we can use it. + if (cachedHash == null + || cachedHash._isCaseSensitive != isCaseSensitive + || cachedHash._hashes.Length < hashFunctionCount + || cachedHash._value != value) + { + cachedHash = new BloomFilterHash(value, isCaseSensitive, hashFunctionCount); + s_cachedHash = cachedHash; + } + + return cachedHash._hashes; + } + + // Used only by tests + internal static bool TryGetCachedEntry(out bool isCaseSensitive, out string value) + { + var cachedHash = s_cachedHash; + + if (cachedHash == null) + { + isCaseSensitive = false; + value = string.Empty; + + return false; + } + + isCaseSensitive = cachedHash._isCaseSensitive; + value = cachedHash._value; + + return true; + } + + internal static void ResetCachedEntry() + { + s_cachedHash = null; + } + } } diff --git a/src/Workspaces/Core/Portable/Shared/Utilities/ProducerConsumer.cs b/src/Workspaces/Core/Portable/Shared/Utilities/ProducerConsumer.cs new file mode 100644 index 0000000000000..a4b3e0854ad7b --- /dev/null +++ b/src/Workspaces/Core/Portable/Shared/Utilities/ProducerConsumer.cs @@ -0,0 +1,338 @@ +// 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.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Shared.Utilities; + +internal readonly record struct ProducerConsumerOptions +{ + /// + /// Used when the consumeItems routine will only pull items on a single thread (never concurrently). produceItems + /// can be called concurrently on many threads. + /// + public static readonly ProducerConsumerOptions SingleReaderOptions = new() { SingleReader = true }; + + /// + /// Used when the consumeItems routine will only pull items on a single thread (never concurrently). produceItems + /// can be called on a single thread as well (never concurrently). + /// + public static readonly ProducerConsumerOptions SingleReaderWriterOptions = new() { SingleReader = true, SingleWriter = true }; + + /// + public bool SingleWriter { get; init; } + + /// + public bool SingleReader { get; init; } +} + +internal static class ProducerConsumer +{ + private static async Task BatchReaderIntoArraysAsync( + ChannelReader reader, + Func, TArgs, CancellationToken, Task> consumeItems, + TArgs args, + CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var items); + while (await reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) + { + // Grab as many items as we can from the channel at once and report in a single array. Then wait for the + // next set of items to be available. + while (reader.TryRead(out var item)) + items.Add(item); + + await consumeItems(items.ToImmutableAndClear(), args, cancellationToken).ConfigureAwait(false); + } + + return default; + } + + /// + /// Version of when caller the prefers the results being pre-packaged into arrays to process. + /// + public static Task RunAsync( + ProducerConsumerOptions options, + Func, TArgs, CancellationToken, Task> produceItems, + Func, TArgs, CancellationToken, Task> consumeItems, + TArgs args, + CancellationToken cancellationToken) + { + return RunChannelAsync( + options, + static (onItemFound, args, cancellationToken) => args.produceItems(onItemFound, args.args, cancellationToken), + static (reader, args, cancellationToken) => BatchReaderIntoArraysAsync(reader, args.consumeItems, args.args, cancellationToken), + args: (produceItems, consumeItems, args), + cancellationToken); + } + + /// + /// Version of when the caller prefers working with a stream of results. + /// + public static Task RunAsync( + ProducerConsumerOptions options, + Func, TArgs, CancellationToken, Task> produceItems, + Func, TArgs, CancellationToken, Task> consumeItems, + TArgs args, + CancellationToken cancellationToken) + { + // Bridge to sibling helper that takes a consumeItems that returns a value. + return RunChannelAsync( + options, + produceItems: static (callback, args, cancellationToken) => args.produceItems(callback, args.args, cancellationToken), + consumeItems: static async (items, args, cancellationToken) => + { + await args.consumeItems(items.ReadAllAsync(cancellationToken), args.args, cancellationToken).ConfigureAwait(false); + return default(VoidResult); + }, + args: (produceItems, consumeItems, args), + cancellationToken); + } + + /// + /// IEnumerable<TSource> -> Task. Callback receives IAsyncEnumerable items. + /// + public static Task RunParallelAsync( + IEnumerable source, + Func, TArgs, CancellationToken, Task> produceItems, + Func, TArgs, CancellationToken, Task> consumeItems, + TArgs args, + CancellationToken cancellationToken) + { + // Bridge to sibling helper that operates on an IAsyncEnumerable. + return RunParallelAsync(source.AsAsyncEnumerable(), produceItems, consumeItems, args, cancellationToken); + } + + /// + /// IAsyncEnumerable<TSource> -> Task. Callback receives IAsyncEnumerable items. + /// + public static Task RunParallelAsync( + IAsyncEnumerable source, + Func, TArgs, CancellationToken, Task> produceItems, + Func, TArgs, CancellationToken, Task> consumeItems, + TArgs args, + CancellationToken cancellationToken) + { + // Bridge to sibling helper that takes a consumeItems that returns a value. + return RunParallelAsync( + source, + produceItems: static (item, callback, args, cancellationToken) => args.produceItems(item, callback, args.args, cancellationToken), + consumeItems: static async (items, args, cancellationToken) => + { + await args.consumeItems(items, args.args, cancellationToken).ConfigureAwait(false); + return default(VoidResult); + }, + args: (produceItems, consumeItems, args), + cancellationToken); + } + + /// + /// IEnumerable<TSource> -> Task. Callback receives ImmutableArray of items. + /// + public static Task RunParallelAsync( + IEnumerable source, + Func, TArgs, CancellationToken, Task> produceItems, + Func, TArgs, CancellationToken, Task> consumeItems, + TArgs args, + CancellationToken cancellationToken) + { + // Bridge to sibling helper that operates on an IAsyncEnumerable. + return RunParallelAsync(source.AsAsyncEnumerable(), produceItems, consumeItems, args, cancellationToken); + } + + /// + /// IAsyncEnumerable<TSource> -> Task. Callback receives ImmutableArray of items. + /// + public static Task RunParallelAsync( + IAsyncEnumerable source, + Func, TArgs, CancellationToken, Task> produceItems, + Func, TArgs, CancellationToken, Task> consumeItems, + TArgs args, + CancellationToken cancellationToken) + { + // Bridge to sibling helper that takes a consumeItems that returns a value. + return RunParallelChannelAsync( + source, + produceItems: static (item, callback, args, cancellationToken) => args.produceItems(item, callback, args.args, cancellationToken), + consumeItems: static (items, args, cancellationToken) => BatchReaderIntoArraysAsync(items, args.consumeItems, args.args, cancellationToken), + args: (produceItems, consumeItems, args), + cancellationToken); + } + + /// + /// IEnumerable<TSource> -> Task<TResult> Callback receives an IAsyncEnumerable of items. + /// + public static Task RunParallelAsync( + IEnumerable source, + Func, TArgs, CancellationToken, Task> produceItems, + Func, TArgs, CancellationToken, Task> consumeItems, + TArgs args, + CancellationToken cancellationToken) + { + // Bridge to sibling helper that operates on an IAsyncEnumerable. + return RunParallelAsync(source.AsAsyncEnumerable(), produceItems, consumeItems, args, cancellationToken); + } + + /// + /// IAsyncEnumerable<TSource> -> Task<TResult>. Callback receives an IAsyncEnumerable of items. + /// + public static Task RunParallelAsync( + IAsyncEnumerable source, + Func, TArgs, CancellationToken, Task> produceItems, + Func, TArgs, CancellationToken, Task> consumeItems, + TArgs args, + CancellationToken cancellationToken) + { + return RunParallelChannelAsync( + source, + produceItems: static (item, callback, args, cancellationToken) => args.produceItems(item, callback, args.args, cancellationToken), + consumeItems: static (reader, args, cancellationToken) => args.consumeItems(reader.ReadAllAsync(cancellationToken), args.args, cancellationToken), + args: (produceItems, consumeItems, args), + cancellationToken); + } + + #region helpers that return arrays + + /// + /// IEnumerable<TSource> -> Task<ImmutableArray<TResult>> + /// + public static Task> RunParallelAsync( + IEnumerable source, + Func, TArgs, CancellationToken, Task> produceItems, + TArgs args, + CancellationToken cancellationToken) + { + // Bridge to sibling helper that operates on an IAsyncEnumerable. + return RunParallelAsync(source.AsAsyncEnumerable(), produceItems, args, cancellationToken); + } + + /// + /// IAsyncEnumerable<TSource> -> Task<ImmutableArray<TResult>> + /// + public static async Task> RunParallelAsync( + IAsyncEnumerable source, + Func, TArgs, CancellationToken, Task> produceItems, + TArgs args, + CancellationToken cancellationToken) + { + // Bridge to sibling helper that takes a consumeItems that returns a value. + return await RunParallelAsync( + source, + produceItems: static (item, callback, args, cancellationToken) => args.produceItems(item, callback, args.args, cancellationToken), + consumeItems: static (stream, args, cancellationToken) => stream.ToImmutableArrayAsync(cancellationToken), + args: (produceItems, args), + cancellationToken).ConfigureAwait(false); + } + + #endregion + + #region Core channel-based impl + + private static Task RunParallelChannelAsync( + IAsyncEnumerable source, + Func, TArgs, CancellationToken, Task> produceItems, + Func, TArgs, CancellationToken, Task> consumeItems, + TArgs args, + CancellationToken cancellationToken) + { + return RunChannelAsync( + // We're running in parallel, so we def have multiple writers + ProducerConsumerOptions.SingleReaderOptions, + produceItems: static (callback, args, cancellationToken) => + RoslynParallel.ForEachAsync( + args.source, + cancellationToken, + async (source, cancellationToken) => + await args.produceItems(source, callback, args.args, cancellationToken).ConfigureAwait(false)), + consumeItems: static (enumerable, args, cancellationToken) => args.consumeItems(enumerable, args.args, cancellationToken), + args: (source, produceItems, consumeItems, args), + cancellationToken); + } + + /// + /// Helper utility for the pattern of a pair of a production routine and consumption routine using a channel to + /// coordinate data transfer. The provided are used to create a , which will then then manage the rules and behaviors around the routines. Importantly, the + /// channel handles backpressure, ensuring that if the consumption routine cannot keep up, that the production + /// routine will be throttled. + /// + /// is the routine called to actually produce the items. It will be passed an + /// action that can be used to write items to the channel. Note: the channel itself will have rules depending on if + /// that writing can happen concurrently multiple write threads or just a single writer. See for control of this when creating the channel. + /// + /// is the routine called to consume the items. Similarly, reading can have just a + /// single reader or multiple readers, depending on the value passed into . + /// + private static async Task RunChannelAsync( + ProducerConsumerOptions options, + Func, TArgs, CancellationToken, Task> produceItems, + Func, TArgs, CancellationToken, Task> consumeItems, + TArgs args, + CancellationToken cancellationToken) + { + var channel = Channel.CreateUnbounded(new() + { + SingleReader = options.SingleReader, + SingleWriter = options.SingleWriter, + }); + + // When cancellation happens, attempt to close the channel. That will unblock the task processing the items. + // Capture-free version is only available on netcore unfortunately. + using var _ = cancellationToken.Register( +#if NET + static (obj, cancellationToken) => ((Channel)obj!).Writer.TryComplete(new OperationCanceledException(cancellationToken)), + state: channel); +#else + () => channel.Writer.TryComplete(new OperationCanceledException(cancellationToken))); +#endif + + var writeTask = ProduceItemsAndWriteToChannelAsync(); + var readTask = ReadFromChannelAndConsumeItemsAsync(); + await Task.WhenAll(writeTask, readTask).ConfigureAwait(false); + + return await readTask.ConfigureAwait(false); + + async Task ReadFromChannelAndConsumeItemsAsync() + { + await Task.Yield().ConfigureAwait(false); + return await consumeItems(channel.Reader, args, cancellationToken).ConfigureAwait(false); + } + + async Task ProduceItemsAndWriteToChannelAsync() + { + Exception? exception = null; + try + { + await Task.Yield().ConfigureAwait(false); + + // It's ok to use TryWrite here. TryWrite always succeeds unless the channel is completed. And the + // channel is only ever completed by us (after produceItems completes or throws an exception) or if the + // cancellationToken is triggered above in RunAsync. In that latter case, it's ok for writing to the + // channel to do nothing as we no longer need to write out those assets to the pipe. + await produceItems(item => channel.Writer.TryWrite(item), args, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) when ((exception = ex) == null) + { + throw ExceptionUtilities.Unreachable(); + } + finally + { + // No matter what path we take (exceptional or non-exceptional), always complete the channel so the + // writing task knows it's done. + channel.Writer.TryComplete(exception); + } + } + } + + #endregion +} diff --git a/src/Workspaces/Core/Portable/Shared/Utilities/RoslynParallel.NetFramework.cs b/src/Workspaces/Core/Portable/Shared/Utilities/RoslynParallel.NetFramework.cs new file mode 100644 index 0000000000000..d24e62bb1e4eb --- /dev/null +++ b/src/Workspaces/Core/Portable/Shared/Utilities/RoslynParallel.NetFramework.cs @@ -0,0 +1,652 @@ +// 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 + +#pragma warning disable CA1068 // CancellationToken parameters must come last +#pragma warning disable IDE0007 // Use implicit type +#pragma warning disable IDE2003 // Blank line required between block and subsequent statement +#pragma warning disable IDE2004 // Blank line not allowed after constructor initializer colon +#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods + +// Ported from +// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Threading.Tasks.Parallel/src/System/Threading/Tasks/Parallel.ForEachAsync.cs +// With only changes to make the code work on NetFx. Where changes have been made, the original code is kept around in +// an ifdef'ed block to see what it was doing. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Shared.Utilities; + +internal static partial class RoslynParallel +{ + private static class NetFramework + { + /// Executes a for each operation on an in which iterations may run in parallel. + /// The type of the data in the source. + /// An enumerable data source. + /// An asynchronous delegate that is invoked once per element in the data source. + /// The argument or argument is . + /// A task that represents the entire for each operation. + /// The operation will execute at most operations in parallel. + public static Task ForEachAsync(IEnumerable source, Func body) + { +#if false + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(body); +#endif + + return ForEachAsync(source, DefaultDegreeOfParallelism, TaskScheduler.Default, default(CancellationToken), body); + } + + /// Executes a for each operation on an in which iterations may run in parallel. + /// The type of the data in the source. + /// An enumerable data source. + /// A cancellation token that may be used to cancel the for each operation. + /// An asynchronous delegate that is invoked once per element in the data source. + /// The argument or argument is . + /// A task that represents the entire for each operation. + /// The operation will execute at most operations in parallel. + public static Task ForEachAsync(IEnumerable source, CancellationToken cancellationToken, Func body) + { +#if false + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(body); +#endif + + return ForEachAsync(source, DefaultDegreeOfParallelism, TaskScheduler.Default, cancellationToken, body); + } + + /// Executes a for each operation on an in which iterations may run in parallel. + /// The type of the data in the source. + /// An enumerable data source. + /// An object that configures the behavior of this operation. + /// An asynchronous delegate that is invoked once per element in the data source. + /// The argument or argument is . + /// A task that represents the entire for each operation. + public static Task ForEachAsync(IEnumerable source, ParallelOptions parallelOptions, Func body) + { +#if false + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(parallelOptions); + ArgumentNullException.ThrowIfNull(body); + + return ForEachAsync(source, parallelOptions.EffectiveMaxConcurrencyLevel, parallelOptions.EffectiveTaskScheduler, parallelOptions.CancellationToken, body); +#else + return ForEachAsync(source, EffectiveMaxConcurrencyLevel(parallelOptions), EffectiveTaskScheduler(parallelOptions), parallelOptions.CancellationToken, body); +#endif + } + + // Copied from https://github.com/dotnet/runtime/blob/6f18b5ef46a8fbc6675b07d4b256c35b36fc4e3c/src/libraries/System.Threading.Tasks.Parallel/src/System/Threading/Tasks/Parallel.cs#L64 + // Convenience property used by TPL logic + private static TaskScheduler EffectiveTaskScheduler(ParallelOptions options) => options.TaskScheduler ?? TaskScheduler.Current; + + private static int EffectiveMaxConcurrencyLevel(ParallelOptions options) + { + int rval = options.MaxDegreeOfParallelism; + int schedulerMax = EffectiveTaskScheduler(options).MaximumConcurrencyLevel; + if ((schedulerMax > 0) && (schedulerMax != int.MaxValue)) + { + rval = (rval == -1) ? schedulerMax : Math.Min(schedulerMax, rval); + } + return rval; + } + + /// Executes a for each operation on an in which iterations may run in parallel. + /// The type of the data in the source. + /// An enumerable data source. + /// A integer indicating how many operations to allow to run in parallel. + /// The task scheduler on which all code should execute. + /// A cancellation token that may be used to cancel the for each operation. + /// An asynchronous delegate that is invoked once per element in the data source. + /// The argument or argument is . + /// A task that represents the entire for each operation. + private static Task ForEachAsync(IEnumerable source, int dop, TaskScheduler scheduler, CancellationToken cancellationToken, Func body) + { + Debug.Assert(source != null); + Debug.Assert(scheduler != null); + Debug.Assert(body != null); + + // One fast up-front check for cancellation before we start the whole operation. + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + // The worker body. Each worker will execute this same body. + Func taskBody = static async o => + { + var state = (SyncForEachAsyncState)o; + bool launchedNext = false; + +#pragma warning disable CA2007 // Explicitly don't use ConfigureAwait, as we want to perform all work on the specified scheduler that's now current + try + { + // Continue to loop while there are more elements to be processed. + while (!state.Cancellation.IsCancellationRequested) + { + // Get the next element from the enumerator. This requires asynchronously locking around MoveNext/Current. + TSource element; + await state.AcquireLock(); + try + { + if (state.Cancellation.IsCancellationRequested || // check now that the lock has been acquired + !state.Enumerator.MoveNext()) + { + break; + } + + element = state.Enumerator.Current; + } + finally + { + state.ReleaseLock(); + } + + // If the remaining dop allows it and we've not yet queued the next worker, do so now. We wait + // until after we've grabbed an item from the enumerator to a) avoid unnecessary contention on the + // serialized resource, and b) avoid queueing another work if there aren't any more items. Each worker + // is responsible only for creating the next worker, which in turn means there can't be any contention + // on creating workers (though it's possible one worker could be executing while we're creating the next). + if (!launchedNext) + { + launchedNext = true; + state.QueueWorkerIfDopAvailable(); + } + + // Process the loop body. + await state.LoopBody(element, state.Cancellation.Token); + } + } + catch (Exception e) + { + // Record the failure and then don't let the exception propagate. The last worker to complete + // will propagate exceptions as is appropriate to the top-level task. + state.RecordException(e); + } + finally + { + // If we're the last worker to complete, clean up and complete the operation. + if (state.SignalWorkerCompletedIterating()) + { + try + { + state.Dispose(); + } + catch (Exception e) + { + state.RecordException(e); + } + + // Finally, complete the task returned to the ForEachAsync caller. + // This must be the very last thing done. + state.Complete(); + } + } +#pragma warning restore CA2007 + }; + + try + { + // Construct a state object that encapsulates all state to be passed and shared between + // the workers, and queues the first worker. + var state = new SyncForEachAsyncState(source, taskBody, dop, scheduler, cancellationToken, body); + state.QueueWorkerIfDopAvailable(); + return state.Task; + } + catch (Exception e) + { + return Task.FromException(e); + } + } + + /// Executes a for each operation on an in which iterations may run in parallel. + /// The type of the data in the source. + /// An asynchronous enumerable data source. + /// An asynchronous delegate that is invoked once per element in the data source. + /// The argument or argument is . + /// A task that represents the entire for each operation. + /// The operation will execute at most operations in parallel. + public static Task ForEachAsync(IAsyncEnumerable source, Func body) + { +#if false + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(body); +#endif + + return ForEachAsync(source, DefaultDegreeOfParallelism, TaskScheduler.Default, default(CancellationToken), body); + } + + /// Executes a for each operation on an in which iterations may run in parallel. + /// The type of the data in the source. + /// An asynchronous enumerable data source. + /// A cancellation token that may be used to cancel the for each operation. + /// An asynchronous delegate that is invoked once per element in the data source. + /// The argument or argument is . + /// A task that represents the entire for each operation. + /// The operation will execute at most operations in parallel. + public static Task ForEachAsync(IAsyncEnumerable source, CancellationToken cancellationToken, Func body) + { +#if false + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(body); +#endif + + return ForEachAsync(source, DefaultDegreeOfParallelism, TaskScheduler.Default, cancellationToken, body); + } + + /// Executes a for each operation on an in which iterations may run in parallel. + /// The type of the data in the source. + /// An asynchronous enumerable data source. + /// An object that configures the behavior of this operation. + /// An asynchronous delegate that is invoked once per element in the data source. + /// The argument or argument is . + /// A task that represents the entire for each operation. + public static Task ForEachAsync(IAsyncEnumerable source, ParallelOptions parallelOptions, Func body) + { +#if false + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(parallelOptions); + ArgumentNullException.ThrowIfNull(body); +#endif + +#if false + return ForEachAsync(source, parallelOptions.EffectiveMaxConcurrencyLevel, parallelOptions.EffectiveTaskScheduler, parallelOptions.CancellationToken, body); +#else + return ForEachAsync(source, EffectiveMaxConcurrencyLevel(parallelOptions), EffectiveTaskScheduler(parallelOptions), parallelOptions.CancellationToken, body); +#endif + } + + /// Executes a for each operation on an in which iterations may run in parallel. + /// The type of the data in the source. + /// An asynchronous enumerable data source. + /// A integer indicating how many operations to allow to run in parallel. + /// The task scheduler on which all code should execute. + /// A cancellation token that may be used to cancel the for each operation. + /// An asynchronous delegate that is invoked once per element in the data source. + /// The argument or argument is . + /// A task that represents the entire for each operation. + private static Task ForEachAsync(IAsyncEnumerable source, int dop, TaskScheduler scheduler, CancellationToken cancellationToken, Func body) + { + Debug.Assert(source != null); + Debug.Assert(scheduler != null); + Debug.Assert(body != null); + + // One fast up-front check for cancellation before we start the whole operation. + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + // The worker body. Each worker will execute this same body. + Func taskBody = static async o => + { + var state = (AsyncForEachAsyncState)o; + bool launchedNext = false; + +#pragma warning disable CA2007 // Explicitly don't use ConfigureAwait, as we want to perform all work on the specified scheduler that's now current + try + { + // Continue to loop while there are more elements to be processed. + while (!state.Cancellation.IsCancellationRequested) + { + // Get the next element from the enumerator. This requires asynchronously locking around MoveNextAsync/Current. + TSource element; + await state.AcquireLock(); + try + { + if (state.Cancellation.IsCancellationRequested || // check now that the lock has been acquired + !await state.Enumerator.MoveNextAsync()) + { + break; + } + + element = state.Enumerator.Current; + } + finally + { + state.ReleaseLock(); + } + + // If the remaining dop allows it and we've not yet queued the next worker, do so now. We wait + // until after we've grabbed an item from the enumerator to a) avoid unnecessary contention on the + // serialized resource, and b) avoid queueing another work if there aren't any more items. Each worker + // is responsible only for creating the next worker, which in turn means there can't be any contention + // on creating workers (though it's possible one worker could be executing while we're creating the next). + if (!launchedNext) + { + launchedNext = true; + state.QueueWorkerIfDopAvailable(); + } + + // Process the loop body. + await state.LoopBody(element, state.Cancellation.Token); + } + } + catch (Exception e) + { + // Record the failure and then don't let the exception propagate. The last worker to complete + // will propagate exceptions as is appropriate to the top-level task. + state.RecordException(e); + } + finally + { + // If we're the last worker to complete, clean up and complete the operation. + if (state.SignalWorkerCompletedIterating()) + { + try + { + await state.DisposeAsync(); + } + catch (Exception e) + { + state.RecordException(e); + } + + // Finally, complete the task returned to the ForEachAsync caller. + // This must be the very last thing done. + state.Complete(); + } + } +#pragma warning restore CA2007 + }; + + try + { + // Construct a state object that encapsulates all state to be passed and shared between + // the workers, and queues the first worker. + var state = new AsyncForEachAsyncState(source, taskBody, dop, scheduler, cancellationToken, body); + state.QueueWorkerIfDopAvailable(); + return state.Task; + } + catch (Exception e) + { + return Task.FromException(e); + } + } + + /// Gets the default degree of parallelism to use when none is explicitly provided. + private static int DefaultDegreeOfParallelism => Environment.ProcessorCount; + + /// Stores the state associated with a ForEachAsync operation, shared between all its workers. + /// Specifies the type of data being enumerated. +#if false + private abstract class ForEachAsyncState : TaskCompletionSource, IThreadPoolWorkItem +#else + private abstract class ForEachAsyncState : TaskCompletionSource +#endif + { + /// The caller-provided cancellation token. + private readonly CancellationToken _externalCancellationToken; + /// Registration with caller-provided cancellation token. + protected readonly CancellationTokenRegistration _registration; + /// + /// The delegate to invoke on each worker to run the enumerator processing loop. + /// + /// + /// This could have been an action rather than a func, but it returns a task so that the task body is an async Task + /// method rather than async void, even though the worker body catches all exceptions and the returned Task is ignored. + /// + private readonly Func _taskBody; + /// The on which all work should be performed. + private readonly TaskScheduler _scheduler; +#if false + /// The present at the time of the ForEachAsync invocation. This is only used if on the default scheduler. + private readonly ExecutionContext? _executionContext; +#endif + /// Semaphore used to provide exclusive access to the enumerator. + private readonly SemaphoreSlim? _lock; + + /// The number of outstanding workers. When this hits 0, the operation has completed. + private int _completionRefCount; + /// Any exceptions incurred during execution. + private List? _exceptions; + /// The number of workers that may still be created. + private int _remainingDop; + + /// The delegate to invoke for each element yielded by the enumerator. + public readonly Func LoopBody; + /// The internal token source used to cancel pending work. + public readonly CancellationTokenSource Cancellation = new CancellationTokenSource(); + + /// Initializes the state object. + protected ForEachAsyncState(Func taskBody, bool needsLock, int dop, TaskScheduler scheduler, CancellationToken cancellationToken, Func body) + { + _taskBody = taskBody; + _lock = needsLock ? new SemaphoreSlim(initialCount: 1, maxCount: 1) : null; + _remainingDop = dop < 0 ? DefaultDegreeOfParallelism : dop; + LoopBody = body; + _scheduler = scheduler; + +#if false + if (scheduler == TaskScheduler.Default) + { + _executionContext = ExecutionContext.Capture(); + } +#endif + + _externalCancellationToken = cancellationToken; +#if false + _registration = cancellationToken.UnsafeRegister(static o => ((ForEachAsyncState)o!).Cancellation.Cancel(), this); +#else + _registration = cancellationToken.Register(static o => ((ForEachAsyncState)o!).Cancellation.Cancel(), this); +#endif + } + + /// Queues another worker if allowed by the remaining degree of parallelism permitted. + /// This is not thread-safe and must only be invoked by one worker at a time. + public void QueueWorkerIfDopAvailable() + { + if (_remainingDop > 0) + { + _remainingDop--; + + // Queue the invocation of the worker/task body. Note that we explicitly do not pass a cancellation token here, + // as the task body is what's responsible for completing the ForEachAsync task, for decrementing the reference count + // on pending tasks, and for cleaning up state. If a token were passed to StartNew (which simply serves to stop the + // task from starting to execute if it hasn't yet by the time cancellation is requested), all of that logic could be + // skipped, and bad things could ensue, e.g. deadlocks, leaks, etc. Also note that we need to increment the pending + // work item ref count prior to queueing the worker in order to avoid race conditions that could lead to temporarily + // and erroneously bouncing at zero, which would trigger completion too early. + Interlocked.Increment(ref _completionRefCount); +#if false + if (_scheduler == TaskScheduler.Default) + { + // If the scheduler is the default, we can avoid the overhead of the StartNew Task by just queueing + // this state object as the work item. + ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false); + } + else +#endif + { + // We're targeting a non-default TaskScheduler, so queue the task body to it. + System.Threading.Tasks.Task.Factory.StartNew(_taskBody!, this, default(CancellationToken), TaskCreationOptions.DenyChildAttach, _scheduler); + } + } + } + + /// Signals that the worker has completed iterating. + /// true if this is the last worker to complete iterating; otherwise, false. + public bool SignalWorkerCompletedIterating() => Interlocked.Decrement(ref _completionRefCount) == 0; + + /// Asynchronously acquires exclusive access to the enumerator. + public Task AcquireLock() + { + // We explicitly don't pass this.Cancellation to WaitAsync. Doing so adds overhead, and it isn't actually + // necessary. All of the operations that monitor the lock are part of the same ForEachAsync operation, and the Task + // returned from ForEachAsync can't complete until all of the constituent operations have completed, including whoever + // holds the lock while this worker is waiting on the lock. Thus, the lock will need to be released for the overall + // operation to complete. Passing the token would allow the overall operation to potentially complete a bit faster in + // the face of cancellation, in exchange for making it a bit slower / more overhead in the common case of cancellation + // not being requested. We want to optimize for the latter. This also then avoids an exception throw / catch when + // cancellation is requested. + Debug.Assert(_lock is not null, "Should only be invoked when _lock is non-null"); + return _lock.WaitAsync(CancellationToken.None); + } + + /// Relinquishes exclusive access to the enumerator. + public void ReleaseLock() + { + Debug.Assert(_lock is not null, "Should only be invoked when _lock is non-null"); + _lock.Release(); + } + + /// Stores an exception and triggers cancellation in order to alert all workers to stop as soon as possible. + /// The exception. + public void RecordException(Exception e) + { + // Store the exception. + lock (this) + { + (_exceptions ??= new List()).Add(e); + } + + // Trigger cancellation of all workers. If cancellation has already been triggered + // due to a previous exception occurring, this is a nop. + try + { + Cancellation.Cancel(); + } + catch (AggregateException ae) + { + // If cancellation callbacks erroneously throw exceptions, include those exceptions in the list. + lock (this) + { + _exceptions.AddRange(ae.InnerExceptions); + } + } + } + + /// Completes the ForEachAsync task based on the status of this state object. + public void Complete() + { + Debug.Assert(_completionRefCount == 0, $"Expected {nameof(_completionRefCount)} == 0, got {_completionRefCount}"); + + bool taskSet; + if (_externalCancellationToken.IsCancellationRequested) + { + // The externally provided token had cancellation requested. Assume that any exceptions + // then are due to that, and just cancel the resulting task. + taskSet = TrySetCanceled(_externalCancellationToken); + } + else if (_exceptions is null) + { + // Everything completed successfully. + Debug.Assert(!Cancellation.IsCancellationRequested); +#if false + taskSet = TrySetResult(); +#else + taskSet = TrySetResult(default(VoidResult)); +#endif + } + else + { + // Fail the task with the resulting exceptions. The first should be the initial + // exception that triggered the operation to shut down. The others, if any, may + // include cancellation exceptions from other concurrent operations being canceled + // in response to the primary exception. + taskSet = TrySetException(_exceptions); + } + + Debug.Assert(taskSet, "Complete should only be called once."); + } + +#if false + /// Executes the task body using the captured when ForEachAsync was invoked. + void IThreadPoolWorkItem.Execute() + { + Debug.Assert(_scheduler == TaskScheduler.Default, $"Expected {nameof(_scheduler)} == TaskScheduler.Default, got {_scheduler}"); + + if (_executionContext is null) + { + _taskBody(this); + } + else + { + ExecutionContext.Run(_executionContext, static o => ((ForEachAsyncState)o!)._taskBody(o), this); + } + } +#endif + } + + /// Stores the state associated with an IEnumerable ForEachAsync operation, shared between all its workers. + /// Specifies the type of data being enumerated. + private sealed class SyncForEachAsyncState : ForEachAsyncState, IDisposable + { + public readonly IEnumerator Enumerator; + + public SyncForEachAsyncState( + IEnumerable source, Func taskBody, + int dop, TaskScheduler scheduler, CancellationToken cancellationToken, + Func body) : + base(taskBody, needsLock: true, dop, scheduler, cancellationToken, body) + { +#if false + Enumerator = source.GetEnumerator() ?? throw new InvalidOperationException(SR.Parallel_ForEach_NullEnumerator); +#else + Enumerator = source.GetEnumerator() ?? throw new InvalidOperationException(); +#endif + } + + public void Dispose() + { + _registration.Dispose(); + Enumerator.Dispose(); + } + } + + /// Stores the state associated with an IAsyncEnumerable ForEachAsync operation, shared between all its workers. + /// Specifies the type of data being enumerated. + private sealed class AsyncForEachAsyncState : ForEachAsyncState, IAsyncDisposable + { + public readonly IAsyncEnumerator Enumerator; + + public AsyncForEachAsyncState( + IAsyncEnumerable source, Func taskBody, + int dop, TaskScheduler scheduler, CancellationToken cancellationToken, + Func body) : + base(taskBody, needsLock: true, dop, scheduler, cancellationToken, body) + { +#if false + Enumerator = source.GetAsyncEnumerator(Cancellation.Token) ?? throw new InvalidOperationException(SR.Parallel_ForEach_NullEnumerator); +#else + Enumerator = source.GetAsyncEnumerator(Cancellation.Token) ?? throw new InvalidOperationException(); +#endif + } + + public ValueTask DisposeAsync() + { + _registration.Dispose(); + return Enumerator.DisposeAsync(); + } + } + + /// Stores the state associated with an IAsyncEnumerable ForEachAsync operation, shared between all its workers. + /// Specifies the type of data being enumerated. + private sealed class ForEachState : ForEachAsyncState, IDisposable + { + public T NextAvailable; + public readonly T ToExclusive; + + public ForEachState( + T fromExclusive, T toExclusive, Func taskBody, + bool needsLock, int dop, TaskScheduler scheduler, CancellationToken cancellationToken, + Func body) : + base(taskBody, needsLock, dop, scheduler, cancellationToken, body) + { + NextAvailable = fromExclusive; + ToExclusive = toExclusive; + } + + public void Dispose() => _registration.Dispose(); + } + } +} + +#endif diff --git a/src/Workspaces/Core/Portable/Shared/Utilities/RoslynParallel.cs b/src/Workspaces/Core/Portable/Shared/Utilities/RoslynParallel.cs new file mode 100644 index 0000000000000..5760eec512e1d --- /dev/null +++ b/src/Workspaces/Core/Portable/Shared/Utilities/RoslynParallel.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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.Shared.Utilities; + +#pragma warning disable CA1068 // CancellationToken parameters must come last + +internal static partial class RoslynParallel +{ + // For all these helpers, we defer to the native .net core version if we're on .net core. Otherwise, we defer to + // our ported version of that code when on .net framework. + + public static Task ForEachAsync( + IEnumerable source, + CancellationToken cancellationToken, + Func body) + { +#if NET + return Parallel.ForEachAsync(source, cancellationToken, body); +#else + return NetFramework.ForEachAsync(source, cancellationToken, body); +#endif + } + + public static Task ForEachAsync( + IEnumerable source, + ParallelOptions parallelOptions, + Func body) + { +#if NET + return Parallel.ForEachAsync(source, parallelOptions, body); +#else + return NetFramework.ForEachAsync(source, parallelOptions, body); +#endif + } + + public static Task ForEachAsync( + IAsyncEnumerable source, + CancellationToken cancellationToken, + Func body) + { +#if NET + return Parallel.ForEachAsync(source, cancellationToken, body); +#else + return NetFramework.ForEachAsync(source, cancellationToken, body); +#endif + } + + public static Task ForEachAsync( + IAsyncEnumerable source, + ParallelOptions parallelOptions, + Func body) + { +#if NET + return Parallel.ForEachAsync(source, parallelOptions, body); +#else + return NetFramework.ForEachAsync(source, parallelOptions, body); +#endif + } +} diff --git a/src/Workspaces/Core/Portable/Storage/AbstractPersistentStorageService.cs b/src/Workspaces/Core/Portable/Storage/AbstractPersistentStorageService.cs index 0e6feed57a96e..15a9ef2cd4aa3 100644 --- a/src/Workspaces/Core/Portable/Storage/AbstractPersistentStorageService.cs +++ b/src/Workspaces/Core/Portable/Storage/AbstractPersistentStorageService.cs @@ -17,19 +17,12 @@ namespace Microsoft.CodeAnalysis.Storage; /// A service that enables storing and retrieving of information associated with solutions, /// projects or documents across runtime sessions. /// -internal abstract partial class AbstractPersistentStorageService : IChecksummedPersistentStorageService +internal abstract partial class AbstractPersistentStorageService(IPersistentStorageConfiguration configuration) : IChecksummedPersistentStorageService { - protected readonly IPersistentStorageConfiguration Configuration; + protected readonly IPersistentStorageConfiguration Configuration = configuration; - /// - /// This lock guards all mutable fields in this type. - /// private readonly SemaphoreSlim _lock = new(initialCount: 1); - private ReferenceCountedDisposable? _currentPersistentStorage; - private SolutionId? _currentPersistentStorageSolutionId; - - protected AbstractPersistentStorageService(IPersistentStorageConfiguration configuration) - => Configuration = configuration; + private IChecksummedPersistentStorage? _currentPersistentStorage; protected abstract string GetDatabaseFilePath(string workingFolderPath); @@ -38,88 +31,65 @@ protected AbstractPersistentStorageService(IPersistentStorageConfiguration confi /// to delete the database and retry opening one more time. If that fails again, the instance will be used. /// - protected abstract ValueTask TryOpenDatabaseAsync(SolutionKey solutionKey, string workingFolderPath, string databaseFilePath, CancellationToken cancellationToken); - protected abstract bool ShouldDeleteDatabase(Exception exception); + protected abstract ValueTask TryOpenDatabaseAsync(SolutionKey solutionKey, string workingFolderPath, string databaseFilePath, IPersistentStorageFaultInjector? faultInjector, CancellationToken cancellationToken); public ValueTask GetStorageAsync(SolutionKey solutionKey, CancellationToken cancellationToken) - { - return solutionKey.FilePath == null - ? new(NoOpPersistentStorage.GetOrThrow(Configuration.ThrowOnFailure)) - : GetStorageWorkerAsync(solutionKey, cancellationToken); - } + => GetStorageAsync(solutionKey, this.Configuration, faultInjector: null, cancellationToken); - internal async ValueTask GetStorageWorkerAsync(SolutionKey solutionKey, CancellationToken cancellationToken) + public async ValueTask GetStorageAsync( + SolutionKey solutionKey, + IPersistentStorageConfiguration configuration, + IPersistentStorageFaultInjector? faultInjector, + CancellationToken cancellationToken) { - using (await _lock.DisposableWaitAsync(cancellationToken).ConfigureAwait(false)) - { - // Do we already have storage for this? - if (solutionKey.Id == _currentPersistentStorageSolutionId) - { - // We do, great. Increment our ref count for our caller. They'll decrement it - // when done with it. - return PersistentStorageReferenceCountedDisposableWrapper.AddReferenceCountToAndCreateWrapper(_currentPersistentStorage!); - } - - var workingFolder = Configuration.TryGetStorageLocation(solutionKey); - if (workingFolder == null) - return NoOpPersistentStorage.GetOrThrow(Configuration.ThrowOnFailure); - - // If we already had some previous cached service, let's let it start cleaning up - if (_currentPersistentStorage != null) - { - var storageToDispose = _currentPersistentStorage; - - // Kick off a task to actually go dispose the previous cached storage instance. - // This will remove the single ref count we ourselves added when we cached the - // instance. Then once all other existing clients who are holding onto this - // instance let go, it will finally get truly disposed. - // This operation is not safe to cancel (as dispose must happen). - _ = Task.Run(storageToDispose.Dispose, CancellationToken.None); + if (solutionKey.FilePath == null) + return NoOpPersistentStorage.GetOrThrow(solutionKey, Configuration.ThrowOnFailure); - _currentPersistentStorage = null; - _currentPersistentStorageSolutionId = null; - } + // Without taking the lock, see if we can lookup a storage for this key. + var existing = _currentPersistentStorage; + if (existing?.SolutionKey == solutionKey) + return existing; - var storage = await CreatePersistentStorageAsync(solutionKey, workingFolder, cancellationToken).ConfigureAwait(false); - Contract.ThrowIfNull(storage); + var workingFolder = configuration.TryGetStorageLocation(solutionKey); + if (workingFolder == null) + return NoOpPersistentStorage.GetOrThrow(solutionKey, Configuration.ThrowOnFailure); - // Create and cache a new storage instance associated with this particular solution. - // It will initially have a ref-count of 1 due to our reference to it. - _currentPersistentStorage = new ReferenceCountedDisposable(storage); - _currentPersistentStorageSolutionId = solutionKey.Id; + using (await _lock.DisposableWaitAsync(cancellationToken).ConfigureAwait(false)) + { + // Recheck if we have a storage for this key after taking the lock. + if (_currentPersistentStorage?.SolutionKey != solutionKey) + _currentPersistentStorage = await CreatePersistentStorageAsync(solutionKey, workingFolder, faultInjector, cancellationToken).ConfigureAwait(false); - // Now increment the reference count and return to our caller. The current ref - // count for this instance will be 2. Until all the callers *and* us decrement - // the refcounts, this instance will not be actually disposed. - return PersistentStorageReferenceCountedDisposableWrapper.AddReferenceCountToAndCreateWrapper(_currentPersistentStorage); + return _currentPersistentStorage; } } private async ValueTask CreatePersistentStorageAsync( - SolutionKey solutionKey, string workingFolderPath, CancellationToken cancellationToken) + SolutionKey solutionKey, string workingFolderPath, IPersistentStorageFaultInjector? faultInjector, CancellationToken cancellationToken) { // Attempt to create the database up to two times. The first time we may encounter // some sort of issue (like DB corruption). We'll then try to delete the DB and can // try to create it again. If we can't create it the second time, then there's nothing // we can do and we have to store things in memory. - var result = await TryCreatePersistentStorageAsync(solutionKey, workingFolderPath, cancellationToken).ConfigureAwait(false) ?? - await TryCreatePersistentStorageAsync(solutionKey, workingFolderPath, cancellationToken).ConfigureAwait(false); + var result = await TryCreatePersistentStorageAsync(solutionKey, workingFolderPath, faultInjector, cancellationToken).ConfigureAwait(false) ?? + await TryCreatePersistentStorageAsync(solutionKey, workingFolderPath, faultInjector, cancellationToken).ConfigureAwait(false); if (result != null) return result; - return NoOpPersistentStorage.GetOrThrow(Configuration.ThrowOnFailure); + return NoOpPersistentStorage.GetOrThrow(solutionKey, Configuration.ThrowOnFailure); } private async ValueTask TryCreatePersistentStorageAsync( SolutionKey solutionKey, string workingFolderPath, + IPersistentStorageFaultInjector? faultInjector, CancellationToken cancellationToken) { var databaseFilePath = GetDatabaseFilePath(workingFolderPath); try { - return await TryOpenDatabaseAsync(solutionKey, workingFolderPath, databaseFilePath, cancellationToken).ConfigureAwait(false); + return await TryOpenDatabaseAsync(solutionKey, workingFolderPath, databaseFilePath, faultInjector, cancellationToken).ConfigureAwait(false); } catch (Exception e) when (Recover(e)) { @@ -131,133 +101,14 @@ bool Recover(Exception ex) StorageDatabaseLogger.LogException(ex); if (Configuration.ThrowOnFailure) - { return false; - } - if (ShouldDeleteDatabase(ex)) - { - // this was not a normal exception that we expected during DB open. - // Report this so we can try to address whatever is causing this. - FatalError.ReportAndCatch(ex); - IOUtilities.PerformIO(() => Directory.Delete(Path.GetDirectoryName(databaseFilePath)!, recursive: true)); - } + // this was not a normal exception that we expected during DB open. + // Report this so we can try to address whatever is causing this. + FatalError.ReportAndCatch(ex); + IOUtilities.PerformIO(() => Directory.Delete(Path.GetDirectoryName(databaseFilePath)!, recursive: true)); return true; } } - - private void Shutdown() - { - ReferenceCountedDisposable? storage = null; - - lock (_lock) - { - // We will transfer ownership in a thread-safe way out so we can dispose outside the lock - storage = _currentPersistentStorage; - _currentPersistentStorage = null; - _currentPersistentStorageSolutionId = null; - } - - // Dispose storage outside of the lock. Note this only removes our reference count; clients who are still - // using this will still be holding a reference count. - storage?.Dispose(); - } - - internal TestAccessor GetTestAccessor() - => new(this); - - internal readonly struct TestAccessor(AbstractPersistentStorageService service) - { - public void Shutdown() - => service.Shutdown(); - } - - /// - /// A trivial wrapper that we can hand out for instances from the - /// that wraps the underlying singleton. - /// - private sealed class PersistentStorageReferenceCountedDisposableWrapper : IChecksummedPersistentStorage - { - private readonly ReferenceCountedDisposable _storage; - - private PersistentStorageReferenceCountedDisposableWrapper(ReferenceCountedDisposable storage) - => _storage = storage; - - public static IChecksummedPersistentStorage AddReferenceCountToAndCreateWrapper(ReferenceCountedDisposable storage) - { - // This should only be called from a caller that has a non-null storage that it - // already has a reference on. So .TryAddReference cannot fail. - return new PersistentStorageReferenceCountedDisposableWrapper(storage.TryAddReference() ?? throw ExceptionUtilities.Unreachable()); - } - - public void Dispose() - => _storage.Dispose(); - - public ValueTask DisposeAsync() - => _storage.DisposeAsync(); - - public Task ChecksumMatchesAsync(string name, Checksum checksum, CancellationToken cancellationToken) - => _storage.Target.ChecksumMatchesAsync(name, checksum, cancellationToken); - - public Task ChecksumMatchesAsync(Project project, string name, Checksum checksum, CancellationToken cancellationToken) - => _storage.Target.ChecksumMatchesAsync(project, name, checksum, cancellationToken); - - public Task ChecksumMatchesAsync(Document document, string name, Checksum checksum, CancellationToken cancellationToken) - => _storage.Target.ChecksumMatchesAsync(document, name, checksum, cancellationToken); - - public Task ChecksumMatchesAsync(ProjectKey project, string name, Checksum checksum, CancellationToken cancellationToken) - => _storage.Target.ChecksumMatchesAsync(project, name, checksum, cancellationToken); - - public Task ChecksumMatchesAsync(DocumentKey document, string name, Checksum checksum, CancellationToken cancellationToken) - => _storage.Target.ChecksumMatchesAsync(document, name, checksum, cancellationToken); - - public Task ReadStreamAsync(string name, CancellationToken cancellationToken) - => _storage.Target.ReadStreamAsync(name, cancellationToken); - - public Task ReadStreamAsync(Project project, string name, CancellationToken cancellationToken) - => _storage.Target.ReadStreamAsync(project, name, cancellationToken); - - public Task ReadStreamAsync(Document document, string name, CancellationToken cancellationToken) - => _storage.Target.ReadStreamAsync(document, name, cancellationToken); - - public Task ReadStreamAsync(string name, Checksum? checksum, CancellationToken cancellationToken) - => _storage.Target.ReadStreamAsync(name, checksum, cancellationToken); - - public Task ReadStreamAsync(Project project, string name, Checksum? checksum, CancellationToken cancellationToken) - => _storage.Target.ReadStreamAsync(project, name, checksum, cancellationToken); - - public Task ReadStreamAsync(Document document, string name, Checksum? checksum, CancellationToken cancellationToken) - => _storage.Target.ReadStreamAsync(document, name, checksum, cancellationToken); - - public Task ReadStreamAsync(ProjectKey project, string name, Checksum? checksum, CancellationToken cancellationToken) - => _storage.Target.ReadStreamAsync(project, name, checksum, cancellationToken); - - public Task ReadStreamAsync(DocumentKey document, string name, Checksum? checksum, CancellationToken cancellationToken) - => _storage.Target.ReadStreamAsync(document, name, checksum, cancellationToken); - - public Task WriteStreamAsync(string name, Stream stream, CancellationToken cancellationToken) - => _storage.Target.WriteStreamAsync(name, stream, cancellationToken); - - public Task WriteStreamAsync(Project project, string name, Stream stream, CancellationToken cancellationToken) - => _storage.Target.WriteStreamAsync(project, name, stream, cancellationToken); - - public Task WriteStreamAsync(Document document, string name, Stream stream, CancellationToken cancellationToken) - => _storage.Target.WriteStreamAsync(document, name, stream, cancellationToken); - - public Task WriteStreamAsync(string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken) - => _storage.Target.WriteStreamAsync(name, stream, checksum, cancellationToken); - - public Task WriteStreamAsync(Project project, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken) - => _storage.Target.WriteStreamAsync(project, name, stream, checksum, cancellationToken); - - public Task WriteStreamAsync(Document document, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken) - => _storage.Target.WriteStreamAsync(document, name, stream, checksum, cancellationToken); - - public Task WriteStreamAsync(ProjectKey projectKey, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken) - => _storage.Target.WriteStreamAsync(projectKey, name, stream, checksum, cancellationToken); - - public Task WriteStreamAsync(DocumentKey documentKey, string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken) - => _storage.Target.WriteStreamAsync(documentKey, name, stream, checksum, cancellationToken); - } } diff --git a/src/Workspaces/Core/Portable/Storage/LegacyPersistentStorageService.cs b/src/Workspaces/Core/Portable/Storage/LegacyPersistentStorageService.cs index e9a3d231cd09f..4418282eb1cb0 100644 --- a/src/Workspaces/Core/Portable/Storage/LegacyPersistentStorageService.cs +++ b/src/Workspaces/Core/Portable/Storage/LegacyPersistentStorageService.cs @@ -36,5 +36,5 @@ public LegacyPersistentStorageService() } public IPersistentStorage GetStorage(Solution solution) - => NoOpPersistentStorage.GetOrThrow(throwOnFailure: false); + => NoOpPersistentStorage.GetOrThrow(SolutionKey.ToSolutionKey(solution), throwOnFailure: false); } diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/AbstractPersistentStorageService+SQLiteTestAccessor.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/AbstractPersistentStorageService+SQLiteTestAccessor.cs new file mode 100644 index 0000000000000..57f78ef598176 --- /dev/null +++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/AbstractPersistentStorageService+SQLiteTestAccessor.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis.SQLite.v2; + +namespace Microsoft.CodeAnalysis.Storage; + +internal partial class AbstractPersistentStorageService +{ + internal TestAccessor GetTestAccessor() + => new(this); + + internal readonly struct TestAccessor(AbstractPersistentStorageService service) + { + public void Shutdown() + { + (service._currentPersistentStorage as SQLitePersistentStorage)?.DatabaseOwnership.Dispose(); + service._currentPersistentStorage = null; + } + } +} diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/Interop/SqlConnection.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/Interop/SqlConnection.cs index 440b6409cd496..c710a86dd43d2 100644 --- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/Interop/SqlConnection.cs +++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/Interop/SqlConnection.cs @@ -21,16 +21,16 @@ namespace Microsoft.CodeAnalysis.SQLite.v2.Interop; /// Encapsulates a connection to a sqlite database. On construction an attempt will be made /// to open the DB if it exists, or create it if it does not. /// -/// Connections are considered relatively heavyweight and are pooled until the -/// is d. Connections can be used by different threads, -/// but only as long as they are used by one thread at a time. They are not safe for concurrent -/// use by several threads. +/// Connections are considered relatively heavyweight and are pooled (see ). Connections can be used by different +/// threads, but only as long as they are used by one thread at a time. They are not safe for concurrent use by several +/// threads. /// /// s can be created through the user of . /// These statements are cached for the lifetime of the connection and are only finalized /// (i.e. destroyed) when the connection is closed. /// -internal class SqlConnection +internal sealed class SqlConnection { // Cached UTF-8 (and null terminated) versions of the common strings we need to pass to sqlite. Used to prevent // having to convert these names to/from utf16 to UTF-8 on every call. Sqlite requires these be null terminated. diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLiteConnectionPool+PooledConnection.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLiteConnectionPool+PooledConnection.cs deleted file mode 100644 index 1b3750e7a9289..0000000000000 --- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLiteConnectionPool+PooledConnection.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using Microsoft.CodeAnalysis.SQLite.v2.Interop; - -namespace Microsoft.CodeAnalysis.SQLite.v2; - -internal partial class SQLiteConnectionPool -{ - internal readonly struct PooledConnection(SQLiteConnectionPool connectionPool, SqlConnection sqlConnection) : IDisposable - { - public readonly SqlConnection Connection = sqlConnection; - - public void Dispose() - => connectionPool.ReleaseConnection(Connection); - } -} diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLiteConnectionPoolService.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLiteConnectionPoolService.cs deleted file mode 100644 index bafe6a92c2e1f..0000000000000 --- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLiteConnectionPoolService.cs +++ /dev/null @@ -1,159 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Composition; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.ErrorReporting; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.Shared.Utilities; -using Microsoft.CodeAnalysis.SQLite.v2.Interop; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.SQLite.v2; - -[Export] -[Shared] -internal sealed class SQLiteConnectionPoolService : IDisposable -{ - private const string LockFile = "db.lock"; - - private readonly object _gate = new(); - - /// - /// Maps from database file path to connection pool. - /// - /// - /// Access to this field is synchronized through . - /// - private readonly Dictionary> _connectionPools = []; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public SQLiteConnectionPoolService() - { - } - - /// - /// Use a to simulate a reader-writer lock. - /// Read operations are performed on the - /// and writes are performed on the . - /// - /// We use this as a condition of using the in-memory shared-cache sqlite DB. This DB - /// doesn't busy-wait when attempts are made to lock the tables in it, which can lead to - /// deadlocks. Specifically, consider two threads doing the following: - /// - /// Thread A starts a transaction that starts as a reader, and later attempts to perform a - /// write. Thread B is a writer (either started that way, or started as a reader and - /// promoted to a writer first). B holds a RESERVED lock, waiting for readers to clear so it - /// can start writing. A holds a SHARED lock (it's a reader) and tries to acquire RESERVED - /// lock (so it can start writing). The only way to make progress in this situation is for - /// one of the transactions to roll back. No amount of waiting will help, so when SQLite - /// detects this situation, it doesn't honor the busy timeout. - /// - /// To prevent this scenario, we control our access to the db explicitly with operations that - /// can concurrently read, and operations that exclusively write. - /// - /// All code that reads or writes from the db should go through this. - /// - public ConcurrentExclusiveSchedulerPair Scheduler { get; } = new(); - - public void Dispose() - { - lock (_gate) - { - foreach (var (_, pool) in _connectionPools) - pool.Dispose(); - - _connectionPools.Clear(); - } - } - - public ReferenceCountedDisposable? TryOpenDatabase( - string databaseFilePath, - IPersistentStorageFaultInjector? faultInjector, - Action initializer, - CancellationToken cancellationToken) - { - lock (_gate) - { - if (_connectionPools.TryGetValue(databaseFilePath, out var pool)) - { - return pool.TryAddReference() ?? throw ExceptionUtilities.Unreachable(); - } - - // try to get db ownership lock. if someone else already has the lock. it will throw - var ownershipLock = TryGetDatabaseOwnership(databaseFilePath); - if (ownershipLock == null) - { - return null; - } - - try - { - pool = new ReferenceCountedDisposable( - new SQLiteConnectionPool(this, faultInjector, databaseFilePath, ownershipLock)); - - pool.Target.Initialize(initializer, cancellationToken); - - // Place the initial ownership reference in _connectionPools, and return another - _connectionPools.Add(databaseFilePath, pool); - return pool.TryAddReference() ?? throw ExceptionUtilities.Unreachable(); - } - catch (Exception ex) when (FatalError.ReportAndCatchUnlessCanceled(ex, cancellationToken)) - { - if (pool is not null) - { - // Dispose of the connection pool, releasing the ownership lock. - pool.Dispose(); - } - else - { - // The storage was not created so nothing owns the lock. - // Dispose the lock to allow reuse. - ownershipLock.Dispose(); - } - - throw; - } - } - } - - /// - /// Returns null in the case where an IO exception prevented us from being able to acquire - /// the db lock file. - /// - private static IDisposable? TryGetDatabaseOwnership(string databaseFilePath) - { - return IOUtilities.PerformIO(() => - { - // make sure directory exist first. - EnsureDirectory(databaseFilePath); - - var directoryName = Path.GetDirectoryName(databaseFilePath); - Contract.ThrowIfNull(directoryName); - - return File.Open( - Path.Combine(directoryName, LockFile), - FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); - }, defaultValue: null); - } - - private static void EnsureDirectory(string databaseFilePath) - { - var directory = Path.GetDirectoryName(databaseFilePath); - Contract.ThrowIfNull(directory); - - if (Directory.Exists(directory)) - { - return; - } - - Directory.CreateDirectory(directory); - } -} diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLiteConnectionPool.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage+PooledConnection.cs similarity index 55% rename from src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLiteConnectionPool.cs rename to src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage+PooledConnection.cs index f961393cb00e5..bc0cd558919b9 100644 --- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLiteConnectionPool.cs +++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage+PooledConnection.cs @@ -3,61 +3,20 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.SQLite.v2.Interop; namespace Microsoft.CodeAnalysis.SQLite.v2; -internal sealed partial class SQLiteConnectionPool(SQLiteConnectionPoolService connectionPoolService, IPersistentStorageFaultInjector? faultInjector, string databasePath, IDisposable ownershipLock) : IDisposable +internal partial class SQLitePersistentStorage { - // We pool connections to the DB so that we don't have to take the hit of - // reconnecting. The connections also cache the prepared statements used - // to get/set data from the db. A connection is safe to use by one thread - // at a time, but is not safe for simultaneous use by multiple threads. - private readonly object _connectionGate = new(); - private readonly Stack _connectionsPool = new(); - - private readonly CancellationTokenSource _shutdownTokenSource = new(); - - internal void Initialize( - Action initializer, - CancellationToken cancellationToken) - { - // This is our startup path. No other code can be running. So it's safe for us to access a connection that - // can talk to the db without having to be on the reader/writer scheduler queue. - using var _ = GetPooledConnection(checkScheduler: false, out var connection); - - initializer(connection, cancellationToken); - } - - public void Dispose() + private readonly struct PooledConnection(SQLitePersistentStorage storage, SqlConnection sqlConnection) : IDisposable { - // Flush all pending writes so that all data our features wanted written - // are definitely persisted to the DB. - try - { - _shutdownTokenSource.Cancel(); - CloseWorker(); - } - finally - { - // let the lock go - ownershipLock.Dispose(); - } - } + public readonly SqlConnection Connection = sqlConnection; - private void CloseWorker() - { - lock (_connectionGate) - { - // Go through all our pooled connections and close them. - while (_connectionsPool.TryPop(out var connection)) - connection.Close_OnlyForUseBySQLiteConnectionPool(); - } + public void Dispose() + => storage.ReleaseConnection(Connection); } /// @@ -68,7 +27,7 @@ private void CloseWorker() /// longer in use. In particular, make sure to avoid letting a connection lease cross an /// boundary, as it will prevent code in the asynchronous operation from using the existing connection. /// - internal PooledConnection GetPooledConnection(out SqlConnection connection) + private PooledConnection GetPooledConnection(out SqlConnection connection) => GetPooledConnection(checkScheduler: true, out connection); /// @@ -81,8 +40,8 @@ private PooledConnection GetPooledConnection(bool checkScheduler, out SqlConnect if (checkScheduler) { var scheduler = TaskScheduler.Current; - if (scheduler != connectionPoolService.Scheduler.ConcurrentScheduler && scheduler != connectionPoolService.Scheduler.ExclusiveScheduler) - throw new InvalidOperationException($"Cannot get a connection to the DB unless running on one of {nameof(SQLiteConnectionPoolService)}'s schedulers"); + if (scheduler != this.Scheduler.ConcurrentScheduler && scheduler != this.Scheduler.ExclusiveScheduler) + throw new InvalidOperationException($"Cannot get a connection to the DB unless running on one of {nameof(SQLitePersistentStorage)}'s schedulers"); } var result = new PooledConnection(this, GetConnection()); @@ -100,7 +59,7 @@ private SqlConnection GetConnection() } // Otherwise create a new connection. - return SqlConnection.Create(faultInjector, databasePath); + return SqlConnection.Create(_faultInjector, this.DatabaseFile); } private void ReleaseConnection(SqlConnection connection) diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage.Accessor.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage.Accessor.cs index 4ea118171b220..0765d555aa450 100644 --- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage.Accessor.cs +++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage.Accessor.cs @@ -163,13 +163,13 @@ private Optional ReadColumn( // We're reading. All current scenarios have this happening under the concurrent/read-only scheduler. // If this assert fires either a bug has been introduced, or there is a valid scenario for a writing // codepath to read a column and this assert should be adjusted. - Contract.ThrowIfFalse(TaskScheduler.Current == Storage._connectionPoolService.Scheduler.ConcurrentScheduler); + Contract.ThrowIfFalse(TaskScheduler.Current == this.Storage.Scheduler.ConcurrentScheduler); cancellationToken.ThrowIfCancellationRequested(); if (!Storage._shutdownTokenSource.IsCancellationRequested) { - using var _ = Storage._connectionPool.Target.GetPooledConnection(out var connection); + using var _ = this.Storage.GetPooledConnection(out var connection); // We're in the reading-only scheduler path, so we can't allow TryGetDatabaseId to write. Note that // this is ok, and actually provides the semantics we want. Specifically, we can be trying to read @@ -222,13 +222,13 @@ public Task WriteStreamAsync(TKey key, string name, Stream stream, Checksu private bool WriteStream(TKey key, string dataName, Stream stream, Checksum? checksum, CancellationToken cancellationToken) { // We're writing. This better always be under the exclusive scheduler. - Contract.ThrowIfFalse(TaskScheduler.Current == Storage._connectionPoolService.Scheduler.ExclusiveScheduler); + Contract.ThrowIfFalse(TaskScheduler.Current == this.Storage.Scheduler.ExclusiveScheduler); cancellationToken.ThrowIfCancellationRequested(); if (!Storage._shutdownTokenSource.IsCancellationRequested) { - using var _ = Storage._connectionPool.Target.GetPooledConnection(out var connection); + using var _ = this.Storage.GetPooledConnection(out var connection); // Determine the appropriate data-id to store this stream at. We already are running // with an exclusive write lock on the DB, so it's safe for us to write the data id to @@ -361,7 +361,7 @@ private void InsertOrReplaceBlobIntoWriteCache( ReadOnlySpan dataBytes) { // We're writing. This better always be under the exclusive scheduler. - Contract.ThrowIfFalse(TaskScheduler.Current == Storage._connectionPoolService.Scheduler.ExclusiveScheduler); + Contract.ThrowIfFalse(TaskScheduler.Current == this.Storage.Scheduler.ExclusiveScheduler); using (var resettableStatement = connection.GetResettableStatement( _insert_or_replace_into_writecache_table_values_0primarykey_1checksum_2data)) diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage.cs index 5d31591fd3d4a..9ea33b38eff0f 100644 --- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage.cs +++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage.cs @@ -4,11 +4,13 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Shared.TestHooks; +using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.SQLite.Interop; using Microsoft.CodeAnalysis.SQLite.v2.Interop; using Microsoft.CodeAnalysis.Storage; @@ -23,15 +25,30 @@ namespace Microsoft.CodeAnalysis.SQLite.v2; /// internal sealed partial class SQLitePersistentStorage : AbstractPersistentStorage { + private const string LockFile = "db.lock"; + private readonly CancellationTokenSource _shutdownTokenSource = new(); - private readonly SolutionKey _solutionKey; private readonly string _solutionDirectory; - private readonly SQLiteConnectionPoolService _connectionPoolService; - private readonly ReferenceCountedDisposable _connectionPool; + // We pool connections to the DB so that we don't have to take the hit of + // reconnecting. The connections also cache the prepared statements used + // to get/set data from the db. A connection is safe to use by one thread + // at a time, but is not safe for simultaneous use by multiple threads. + private readonly object _connectionGate = new(); + private readonly Stack _connectionsPool = new(); private readonly Action _flushInMemoryDataToDisk; + /// + /// Lock file that ensures only one database is made per process per solution. + /// + public readonly IDisposable DatabaseOwnership; + + /// + /// For testing purposes. Allows us to test what happens when we fail to acquire the db lock file. + /// + private readonly IPersistentStorageFaultInjector? _faultInjector; + // Accessors that allow us to retrieve/store data into specific DB tables. The // core Accessor type has logic that we to share across all reading/writing, while // the derived types contain only enough logic to specify how to read/write from @@ -47,31 +64,48 @@ internal sealed partial class SQLitePersistentStorage : AbstractPersistentStorag private readonly string _select_star_from_string_table_where_0_limit_one = $"select * from {StringInfoTableName} where ({DataColumnName} = ?) limit 1"; private readonly string _select_star_from_string_table = $"select * from {StringInfoTableName}"; + /// + /// Use a to simulate a reader-writer lock. + /// Read operations are performed on the + /// and writes are performed on the . + /// + /// We use this as a condition of using the in-memory shared-cache sqlite DB. This DB + /// doesn't busy-wait when attempts are made to lock the tables in it, which can lead to + /// deadlocks. Specifically, consider two threads doing the following: + /// + /// Thread A starts a transaction that starts as a reader, and later attempts to perform a + /// write. Thread B is a writer (either started that way, or started as a reader and + /// promoted to a writer first). B holds a RESERVED lock, waiting for readers to clear so it + /// can start writing. A holds a SHARED lock (it's a reader) and tries to acquire RESERVED + /// lock (so it can start writing). The only way to make progress in this situation is for + /// one of the transactions to roll back. No amount of waiting will help, so when SQLite + /// detects this situation, it doesn't honor the busy timeout. + /// + /// To prevent this scenario, we control our access to the db explicitly with operations that + /// can concurrently read, and operations that exclusively write. + /// + /// All code that reads or writes from the db should go through this. + /// + private ConcurrentExclusiveSchedulerPair Scheduler { get; } = new(); + private SQLitePersistentStorage( - SQLiteConnectionPoolService connectionPoolService, SolutionKey solutionKey, string workingFolderPath, string databaseFile, IAsynchronousOperationListener asyncListener, - IPersistentStorageFaultInjector? faultInjector) - : base(workingFolderPath, solutionKey.FilePath!, databaseFile) + IPersistentStorageFaultInjector? faultInjector, + IDisposable databaseOwnership) + : base(solutionKey, workingFolderPath, databaseFile) { Contract.ThrowIfNull(solutionKey.FilePath); - _solutionKey = solutionKey; _solutionDirectory = PathUtilities.GetDirectoryName(solutionKey.FilePath); - _connectionPoolService = connectionPoolService; _solutionAccessor = new SolutionAccessor(this); _projectAccessor = new ProjectAccessor(this); _documentAccessor = new DocumentAccessor(this); - // This assignment violates the declared non-nullability of _connectionPool, but the caller ensures that - // the constructed object is only used if the nullability post-conditions are met. - _connectionPool = connectionPoolService.TryOpenDatabase( - databaseFile, - faultInjector, - Initialize, - CancellationToken.None)!; + _faultInjector = faultInjector; + DatabaseOwnership = databaseOwnership; // Create a delay to batch up requests to flush. We'll won't flush more than every FlushAllDelayMS. _flushInMemoryDataToDisk = FlushInMemoryDataToDisk; @@ -83,49 +117,52 @@ private SQLitePersistentStorage( } public static SQLitePersistentStorage? TryCreate( - SQLiteConnectionPoolService connectionPoolService, SolutionKey solutionKey, string workingFolderPath, string databaseFile, IAsynchronousOperationListener asyncListener, IPersistentStorageFaultInjector? faultInjector) { - var sqlStorage = new SQLitePersistentStorage( - connectionPoolService, solutionKey, workingFolderPath, databaseFile, asyncListener, faultInjector); - if (sqlStorage._connectionPool is null) - { - // The connection pool failed to initialize + var databaseOwnership = TryGetDatabaseOwnership(databaseFile); + if (databaseOwnership is null) return null; - } - return sqlStorage; + var storage = new SQLitePersistentStorage( + solutionKey, workingFolderPath, databaseFile, asyncListener, faultInjector, databaseOwnership); + storage.Initialize(); + return storage; } - public override void Dispose() + /// + /// Returns null in the case where an IO exception prevented us from being able to acquire + /// the db lock file. + /// + private static IDisposable? TryGetDatabaseOwnership(string databaseFilePath) { - var task = DisposeAsync().AsTask(); - task.Wait(); - } + return IOUtilities.PerformIO(() => + { + // make sure directory exist first. + EnsureDirectory(databaseFilePath); - public override async ValueTask DisposeAsync() - { - try + var directoryName = Path.GetDirectoryName(databaseFilePath); + Contract.ThrowIfNull(directoryName); + + return File.Open( + Path.Combine(directoryName, LockFile), + FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + }, defaultValue: null); + + static void EnsureDirectory(string databaseFilePath) { - // Flush all pending writes so that all data our features wanted written are definitely - // persisted to the DB. - try - { - await FlushWritesOnCloseAsync().ConfigureAwait(false); - } - catch (Exception e) + var directory = Path.GetDirectoryName(databaseFilePath); + Contract.ThrowIfNull(directory); + + if (Directory.Exists(directory)) { - // Flushing may fail. We still have to close all our connections. - StorageDatabaseLogger.LogException(e); + return; } - } - finally - { - _connectionPool.Dispose(); + + Directory.CreateDirectory(directory); } } @@ -142,17 +179,11 @@ public static KeyValueLogMessage GetLogMessage(SqlException exception) d["Message"] = exception.Message; }); - private void Initialize(SqlConnection connection, CancellationToken cancellationToken) + private void Initialize() { - if (cancellationToken.IsCancellationRequested) - { - // Someone tried to get a connection *after* a call to Dispose the storage system - // happened. That should never happen. We only Dispose when the last ref to the - // storage system goes away. Once that happens, it's an error for there to be any - // future or existing consumers of the storage service. So nothing should be doing - // anything that wants to get an connection. - throw new InvalidOperationException(); - } + // This is our startup path. No other code can be running. So it's safe for us to access a connection that can + // talk to the db without having to be on the reader/writer scheduler queue. + using var _ = GetPooledConnection(checkScheduler: false, out var connection); // Ensure the database has tables for the types we care about. diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageService.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageService.cs index fe7686516eb3d..9f1570f00f3b6 100644 --- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageService.cs +++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorageService.cs @@ -17,7 +17,6 @@ namespace Microsoft.CodeAnalysis.SQLite.v2; internal sealed class SQLitePersistentStorageService( - SQLiteConnectionPoolService connectionPoolService, IPersistentStorageConfiguration configuration, IAsynchronousOperationListener asyncListener) : AbstractPersistentStorageService(configuration), IWorkspaceService { @@ -25,13 +24,12 @@ internal sealed class SQLitePersistentStorageService( [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed class ServiceFactory( - SQLiteConnectionPoolService connectionPoolService, IAsynchronousOperationListenerProvider asyncOperationListenerProvider) : IWorkspaceServiceFactory { private readonly IAsynchronousOperationListener _asyncListener = asyncOperationListenerProvider.GetListener(FeatureAttribute.PersistentStorage); public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => new SQLitePersistentStorageService(connectionPoolService, workspaceServices.GetRequiredService(), _asyncListener); + => new SQLitePersistentStorageService(workspaceServices.GetRequiredService(), _asyncListener); } private const string StorageExtension = "sqlite3"; @@ -61,18 +59,6 @@ private static bool TryInitializeLibrariesLazy() return true; } - private readonly IPersistentStorageFaultInjector? _faultInjector; - - public SQLitePersistentStorageService( - SQLiteConnectionPoolService connectionPoolService, - IPersistentStorageConfiguration configuration, - IAsynchronousOperationListener asyncListener, - IPersistentStorageFaultInjector? faultInjector) - : this(connectionPoolService, configuration, asyncListener) - { - _faultInjector = faultInjector; - } - protected override string GetDatabaseFilePath(string workingFolderPath) { Contract.ThrowIfTrue(string.IsNullOrWhiteSpace(workingFolderPath)); @@ -80,7 +66,7 @@ protected override string GetDatabaseFilePath(string workingFolderPath) } protected override ValueTask TryOpenDatabaseAsync( - SolutionKey solutionKey, string workingFolderPath, string databaseFilePath, CancellationToken cancellationToken) + SolutionKey solutionKey, string workingFolderPath, string databaseFilePath, IPersistentStorageFaultInjector? faultInjector, CancellationToken cancellationToken) { if (!TryInitializeLibraries()) { @@ -89,17 +75,13 @@ protected override string GetDatabaseFilePath(string workingFolderPath) } if (solutionKey.FilePath == null) - return new(NoOpPersistentStorage.GetOrThrow(Configuration.ThrowOnFailure)); + return new(NoOpPersistentStorage.GetOrThrow(solutionKey, Configuration.ThrowOnFailure)); return new(SQLitePersistentStorage.TryCreate( - connectionPoolService, solutionKey, workingFolderPath, databaseFilePath, asyncListener, - _faultInjector)); + faultInjector)); } - - // Error occurred when trying to open this DB. Try to remove it so we can create a good DB. - protected override bool ShouldDeleteDatabase(Exception exception) => true; } diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_FlushWrites.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_FlushWrites.cs index 1988badfbe5db..e0d6ca9943104 100644 --- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_FlushWrites.cs +++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_FlushWrites.cs @@ -29,47 +29,17 @@ private async ValueTask FlushInMemoryDataToDiskIfNotShutdownAsync(CancellationTo await PerformWriteAsync(_flushInMemoryDataToDisk, cancellationToken).ConfigureAwait(false); } - private Task FlushWritesOnCloseAsync() - { - // Issue a write task to write this all out to disk. - // - // Note: this only happens on close, so we don't try to avoid allocations here. - - return PerformWriteAsync( - () => - { - // Perform the actual write while having exclusive access to the scheduler. - FlushInMemoryDataToDisk(); - - // Now that we've done this, definitely cancel any further work. From this point on, it is now - // invalid for any codepaths to try to acquire a db connection for any purpose (beyond us - // disposing things below). - // - // This will also ensure that if we have a bg flush task still pending, when it wakes up it will - // see that we're shutdown and not proceed (and importantly won't acquire a connection). Because - // both the bg task and us run serialized, there is no way for it to miss this token - // cancellation. If it runs after us, then it sees this. If it runs before us, then we just - // block until it finishes. - // - // We don't have to worry about reads/writes getting connections either. - // The only way we can get disposed in the first place is if every user of this storage instance - // has released their ref on us. In that case, it would be an error on their part to ever try to - // read/write after releasing us. - _shutdownTokenSource.Cancel(); - }, CancellationToken.None); - } - private void FlushInMemoryDataToDisk() { // We're writing. This better always be under the exclusive scheduler. - Contract.ThrowIfFalse(TaskScheduler.Current == _connectionPoolService.Scheduler.ExclusiveScheduler); + Contract.ThrowIfFalse(TaskScheduler.Current == this.Scheduler.ExclusiveScheduler); // Don't flush from a bg task if we've been asked to shutdown. The shutdown logic in the storage service // will take care of the final writes to the main db. if (_shutdownTokenSource.IsCancellationRequested) return; - using var _ = _connectionPool.Target.GetPooledConnection(out var connection); + using var _ = this.GetPooledConnection(out var connection); var exception = connection.RunInTransaction(static state => { diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_SolutionSerialization.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_SolutionSerialization.cs index e17c3f8591134..5e4ef383324fb 100644 --- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_SolutionSerialization.cs +++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_SolutionSerialization.cs @@ -16,13 +16,13 @@ namespace Microsoft.CodeAnalysis.SQLite.v2; internal partial class SQLitePersistentStorage { public override Task ChecksumMatchesAsync(string name, Checksum checksum, CancellationToken cancellationToken) - => _solutionAccessor.ChecksumMatchesAsync(_solutionKey, name, checksum, cancellationToken); + => _solutionAccessor.ChecksumMatchesAsync(this.SolutionKey, name, checksum, cancellationToken); public override Task ReadStreamAsync(string name, Checksum? checksum, CancellationToken cancellationToken) - => _solutionAccessor.ReadStreamAsync(_solutionKey, name, checksum, cancellationToken); + => _solutionAccessor.ReadStreamAsync(this.SolutionKey, name, checksum, cancellationToken); public override Task WriteStreamAsync(string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken) - => _solutionAccessor.WriteStreamAsync(_solutionKey, name, stream, checksum, cancellationToken); + => _solutionAccessor.WriteStreamAsync(this.SolutionKey, name, stream, checksum, cancellationToken); private readonly record struct SolutionPrimaryKey(); diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_StringIds.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_StringIds.cs index be276efa9a39a..c9c4e8c126b2c 100644 --- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_StringIds.cs +++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_StringIds.cs @@ -47,8 +47,8 @@ internal partial class SQLitePersistentStorage { // We're reading or writing. This can be under either of our schedulers. Contract.ThrowIfFalse( - TaskScheduler.Current == _connectionPoolService.Scheduler.ExclusiveScheduler || - TaskScheduler.Current == _connectionPoolService.Scheduler.ConcurrentScheduler); + TaskScheduler.Current == this.Scheduler.ExclusiveScheduler || + TaskScheduler.Current == this.Scheduler.ConcurrentScheduler); // First, check if we can find that string in the string table. var stringId = TryGetStringIdFromDatabaseWorker(connection, value, canReturnNull: true); @@ -65,7 +65,7 @@ internal partial class SQLitePersistentStorage return null; // We're writing. This better always be under the exclusive scheduler. - Contract.ThrowIfFalse(TaskScheduler.Current == _connectionPoolService.Scheduler.ExclusiveScheduler); + Contract.ThrowIfFalse(TaskScheduler.Current == this.Scheduler.ExclusiveScheduler); // The string wasn't in the db string table. Add it. Note: this may fail if some // other thread/process beats us there as this table has a 'unique' constraint on the diff --git a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_Threading.cs b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_Threading.cs index 72edf6a941550..0ff47ae62e10f 100644 --- a/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_Threading.cs +++ b/src/Workspaces/Core/Portable/Storage/SQLite/v2/SQLitePersistentStorage_Threading.cs @@ -38,7 +38,7 @@ private Task PerformReadAsync(Func func, // Dispose method that restores ExecutionContext flow must run on the same thread where SuppressFlow was // originally run. using var _ = FlowControlHelper.TrySuppressFlow(); - return PerformTaskAsync(func, arg, _connectionPoolService.Scheduler.ConcurrentScheduler, cancellationToken); + return PerformTaskAsync(func, arg, this.Scheduler.ConcurrentScheduler, cancellationToken); } // Write tasks go to the exclusive-scheduler so they run exclusively of all other threading @@ -53,7 +53,7 @@ public Task PerformWriteAsync(Func func, // Dispose method that restores ExecutionContext flow must run on the same thread where SuppressFlow was // originally run. using var _ = FlowControlHelper.TrySuppressFlow(); - return PerformTaskAsync(func, arg, _connectionPoolService.Scheduler.ExclusiveScheduler, cancellationToken); + return PerformTaskAsync(func, arg, this.Scheduler.ExclusiveScheduler, cancellationToken); } public Task PerformWriteAsync(Action action, CancellationToken cancellationToken) diff --git a/src/Workspaces/Core/Portable/Telemetry/TelemetryLogging.cs b/src/Workspaces/Core/Portable/Telemetry/TelemetryLogging.cs index b26b770f7dcf0..23aded8e6276a 100644 --- a/src/Workspaces/Core/Portable/Telemetry/TelemetryLogging.cs +++ b/src/Workspaces/Core/Portable/Telemetry/TelemetryLogging.cs @@ -3,26 +3,44 @@ // See the LICENSE file in the project root for more information. using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.CodeAnalysis.Internal.Log; +using Microsoft.CodeAnalysis.Shared.TestHooks; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Telemetry; /// /// Provides access to posting telemetry events or adding information -/// to aggregated telemetry events. +/// to aggregated telemetry events. Posts pending telemetry at 30 +/// minute intervals. /// internal static class TelemetryLogging { private static ITelemetryLogProvider? s_logProvider; + private static AsyncBatchingWorkQueue? s_postTelemetryQueue; public const string KeyName = "Name"; public const string KeyValue = "Value"; public const string KeyLanguageName = "LanguageName"; public const string KeyMetricName = "MetricName"; - public static void SetLogProvider(ITelemetryLogProvider logProvider) + public static event EventHandler? Flushed; + + public static void SetLogProvider(ITelemetryLogProvider logProvider, IAsynchronousOperationListener asyncListener) { s_logProvider = logProvider; + + InterlockedOperations.Initialize(ref s_postTelemetryQueue, () => + new AsyncBatchingWorkQueue( + TimeSpan.FromMinutes(30), + PostCollectedTelemetryAsync, + asyncListener, + CancellationToken.None)); + + // Add the initial item to the queue to ensure later processing. + s_postTelemetryQueue?.AddWork(); } /// @@ -112,5 +130,17 @@ public static void LogAggregated(FunctionId functionId, KeyValueLogMessage logMe public static void Flush() { s_logProvider?.Flush(); + + Flushed?.Invoke(null, EventArgs.Empty); + } + + private static ValueTask PostCollectedTelemetryAsync(CancellationToken cancellationToken) + { + Flush(); + + // Ensure PostCollectedTelemetryAsync will get fired again after the collection period. + s_postTelemetryQueue?.AddWork(); + + return ValueTaskFactory.CompletedTask; } } diff --git a/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.DirectMemoryAccessStreamReader.cs b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.DirectMemoryAccessStreamReader.cs new file mode 100644 index 0000000000000..9918414654ea1 --- /dev/null +++ b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.DirectMemoryAccessStreamReader.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Runtime.InteropServices; +using Microsoft.CodeAnalysis.Shared.Utilities; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Host; + +internal sealed partial class TemporaryStorageService +{ + private unsafe class DirectMemoryAccessStreamReader : TextReaderWithLength + { + private char* _position; + private readonly char* _end; + + public DirectMemoryAccessStreamReader(char* src, int length) + : base(length) + { + RoslynDebug.Assert(src != null); + RoslynDebug.Assert(length >= 0); + + _position = src; + _end = _position + length; + } + + public override int Peek() + { + if (_position >= _end) + { + return -1; + } + + return *_position; + } + + public override int Read() + { + if (_position >= _end) + { + return -1; + } + + return *_position++; + } + + public override int Read(char[] buffer, int index, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (index < 0 || index >= buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (count < 0 || (index + count) > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + count = Math.Min(count, (int)(_end - _position)); + if (count > 0) + { + Marshal.Copy((IntPtr)_position, buffer, index, count); + _position += count; + } + + return count; + } + } +} diff --git a/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.Factory.cs b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.Factory.cs index 0d08d75a58210..e58f2b4935d22 100644 --- a/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.Factory.cs +++ b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.Factory.cs @@ -22,13 +22,7 @@ internal partial class Factory( public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) { var textFactory = workspaceServices.GetRequiredService(); - - // MemoryMapped files which are used by the TemporaryStorageService are present in .NET Framework (including Mono) - // and .NET Core Windows. For non-Windows .NET Core scenarios, we can return the TrivialTemporaryStorageService - // until https://github.com/dotnet/runtime/issues/30878 is fixed. - return PlatformInformation.IsWindows || PlatformInformation.IsRunningOnMono - ? new TemporaryStorageService(workspaceThreadingService, textFactory) - : TrivialTemporaryStorageService.Instance; + return new TemporaryStorageService(workspaceThreadingService, textFactory); } } } diff --git a/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.MemoryMappedInfo.cs b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.MemoryMappedInfo.cs index 0bd1fd88552e7..0e0c87e785c27 100644 --- a/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.MemoryMappedInfo.cs +++ b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.MemoryMappedInfo.cs @@ -7,7 +7,6 @@ using System.IO; using System.IO.MemoryMappedFiles; using System.Runtime; -using System.Runtime.InteropServices; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Host; @@ -22,54 +21,38 @@ internal partial class TemporaryStorageService /// metadata dll shadow copy. shared view will help those cases. /// /// - /// Instances of this class should be disposed when they are no longer needed. After disposing this - /// instance, it should no longer be used. However, streams obtained through - /// or will not be invalidated until they are disposed independently (which - /// may occur before or after the is disposed. - /// - /// This class and its nested types have familiar APIs and predictable behavior when used in other code, - /// but are non-trivial to work on. The implementations of adhere to the best - /// practices described in - /// DG - /// Update: Dispose, Finalization, and Resource Management. Additional notes regarding operating system - /// behavior leveraged for efficiency are given in comments. + /// This class and its nested types have familiar APIs and predictable behavior when used in other code, but + /// are non-trivial to work on. /// - internal sealed class MemoryMappedInfo(ReferenceCountedDisposable memoryMappedFile, string name, long offset, long size) : IDisposable + internal sealed class MemoryMappedInfo(MemoryMappedFile memoryMappedFile, string? name, long offset, long size) { /// /// The memory mapped file. /// - /// - /// It is possible for the file to be disposed prior to the view and/or the streams which use it. - /// However, the operating system does not actually close the views which are in use until the file handles - /// are closed as well, even if the file is disposed first. - /// - private readonly ReferenceCountedDisposable _memoryMappedFile = memoryMappedFile; + public readonly MemoryMappedFile MemoryMappedFile = memoryMappedFile; /// /// A weak reference to a read-only view for the memory mapped file. /// /// - /// This holds a weak counted reference to current , which - /// allows additional accessors for the same address space to be obtained up until the point when no - /// external code is using it. When the memory is no longer being used by any objects, the view of the memory mapped file is unmapped, - /// making the process address space it previously claimed available for other purposes. If/when it is - /// needed again, a new view is created. + /// This holds a weak counted reference to current , which allows + /// additional accessors for the same address space to be obtained up until the point when no external code is + /// using it. When the memory is no longer being used by any + /// objects, the view of the memory mapped file is unmapped, making the process address space it previously + /// claimed available for other purposes. If/when it is needed again, a new view is created. /// /// This view is read-only, so it is only used by . /// private ReferenceCountedDisposable.WeakReference _weakReadAccessor; - public MemoryMappedInfo(string name, long offset, long size) - : this(new ReferenceCountedDisposable(MemoryMappedFile.OpenExisting(name)), name, offset, size) - { - } + public static MemoryMappedInfo CreateNew(string? name, long size) + => new(MemoryMappedFile.CreateNew(name, size), name, offset: 0, size); /// - /// The name of the memory mapped file. + /// The name of the memory mapped file. Non null on systems that support named memory mapped files, null + /// otherwise.. /// - public string Name { get; } = name; + public string? Name { get; } = name; /// /// The offset into the memory mapped file of the region described by the current @@ -89,21 +72,14 @@ public MemoryMappedInfo(string name, long offset, long size) /// public UnmanagedMemoryStream CreateReadableStream() { - // Note: TryAddReference behaves according to its documentation even if the target object has been - // disposed. If it returns non-null, then the object will not be disposed before the returned - // reference is disposed (see comments on _memoryMappedFile and TryAddReference). + // Note: TryAddReference behaves according to its documentation even if the target object has been disposed. + // If it returns non-null, then the object will not be disposed before the returned reference is disposed + // (see comments on _memoryMappedFile and TryAddReference). var streamAccessor = _weakReadAccessor.TryAddReference(); if (streamAccessor == null) { var rawAccessor = RunWithCompactingGCFallback( - static info => - { - using var memoryMappedFile = info._memoryMappedFile.TryAddReference(); - if (memoryMappedFile is null) - throw new ObjectDisposedException(typeof(MemoryMappedInfo).FullName); - - return memoryMappedFile.Target.CreateViewAccessor(info.Offset, info.Size, MemoryMappedFileAccess.Read); - }, + static info => info.MemoryMappedFile.CreateViewAccessor(info.Offset, info.Size, MemoryMappedFileAccess.Read), this); streamAccessor = new ReferenceCountedDisposable(rawAccessor); _weakReadAccessor = new ReferenceCountedDisposable.WeakReference(streamAccessor); @@ -120,28 +96,20 @@ public UnmanagedMemoryStream CreateReadableStream() public Stream CreateWritableStream() { return RunWithCompactingGCFallback( - static info => - { - using var memoryMappedFile = info._memoryMappedFile.TryAddReference(); - if (memoryMappedFile is null) - throw new ObjectDisposedException(typeof(MemoryMappedInfo).FullName); - - return memoryMappedFile.Target.CreateViewStream(info.Offset, info.Size, MemoryMappedFileAccess.Write); - }, + static info => info.MemoryMappedFile.CreateViewStream(info.Offset, info.Size, MemoryMappedFileAccess.Write), this); } /// - /// Run a function which may fail with an if not enough memory is available to - /// satisfy the request. In this case, a full compacting GC pass is forced and the function is attempted - /// again. + /// Run a function which may fail with an if not enough memory is available to satisfy + /// the request. In this case, a full compacting GC pass is forced and the function is attempted again. /// /// - /// and - /// will use a native - /// memory map, which can't trigger a GC. In this case, we'd otherwise crash with OOM, so we don't care - /// about creating a UI delay with a full forced compacting GC. If it crashes the second try, it means we're - /// legitimately out of resources. + /// and will use a native memory map, + /// which can't trigger a GC. In this case, we'd otherwise crash with OOM, so we don't care about creating a UI + /// delay with a full forced compacting GC. If it crashes the second try, it means we're legitimately out of + /// resources. /// /// The type of argument to pass to the callback. /// The type returned by the function. @@ -171,42 +139,18 @@ private static void ForceCompactingGC() GC.Collect(); } - public void Dispose() - { - // See remarks on field for relation between _memoryMappedFile and the views/streams. There is no - // need to write _weakReadAccessor here since lifetime of the target is not owned by this instance. - _memoryMappedFile.Dispose(); - } - - private sealed unsafe class MemoryMappedViewUnmanagedMemoryStream : UnmanagedMemoryStream + private sealed unsafe class MemoryMappedViewUnmanagedMemoryStream( + ReferenceCountedDisposable accessor, + long length) : UnmanagedMemoryStream( + (byte*)accessor.Target.SafeMemoryMappedViewHandle.DangerousGetHandle() + accessor.Target.PointerOffset, + length) { - private readonly ReferenceCountedDisposable _accessor; - private byte* _start; - - public MemoryMappedViewUnmanagedMemoryStream(ReferenceCountedDisposable accessor, long length) - : base((byte*)accessor.Target.SafeMemoryMappedViewHandle.DangerousGetHandle() + accessor.Target.PointerOffset, length) - { - _accessor = accessor; - _start = this.PositionPointer; - } - protected override void Dispose(bool disposing) { base.Dispose(disposing); - if (disposing) - { - _accessor.Dispose(); - } - - _start = null; + accessor.Dispose(); } - - /// - /// Get underlying native memory directly. - /// - public IntPtr GetPointer() - => (IntPtr)_start; } } } diff --git a/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.TemporaryStorageStreamHandle.cs b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.TemporaryStorageStreamHandle.cs new file mode 100644 index 0000000000000..52d97d1bd762c --- /dev/null +++ b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.TemporaryStorageStreamHandle.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using System.IO.MemoryMappedFiles; +using System.Threading; +using Microsoft.CodeAnalysis.Internal.Log; + +namespace Microsoft.CodeAnalysis.Host; + +internal sealed partial class TemporaryStorageService +{ + public sealed class TemporaryStorageStreamHandle( + MemoryMappedFile memoryMappedFile, + TemporaryStorageIdentifier identifier) + : ITemporaryStorageStreamHandle + { + public TemporaryStorageIdentifier Identifier => identifier; + + public UnmanagedMemoryStream ReadFromTemporaryStorage(CancellationToken cancellationToken) + { + using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_ReadStream, cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + var info = new MemoryMappedInfo(memoryMappedFile, Identifier.Name, Identifier.Offset, Identifier.Size); + return info.CreateReadableStream(); + } + } + } +} diff --git a/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.cs b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.cs new file mode 100644 index 0000000000000..56a205c39481b --- /dev/null +++ b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.cs @@ -0,0 +1,302 @@ +// 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.IO; +using System.IO.MemoryMappedFiles; +using System.Runtime.Versioning; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Internal.Log; +using Microsoft.CodeAnalysis.Shared.Utilities; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Host; + +/// +/// Temporarily stores text and streams in memory mapped files. +/// +#if NETCOREAPP +[SupportedOSPlatform("windows")] +#endif +internal sealed partial class TemporaryStorageService : ITemporaryStorageServiceInternal +{ + /// + /// The maximum size in bytes of a single storage unit in a memory mapped file which is shared with other storage + /// units. + /// + /// + /// The value of 256k reduced the number of files dumped to separate memory mapped files by 60% compared to + /// the next lower power-of-2 size for Roslyn.sln itself. + /// + /// + private const long SingleFileThreshold = 256 * 1024; + + /// + /// The size in bytes of a memory mapped file created to store multiple temporary objects. + /// + /// + /// This value (8mb) creates roughly 35 memory mapped files (around 300MB) to store the contents of all of + /// Roslyn.sln a snapshot. This keeps the data safe, so that we can drop it from memory when not needed, but + /// reconstitute the contents we originally had in the snapshot in case the original files change on disk. + /// + /// + private const long MultiFileBlockSize = SingleFileThreshold * 32; + + private readonly IWorkspaceThreadingService? _workspaceThreadingService; + private readonly ITextFactoryService _textFactory; + + /// + /// The synchronization object for accessing the memory mapped file related fields (indicated in the remarks + /// of each field). + /// + /// + /// PERF DEV NOTE: A concurrent (but complex) implementation of this type with identical semantics is + /// available in source control history. The use of exclusive locks was not causing any measurable + /// performance overhead even on 28-thread machines at the time this was written. + /// + private readonly object _gate = new(); + + /// + /// The most recent memory mapped file for creating multiple storage units. It will be used via bump-pointer + /// allocation until space is no longer available in it. Access should be synchronized on + /// + private MemoryMappedFile? _fileReference; + + /// The name of the current memory mapped file for multiple storage units. Access should be synchronized on + /// + /// + private string? _name; + + /// The total size of the current memory mapped file for multiple storage units. Access should be + /// synchronized on + /// + private long _fileSize; + + /// + /// The offset into the current memory mapped file where the next storage unit can be held. Access should be + /// synchronized on . + /// + /// + private long _offset; + + [Obsolete(MefConstruction.FactoryMethodMessage, error: true)] + private TemporaryStorageService(IWorkspaceThreadingService? workspaceThreadingService, ITextFactoryService textFactory) + { + _workspaceThreadingService = workspaceThreadingService; + _textFactory = textFactory; + } + + ITemporaryStorageTextHandle ITemporaryStorageServiceInternal.WriteToTemporaryStorage(SourceText text, CancellationToken cancellationToken) + => WriteToTemporaryStorage(text, cancellationToken); + + async Task ITemporaryStorageServiceInternal.WriteToTemporaryStorageAsync(SourceText text, CancellationToken cancellationToken) + => await WriteToTemporaryStorageAsync(text, cancellationToken).ConfigureAwait(false); + + public TemporaryStorageTextHandle WriteToTemporaryStorage(SourceText text, CancellationToken cancellationToken) + { + var memoryMappedInfo = WriteToMemoryMappedFile(); + var identifier = new TemporaryStorageIdentifier(memoryMappedInfo.Name, memoryMappedInfo.Offset, memoryMappedInfo.Size); + return new(this, memoryMappedInfo.MemoryMappedFile, identifier, text.ChecksumAlgorithm, text.Encoding, text.GetContentHash()); + + MemoryMappedInfo WriteToMemoryMappedFile() + { + using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_WriteText, cancellationToken)) + { + // the method we use to get text out of SourceText uses Unicode (2bytes per char). + var size = Encoding.Unicode.GetMaxByteCount(text.Length); + var memoryMappedInfo = this.CreateTemporaryStorage(size); + + // Write the source text out as Unicode. We expect that to be cheap. + using var stream = memoryMappedInfo.CreateWritableStream(); + { + using var writer = new StreamWriter(stream, Encoding.Unicode); + text.Write(writer, cancellationToken); + } + + return memoryMappedInfo; + } + } + } + + public async Task WriteToTemporaryStorageAsync(SourceText text, CancellationToken cancellationToken) + { + if (this._workspaceThreadingService is { IsOnMainThread: true }) + { + await Task.Yield().ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + } + + return WriteToTemporaryStorage(text, cancellationToken); + } + + ITemporaryStorageStreamHandle ITemporaryStorageServiceInternal.WriteToTemporaryStorage(Stream stream, CancellationToken cancellationToken) + => WriteToTemporaryStorage(stream, cancellationToken); + + public TemporaryStorageStreamHandle WriteToTemporaryStorage(Stream stream, CancellationToken cancellationToken) + { + stream.Position = 0; + var memoryMappedInfo = WriteToMemoryMappedFile(); + var identifier = new TemporaryStorageIdentifier(memoryMappedInfo.Name, memoryMappedInfo.Offset, memoryMappedInfo.Size); + return new(memoryMappedInfo.MemoryMappedFile, identifier); + + MemoryMappedInfo WriteToMemoryMappedFile() + { + using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_WriteStream, cancellationToken)) + { + var size = stream.Length; + var memoryMappedInfo = this.CreateTemporaryStorage(size); + using var viewStream = memoryMappedInfo.CreateWritableStream(); + { + using var pooledObject = SharedPools.ByteArray.GetPooledObject(); + var buffer = pooledObject.Object; + while (true) + { + var count = stream.Read(buffer, 0, buffer.Length); + if (count == 0) + break; + + viewStream.Write(buffer, 0, count); + } + } + + return memoryMappedInfo; + } + } + } + + internal static TemporaryStorageStreamHandle GetStreamHandle(TemporaryStorageIdentifier storageIdentifier) + { + Contract.ThrowIfNull(storageIdentifier.Name, $"{nameof(GetStreamHandle)} should only be called for VS on Windows (where named memory mapped files as supported)"); + var memoryMappedFile = MemoryMappedFile.OpenExisting(storageIdentifier.Name); + return new(memoryMappedFile, storageIdentifier); + } + + internal TemporaryStorageTextHandle GetTextHandle( + TemporaryStorageIdentifier storageIdentifier, + SourceHashAlgorithm checksumAlgorithm, + Encoding? encoding, + ImmutableArray contentHash) + { + Contract.ThrowIfNull(storageIdentifier.Name, $"{nameof(GetTextHandle)} should only be called for VS on Windows (where named memory mapped files as supported)"); + var memoryMappedFile = MemoryMappedFile.OpenExisting(storageIdentifier.Name); + return new(this, memoryMappedFile, storageIdentifier, checksumAlgorithm, encoding, contentHash); + } + + /// + /// Allocate shared storage of a specified size. + /// + /// + /// "Small" requests are fulfilled from oversized memory mapped files which support several individual + /// storage units. Larger requests are allocated in their own memory mapped files. + /// + /// The size of the shared storage block to allocate. + /// A describing the allocated block. + private MemoryMappedInfo CreateTemporaryStorage(long size) + { + // Larger blocks are allocated separately + if (size >= SingleFileThreshold) + return MemoryMappedInfo.CreateNew(CreateUniqueName(size), size: size); + + lock (_gate) + { + // Obtain a reference to the memory mapped file, creating one if necessary. If a reference counted + // handle to a memory mapped file is obtained in this section, it must either be disposed before + // returning or returned to the caller who will own it through the MemoryMappedInfo. + var reference = _fileReference; + if (reference == null || _offset + size > _fileSize) + { + var mapName = CreateUniqueName(MultiFileBlockSize); + + reference = MemoryMappedFile.CreateNew(mapName, MultiFileBlockSize); + _fileReference = reference; + _name = mapName; + _fileSize = MultiFileBlockSize; + _offset = size; + return new MemoryMappedInfo(reference, _name, offset: 0, size: size); + } + else + { + // Reserve additional space in the existing storage location + _offset += size; + return new MemoryMappedInfo(reference, _name, _offset - size, size); + } + } + } + + public static string? CreateUniqueName(long size) + { + // MemoryMapped files which are used by the TemporaryStorageService are present in .NET Framework (including + // Mono) and .NET Core Windows. For non-Windows .NET Core scenarios, we return null to enable create the memory + // mapped file (just not in a way that can be shared across processes). + return PlatformInformation.IsWindows || PlatformInformation.IsRunningOnMono + ? $"Roslyn Shared File: Size={size} Id={Guid.NewGuid():N}" + : null; + } + + public sealed class TemporaryStorageTextHandle( + TemporaryStorageService storageService, + MemoryMappedFile memoryMappedFile, + TemporaryStorageIdentifier identifier, + SourceHashAlgorithm checksumAlgorithm, + Encoding? encoding, + ImmutableArray contentHash) + : ITemporaryStorageTextHandle + { + public TemporaryStorageIdentifier Identifier => identifier; + public SourceHashAlgorithm ChecksumAlgorithm => checksumAlgorithm; + public Encoding? Encoding => encoding; + public ImmutableArray ContentHash => contentHash; + + public async Task ReadFromTemporaryStorageAsync(CancellationToken cancellationToken) + { + // There is a reason for implementing it like this: proper async implementation + // that reads the underlying memory mapped file stream in an asynchronous fashion + // doesn't actually work. Windows doesn't offer + // any non-blocking way to read from a memory mapped file; the underlying memcpy + // may block as the memory pages back in and that's something you have to live + // with. Therefore, any implementation that attempts to use async will still + // always be blocking at least one threadpool thread in the memcpy in the case + // of a page fault. Therefore, if we're going to be blocking a thread, we should + // just block one thread and do the whole thing at once vs. a fake "async" + // implementation which will continue to requeue work back to the thread pool. + if (storageService._workspaceThreadingService is { IsOnMainThread: true }) + { + await Task.Yield().ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + } + + return ReadFromTemporaryStorage(cancellationToken); + } + + public SourceText ReadFromTemporaryStorage(CancellationToken cancellationToken) + { + using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_ReadText, cancellationToken)) + { + var info = new MemoryMappedInfo(memoryMappedFile, Identifier.Name, Identifier.Offset, Identifier.Size); + using var stream = info.CreateReadableStream(); + using var reader = CreateTextReaderFromTemporaryStorage(stream); + + // we pass in encoding we got from original source text even if it is null. + return storageService._textFactory.CreateText(reader, encoding, checksumAlgorithm, cancellationToken); + } + } + + private static unsafe DirectMemoryAccessStreamReader CreateTextReaderFromTemporaryStorage(UnmanagedMemoryStream stream) + { + var src = (char*)stream.PositionPointer; + + // BOM: Unicode, little endian + // Skip the BOM when creating the reader + Debug.Assert(*src == 0xFEFF); + + return new DirectMemoryAccessStreamReader(src + 1, (int)stream.Length / sizeof(char) - 1); + } + } +} diff --git a/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageServiceFactory.cs b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageServiceFactory.cs deleted file mode 100644 index 451e15a091a56..0000000000000 --- a/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageServiceFactory.cs +++ /dev/null @@ -1,490 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Immutable; -using System.Diagnostics; -using System.IO; -using System.IO.MemoryMappedFiles; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.Internal.Log; -using Microsoft.CodeAnalysis.Shared.Utilities; -using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.Host; - -/// -/// Temporarily stores text and streams in memory mapped files. -/// -#if NETCOREAPP -[SupportedOSPlatform("windows")] -#endif -internal sealed partial class TemporaryStorageService : ITemporaryStorageServiceInternal -{ - /// - /// The maximum size in bytes of a single storage unit in a memory mapped file which is shared with other - /// storage units. - /// - /// - /// This value was arbitrarily chosen and appears to work well. Can be changed if data suggests - /// something better. - /// - /// - private const long SingleFileThreshold = 128 * 1024; - - /// - /// The size in bytes of a memory mapped file created to store multiple temporary objects. - /// - /// - /// This value was arbitrarily chosen and appears to work well. Can be changed if data suggests - /// something better. - /// - /// - private const long MultiFileBlockSize = SingleFileThreshold * 32; - - private readonly IWorkspaceThreadingService? _workspaceThreadingService; - private readonly ITextFactoryService _textFactory; - - /// - /// The synchronization object for accessing the memory mapped file related fields (indicated in the remarks - /// of each field). - /// - /// - /// PERF DEV NOTE: A concurrent (but complex) implementation of this type with identical semantics is - /// available in source control history. The use of exclusive locks was not causing any measurable - /// performance overhead even on 28-thread machines at the time this was written. - /// - private readonly object _gate = new(); - - /// - /// The most recent memory mapped file for creating multiple storage units. It will be used via bump-pointer - /// allocation until space is no longer available in it. - /// - /// - /// Access should be synchronized on . - /// - private ReferenceCountedDisposable.WeakReference _weakFileReference; - - /// The name of the current memory mapped file for multiple storage units. - /// - /// Access should be synchronized on . - /// - /// - private string? _name; - - /// The total size of the current memory mapped file for multiple storage units. - /// - /// Access should be synchronized on . - /// - /// - private long _fileSize; - - /// - /// The offset into the current memory mapped file where the next storage unit can be held. - /// - /// - /// Access should be synchronized on . - /// - /// - private long _offset; - - [Obsolete(MefConstruction.FactoryMethodMessage, error: true)] - private TemporaryStorageService(IWorkspaceThreadingService? workspaceThreadingService, ITextFactoryService textFactory) - { - _workspaceThreadingService = workspaceThreadingService; - _textFactory = textFactory; - } - - public ITemporaryTextStorageInternal CreateTemporaryTextStorage() - => new TemporaryTextStorage(this); - - public TemporaryTextStorage AttachTemporaryTextStorage( - string storageName, long offset, long size, SourceHashAlgorithm checksumAlgorithm, Encoding? encoding, ImmutableArray contentHash) - => new(this, storageName, offset, size, checksumAlgorithm, encoding, contentHash); - - ITemporaryStreamStorageInternal ITemporaryStorageServiceInternal.CreateTemporaryStreamStorage() - => CreateTemporaryStreamStorage(); - - internal TemporaryStreamStorage CreateTemporaryStreamStorage() - => new(this); - - public TemporaryStreamStorage AttachTemporaryStreamStorage(string storageName, long offset, long size) - => new(this, storageName, offset, size); - - /// - /// Allocate shared storage of a specified size. - /// - /// - /// "Small" requests are fulfilled from oversized memory mapped files which support several individual - /// storage units. Larger requests are allocated in their own memory mapped files. - /// - /// The size of the shared storage block to allocate. - /// A describing the allocated block. - private MemoryMappedInfo CreateTemporaryStorage(long size) - { - if (size >= SingleFileThreshold) - { - // Larger blocks are allocated separately - var mapName = CreateUniqueName(size); - var storage = MemoryMappedFile.CreateNew(mapName, size); - return new MemoryMappedInfo(new ReferenceCountedDisposable(storage), mapName, offset: 0, size: size); - } - - lock (_gate) - { - // Obtain a reference to the memory mapped file, creating one if necessary. If a reference counted - // handle to a memory mapped file is obtained in this section, it must either be disposed before - // returning or returned to the caller who will own it through the MemoryMappedInfo. - var reference = _weakFileReference.TryAddReference(); - if (reference == null || _offset + size > _fileSize) - { - var mapName = CreateUniqueName(MultiFileBlockSize); - var file = MemoryMappedFile.CreateNew(mapName, MultiFileBlockSize); - - reference = new ReferenceCountedDisposable(file); - _weakFileReference = new ReferenceCountedDisposable.WeakReference(reference); - _name = mapName; - _fileSize = MultiFileBlockSize; - _offset = size; - return new MemoryMappedInfo(reference, _name, offset: 0, size: size); - } - else - { - // Reserve additional space in the existing storage location - Contract.ThrowIfNull(_name); - _offset += size; - return new MemoryMappedInfo(reference, _name, _offset - size, size); - } - } - } - - public static string CreateUniqueName(long size) - => "Roslyn Temp Storage " + size.ToString() + " " + Guid.NewGuid().ToString("N"); - - public sealed class TemporaryTextStorage : ITemporaryTextStorageInternal, ITemporaryStorageWithName - { - private readonly TemporaryStorageService _service; - private SourceHashAlgorithm _checksumAlgorithm; - private Encoding? _encoding; - private ImmutableArray _contentHash; - private MemoryMappedInfo? _memoryMappedInfo; - - public TemporaryTextStorage(TemporaryStorageService service) - => _service = service; - - public TemporaryTextStorage( - TemporaryStorageService service, - string storageName, - long offset, - long size, - SourceHashAlgorithm checksumAlgorithm, - Encoding? encoding, - ImmutableArray contentHash) - { - _service = service; - _checksumAlgorithm = checksumAlgorithm; - _encoding = encoding; - _contentHash = contentHash; - _memoryMappedInfo = new MemoryMappedInfo(storageName, offset, size); - } - - // TODO: cleanup https://github.com/dotnet/roslyn/issues/43037 - // Offset, Size not accessed if Name is null - public string? Name => _memoryMappedInfo?.Name; - public long Offset => _memoryMappedInfo!.Offset; - public long Size => _memoryMappedInfo!.Size; - - /// - /// Gets the value for the property for the - /// represented by this temporary storage. - /// - public SourceHashAlgorithm ChecksumAlgorithm => _checksumAlgorithm; - - /// - /// Gets the value for the property for the - /// represented by this temporary storage. - /// - public Encoding? Encoding => _encoding; - - /// - /// Gets the checksum for the represented by this temporary storage. This is equivalent - /// to calling . - /// - public ImmutableArray ContentHash => _contentHash; - - public void Dispose() - { - // Destructors of SafeHandle and FileStream in MemoryMappedFile - // will eventually release resources if this Dispose is not called - // explicitly - _memoryMappedInfo?.Dispose(); - - _memoryMappedInfo = null; - _encoding = null; - _contentHash = default; - } - - public SourceText ReadText(CancellationToken cancellationToken) - { - if (_memoryMappedInfo == null) - { - throw new InvalidOperationException(); - } - - using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_ReadText, cancellationToken)) - { - using var stream = _memoryMappedInfo.CreateReadableStream(); - using var reader = CreateTextReaderFromTemporaryStorage(stream); - - // we pass in encoding we got from original source text even if it is null. - return _service._textFactory.CreateText(reader, _encoding, _checksumAlgorithm, cancellationToken); - } - } - - public async Task ReadTextAsync(CancellationToken cancellationToken) - { - // There is a reason for implementing it like this: proper async implementation - // that reads the underlying memory mapped file stream in an asynchronous fashion - // doesn't actually work. Windows doesn't offer - // any non-blocking way to read from a memory mapped file; the underlying memcpy - // may block as the memory pages back in and that's something you have to live - // with. Therefore, any implementation that attempts to use async will still - // always be blocking at least one threadpool thread in the memcpy in the case - // of a page fault. Therefore, if we're going to be blocking a thread, we should - // just block one thread and do the whole thing at once vs. a fake "async" - // implementation which will continue to requeue work back to the thread pool. - if (_service._workspaceThreadingService is { IsOnMainThread: true }) - { - await Task.Yield().ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - } - - return ReadText(cancellationToken); - } - - public void WriteText(SourceText text, CancellationToken cancellationToken) - { - if (_memoryMappedInfo != null) - { - throw new InvalidOperationException(WorkspacesResources.Temporary_storage_cannot_be_written_more_than_once); - } - - using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_WriteText, cancellationToken)) - { - _checksumAlgorithm = text.ChecksumAlgorithm; - _encoding = text.Encoding; - _contentHash = text.GetContentHash(); - - // the method we use to get text out of SourceText uses Unicode (2bytes per char). - var size = Encoding.Unicode.GetMaxByteCount(text.Length); - _memoryMappedInfo = _service.CreateTemporaryStorage(size); - - // Write the source text out as Unicode. We expect that to be cheap. - using var stream = _memoryMappedInfo.CreateWritableStream(); - using var writer = new StreamWriter(stream, Encoding.Unicode); - - text.Write(writer, cancellationToken); - } - } - - public async Task WriteTextAsync(SourceText text, CancellationToken cancellationToken) - { - // See commentary in ReadTextAsync for why this is implemented this way. - if (_service._workspaceThreadingService is { IsOnMainThread: true }) - { - await Task.Yield().ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - } - - WriteText(text, cancellationToken); - } - - private static unsafe TextReader CreateTextReaderFromTemporaryStorage(UnmanagedMemoryStream stream) - { - var src = (char*)stream.PositionPointer; - - // BOM: Unicode, little endian - // Skip the BOM when creating the reader - Debug.Assert(*src == 0xFEFF); - - return new DirectMemoryAccessStreamReader(src + 1, (int)stream.Length / sizeof(char) - 1); - } - } - - internal sealed class TemporaryStreamStorage : ITemporaryStreamStorageInternal, ITemporaryStorageWithName - { - private readonly TemporaryStorageService _service; - private MemoryMappedInfo? _memoryMappedInfo; - - public TemporaryStreamStorage(TemporaryStorageService service) - => _service = service; - - public TemporaryStreamStorage(TemporaryStorageService service, string storageName, long offset, long size) - { - _service = service; - _memoryMappedInfo = new MemoryMappedInfo(storageName, offset, size); - } - - // TODO: clean up https://github.com/dotnet/roslyn/issues/43037 - // Offset, Size is only used when Name is not null. - public string? Name => _memoryMappedInfo?.Name; - public long Offset => _memoryMappedInfo!.Offset; - public long Size => _memoryMappedInfo!.Size; - - public void Dispose() - { - // Destructors of SafeHandle and FileStream in MemoryMappedFile - // will eventually release resources if this Dispose is not called - // explicitly - _memoryMappedInfo?.Dispose(); - _memoryMappedInfo = null; - } - - Stream ITemporaryStreamStorageInternal.ReadStream(CancellationToken cancellationToken) - => ReadStream(cancellationToken); - - public UnmanagedMemoryStream ReadStream(CancellationToken cancellationToken) - { - if (_memoryMappedInfo == null) - { - throw new InvalidOperationException(); - } - - using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_ReadStream, cancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - - return _memoryMappedInfo.CreateReadableStream(); - } - } - - public Task ReadStreamAsync(CancellationToken cancellationToken = default) - { - // See commentary in ReadTextAsync for why this is implemented this way. - return Task.Factory.StartNew(() => ReadStream(cancellationToken), cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - public void WriteStream(Stream stream, CancellationToken cancellationToken = default) - { - // The Wait() here will not actually block, since with useAsync: false, the - // entire operation will already be done when WaitStreamMaybeAsync completes. - WriteStreamMaybeAsync(stream, useAsync: false, cancellationToken: cancellationToken).GetAwaiter().GetResult(); - } - - public Task WriteStreamAsync(Stream stream, CancellationToken cancellationToken = default) - => WriteStreamMaybeAsync(stream, useAsync: true, cancellationToken: cancellationToken); - - private async Task WriteStreamMaybeAsync(Stream stream, bool useAsync, CancellationToken cancellationToken) - { - if (_memoryMappedInfo != null) - { - throw new InvalidOperationException(WorkspacesResources.Temporary_storage_cannot_be_written_more_than_once); - } - - using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_WriteStream, cancellationToken)) - { - var size = stream.Length; - _memoryMappedInfo = _service.CreateTemporaryStorage(size); - using var viewStream = _memoryMappedInfo.CreateWritableStream(); - - var buffer = SharedPools.ByteArray.Allocate(); - try - { - while (true) - { - int count; - if (useAsync) - { - count = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); - } - else - { - count = stream.Read(buffer, 0, buffer.Length); - } - - if (count == 0) - { - break; - } - - viewStream.Write(buffer, 0, count); - } - } - finally - { - SharedPools.ByteArray.Free(buffer); - } - } - } - } -} - -internal unsafe class DirectMemoryAccessStreamReader : TextReaderWithLength -{ - private char* _position; - private readonly char* _end; - - public DirectMemoryAccessStreamReader(char* src, int length) - : base(length) - { - RoslynDebug.Assert(src != null); - RoslynDebug.Assert(length >= 0); - - _position = src; - _end = _position + length; - } - - public override int Peek() - { - if (_position >= _end) - { - return -1; - } - - return *_position; - } - - public override int Read() - { - if (_position >= _end) - { - return -1; - } - - return *_position++; - } - - public override int Read(char[] buffer, int index, int count) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - if (index < 0 || index >= buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - if (count < 0 || (index + count) > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - count = Math.Min(count, (int)(_end - _position)); - if (count > 0) - { - Marshal.Copy((IntPtr)_position, buffer, index, count); - _position += count; - } - - return count; - } -} diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/AbstractPersistentStorage.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/AbstractPersistentStorage.cs index cbefb3294b7c1..81913cc81c0cb 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/AbstractPersistentStorage.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/AbstractPersistentStorage.cs @@ -13,8 +13,9 @@ namespace Microsoft.CodeAnalysis.Host; internal abstract class AbstractPersistentStorage : IChecksummedPersistentStorage { + public SolutionKey SolutionKey { get; } + public string WorkingFolderPath { get; } - public string SolutionFilePath { get; } public string DatabaseFile { get; } public string DatabaseDirectory => Path.GetDirectoryName(DatabaseFile) ?? throw ExceptionUtilities.UnexpectedValue(DatabaseFile); @@ -22,12 +23,12 @@ internal abstract class AbstractPersistentStorage : IChecksummedPersistentStorag private bool _isDisabled; protected AbstractPersistentStorage( + SolutionKey solutionKey, string workingFolderPath, - string solutionFilePath, string databaseFile) { + this.SolutionKey = solutionKey; this.WorkingFolderPath = workingFolderPath; - this.SolutionFilePath = solutionFilePath; this.DatabaseFile = databaseFile; if (!Directory.Exists(this.DatabaseDirectory)) @@ -42,9 +43,6 @@ private bool IsDisabled protected void DisableStorage() => Volatile.Write(ref _isDisabled, true); - public abstract void Dispose(); - public abstract ValueTask DisposeAsync(); - public abstract Task ChecksumMatchesAsync(string name, Checksum checksum, CancellationToken cancellationToken); public abstract Task ReadStreamAsync(string name, Checksum? checksum, CancellationToken cancellationToken); public abstract Task WriteStreamAsync(string name, Stream stream, Checksum? checksum, CancellationToken cancellationToken); diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IChecksummedPersistentStorage.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IChecksummedPersistentStorage.cs index 27512482e6872..a0feb6908ba70 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IChecksummedPersistentStorage.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IChecksummedPersistentStorage.cs @@ -12,6 +12,11 @@ namespace Microsoft.CodeAnalysis.Host; internal interface IChecksummedPersistentStorage : IPersistentStorage { + /// + /// The solution this is a storage instance for. + /// + SolutionKey SolutionKey { get; } + /// /// if the data we have for the solution with the given has the /// provided . diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorage.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorage.cs index 4b81585424cf5..9d5d44929fbc8 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorage.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorage.cs @@ -14,7 +14,7 @@ namespace Microsoft.CodeAnalysis.Host; /// disposal should always be preferred as the implementation of synchronous disposal may end up blocking the caller /// on async work. /// -public interface IPersistentStorage : IDisposable, IAsyncDisposable +public interface IPersistentStorage { Task ReadStreamAsync(string name, CancellationToken cancellationToken = default); Task ReadStreamAsync(Project project, string name, CancellationToken cancellationToken = default); diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorageConfiguration.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorageConfiguration.cs index e73c12d226a0f..823d3ae7e5cca 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorageConfiguration.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorageConfiguration.cs @@ -37,7 +37,7 @@ internal sealed class DefaultPersistentStorageConfiguration : IPersistentStorage /// path. For example, Base64 encoding will use / which is something that we definitely do not want /// errantly added to a path. /// - private static readonly ImmutableArray s_invalidPathChars = Path.GetInvalidPathChars().Concat('/').ToImmutableArray(); + private static readonly ImmutableArray s_invalidPathChars = [.. Path.GetInvalidPathChars(), '/']; private static readonly string s_cacheDirectory; private static readonly string s_moduleFileName; diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorage.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorage.cs index 14840b4d8f5a9..c924931a44441 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorage.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorage.cs @@ -11,27 +11,14 @@ namespace Microsoft.CodeAnalysis.Host; -internal class NoOpPersistentStorage : IChecksummedPersistentStorage +internal class NoOpPersistentStorage(SolutionKey solutionKey) : IChecksummedPersistentStorage { - private static readonly IChecksummedPersistentStorage Instance = new NoOpPersistentStorage(); + public SolutionKey SolutionKey => solutionKey; - private NoOpPersistentStorage() - { - } - - public static IChecksummedPersistentStorage GetOrThrow(bool throwOnFailure) + public static IChecksummedPersistentStorage GetOrThrow(SolutionKey solutionKey, bool throwOnFailure) => throwOnFailure ? throw new InvalidOperationException("Database was not supported") - : Instance; - - public void Dispose() - { - } - - public ValueTask DisposeAsync() - { - return ValueTaskFactory.CompletedTask; - } + : new NoOpPersistentStorage(solutionKey); public Task ChecksumMatchesAsync(string name, Checksum checksum, CancellationToken cancellationToken) => SpecializedTasks.False; @@ -98,6 +85,6 @@ public Task WriteStreamAsync(DocumentKey documentKey, string name, Stream public readonly struct TestAccessor { - public static readonly IChecksummedPersistentStorage StorageInstance = Instance; + public static IChecksummedPersistentStorage GetStorageInstance(SolutionKey solutionKey) => new NoOpPersistentStorage(solutionKey); } } diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorageService.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorageService.cs index b3015195fd3f3..5f5bc076f5df3 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorageService.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/NoOpPersistentStorageService.cs @@ -23,5 +23,5 @@ public static IChecksummedPersistentStorageService GetOrThrow(IPersistentStorage : Instance; public ValueTask GetStorageAsync(SolutionKey solutionKey, CancellationToken cancellationToken) - => new(NoOpPersistentStorage.GetOrThrow(throwOnFailure: false)); + => new(NoOpPersistentStorage.GetOrThrow(solutionKey, throwOnFailure: false)); } diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/SolutionKey.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/SolutionKey.cs index 13d48d4dd1aec..41ea22e495640 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/SolutionKey.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/SolutionKey.cs @@ -14,14 +14,10 @@ namespace Microsoft.CodeAnalysis.Storage; /// solution load), but querying the data is still desired. /// [DataContract] -internal readonly struct SolutionKey(SolutionId id, string? filePath) +internal readonly record struct SolutionKey( + [property: DataMember(Order = 0)] SolutionId Id, + [property: DataMember(Order = 1)] string? FilePath) { - [DataMember(Order = 0)] - public readonly SolutionId Id = id; - - [DataMember(Order = 1)] - public readonly string? FilePath = filePath; - public static SolutionKey ToSolutionKey(Solution solution) => ToSolutionKey(solution.SolutionState); diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorage.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorage.cs index f1848b42c8618..d458ba5ca481c 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorage.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorage.cs @@ -12,7 +12,7 @@ namespace Microsoft.CodeAnalysis.Host; -[Obsolete("Roslyn no longer exports a mechanism to store arbitrary data in-memory.")] +[Obsolete("Roslyn no longer exports a mechanism to store arbitrary data in-memory.", error: true)] public interface ITemporaryTextStorage : IDisposable { SourceText ReadText(CancellationToken cancellationToken = default); @@ -21,7 +21,7 @@ public interface ITemporaryTextStorage : IDisposable Task WriteTextAsync(SourceText text, CancellationToken cancellationToken = default); } -[Obsolete("Roslyn no longer exports a mechanism to store arbitrary data in-memory.")] +[Obsolete("Roslyn no longer exports a mechanism to store arbitrary data in-memory.", error: true)] public interface ITemporaryStreamStorage : IDisposable { Stream ReadStream(CancellationToken cancellationToken = default); @@ -29,22 +29,3 @@ public interface ITemporaryStreamStorage : IDisposable void WriteStream(Stream stream, CancellationToken cancellationToken = default); Task WriteStreamAsync(Stream stream, CancellationToken cancellationToken = default); } - -/// -/// TemporaryStorage can be used to read and write text to a temporary storage location. -/// -internal interface ITemporaryTextStorageInternal : IDisposable -{ - SourceText ReadText(CancellationToken cancellationToken = default); - Task ReadTextAsync(CancellationToken cancellationToken = default); - void WriteText(SourceText text, CancellationToken cancellationToken = default); - Task WriteTextAsync(SourceText text, CancellationToken cancellationToken = default); -} - -internal interface ITemporaryStreamStorageInternal : IDisposable -{ - Stream ReadStream(CancellationToken cancellationToken = default); - Task ReadStreamAsync(CancellationToken cancellationToken = default); - void WriteStream(Stream stream, CancellationToken cancellationToken = default); - Task WriteStreamAsync(Stream stream, CancellationToken cancellationToken = default); -} diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageService.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageService.cs index 694b26ba147b0..c19c7b99a852b 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageService.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageService.cs @@ -3,19 +3,56 @@ // See the LICENSE file in the project root for more information. using System; +using System.IO; using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Host; -[Obsolete("API is no longer available")] +[Obsolete("API is no longer available", error: true)] public interface ITemporaryStorageService : IWorkspaceService { ITemporaryStreamStorage CreateTemporaryStreamStorage(CancellationToken cancellationToken = default); ITemporaryTextStorage CreateTemporaryTextStorage(CancellationToken cancellationToken = default); } +/// +/// API to allow a client to write data to memory-mapped-file storage. That data can be read back in within the same +/// process using a handle returned from the writing call. The data can optionally be read back in from a different +/// process, using the information contained with the handle's Identifier (see ), but only on systems that support named memory mapped files. Currently, this +/// is any .net on Windows and mono on unix systems. This is not supported on .net core on unix systems (tracked here +/// https://github.com/dotnet/runtime/issues/30878). This is not a problem in practice as cross process sharing is only +/// needed by the VS host, which is windows only. +/// internal interface ITemporaryStorageServiceInternal : IWorkspaceService { - ITemporaryStreamStorageInternal CreateTemporaryStreamStorage(); - ITemporaryTextStorageInternal CreateTemporaryTextStorage(); + /// + /// Write the provided to a new memory-mapped-file. Returns a handle to the data that can + /// be used to identify the data across processes allowing it to be read back in in any process. + /// + /// + /// This type is primarily used to allow dumping metadata to disk. This then allowing them to be read in by mapping + /// their data into types like . It also allows them to be read in by our server + /// process, without having to transmit the data over the wire. + /// Note: The stream provided must support . The stream will also be reset to + /// 0 within this method. The caller does not need to reset the stream + /// itself. + /// + ITemporaryStorageStreamHandle WriteToTemporaryStorage(Stream stream, CancellationToken cancellationToken); + + /// + /// Write the provided to a new memory-mapped-file. Returns a handle to the data that can + /// be used to identify the data across processes allowing it to be read back in in any process. + /// + /// + /// This type is primarily used to allow dumping source texts to disk. This then allowing them to be read in by + /// mapping their data into types like . It also allows them + /// to be read in by our server process, without having to transmit the data over the wire. + /// + ITemporaryStorageTextHandle WriteToTemporaryStorage(SourceText text, CancellationToken cancellationToken); + + /// "/> + Task WriteToTemporaryStorageAsync(SourceText text, CancellationToken cancellationToken); } diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageStreamHandle.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageStreamHandle.cs new file mode 100644 index 0000000000000..a278f5a73ab30 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageStreamHandle.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.IO; +using System.Threading; + +namespace Microsoft.CodeAnalysis.Host; + +/// +/// Represents a handle to data stored to temporary storage (generally a memory mapped file). As long as this handle is +/// alive, the data should remain in storage and can be readable from any process using the information provided in . Use to write the data to temporary storage and get a handle to it. Use to read the data back in any process. +/// +internal interface ITemporaryStorageStreamHandle +{ + public TemporaryStorageIdentifier Identifier { get; } + + /// + /// Reads the data indicated to by this handle into a stream. This stream can be created in a different process + /// than the one that wrote the data originally. + /// + UnmanagedMemoryStream ReadFromTemporaryStorage(CancellationToken cancellationToken); +} diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageTextHandle.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageTextHandle.cs new file mode 100644 index 0000000000000..d2c1dcccc88b1 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageTextHandle.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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Host; + +internal interface ITemporaryStorageTextHandle +{ + public TemporaryStorageIdentifier Identifier { get; } + + SourceText ReadFromTemporaryStorage(CancellationToken cancellationToken); + Task ReadFromTemporaryStorageAsync(CancellationToken cancellationToken); +} diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageWithName.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageWithName.cs deleted file mode 100644 index 3d635adbe6c8d..0000000000000 --- a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageWithName.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace Microsoft.CodeAnalysis.Host; - -/// -/// TemporaryStorage can be used to read and write text to a temporary storage location. -/// -internal interface ITemporaryStorageWithName -{ - // TODO: clean up https://github.com/dotnet/roslyn/issues/43037 - // Name shouldn't be nullable. - - /// - /// Get name of the temporary storage - /// - string? Name { get; } - - /// - /// Get offset of the temporary storage - /// - long Offset { get; } - - /// - /// Get size of the temporary storage - /// - long Size { get; } -} diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStreamStorageExtensions.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStreamStorageExtensions.cs deleted file mode 100644 index af0d841bea463..0000000000000 --- a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStreamStorageExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -#nullable disable - -using System.Collections.Generic; -using System.Collections.Immutable; -using System.IO; - -namespace Microsoft.CodeAnalysis.Host; - -internal static class ITemporaryStreamStorageExtensions -{ - public static void WriteAllLines(this ITemporaryStreamStorageInternal storage, ImmutableArray values) - { - using var stream = SerializableBytes.CreateWritableStream(); - using var writer = new StreamWriter(stream); - - foreach (var value in values) - { - writer.WriteLine(value); - } - - writer.Flush(); - stream.Position = 0; - - storage.WriteStream(stream); - } - - public static ImmutableArray ReadLines(this ITemporaryStreamStorageInternal storage) - { - return EnumerateLines(storage).ToImmutableArray(); - } - - private static IEnumerable EnumerateLines(ITemporaryStreamStorageInternal storage) - { - using var stream = storage.ReadStream(); - using var reader = new StreamReader(stream); - - string line; - while ((line = reader.ReadLine()) != null) - { - yield return line; - } - } -} diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TemporaryStorageIdentifier.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TemporaryStorageIdentifier.cs new file mode 100644 index 0000000000000..8ac99018bef6b --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TemporaryStorageIdentifier.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Host; + +/// +/// Identifier for a stream of data placed in a segment of a memory mapped file. Can be used to identify that segment +/// across processes (where supported), allowing for efficient sharing of data. +/// +/// The name of the segment in the temporary storage. on platforms that don't +/// support cross process sharing of named memory mapped files. +internal sealed record TemporaryStorageIdentifier( + string? Name, long Offset, long Size) +{ + public static TemporaryStorageIdentifier ReadFrom(ObjectReader reader) + => new( + reader.ReadString(), + reader.ReadInt64(), + reader.ReadInt64()); + + public void WriteTo(ObjectWriter writer) + { + writer.WriteString(Name); + writer.WriteInt64(Offset); + writer.WriteInt64(Size); + } +} diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TrivialTemporaryStorageService.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TrivialTemporaryStorageService.cs deleted file mode 100644 index 0f3641b459ca7..0000000000000 --- a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TrivialTemporaryStorageService.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.CodeAnalysis; - -internal sealed class TrivialTemporaryStorageService : ITemporaryStorageServiceInternal -{ - public static readonly TrivialTemporaryStorageService Instance = new(); - - private TrivialTemporaryStorageService() - { - } - - public ITemporaryStreamStorageInternal CreateTemporaryStreamStorage() - => new StreamStorage(); - - public ITemporaryTextStorageInternal CreateTemporaryTextStorage() - => new TextStorage(); - - private sealed class StreamStorage : ITemporaryStreamStorageInternal - { - private MemoryStream? _stream; - - public void Dispose() - { - _stream?.Dispose(); - _stream = null; - } - - public Stream ReadStream(CancellationToken cancellationToken) - { - var stream = _stream ?? throw new InvalidOperationException(); - - // Return a read-only view of the underlying buffer to prevent users from overwriting or directly - // disposing the backing storage. - return new MemoryStream(stream.GetBuffer(), 0, (int)stream.Length, writable: false); - } - - public Task ReadStreamAsync(CancellationToken cancellationToken) - { - return Task.FromResult(ReadStream(cancellationToken)); - } - - public void WriteStream(Stream stream, CancellationToken cancellationToken) - { - var newStream = new MemoryStream(); - stream.CopyTo(newStream); - var existingValue = Interlocked.CompareExchange(ref _stream, newStream, null); - if (existingValue is not null) - { - throw new InvalidOperationException(WorkspacesResources.Temporary_storage_cannot_be_written_more_than_once); - } - } - - public async Task WriteStreamAsync(Stream stream, CancellationToken cancellationToken) - { - var newStream = new MemoryStream(); -#if NETCOREAPP - await stream.CopyToAsync(newStream, cancellationToken).ConfigureAwait(false); -# else - await stream.CopyToAsync(newStream).ConfigureAwait(false); -#endif - var existingValue = Interlocked.CompareExchange(ref _stream, newStream, null); - if (existingValue is not null) - { - throw new InvalidOperationException(WorkspacesResources.Temporary_storage_cannot_be_written_more_than_once); - } - } - } - - private sealed class TextStorage : ITemporaryTextStorageInternal - { - private SourceText? _sourceText; - - public void Dispose() - => _sourceText = null; - - public SourceText ReadText(CancellationToken cancellationToken) - => _sourceText ?? throw new InvalidOperationException(); - - public Task ReadTextAsync(CancellationToken cancellationToken) - => Task.FromResult(ReadText(cancellationToken)); - - public void WriteText(SourceText text, CancellationToken cancellationToken) - { - // This is a trivial implementation, indeed. Note, however, that we retain a strong - // reference to the source text, which defeats the intent of RecoverableTextAndVersion, but - // is appropriate for this trivial implementation. - var existingValue = Interlocked.CompareExchange(ref _sourceText, text, null); - if (existingValue is not null) - { - throw new InvalidOperationException(WorkspacesResources.Temporary_storage_cannot_be_written_more_than_once); - } - } - - public Task WriteTextAsync(SourceText text, CancellationToken cancellationToken = default) - { - WriteText(text, cancellationToken); - return Task.CompletedTask; - } - } -} diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.BatchingDocumentCollection.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.BatchingDocumentCollection.cs index 04ba30d41afd4..0f6ed5627c13a 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.BatchingDocumentCollection.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.BatchingDocumentCollection.cs @@ -557,7 +557,7 @@ internal void UpdateSolutionForBatch( ClearAndZeroCapacity(_documentsAddedInBatch); // Document removing... - solutionChanges.UpdateSolutionForRemovedDocumentAction(removeDocuments(solutionChanges.Solution, _documentsRemovedInBatch.ToImmutableArray()), + solutionChanges.UpdateSolutionForRemovedDocumentAction(removeDocuments(solutionChanges.Solution, [.. _documentsRemovedInBatch]), removeDocumentChangeKind, _documentsRemovedInBatch); diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs index 6249e92df4226..4f387f594bed4 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs @@ -191,21 +191,30 @@ internal ProjectSystemProject( _filePath = filePath; _parseOptions = parseOptions; - var fileExtensionToWatch = language switch { LanguageNames.CSharp => ".cs", LanguageNames.VisualBasic => ".vb", _ => null }; + var watchedDirectories = GetWatchedDirectories(language, filePath); + _documentFileChangeContext = _projectSystemProjectFactory.FileChangeWatcher.CreateContext(watchedDirectories); + _documentFileChangeContext.FileChanged += DocumentFileChangeContext_FileChanged; - if (filePath != null && fileExtensionToWatch != null) - { - // Since we have a project directory, we'll just watch all the files under that path; that'll avoid extra overhead of - // having to add explicit file watches everywhere. - var projectDirectoryToWatch = new WatchedDirectory(Path.GetDirectoryName(filePath)!, fileExtensionToWatch); - _documentFileChangeContext = _projectSystemProjectFactory.FileChangeWatcher.CreateContext(projectDirectoryToWatch); - } - else + static WatchedDirectory[] GetWatchedDirectories(string? language, string? filePath) { - _documentFileChangeContext = _projectSystemProjectFactory.FileChangeWatcher.CreateContext(); - } + if (filePath is null) + { + return []; + } - _documentFileChangeContext.FileChanged += DocumentFileChangeContext_FileChanged; + var rootPath = Path.GetDirectoryName(filePath); + if (rootPath is null) + { + return []; + } + + return language switch + { + LanguageNames.VisualBasic => [new(rootPath, ".vb")], + LanguageNames.CSharp => [new(rootPath, ".cs"), new(rootPath, ".razor"), new(rootPath, ".cshtml")], + _ => [] + }; + } } private void ChangeProjectProperty(ref T field, T newValue, Func updateSolution, bool logThrowAwayTelemetry = false) diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectOptionsProcessor.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectOptionsProcessor.cs index 11be0ef802128..9c2059d1006c3 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectOptionsProcessor.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectOptionsProcessor.cs @@ -3,8 +3,10 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.IO; +using System.Threading; using Microsoft.CodeAnalysis.Host; using Roslyn.Utilities; @@ -36,7 +38,7 @@ internal class ProjectSystemProjectOptionsProcessor : IDisposable /// (especially in cases with many references). /// /// Note: this will be null in the case that the command line is an empty array. - private ITemporaryStreamStorageInternal? _commandLineStorage; + private ITemporaryStorageStreamHandle? _commandLineStorageHandle; private CommandLineArguments _commandLineArgumentsForCommandLine; private string? _explicitRuleSetFilePath; @@ -71,12 +73,17 @@ private bool ReparseCommandLineIfChanged_NoLock(ImmutableArray arguments // Dispose the existing stored command-line and then persist the new one so we can // recover it later. Only bother persisting things if we have a non-empty string. - _commandLineStorage?.Dispose(); - _commandLineStorage = null; + _commandLineStorageHandle = null; if (!arguments.IsEmpty) { - _commandLineStorage = _temporaryStorageService.CreateTemporaryStreamStorage(); - _commandLineStorage.WriteAllLines(arguments); + using var stream = SerializableBytes.CreateWritableStream(); + using var writer = new StreamWriter(stream); + + foreach (var value in arguments) + writer.WriteLine(value); + + writer.Flush(); + _commandLineStorageHandle = _temporaryStorageService.WriteToTemporaryStorage(stream, CancellationToken.None); } ReparseCommandLine_NoLock(arguments); @@ -237,12 +244,24 @@ private void RuleSetFile_UpdatedOnDisk(object? sender, EventArgs e) // effective values was potentially done by the act of parsing the command line. Even though the command line didn't change textually, // the effective result did. Then we call UpdateProjectOptions_NoLock to reapply any values; that will also re-acquire the new ruleset // includes in the IDE so we can be watching for changes again. - var commandLine = _commandLineStorage == null ? ImmutableArray.Empty : _commandLineStorage.ReadLines(); + var commandLine = _commandLineStorageHandle == null + ? ImmutableArray.Empty + : EnumerateLines(_commandLineStorageHandle).ToImmutableArray(); DisposeOfRuleSetFile_NoLock(); ReparseCommandLine_NoLock(commandLine); UpdateProjectOptions_NoLock(); } + + static IEnumerable EnumerateLines( + ITemporaryStorageStreamHandle storageHandle) + { + using var stream = storageHandle.ReadFromTemporaryStorage(CancellationToken.None); + using var reader = new StreamReader(stream); + + while (reader.ReadLine() is string line) + yield return line; + } } /// @@ -276,7 +295,6 @@ public void Dispose() lock (_gate) { DisposeOfRuleSetFile_NoLock(); - _commandLineStorage?.Dispose(); } } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Checksum_Factory.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Checksum_Factory.cs index 95519835568d7..ead02e2f244d2 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Checksum_Factory.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Checksum_Factory.cs @@ -105,16 +105,12 @@ public static Checksum Create(ImmutableArray bytes) writer.WriteByte(b); }); - public static Checksum Create(T value, ISerializerService serializer) - { - using var context = new SolutionReplicationContext(); - - return Create( - (value, serializer, context), + public static Checksum Create(T value, ISerializerService serializer, CancellationToken cancellationToken) + => Create( + (value, serializer, cancellationToken), static (tuple, writer) => { - var (value, serializer, context) = tuple; - serializer.Serialize(value!, writer, context, CancellationToken.None); + var (value, serializer, cancellationToken) = tuple; + serializer.Serialize(value!, writer, cancellationToken); }); - } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ConstantTextAndVersionSource.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ConstantTextAndVersionSource.cs index 32714a34a1c44..3313ec6f1c845 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ConstantTextAndVersionSource.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ConstantTextAndVersionSource.cs @@ -19,6 +19,12 @@ internal sealed class ConstantTextAndVersionSource(TextAndVersion value) : IText public bool CanReloadText => false; + /// + /// Not built from a text loader. + /// + public TextLoader? TextLoader + => null; + public TextAndVersion GetValue(LoadTextOptions options, CancellationToken cancellationToken) => _value; diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs index 9c92c71478ee4..8e7daaa323d16 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs @@ -31,7 +31,7 @@ internal partial class DocumentState : TextDocumentState private readonly ParseOptions? _options; // null if the document doesn't support syntax trees: - private readonly AsyncLazy? _treeSource; + private readonly ITreeAndVersionSource? _treeSource; protected DocumentState( LanguageServices languageServices, @@ -40,7 +40,7 @@ protected DocumentState( ParseOptions? options, ITextAndVersionSource textSource, LoadTextOptions loadTextOptions, - AsyncLazy? treeSource) + ITreeAndVersionSource? treeSource) : base(languageServices.SolutionServices, documentServiceProvider, attributes, textSource, loadTextOptions) { Contract.ThrowIfFalse(_options is null == _treeSource is null); @@ -79,7 +79,7 @@ public DocumentState( } } - public AsyncLazy? TreeSource => _treeSource; + public ITreeAndVersionSource? TreeSource => _treeSource; [MemberNotNullWhen(true, nameof(_treeSource))] [MemberNotNullWhen(true, nameof(TreeSource))] @@ -97,7 +97,7 @@ public SourceCodeKind SourceCodeKind public bool IsGenerated => Attributes.IsGenerated; - protected static AsyncLazy CreateLazyFullyParsedTree( + protected static ITreeAndVersionSource CreateLazyFullyParsedTree( ITextAndVersionSource newTextSource, LoadTextOptions loadTextOptions, string? filePath, @@ -105,7 +105,7 @@ protected static AsyncLazy CreateLazyFullyParsedTree( LanguageServices languageServices, PreservationMode mode = PreservationMode.PreserveValue) { - return AsyncLazy.Create( + return SimpleTreeAndVersionSource.Create( static (arg, c) => FullyParseTreeAsync(arg.newTextSource, arg.loadTextOptions, arg.filePath, arg.options, arg.languageServices, arg.mode, c), static (arg, c) => FullyParseTree(arg.newTextSource, arg.loadTextOptions, arg.filePath, arg.options, arg.languageServices, arg.mode, c), arg: (newTextSource, loadTextOptions, filePath, options, languageServices, mode)); @@ -163,19 +163,19 @@ private static TreeAndVersion CreateTreeAndVersion( return new TreeAndVersion(tree, textAndVersion.Version); } - private static AsyncLazy CreateLazyIncrementallyParsedTree( - AsyncLazy oldTreeSource, + private static ITreeAndVersionSource CreateLazyIncrementallyParsedTree( + ITreeAndVersionSource oldTreeSource, ITextAndVersionSource newTextSource, LoadTextOptions loadTextOptions) { - return AsyncLazy.Create( + return SimpleTreeAndVersionSource.Create( static (arg, c) => IncrementallyParseTreeAsync(arg.oldTreeSource, arg.newTextSource, arg.loadTextOptions, c), static (arg, c) => IncrementallyParseTree(arg.oldTreeSource, arg.newTextSource, arg.loadTextOptions, c), arg: (oldTreeSource, newTextSource, loadTextOptions)); } private static async Task IncrementallyParseTreeAsync( - AsyncLazy oldTreeSource, + ITreeAndVersionSource oldTreeSource, ITextAndVersionSource newTextSource, LoadTextOptions loadTextOptions, CancellationToken cancellationToken) @@ -197,7 +197,7 @@ private static async Task IncrementallyParseTreeAsync( } private static TreeAndVersion IncrementallyParseTree( - AsyncLazy oldTreeSource, + ITreeAndVersionSource oldTreeSource, ITextAndVersionSource newTextSource, LoadTextOptions loadTextOptions, CancellationToken cancellationToken) @@ -346,7 +346,7 @@ private DocumentState SetParseOptions(ParseOptions options, bool onlyPreprocesso throw new InvalidOperationException(); } - AsyncLazy? newTreeSource = null; + ITreeAndVersionSource? newTreeSource = null; // Optimization: if we are only changing preprocessor directives, and we've already parsed the existing tree // and it didn't have any, we can avoid a reparse since the tree will be parsed the same. @@ -368,7 +368,7 @@ private DocumentState SetParseOptions(ParseOptions options, bool onlyPreprocesso } if (newTree is not null) - newTreeSource = AsyncLazy.Create(new TreeAndVersion(newTree, existingTreeAndVersion.Version)); + newTreeSource = SimpleTreeAndVersionSource.Create(new TreeAndVersion(newTree, existingTreeAndVersion.Version)); } // If we weren't able to reuse in a smart way, just reparse @@ -457,7 +457,7 @@ public DocumentState UpdateFilePath(string? filePath) protected override TextDocumentState UpdateText(ITextAndVersionSource newTextSource, PreservationMode mode, bool incremental) { - AsyncLazy? newTreeSource; + ITreeAndVersionSource? newTreeSource; if (!SupportsSyntaxTree) { @@ -528,7 +528,7 @@ internal DocumentState UpdateTree(SyntaxNode newRoot, PreservationMode mode) _options, textSource: text, LoadTextOptions, - treeSource: AsyncLazy.Create(treeAndVersion)); + treeSource: SimpleTreeAndVersionSource.Create(treeAndVersion)); // use static method so we don't capture references to this static (ITextAndVersionSource, TreeAndVersion) CreateTreeWithLazyText( diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_LinkedFileReuse.cs b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_LinkedFileReuse.cs index 9353d1f140a87..8c481c1b4a3be 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_LinkedFileReuse.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_LinkedFileReuse.cs @@ -13,6 +13,29 @@ namespace Microsoft.CodeAnalysis; internal partial class DocumentState { + /// + /// when we're linked to another file (a 'sibling') and will attempt to reuse + /// that sibling's tree as our own. Note: we won't know if we can actually use the contents of that sibling file + /// until we actually go and realize it, as it may contains constructs (like pp-directives) that prevent use. In + /// that case, we'll fall back to a normal incremental parse between our original and the latest text contents of our sibling's file. + /// + private sealed class LinkedFileReuseTreeAndVersionSource( + ITreeAndVersionSource originalTreeSource, + AsyncLazy lazyComputation) : ITreeAndVersionSource + { + public readonly ITreeAndVersionSource OriginalTreeSource = originalTreeSource; + + public Task GetValueAsync(CancellationToken cancellationToken) + => lazyComputation.GetValueAsync(cancellationToken); + + public TreeAndVersion GetValue(CancellationToken cancellationToken) + => lazyComputation.GetValue(cancellationToken); + + public bool TryGetValue([NotNullWhen(true)] out TreeAndVersion? value) + => lazyComputation.TryGetValue(out value); + } + /// /// Returns a new instance of this document state that points to as the /// text contents of the document, and which will produce a syntax tree that reuses from public DocumentState UpdateTextAndTreeContents( ITextAndVersionSource siblingTextSource, - AsyncLazy? siblingTreeSource, + ITreeAndVersionSource? siblingTreeSource, bool forceEvenIfTreesWouldDiffer) { if (!SupportsSyntaxTree) @@ -38,50 +61,52 @@ public DocumentState UpdateTextAndTreeContents( Contract.ThrowIfNull(siblingTreeSource); + // We don't want to point at a long chain of transformations as our sibling files change, deferring to each next + // link of the chain to potentially do the work (or potentially failing out). So, if we're about to do this, + // instead return our original tree-source so that in the case we are unable to use the sibling file's root, we + // can do a single step incremental parse between our original tree and the final sibling text. + // + // We only need to look one deep here as we'll pull that tree source forward to our level. If another link is + // later added to us, it will do the same thing. + var originalTreeSource = this.TreeSource; + if (originalTreeSource is LinkedFileReuseTreeAndVersionSource linkedFileTreeAndVersionSource) + originalTreeSource = linkedFileTreeAndVersionSource.OriginalTreeSource; + // Always pass along the sibling text. We will always be in sync with that. - // if a sibling tree source is provided, then we'll want to attempt to use the tree it creates, to share as - // much memory as possible with linked files. However, we can't point at that source directly. If we did, - // we'd produce the *exact* same tree-reference as another file. That would be bad as it would break the - // invariant that each document gets a unique SyntaxTree. So, instead, we produce a ValueSource that defers - // to the provided source, gets the tree from it, and then wraps its root in a new tree for us. + // Defer to static helper to make sure we don't accidentally capture anything else we don't want off of 'this' + // (like "this.TreeSource"). + return UpdateTextAndTreeContentsWorker( + this.Attributes, this.LanguageServices, this.Services, this.LoadTextOptions, this.ParseOptions, + originalTreeSource, siblingTextSource, siblingTreeSource, forceEvenIfTreesWouldDiffer); + } - // copy data from this entity, and pass to static helper, so we don't keep this green node alive. + private static DocumentState UpdateTextAndTreeContentsWorker( + DocumentInfo.DocumentAttributes attributes, + LanguageServices languageServices, + IDocumentServiceProvider services, + LoadTextOptions loadTextOptions, + ParseOptions parseOptions, + ITreeAndVersionSource originalTreeSource, + ITextAndVersionSource siblingTextSource, + ITreeAndVersionSource siblingTreeSource, + bool forceEvenIfTreesWouldDiffer) + { + // if a sibling tree source is provided, then we'll want to attempt to use the tree it creates, to share as much + // memory as possible with linked files. However, we can't point at that source directly. If we did, we'd + // produce the *exact* same tree-reference as another file. That would be bad as it would break the invariant + // that each document gets a unique SyntaxTree. So, instead, we produce a tree-source that defers to the + // provided source, gets the tree from it, and then wraps its root in a new tree for us. - var filePath = this.Attributes.SyntaxTreeFilePath; - var languageServices = this.LanguageServices; - var loadTextOptions = this.LoadTextOptions; - var parseOptions = this.ParseOptions; - var textAndVersionSource = this.TextAndVersionSource; - var treeSource = this.TreeSource; + var lazyComputation = AsyncLazy.Create( + static (arg, cancellationToken) => TryReuseSiblingTreeAsync(arg.filePath, arg.languageServices, arg.loadTextOptions, arg.parseOptions, arg.originalTreeSource, arg.siblingTextSource, arg.siblingTreeSource, arg.forceEvenIfTreesWouldDiffer, cancellationToken), + static (arg, cancellationToken) => TryReuseSiblingTree(arg.filePath, arg.languageServices, arg.loadTextOptions, arg.parseOptions, arg.originalTreeSource, arg.siblingTextSource, arg.siblingTreeSource, arg.forceEvenIfTreesWouldDiffer, cancellationToken), + arg: (filePath: attributes.SyntaxTreeFilePath, languageServices, loadTextOptions, parseOptions, originalTreeSource, siblingTextSource, siblingTreeSource, forceEvenIfTreesWouldDiffer)); - var newTreeSource = GetReuseTreeSource( - filePath, languageServices, loadTextOptions, parseOptions, treeSource, siblingTextSource, siblingTreeSource, forceEvenIfTreesWouldDiffer); + var newTreeSource = new LinkedFileReuseTreeAndVersionSource(originalTreeSource, lazyComputation); return new DocumentState( - languageServices, - Services, - Attributes, - _options, - siblingTextSource, - LoadTextOptions, - newTreeSource); - - static AsyncLazy GetReuseTreeSource( - string filePath, - LanguageServices languageServices, - LoadTextOptions loadTextOptions, - ParseOptions parseOptions, - AsyncLazy treeSource, - ITextAndVersionSource siblingTextSource, - AsyncLazy siblingTreeSource, - bool forceEvenIfTreesWouldDiffer) - { - return AsyncLazy.Create( - static (arg, cancellationToken) => TryReuseSiblingTreeAsync(arg.filePath, arg.languageServices, arg.loadTextOptions, arg.parseOptions, arg.treeSource, arg.siblingTextSource, arg.siblingTreeSource, arg.forceEvenIfTreesWouldDiffer, cancellationToken), - static (arg, cancellationToken) => TryReuseSiblingTree(arg.filePath, arg.languageServices, arg.loadTextOptions, arg.parseOptions, arg.treeSource, arg.siblingTextSource, arg.siblingTreeSource, arg.forceEvenIfTreesWouldDiffer, cancellationToken), - arg: (filePath, languageServices, loadTextOptions, parseOptions, treeSource, siblingTextSource, siblingTreeSource, forceEvenIfTreesWouldDiffer)); - } + languageServices, services, attributes, parseOptions, siblingTextSource, loadTextOptions, newTreeSource); static bool TryReuseSiblingRoot( string filePath, @@ -186,9 +211,9 @@ static async Task TryReuseSiblingTreeAsync( LanguageServices languageServices, LoadTextOptions loadTextOptions, ParseOptions parseOptions, - AsyncLazy treeSource, + ITreeAndVersionSource treeSource, ITextAndVersionSource siblingTextSource, - AsyncLazy siblingTreeSource, + ITreeAndVersionSource siblingTreeSource, bool forceEvenIfTreesWouldDiffer, CancellationToken cancellationToken) { @@ -209,9 +234,9 @@ static TreeAndVersion TryReuseSiblingTree( LanguageServices languageServices, LoadTextOptions loadTextOptions, ParseOptions parseOptions, - AsyncLazy treeSource, + ITreeAndVersionSource treeSource, ITextAndVersionSource siblingTextSource, - AsyncLazy siblingTreeSource, + ITreeAndVersionSource siblingTreeSource, bool forceEvenIfTreesWouldDiffer, CancellationToken cancellationToken) { diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_TreeTextSource.cs b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_TreeTextSource.cs index 30eaca390f9cb..42553e056311c 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_TreeTextSource.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_TreeTextSource.cs @@ -22,6 +22,12 @@ private sealed class TreeTextSource(AsyncLazy textSource, VersionSta public bool CanReloadText => false; + /// + /// Not created from a text loader. + /// + public TextLoader? TextLoader + => null; + public async Task GetValueAsync(LoadTextOptions options, CancellationToken cancellationToken) { var text = await textSource.GetValueAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs index 1ead6e28e9b29..7cbe5fd3bbb63 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; @@ -287,9 +288,13 @@ public async ValueTask> GetSourceGeneratedD ImmutableInterlocked.GetOrAdd(ref _idToSourceGeneratedDocumentMap, state.Id, s_createSourceGeneratedDocumentFunction, (state, this))); } - internal async ValueTask> GetAllRegularAndSourceGeneratedDocumentsAsync(CancellationToken cancellationToken = default) + internal async IAsyncEnumerable GetAllRegularAndSourceGeneratedDocumentsAsync([EnumeratorCancellation] CancellationToken cancellationToken) { - return Documents.Concat(await GetSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false)); + foreach (var document in this.Documents) + yield return document; + + foreach (var document in await GetSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false)) + yield return document; } public async ValueTask GetSourceGeneratedDocumentAsync(DocumentId documentId, CancellationToken cancellationToken = default) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectDependencyGraph.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectDependencyGraph.cs index 69e3298087b6b..db5c420b28c49 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectDependencyGraph.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectDependencyGraph.cs @@ -270,7 +270,7 @@ private ImmutableHashSet GetProjectsThatThisProjectTransitivelyDepend using var pooledObject = SharedPools.Default>().GetPooledObject(); var results = pooledObject.Object; this.ComputeTransitiveReferences(projectId, results); - transitiveReferences = results.ToImmutableHashSet(); + transitiveReferences = [.. results]; _transitiveReferencesMap = _transitiveReferencesMap.Add(projectId, transitiveReferences); } @@ -323,7 +323,7 @@ private ImmutableHashSet GetProjectsThatTransitivelyDependOnThisProje var results = pooledObject.Object; ComputeReverseTransitiveReferences(projectId, results); - reverseTransitiveReferences = results.ToImmutableHashSet(); + reverseTransitiveReferences = [.. results]; _reverseTransitiveReferencesMap = _reverseTransitiveReferencesMap.Add(projectId, reverseTransitiveReferences); } @@ -367,7 +367,7 @@ private void GetTopologicallySortedProjects_NoLock(CancellationToken cancellatio using var seenProjects = SharedPools.Default>().GetPooledObject(); using var resultList = SharedPools.Default>().GetPooledObject(); this.TopologicalSort(_projectIds, seenProjects.Object, resultList.Object, cancellationToken); - _lazyTopologicallySortedProjects = resultList.Object.ToImmutableArray(); + _lazyTopologicallySortedProjects = [.. resultList.Object]; } } @@ -419,7 +419,7 @@ private ImmutableArray> GetDependencySets_NoLock(Cancella using var seenProjects = SharedPools.Default>().GetPooledObject(); using var results = SharedPools.Default>>().GetPooledObject(); this.ComputeDependencySets(seenProjects.Object, results.Object, cancellationToken); - _lazyDependencySets = results.Object.ToImmutableArray(); + _lazyDependencySets = [.. results.Object]; } return _lazyDependencySets; diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectDependencyGraph_AddProjectReference.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectDependencyGraph_AddProjectReference.cs index 56a51c3f5750a..d7e48d3bf3dd3 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectDependencyGraph_AddProjectReference.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectDependencyGraph_AddProjectReference.cs @@ -66,7 +66,7 @@ private static ImmutableDictionary> Compu } else { - return existingReferencesMap.SetItem(projectId, referencedProjectIds.ToImmutableHashSet()); + return existingReferencesMap.SetItem(projectId, [.. referencedProjectIds]); } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs index 3d6f1098fbd04..df13aee1dcd6a 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs @@ -194,6 +194,14 @@ private static Project CreateProject(ProjectId projectId, Solution solution) internal Project? GetOriginatingProject(ISymbol symbol) => GetProject(GetOriginatingProjectId(symbol)); + /// + /// + /// Returns the that produced the symbol. In the case of a symbol that was retargetted + /// this will be the compilation it was retargtted into, not the original compilation that it was retargetted from. + /// + internal Compilation? GetOriginatingCompilation(ISymbol symbol) + => _compilationState.GetOriginatingProjectInfo(symbol)?.Compilation; + /// /// True if the solution contains the document in one of its projects /// diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.CompilationTrackerState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.CompilationTrackerState.cs index abffc060a6830..347957df7875f 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.CompilationTrackerState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.CompilationTrackerState.cs @@ -147,12 +147,9 @@ private sealed class FinalCompilationTrackerState : CompilationTrackerState public readonly bool HasSuccessfullyLoaded; /// - /// Weak set of the assembly, module and dynamic symbols that this compilation tracker has created. - /// This can be used to determine which project an assembly symbol came from after the fact. This is - /// needed as the compilation an assembly came from can be GC'ed and further requests to get that - /// compilation (or any of it's assemblies) may produce new assembly symbols. + /// Used to determine which project an assembly symbol came from after the fact. /// - public readonly UnrootedSymbolSet UnrootedSymbolSet; + private SingleInitNullable _rootedSymbolSet; /// /// The final compilation, with all references and source generators run. This is distinct from _rootedSymbolSet.Initialize( + static finalCompilationWithGeneratedDocuments => RootedSymbolSet.Create(finalCompilationWithGeneratedDocuments), + this.FinalCompilationWithGeneratedDocuments); + public FinalCompilationTrackerState WithCreationPolicy(CreationPolicy creationPolicy) => creationPolicy == this.CreationPolicy ? this @@ -235,8 +232,7 @@ public FinalCompilationTrackerState WithCreationPolicy(CreationPolicy creationPo FinalCompilationWithGeneratedDocuments, CompilationWithoutGeneratedDocuments, HasSuccessfullyLoaded, - GeneratorInfo, - UnrootedSymbolSet); + GeneratorInfo); private static void RecordAssemblySymbols(ProjectId projectId, Compilation compilation, Dictionary? metadataReferenceToProjectId) { diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs index e0a5cd666f168..5f1b44eaf6799 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs @@ -100,23 +100,22 @@ public GeneratorDriver? GeneratorDriver } } - public bool ContainsAssemblyOrModuleOrDynamic(ISymbol symbol, bool primary, out MetadataReferenceInfo? referencedThrough) + public bool ContainsAssemblyOrModuleOrDynamic( + ISymbol symbol, bool primary, + [NotNullWhen(true)] out Compilation? compilation, + out MetadataReferenceInfo? referencedThrough) { - Debug.Assert(symbol.Kind is SymbolKind.Assembly or - SymbolKind.NetModule or - SymbolKind.DynamicType); - var state = this.ReadState(); - - var unrootedSymbolSet = (state as FinalCompilationTrackerState)?.UnrootedSymbolSet; - if (unrootedSymbolSet == null) + Debug.Assert(symbol.Kind is SymbolKind.Assembly or SymbolKind.NetModule or SymbolKind.DynamicType); + if (this.ReadState() is not FinalCompilationTrackerState finalState) { // this was not a tracker that has handed out a compilation (all compilations handed out must be // owned by a 'FinalState'). So this symbol could not be from us. + compilation = null; referencedThrough = null; return false; } - return unrootedSymbolSet.Value.ContainsAssemblyOrModuleOrDynamic(symbol, primary, out referencedThrough); + return finalState.RootedSymbolSet.ContainsAssemblyOrModuleOrDynamic(symbol, primary, out compilation, out referencedThrough); } /// @@ -529,18 +528,23 @@ await compilationState.GetCompilationAsync( if (creationPolicy.SkeletonReferenceCreationPolicy is SkeletonReferenceCreationPolicy.Create) { - // Client always wants an up to date metadata reference. Produce one for this project reference. - var metadataReference = await compilationState.GetMetadataReferenceAsync(projectReference, this.ProjectState, cancellationToken).ConfigureAwait(false); + // Client always wants an up to date metadata reference. Produce one for this project + // reference. Because the policy is to always 'Create' here, we include cross language + // references, producing skeletons for them if necessary. + var metadataReference = await compilationState.GetMetadataReferenceAsync( + projectReference, this.ProjectState, includeCrossLanguage: true, cancellationToken).ConfigureAwait(false); AddMetadataReference(projectReference, metadataReference); } else { Contract.ThrowIfFalse(creationPolicy.SkeletonReferenceCreationPolicy is SkeletonReferenceCreationPolicy.CreateIfAbsent or SkeletonReferenceCreationPolicy.DoNotCreate); - // If not asked to explicit create an up to date skeleton, attempt to get a partial - // reference, or fallback to the last successful reference for this project if we can - // find one. - var metadataReference = compilationState.GetPartialMetadataReference(projectReference, this.ProjectState); + // Client does not want to force a skeleton reference to be created. Try to get a + // metadata reference cheaply in the case where this is a reference to the same + // language. If that fails, also attempt to get a reference to a skeleton assembly + // produced from one of our prior stale compilations. + var metadataReference = await compilationState.GetMetadataReferenceAsync( + projectReference, this.ProjectState, includeCrossLanguage: false, cancellationToken).ConfigureAwait(false); if (metadataReference is null) { var inProgressCompilationNotRef = staleCompilationWithGeneratedDocuments ?? compilationWithoutGeneratedDocuments; @@ -552,7 +556,8 @@ await compilationState.GetCompilationAsync( // create a real skeleton here. if (metadataReference is null && creationPolicy.SkeletonReferenceCreationPolicy is SkeletonReferenceCreationPolicy.CreateIfAbsent) { - metadataReference = await compilationState.GetMetadataReferenceAsync(projectReference, this.ProjectState, cancellationToken).ConfigureAwait(false); + metadataReference = await compilationState.GetMetadataReferenceAsync( + projectReference, this.ProjectState, includeCrossLanguage: true, cancellationToken).ConfigureAwait(false); } AddMetadataReference(projectReference, metadataReference); @@ -647,32 +652,6 @@ private Compilation CreateEmptyCompilation() } } - /// - /// Attempts to get (without waiting) a metadata reference to a possibly in progress - /// compilation. Only actual compilation references are returned. Could potentially - /// return null if nothing can be provided. - /// - public MetadataReference? GetPartialMetadataReference(ProjectState fromProject, ProjectReference projectReference) - { - if (ProjectState.LanguageServices == fromProject.LanguageServices) - { - // if we have a compilation and its the correct language, use a simple compilation reference in any - // state it happens to be in right now - if (ReadState() is CompilationTrackerState compilationState) - return compilationState.CompilationWithoutGeneratedDocuments.ToMetadataReference(projectReference.Aliases, projectReference.EmbedInteropTypes); - } - else - { - // Cross project reference. We need a skeleton reference. Skeletons are too expensive to - // generate on demand. So just try to see if we can grab the last generated skeleton for that - // project. - var properties = new MetadataReferenceProperties(aliases: projectReference.Aliases, embedInteropTypes: projectReference.EmbedInteropTypes); - return _skeletonReferenceCache.TryGetAlreadyBuiltMetadataReference(properties); - } - - return null; - } - public Task HasSuccessfullyLoadedAsync( SolutionCompilationState compilationState, CancellationToken cancellationToken) { @@ -1019,7 +998,7 @@ private static void ValidateCompilationTreesMatchesProjectState(Compilation comp } /// - /// This is just the same as but throws a custom exception type to make this easier to find in telemetry since the exception type + /// This is just the same as but throws a custom exception type to make this easier to find in telemetry since the exception type /// is easily seen in telemetry. /// private static void ThrowExceptionIfFalse([DoesNotReturnIf(parameterValue: false)] bool condition, string message) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs index 990d4467db2a7..d59b89fe1bcc9 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs @@ -50,18 +50,21 @@ public GeneratedFileReplacingCompilationTracker( _skeletonReferenceCache = underlyingTracker.GetClonedSkeletonReferenceCache(); } - public bool ContainsAssemblyOrModuleOrDynamic(ISymbol symbol, bool primary, out MetadataReferenceInfo? referencedThrough) + public bool ContainsAssemblyOrModuleOrDynamic( + ISymbol symbol, bool primary, + [NotNullWhen(true)] out Compilation? compilation, + out MetadataReferenceInfo? referencedThrough) { if (_compilationWithReplacements == null) { // We don't have a compilation yet, so this couldn't have came from us + compilation = null; referencedThrough = null; return false; } - else - { - return UnrootedSymbolSet.Create(_compilationWithReplacements).ContainsAssemblyOrModuleOrDynamic(symbol, primary, out referencedThrough); - } + + return RootedSymbolSet.Create(_compilationWithReplacements).ContainsAssemblyOrModuleOrDynamic( + symbol, primary, out compilation, out referencedThrough); } public ICompilationTracker Fork(ProjectState newProject, TranslationAction? translate) @@ -147,18 +150,6 @@ private async Task ComputeDependentChecksumAsync(SolutionCompilationSt await UnderlyingTracker.GetDependentChecksumAsync(compilationState, cancellationToken).ConfigureAwait(false), (await _replacementDocumentStates.GetDocumentChecksumsAndIdsAsync(cancellationToken).ConfigureAwait(false)).Checksum); - public MetadataReference? GetPartialMetadataReference(ProjectState fromProject, ProjectReference projectReference) - { - // This method is used if you're forking a solution with partial semantics, and used to quickly produce references. - // So this method should only be called if: - // - // 1. Project A has a open source generated document, and this CompilationTracker represents A - // 2. Project B references that A, and is being frozen for partial semantics. - // - // We generally don't use partial semantics in a different project than the open file, so this isn't a scenario we need to support. - throw new NotImplementedException(); - } - public async ValueTask> GetSourceGeneratedDocumentStatesAsync( SolutionCompilationState compilationState, CancellationToken cancellationToken) { diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.ICompilationTracker.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.ICompilationTracker.cs index c910b9301214d..80ef8db9493ac 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.ICompilationTracker.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.ICompilationTracker.cs @@ -30,7 +30,10 @@ private interface ICompilationTracker /// of the symbols returned by for /// any of the references of the . /// - bool ContainsAssemblyOrModuleOrDynamic(ISymbol symbol, bool primary, out MetadataReferenceInfo? referencedThrough); + bool ContainsAssemblyOrModuleOrDynamic( + ISymbol symbol, bool primary, + [NotNullWhen(true)] out Compilation? compilation, + out MetadataReferenceInfo? referencedThrough); ICompilationTracker Fork(ProjectState newProject, TranslationAction? translate); @@ -49,7 +52,6 @@ private interface ICompilationTracker Task GetDependentSemanticVersionAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken); Task GetDependentChecksumAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken); - MetadataReference? GetPartialMetadataReference(ProjectState fromProject, ProjectReference projectReference); ValueTask> GetSourceGeneratedDocumentStatesAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken); ValueTask> GetSourceGeneratorDiagnosticsAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken); ValueTask GetSourceGeneratorRunResultAsync(SolutionCompilationState solution, CancellationToken cancellationToken); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.RootedSymbolSet.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.RootedSymbolSet.cs new file mode 100644 index 0000000000000..c33523099f78e --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.RootedSymbolSet.cs @@ -0,0 +1,152 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.PooledObjects; +using ReferenceEqualityComparer = Roslyn.Utilities.ReferenceEqualityComparer; + +namespace Microsoft.CodeAnalysis; + +using SecondaryReferencedSymbol = (int hashCode, ISymbol symbol, SolutionCompilationState.MetadataReferenceInfo referenceInfo); + +internal partial class SolutionCompilationState +{ + internal readonly record struct MetadataReferenceInfo(MetadataReferenceProperties Properties, string? FilePath) + { + internal static MetadataReferenceInfo From(MetadataReference reference) + => new(reference.Properties, (reference as PortableExecutableReference)?.FilePath); + } + + /// + /// Information maintained for unrooted symbols. + /// + /// + /// The project the symbol originated from, i.e. the symbol is defined in the project or its metadata reference. + /// + /// + /// The Compilation that produced the symbol. + /// + /// + /// If the symbol is defined in a metadata reference of , information about the + /// reference. + /// + internal sealed record class OriginatingProjectInfo( + ProjectId ProjectId, + Compilation? Compilation, + MetadataReferenceInfo? ReferencedThrough); + + /// + /// A helper type for mapping back to an originating /. + /// + /// + /// In IDE scenarios we have the need to map from an to the that + /// contained a that could have produced that symbol. This is especially needed with OOP + /// scenarios where we have to communicate to OOP from VS (And vice versa) what symbol we are referring to. To do + /// this, we pass along a project where this symbol could be found, and enough information (a ) to resolve that symbol back in that that . + /// + private readonly struct RootedSymbolSet + { + public readonly Compilation Compilation; + + /// + /// The s or s produced through for all the references exposed by . Sorted by the hash code produced by so that it can be binary searched efficiently. + /// + public readonly ImmutableArray SecondaryReferencedSymbols; + + private RootedSymbolSet( + Compilation compilation, + ImmutableArray secondaryReferencedSymbols) + { + Compilation = compilation; + SecondaryReferencedSymbols = secondaryReferencedSymbols; + } + + public static RootedSymbolSet Create(Compilation compilation) + { + // PERF: Preallocate this array so we don't have to resize it as we're adding assembly symbols. + using var _ = ArrayBuilder.GetInstance( + compilation.ExternalReferences.Length + compilation.DirectiveReferences.Length, out var secondarySymbols); + + foreach (var reference in compilation.References) + { + var symbol = compilation.GetAssemblyOrModuleSymbol(reference); + if (symbol == null) + continue; + + secondarySymbols.Add((ReferenceEqualityComparer.GetHashCode(symbol), symbol, MetadataReferenceInfo.From(reference))); + } + + // Sort all the secondary symbols by their hash. This will allow us to easily binary search for them + // afterwards. Note: it is fine for multiple symbols to have the same reference hash. The search algorithm + // will account for that. + secondarySymbols.Sort(static (x, y) => x.hashCode.CompareTo(y.hashCode)); + return new RootedSymbolSet(compilation, secondarySymbols.ToImmutable()); + } + + public bool ContainsAssemblyOrModuleOrDynamic( + ISymbol symbol, bool primary, + [NotNullWhen(true)] out Compilation? compilation, + out MetadataReferenceInfo? referencedThrough) + { + if (primary) + { + if (this.Compilation.Assembly.Equals(symbol)) + { + compilation = this.Compilation; + referencedThrough = null; + return true; + } + + if (this.Compilation.Language == LanguageNames.CSharp && + this.Compilation.DynamicType.Equals(symbol)) + { + compilation = this.Compilation; + referencedThrough = null; + return true; + } + } + else + { + var secondarySymbols = this.SecondaryReferencedSymbols; + + var symbolHash = ReferenceEqualityComparer.GetHashCode(symbol); + + // The secondary symbol array is sorted by the symbols' hash codes. So do a binary search to find + // the location we should start looking at. + var index = secondarySymbols.BinarySearch(symbolHash, static (item, symbolHash) => item.hashCode.CompareTo(symbolHash)); + if (index >= 0) + { + // Could have multiple symbols with the same hash. They will all be placed next to each other, + // so walk backward to hit the first. + while (index > 0 && secondarySymbols[index - 1].hashCode == symbolHash) + index--; + + // Now, walk forward through the stored symbols with the same hash looking to see if any are a reference match. + while (index < secondarySymbols.Length && secondarySymbols[index].hashCode == symbolHash) + { + var cached = secondarySymbols[index]; + if (cached.symbol.Equals(symbol)) + { + referencedThrough = cached.referenceInfo; + compilation = this.Compilation; + return true; + } + + index++; + } + } + } + + compilation = null; + referencedThrough = null; + return false; + } + } +} diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceCache.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceCache.cs index f0e1ffc395db2..2fbf3bc719d90 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceCache.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceCache.cs @@ -218,74 +218,79 @@ public readonly SkeletonReferenceCache Clone() private static SkeletonReferenceSet? CreateSkeletonSet( SolutionServices services, Compilation compilation, CancellationToken cancellationToken) { - var storage = TryCreateMetadataStorage(services, compilation, cancellationToken); - if (storage == null) + var (metadata, storageHandle) = TryCreateMetadataAndHandle(); + if (metadata == null) return null; - var metadata = AssemblyMetadata.CreateFromStream(storage.ReadStream(cancellationToken), leaveOpen: false); - // read in the stream and pass ownership of it to the metadata object. When it is disposed it will dispose // the stream as well. return new SkeletonReferenceSet( metadata, + storageHandle, compilation.AssemblyName, new DeferredDocumentationProvider(compilation)); - } - - private static ITemporaryStreamStorageInternal? TryCreateMetadataStorage(SolutionServices services, Compilation compilation, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var logger = services.GetService(); - try + (AssemblyMetadata? metadata, ITemporaryStorageStreamHandle storageHandle) TryCreateMetadataAndHandle() { - logger?.Log($"Beginning to create a skeleton assembly for {compilation.AssemblyName}..."); + cancellationToken.ThrowIfCancellationRequested(); - using (Logger.LogBlock(FunctionId.Workspace_SkeletonAssembly_EmitMetadataOnlyImage, cancellationToken)) - { - using var stream = SerializableBytes.CreateWritableStream(); + var logger = services.GetService(); - var emitResult = compilation.Emit(stream, options: s_metadataOnlyEmitOptions, cancellationToken: cancellationToken); + try + { + logger?.Log($"Beginning to create a skeleton assembly for {compilation.AssemblyName}..."); - if (emitResult.Success) + using (Logger.LogBlock(FunctionId.Workspace_SkeletonAssembly_EmitMetadataOnlyImage, cancellationToken)) { - logger?.Log($"Successfully emitted a skeleton assembly for {compilation.AssemblyName}"); + // First, emit the data to an in-memory stream. + using var stream = SerializableBytes.CreateWritableStream(); + var emitResult = compilation.Emit(stream, options: s_metadataOnlyEmitOptions, cancellationToken: cancellationToken); - var temporaryStorageService = services.GetRequiredService(); - var storage = temporaryStorageService.CreateTemporaryStreamStorage(); + if (emitResult.Success) + { + logger?.Log($"Successfully emitted a skeleton assembly for {compilation.AssemblyName}"); - stream.Position = 0; - storage.WriteStream(stream, cancellationToken); + var temporaryStorageService = services.GetRequiredService(); - return storage; - } + // Then, dump that in-memory-stream to a memory-mapped file. Doing this allows us to have the + // assembly-metadata point directly to that pointer in memory, instead of it having to make its + // own copy it needs to own the lifetime of. + var handle = temporaryStorageService.WriteToTemporaryStorage(stream, cancellationToken); - if (logger != null) - { - logger.Log($"Failed to create a skeleton assembly for {compilation.AssemblyName}:"); + // Now read the data back from the stream from the memory mapped file. This will come back as an + // UnmanagedMemoryStream, which our assembly/metadata subsystem is optimized around. + var result = AssemblyMetadata.CreateFromStream( + handle.ReadFromTemporaryStorage(cancellationToken), leaveOpen: false); - foreach (var diagnostic in emitResult.Diagnostics) + return (result, handle); + } + + if (logger != null) { - logger.Log(" " + diagnostic.GetMessage()); + logger.Log($"Failed to create a skeleton assembly for {compilation.AssemblyName}:"); + + foreach (var diagnostic in emitResult.Diagnostics) + { + logger.Log(" " + diagnostic.GetMessage()); + } } - } - // log emit failures so that we can improve most common cases - Logger.Log(FunctionId.MetadataOnlyImage_EmitFailure, KeyValueLogMessage.Create(m => - { - // log errors in the format of - // CS0001:1;CS002:10;... - var groups = emitResult.Diagnostics.GroupBy(d => d.Id).Select(g => $"{g.Key}:{g.Count()}"); - m["Errors"] = string.Join(";", groups); - })); + // log emit failures so that we can improve most common cases + Logger.Log(FunctionId.MetadataOnlyImage_EmitFailure, KeyValueLogMessage.Create(m => + { + // log errors in the format of + // CS0001:1;CS002:10;... + var groups = emitResult.Diagnostics.GroupBy(d => d.Id).Select(g => $"{g.Key}:{g.Count()}"); + m["Errors"] = string.Join(";", groups); + })); - return null; + return (null, null!); + } + } + finally + { + logger?.Log($"Done trying to create a skeleton assembly for {compilation.AssemblyName}"); } - } - finally - { - logger?.Log($"Done trying to create a skeleton assembly for {compilation.AssemblyName}"); } } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceSet.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceSet.cs index 033c900fecff5..e3e9b84b07744 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceSet.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceSet.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using Microsoft.CodeAnalysis.Host; namespace Microsoft.CodeAnalysis; @@ -18,6 +19,7 @@ internal partial class SolutionCompilationState /// private sealed class SkeletonReferenceSet( AssemblyMetadata metadata, + ITemporaryStorageStreamHandle storageHandle, string? assemblyName, DeferredDocumentationProvider documentationProvider) { @@ -29,6 +31,8 @@ private sealed class SkeletonReferenceSet( /// private readonly Dictionary _referenceMap = []; + public ITemporaryStorageStreamHandle StorageHandle => storageHandle; + public PortableExecutableReference GetOrCreateMetadataReference(MetadataReferenceProperties properties) { lock (_referenceMap) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SymbolToProjectId.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SymbolToProjectId.cs index 52a13061db566..5e86a295e2ef4 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SymbolToProjectId.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SymbolToProjectId.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Diagnostics; using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Shared.Extensions; @@ -172,13 +173,16 @@ internal partial class SolutionCompilationState return projectId; } - else if (symbol.IsKind(SymbolKind.TypeParameter, out ITypeParameterSymbol? typeParameter) && - typeParameter.TypeParameterKind == TypeParameterKind.Cref) + else if (symbol is ITypeParameterSymbol + { + TypeParameterKind: TypeParameterKind.Cref, + Locations: [{ SourceTree: var typeParameterSourceTree }, ..], + }) { // Cref type parameters don't belong to any containing symbol. But we can map them to a doc/project // using the declaring syntax of the type parameter itself. - if (GetDocumentState(typeParameter.Locations[0].SourceTree, projectId: null) is { } document) - return new OriginatingProjectInfo(document.Id.ProjectId, ReferencedThrough: null); + if (GetDocumentState(typeParameterSourceTree, projectId: null) is { } document) + return new OriginatingProjectInfo(document.Id.ProjectId, Compilation: null, ReferencedThrough: null); } return null; @@ -187,8 +191,8 @@ internal partial class SolutionCompilationState { foreach (var (id, tracker) in _projectIdToTrackerMap) { - if (tracker.ContainsAssemblyOrModuleOrDynamic(symbol, primary, out var referencedThrough)) - return new OriginatingProjectInfo(id, referencedThrough); + if (tracker.ContainsAssemblyOrModuleOrDynamic(symbol, primary, out var compilation, out var referencedThrough)) + return new OriginatingProjectInfo(id, compilation, referencedThrough); } return null; diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.UnrootedSymbolSet.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.UnrootedSymbolSet.cs deleted file mode 100644 index 06837aab31a62..0000000000000 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.UnrootedSymbolSet.cs +++ /dev/null @@ -1,156 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using Microsoft.CodeAnalysis.PooledObjects; -using Roslyn.Utilities; -using ReferenceEqualityComparer = Roslyn.Utilities.ReferenceEqualityComparer; - -namespace Microsoft.CodeAnalysis; - -using SecondaryReferencedSymbol = (int hashCode, WeakReference symbol, SolutionCompilationState.MetadataReferenceInfo referenceInfo); - -internal partial class SolutionCompilationState -{ - internal readonly record struct MetadataReferenceInfo(MetadataReferenceProperties Properties, string? FilePath) - { - internal static MetadataReferenceInfo From(MetadataReference reference) - => new(reference.Properties, (reference as PortableExecutableReference)?.FilePath); - } - - /// - /// Information maintained for unrooted symbols. - /// - /// - /// The project the symbol originated from, i.e. the symbol is defined in the project or its metadata reference. - /// - /// - /// If the symbol is defined in a metadata reference of , information about the reference. - /// - internal sealed record class OriginatingProjectInfo(ProjectId ProjectId, MetadataReferenceInfo? ReferencedThrough); - - /// - /// A helper type for mapping back to an originating . - /// - /// - /// In IDE scenarios we have the need to map from an to the that - /// contained a that could have produced that symbol. This is especially needed with - /// OOP scenarios where we have to communicate to OOP from VS (And vice versa) what symbol we are referring to. - /// To do this, we pass along a project where this symbol could be found, and enough information (a ) to resolve that symbol back in that that . - /// - /// This is challenging however as symbols do not necessarily have back-pointers to s, - /// and as such, we can't just see which Project produced the that produced that . In other words, the doesn't root the compilation. Because - /// of that we keep track of those symbols per project in a weak fashion. Then, we can later see if a - /// symbol came from a particular project by checking if it is one of those weak symbols. We use weakly held - /// symbols to that a instance doesn't hold symbols alive. But, we know if we are - /// holding the symbol itself, then the weak-ref will stay alive such that we can do this containment check. - /// - /// - private readonly struct UnrootedSymbolSet - { - /// - /// The produced directly by . - /// - public readonly WeakReference PrimaryAssemblySymbol; - - /// - /// The produced directly by . Only - /// valid for . - /// - public readonly WeakReference PrimaryDynamicSymbol; - - /// - /// The s or s produced through for all the references exposed by . Sorted by the hash code produced by so that it can be binary searched efficiently. - /// - public readonly ImmutableArray SecondaryReferencedSymbols; - - private UnrootedSymbolSet( - WeakReference primaryAssemblySymbol, - WeakReference primaryDynamicSymbol, - ImmutableArray secondaryReferencedSymbols) - { - PrimaryAssemblySymbol = primaryAssemblySymbol; - PrimaryDynamicSymbol = primaryDynamicSymbol; - SecondaryReferencedSymbols = secondaryReferencedSymbols; - } - - public static UnrootedSymbolSet Create(Compilation compilation) - { - var primaryAssembly = new WeakReference(compilation.Assembly); - - // The dynamic type is also unrooted (i.e. doesn't point back at the compilation or source - // assembly). So we have to keep track of it so we can get back from it to a project in case the - // underlying compilation is GC'ed. - var primaryDynamic = new WeakReference( - compilation.Language == LanguageNames.CSharp ? compilation.DynamicType : null); - - // PERF: Preallocate this array so we don't have to resize it as we're adding assembly symbols. - using var _ = ArrayBuilder.GetInstance( - compilation.ExternalReferences.Length + compilation.DirectiveReferences.Length, out var secondarySymbols); - - foreach (var reference in compilation.References) - { - var symbol = compilation.GetAssemblyOrModuleSymbol(reference); - if (symbol == null) - continue; - - secondarySymbols.Add((ReferenceEqualityComparer.GetHashCode(symbol), new WeakReference(symbol), MetadataReferenceInfo.From(reference))); - } - - // Sort all the secondary symbols by their hash. This will allow us to easily binary search for - // them afterwards. Note: it is fine for multiple symbols to have the same reference hash. The - // search algorithm will account for that. - secondarySymbols.Sort(static (x, y) => x.hashCode.CompareTo(y.hashCode)); - return new UnrootedSymbolSet(primaryAssembly, primaryDynamic, secondarySymbols.ToImmutable()); - } - - public bool ContainsAssemblyOrModuleOrDynamic(ISymbol symbol, bool primary, out MetadataReferenceInfo? referencedThrough) - { - referencedThrough = null; - - if (primary) - { - return symbol.Equals(this.PrimaryAssemblySymbol.GetTarget()) || - symbol.Equals(this.PrimaryDynamicSymbol.GetTarget()); - } - - var secondarySymbols = this.SecondaryReferencedSymbols; - - var symbolHash = ReferenceEqualityComparer.GetHashCode(symbol); - - // The secondary symbol array is sorted by the symbols' hash codes. So do a binary search to find - // the location we should start looking at. - var index = secondarySymbols.BinarySearch(symbolHash, static (item, symbolHash) => item.hashCode.CompareTo(symbolHash)); - if (index < 0) - return false; - - // Could have multiple symbols with the same hash. They will all be placed next to each other, - // so walk backward to hit the first. - while (index > 0 && secondarySymbols[index - 1].hashCode == symbolHash) - index--; - - // Now, walk forward through the stored symbols with the same hash looking to see if any are a reference match. - while (index < secondarySymbols.Length && secondarySymbols[index].hashCode == symbolHash) - { - var cached = secondarySymbols[index]; - if (cached.symbol.TryGetTarget(out var otherSymbol) && otherSymbol == symbol) - { - referencedThrough = cached.referenceInfo; - return true; - } - - index++; - } - - return false; - } - } -} diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs index 899adeef5bfe6..058055eb643fe 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs @@ -1009,24 +1009,6 @@ public ValueTask> GetSourceGeneratorDiagnosticsAsync( return GetCompilationTracker(documentId.ProjectId).TryGetSourceGeneratedDocumentStateForAlreadyGeneratedId(documentId); } - /// - /// Attempt to get the best readily available compilation for the project. It may be a - /// partially built compilation. - /// - private MetadataReference? GetPartialMetadataReference( - ProjectReference projectReference, - ProjectState fromProject) - { - // Try to get the compilation state for this project. If it doesn't exist, don't do any - // more work. - if (!_projectIdToTrackerMap.TryGetValue(projectReference.ProjectId, out var state)) - { - return null; - } - - return state.GetPartialMetadataReference(fromProject, projectReference); - } - /// /// Get a metadata reference to this compilation info's compilation with respect to /// another project. For cross language references produce a skeletal assembly. If the @@ -1034,7 +1016,7 @@ public ValueTask> GetSourceGeneratorDiagnosticsAsync( /// needed and does not exist, it is also built. /// private async Task GetMetadataReferenceAsync( - ICompilationTracker tracker, ProjectState fromProject, ProjectReference projectReference, CancellationToken cancellationToken) + ICompilationTracker tracker, ProjectState fromProject, ProjectReference projectReference, bool includeCrossLanguage, CancellationToken cancellationToken) { try { @@ -1046,6 +1028,9 @@ public ValueTask> GetSourceGeneratorDiagnosticsAsync( return compilation.ToMetadataReference(projectReference.Aliases, projectReference.EmbedInteropTypes); } + if (!includeCrossLanguage) + return null; + // otherwise get a metadata only image reference that is built by emitting the metadata from the // referenced project's compilation and re-importing it. using (Logger.LogBlock(FunctionId.Workspace_SkeletonAssembly_GetMetadataOnlyImage, cancellationToken)) @@ -1065,14 +1050,14 @@ public ValueTask> GetSourceGeneratorDiagnosticsAsync( /// can happen when trying to build a skeleton reference that fails to build. /// public Task GetMetadataReferenceAsync( - ProjectReference projectReference, ProjectState fromProject, CancellationToken cancellationToken) + ProjectReference projectReference, ProjectState fromProject, bool includeCrossLanguage, CancellationToken cancellationToken) { try { // Get the compilation state for this project. If it's not already created, then this // will create it. Then force that state to completion and get a metadata reference to it. var tracker = this.GetCompilationTracker(projectReference.ProjectId); - return GetMetadataReferenceAsync(tracker, fromProject, projectReference, cancellationToken); + return GetMetadataReferenceAsync(tracker, fromProject, projectReference, includeCrossLanguage, cancellationToken); } catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical)) { @@ -1176,7 +1161,7 @@ public SolutionCompilationState WithFrozenSourceGeneratedDocuments( var documentStatesByProjectId = documentStates.ToDictionary(static state => state.Id.ProjectId); var newTrackerMap = CreateCompilationTrackerMap( - documentStatesByProjectId.Keys.ToImmutableArray(), + [.. documentStatesByProjectId.Keys], this.SolutionState.GetProjectDependencyGraph(), static (trackerMap, arg) => { @@ -1570,7 +1555,7 @@ private SolutionCompilationState RemoveDocumentsFromMultipleProjects( var removedDocumentStatesForProject = removedDocumentStates.ToImmutable(); - var compilationTranslationAction = removeDocumentsFromProjectState(oldProjectState, documentIdsInProject.ToImmutableArray(), removedDocumentStatesForProject); + var compilationTranslationAction = removeDocumentsFromProjectState(oldProjectState, [.. documentIdsInProject], removedDocumentStatesForProject); var newProjectState = compilationTranslationAction.NewProjectState; var stateChange = newCompilationState.SolutionState.ForkProject( diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs index c23854f2e7d35..9600215792ec1 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs @@ -1178,7 +1178,7 @@ public static ProjectDependencyGraph CreateDependencyGraph( state.ProjectReferences.Where(pr => projectStates.ContainsKey(pr.ProjectId)).Select(pr => pr.ProjectId).ToImmutableHashSet())) .ToImmutableDictionary(); - return new ProjectDependencyGraph(projectIds.ToImmutableHashSet(), map); + return new ProjectDependencyGraph([.. projectIds], map); } public SolutionState WithOptions(SolutionOptionSet options) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SourceGeneratedDocumentState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SourceGeneratedDocumentState.cs index e404b3a91c413..cb1591b0eb256 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SourceGeneratedDocumentState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SourceGeneratedDocumentState.cs @@ -105,7 +105,7 @@ private SourceGeneratedDocumentState( ITextAndVersionSource textSource, SourceText text, LoadTextOptions loadTextOptions, - AsyncLazy treeSource, + ITreeAndVersionSource treeSource, Lazy lazyContentHash, DateTime generationDateTime) : base(languageServices, documentServiceProvider, attributes, options, textSource, loadTextOptions, treeSource) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/StateChecksums.cs b/src/Workspaces/Core/Portable/Workspace/Solution/StateChecksums.cs index dd1c5d79ec793..080785d0a61fe 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/StateChecksums.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/StateChecksums.cs @@ -486,6 +486,20 @@ public async Task FindAsync( await ChecksumCollection.FindAsync(assetPath, state.AnalyzerConfigDocumentStates, searchingChecksumsLeft, onAssetFound, arg, cancellationToken).ConfigureAwait(false); } } + + public override string ToString() + => $""" + ProjectStateChecksums({ProjectId}) + Info={Info} + CompilationOptions={CompilationOptions} + ParseOptions={ParseOptions} + ProjectReferences={ProjectReferences.Checksum} + MetadataReferences={MetadataReferences.Checksum} + AnalyzerReferences={AnalyzerReferences.Checksum} + Documents={Documents.Checksum} + AdditionalDocuments={AdditionalDocuments.Checksum} + AnalyzerConfigDocuments={AnalyzerConfigDocuments.Checksum} + """; } internal sealed class DocumentStateChecksums( @@ -526,6 +540,9 @@ public async Task FindAsync( onAssetFound(Text, text, arg); } } + + public override string ToString() + => $"DocumentStateChecksums({DocumentId})"; } /// diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState.cs index 33a799e996aba..63fa8cb9f446d 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState.cs @@ -3,9 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; @@ -60,7 +58,7 @@ public TextDocumentState(SolutionServices solutionServices, DocumentInfo info, L info.DocumentServiceProvider, info.Attributes, textAndVersionSource: info.TextLoader != null - ? CreateRecoverableText(info.TextLoader, solutionServices) + ? CreateTextFromLoader(info.TextLoader, PreservationMode.PreserveValue, solutionServices) : CreateStrongText(TextAndVersion.Create(SourceText.From(string.Empty, encoding: null, loadTextOptions.ChecksumAlgorithm), VersionStamp.Default, info.FilePath)), loadTextOptions) { @@ -74,9 +72,6 @@ public TextDocumentState(SolutionServices solutionServices, DocumentInfo info, L private static ITextAndVersionSource CreateStrongText(TextAndVersion text) => new ConstantTextAndVersionSource(text); - private static ITextAndVersionSource CreateStrongText(TextLoader loader) - => new LoadableTextAndVersionSource(loader, cacheResult: true); - private static ITextAndVersionSource CreateRecoverableText(TextAndVersion text, SolutionServices services) { var service = services.GetRequiredService(); @@ -87,18 +82,8 @@ private static ITextAndVersionSource CreateRecoverableText(TextAndVersion text, : new RecoverableTextAndVersion(new ConstantTextAndVersionSource(text), services); } - private static ITextAndVersionSource CreateRecoverableText(TextLoader loader, SolutionServices services) - { - var service = services.GetRequiredService(); - var options = service.Options; - - return options.DisableRecoverableText - ? CreateStrongText(loader) - : new RecoverableTextAndVersion(new LoadableTextAndVersionSource(loader, cacheResult: false), services); - } - - public ITemporaryTextStorageInternal? Storage - => (TextAndVersionSource as RecoverableTextAndVersion)?.Storage; + public ITemporaryStorageTextHandle? StorageHandle + => (TextAndVersionSource as RecoverableTextAndVersion)?.StorageHandle; public bool TryGetText([NotNullWhen(returnValue: true)] out SourceText? text) { @@ -178,13 +163,34 @@ public TextDocumentState UpdateText(SourceText newText, PreservationMode mode) public TextDocumentState UpdateText(TextLoader loader, PreservationMode mode) { // don't blow up on non-text documents. - var newTextSource = mode == PreservationMode.PreserveIdentity - ? CreateStrongText(loader) - : CreateRecoverableText(loader, solutionServices); + var newTextSource = CreateTextFromLoader(loader, mode, this.solutionServices); return UpdateText(newTextSource, mode, incremental: false); } + private static ITextAndVersionSource CreateTextFromLoader(TextLoader loader, PreservationMode mode, SolutionServices solutionServices) + { + var service = solutionServices.GetRequiredService(); + var options = service.Options; + + // If the caller is explicitly stating that identity must be preserved, then we created a source that will load + // from the loader the first time, but then cache that result so that hte same result is *always* returned. + if (mode == PreservationMode.PreserveIdentity || options.DisableRecoverableText) + return new LoadableTextAndVersionSource(loader, cacheResult: true); + + // If the loader asks us to always hold onto it strongly, then we do not want to create a recoverable text + // source here. Instead, we'll go back to the loader each time to get the text. This is useful for when the + // loader knows it can always reconstitute the snapshot exactly as it was before. For example, if the loader + // points at the contents of a memory mapped file in another process. + if (loader.AlwaysHoldStrongly) + return new LoadableTextAndVersionSource(loader, cacheResult: false); + + // Otherwise, we just want to hold onto this loader by value. So we create a loader that will load the + // contents, but not hold onto them strongly, and we wrap it in a recoverable-text that will then take those + // contents and dump it into a memory-mapped-file in this process so that snapshot semantics can be preserved. + return new RecoverableTextAndVersion(new LoadableTextAndVersionSource(loader, cacheResult: false), solutionServices); + } + protected virtual TextDocumentState UpdateText(ITextAndVersionSource newTextSource, PreservationMode mode, bool incremental) { return new TextDocumentState( diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState_Checksum.cs b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState_Checksum.cs index 9c1513be2405e..3f646f09e1971 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState_Checksum.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState_Checksum.cs @@ -36,11 +36,9 @@ private async Task ComputeChecksumsAsync(CancellationTok { using (Logger.LogBlock(FunctionId.DocumentState_ComputeChecksumsAsync, FilePath, cancellationToken)) { - var serializer = solutionServices.GetRequiredService(); - var infoChecksum = this.Attributes.Checksum; var serializableText = await SerializableSourceText.FromTextDocumentStateAsync(this, cancellationToken).ConfigureAwait(false); - var textChecksum = serializer.CreateChecksum(serializableText, cancellationToken); + var textChecksum = serializableText.ContentChecksum; return new DocumentStateChecksums(this.Id, infoChecksum, textChecksum); } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/TextLoader.cs b/src/Workspaces/Core/Portable/Workspace/Solution/TextLoader.cs index e771796d35fac..cf625034f68d8 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/TextLoader.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/TextLoader.cs @@ -31,6 +31,18 @@ public abstract class TextLoader internal virtual string? FilePath => null; + /// + /// if the document that holds onto this loader should do so with a strong reference, versus + /// a reference that will take the contents of this loader and store them in a recoverable form (e.g. a memory + /// mapped file within the same process). This should be used when the underlying data is already stored + /// in a recoverable form somewhere else and it would be wasteful to store another copy. For example, a document + /// that is backed by memory-mapped contents in another process does not need to dump it's content to + /// another memory-mapped file in the process it lives in. It can always recover the text from the original + /// process. + /// + internal virtual bool AlwaysHoldStrongly + => false; + /// /// True if reloads from its original binary representation (e.g. file on disk). /// diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ITextAndVersionSource.cs b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/ITextAndVersionSource.cs similarity index 83% rename from src/Workspaces/Core/Portable/Workspace/Solution/ITextAndVersionSource.cs rename to src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/ITextAndVersionSource.cs index ab0bc21554dde..ac7f86ac6a84e 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ITextAndVersionSource.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/ITextAndVersionSource.cs @@ -16,6 +16,12 @@ internal interface ITextAndVersionSource /// bool CanReloadText { get; } + /// + /// Retrieves the underlying if that's what this was + /// created from and still has access to. + /// + TextLoader? TextLoader { get; } + bool TryGetValue(LoadTextOptions options, [MaybeNullWhen(false)] out TextAndVersion value); TextAndVersion GetValue(LoadTextOptions options, CancellationToken cancellationToken); Task GetValueAsync(LoadTextOptions options, CancellationToken cancellationToken); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/ITreeAndVersionSource.cs b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/ITreeAndVersionSource.cs new file mode 100644 index 0000000000000..75e0d0c670b33 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/ITreeAndVersionSource.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis; + +/// +/// Similar to , but for trees. Allows hiding (or introspecting) the details of how +/// a tree is created for a particular document. +/// +internal interface ITreeAndVersionSource +{ + Task GetValueAsync(CancellationToken cancellationToken); + TreeAndVersion GetValue(CancellationToken cancellationToken); + bool TryGetValue([NotNullWhen(true)] out TreeAndVersion? value); +} diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/LoadableTextAndVersionSource.cs b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/LoadableTextAndVersionSource.cs similarity index 95% rename from src/Workspaces/Core/Portable/Workspace/Solution/LoadableTextAndVersionSource.cs rename to src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/LoadableTextAndVersionSource.cs index 70417d9b9dbad..28a1f3f6fe04f 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/LoadableTextAndVersionSource.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/LoadableTextAndVersionSource.cs @@ -35,10 +35,10 @@ private sealed class LazyValueWithOptions(LoadableTextAndVersionSource source, L private WeakReference? _weakInstance; private Task LoadAsync(CancellationToken cancellationToken) - => Source.Loader.LoadTextAsync(Options, cancellationToken); + => Source.TextLoader.LoadTextAsync(Options, cancellationToken); private TextAndVersion LoadSynchronously(CancellationToken cancellationToken) - => Source.Loader.LoadTextSynchronously(Options, cancellationToken); + => Source.TextLoader.LoadTextSynchronously(Options, cancellationToken); public bool TryGetValue([MaybeNullWhen(false)] out TextAndVersion value) { @@ -102,13 +102,13 @@ private void UpdateWeakAndStrongReferences_NoLock(TextAndVersion textAndVersion) } } - public readonly TextLoader Loader = loader; + public TextLoader TextLoader { get; } = loader; public readonly bool CacheResult = cacheResult; private LazyValueWithOptions? _lazyValue; public bool CanReloadText - => Loader.CanReloadText; + => TextLoader.CanReloadText; private LazyValueWithOptions GetLazyValue(LoadTextOptions options) { diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/RecoverableTextAndVersion.RecoverableText.cs b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/RecoverableTextAndVersion.RecoverableText.cs similarity index 100% rename from src/Workspaces/Core/Portable/Workspace/Solution/RecoverableTextAndVersion.RecoverableText.cs rename to src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/RecoverableTextAndVersion.RecoverableText.cs diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/RecoverableTextAndVersion.cs b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/RecoverableTextAndVersion.cs similarity index 88% rename from src/Workspaces/Core/Portable/Workspace/Solution/RecoverableTextAndVersion.cs rename to src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/RecoverableTextAndVersion.cs index bcc498768b875..7314888e4c9ba 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/RecoverableTextAndVersion.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/RecoverableTextAndVersion.cs @@ -42,8 +42,14 @@ private bool TryGetInitialSourceOrRecoverableText([NotNullWhen(true)] out ITextA return false; } - public ITemporaryTextStorageInternal? Storage - => (_initialSourceOrRecoverableText as RecoverableText)?.Storage; + /// + /// Attempt to return the original loader if we still have it. + /// + public TextLoader? TextLoader + => (_initialSourceOrRecoverableText as ITextAndVersionSource)?.TextLoader; + + public ITemporaryStorageTextHandle? StorageHandle + => (_initialSourceOrRecoverableText as RecoverableText)?.StorageHandle; public bool TryGetValue(LoadTextOptions options, [MaybeNullWhen(false)] out TextAndVersion value) { @@ -137,7 +143,7 @@ private sealed partial class RecoverableText public readonly ITextAndVersionSource? InitialSource; public readonly LoadTextOptions LoadTextOptions; - public ITemporaryTextStorageInternal? _storage; + public ITemporaryStorageTextHandle? _storageHandle; public RecoverableText(ITextAndVersionSource source, TextAndVersion textAndVersion, LoadTextOptions options, SolutionServices services) { @@ -160,37 +166,36 @@ public RecoverableText(ITextAndVersionSource source, TextAndVersion textAndVersi public TextAndVersion ToTextAndVersion(SourceText text) => TextAndVersion.Create(text, Version, LoadDiagnostic); - public ITemporaryTextStorageInternal? Storage => _storage; + public ITemporaryStorageTextHandle? StorageHandle => _storageHandle; private async Task RecoverAsync(CancellationToken cancellationToken) { - Contract.ThrowIfNull(_storage); + Contract.ThrowIfNull(_storageHandle); using (Logger.LogBlock(FunctionId.Workspace_Recoverable_RecoverTextAsync, cancellationToken)) { - return await _storage.ReadTextAsync(cancellationToken).ConfigureAwait(false); + return await _storageHandle.ReadFromTemporaryStorageAsync(cancellationToken).ConfigureAwait(false); } } private SourceText Recover(CancellationToken cancellationToken) { - Contract.ThrowIfNull(_storage); + Contract.ThrowIfNull(_storageHandle); using (Logger.LogBlock(FunctionId.Workspace_Recoverable_RecoverText, cancellationToken)) { - return _storage.ReadText(cancellationToken); + return _storageHandle.ReadFromTemporaryStorage(cancellationToken); } } private async Task SaveAsync(SourceText text, CancellationToken cancellationToken) { - Contract.ThrowIfFalse(_storage == null); // Cannot save more than once + Contract.ThrowIfFalse(_storageHandle == null); // Cannot save more than once - var storage = _storageService.CreateTemporaryTextStorage(); - await storage.WriteTextAsync(text, cancellationToken).ConfigureAwait(false); + var handle = await _storageService.WriteToTemporaryStorageAsync(text, cancellationToken).ConfigureAwait(false); - // make sure write is done before setting _storage field - Interlocked.CompareExchange(ref _storage, storage, null); + // make sure write is done before setting _storageHandle field + Interlocked.CompareExchange(ref _storageHandle, handle, null); // Only set _initialValue to null once writing to the storage service completes fully. If the save did not // complete, we want to keep it around to service future requests. Once we do clear out this value, then diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/SimpleTreeAndVersionSource.cs b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/SimpleTreeAndVersionSource.cs new file mode 100644 index 0000000000000..46be64b1bd3ec --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/SimpleTreeAndVersionSource.cs @@ -0,0 +1,44 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis; + +/// +/// Simple implementation of backed by an opaque ."/> +/// +internal sealed class SimpleTreeAndVersionSource : ITreeAndVersionSource +{ + private readonly AsyncLazy _source; + + private SimpleTreeAndVersionSource(AsyncLazy source) + { + _source = source; + } + + public Task GetValueAsync(CancellationToken cancellationToken) + => _source.GetValueAsync(cancellationToken); + + public TreeAndVersion GetValue(CancellationToken cancellationToken) + => _source.GetValue(cancellationToken); + + public bool TryGetValue([NotNullWhen(true)] out TreeAndVersion? value) + => _source.TryGetValue(out value); + + public static SimpleTreeAndVersionSource Create( + Func> asynchronousComputeFunction, + Func? synchronousComputeFunction, TArg arg) + { + return new(AsyncLazy.Create(asynchronousComputeFunction, synchronousComputeFunction, arg)); + } + + public static SimpleTreeAndVersionSource Create(TreeAndVersion source) + => new(AsyncLazy.Create(source)); +} diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/VersionStamp.cs b/src/Workspaces/Core/Portable/Workspace/Solution/VersionStamp.cs index f0a4fc6bf828a..34872c1b0c2d7 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/VersionStamp.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/VersionStamp.cs @@ -168,41 +168,6 @@ public bool Equals(VersionStamp version) public static bool operator !=(VersionStamp left, VersionStamp right) => !left.Equals(right); - /// - /// Check whether given persisted version is re-usable. Used by VS for Mac - /// - internal static bool CanReusePersistedVersion(VersionStamp baseVersion, VersionStamp persistedVersion) - { - if (baseVersion == persistedVersion) - { - return true; - } - - // there was a collision, we can't use these - if (baseVersion._localIncrement != 0 || persistedVersion._localIncrement != 0) - { - return false; - } - - return baseVersion._utcLastModified == persistedVersion._utcLastModified; - } - - internal void WriteTo(ObjectWriter writer) - { - writer.WriteInt64(_utcLastModified.ToBinary()); - writer.WriteInt32(_localIncrement); - writer.WriteInt32(_globalIncrement); - } - - internal static VersionStamp ReadFrom(ObjectReader reader) - { - var raw = reader.ReadInt64(); - var localIncrement = reader.ReadInt32(); - var globalIncrement = reader.ReadInt32(); - - return new VersionStamp(DateTime.FromBinary(raw), localIncrement, globalIncrement); - } - private static int GetGlobalVersion(VersionStamp version) { // global increment < 0 means it is a global version which has its global increment in local increment diff --git a/src/Workspaces/Core/Portable/WorkspacesResources.resx b/src/Workspaces/Core/Portable/WorkspacesResources.resx index 39e13ef0bb761..7e20b01c147b4 100644 --- a/src/Workspaces/Core/Portable/WorkspacesResources.resx +++ b/src/Workspaces/Core/Portable/WorkspacesResources.resx @@ -492,4 +492,7 @@ Unexpected value '{0}' in DocumentKinds array. + + Running code cleanup on fixed documents + \ No newline at end of file diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.cs.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.cs.xlf index 93569dd7abbc2..60a243b017cfe 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.cs.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.cs.xlf @@ -127,6 +127,11 @@ Přejmenovat {0} na {1} + + Running code cleanup on fixed documents + Running code cleanup on fixed documents + + Solution does not contain specified reference Řešení neobsahuje zadaný odkaz. diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.de.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.de.xlf index 4dbc4bbf1268c..f5a183f74084a 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.de.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.de.xlf @@ -127,6 +127,11 @@ "{0}" in "{1}" umbenennen + + Running code cleanup on fixed documents + Running code cleanup on fixed documents + + Solution does not contain specified reference Der angegebene Verweis ist nicht in der Projektmappe enthalten. diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.es.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.es.xlf index d79f326dae933..66c1cf566b81a 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.es.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.es.xlf @@ -127,6 +127,11 @@ Cambiar el nombre de '{0}' a '{1}' + + Running code cleanup on fixed documents + Running code cleanup on fixed documents + + Solution does not contain specified reference La solución no contiene la referencia especificada diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.fr.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.fr.xlf index 4437971ba7614..910f067fea546 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.fr.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.fr.xlf @@ -127,6 +127,11 @@ Renommer '{0}' en '{1}' + + Running code cleanup on fixed documents + Running code cleanup on fixed documents + + Solution does not contain specified reference La solution ne contient pas la référence spécifiée diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.it.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.it.xlf index 77eab2b223826..131b63119784b 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.it.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.it.xlf @@ -127,6 +127,11 @@ Rinomina '{0}' in '{1}' + + Running code cleanup on fixed documents + Running code cleanup on fixed documents + + Solution does not contain specified reference La soluzione non contiene il riferimento specificato diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ja.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ja.xlf index 7f16149e410ce..f6cc2a2be97c6 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ja.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ja.xlf @@ -127,6 +127,11 @@ '{0}' を '{1}' に名前変更 + + Running code cleanup on fixed documents + Running code cleanup on fixed documents + + Solution does not contain specified reference 指定された参照がソリューションに含まれていません diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ko.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ko.xlf index ab6a3d0da4853..574613a48d02b 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ko.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ko.xlf @@ -127,6 +127,11 @@ '{1}'(으)로 '{0}' 이름 바꾸기 + + Running code cleanup on fixed documents + Running code cleanup on fixed documents + + Solution does not contain specified reference 지정된 참조가 솔루션에 포함되어 있지 않습니다. diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pl.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pl.xlf index 5d6df9750c27b..89229d1e54580 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pl.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pl.xlf @@ -127,6 +127,11 @@ Zmień nazwę elementu „{0}” na „{1}” + + Running code cleanup on fixed documents + Running code cleanup on fixed documents + + Solution does not contain specified reference Rozwiązanie nie zawiera określonego odwołania diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pt-BR.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pt-BR.xlf index d61231dbb8017..7b0d07ee6716c 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pt-BR.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.pt-BR.xlf @@ -127,6 +127,11 @@ Renomear "{0}" para "{1}" + + Running code cleanup on fixed documents + Running code cleanup on fixed documents + + Solution does not contain specified reference A solução não contém a referência especificada diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ru.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ru.xlf index a59fe061c71d0..643fa036d905b 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ru.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.ru.xlf @@ -127,6 +127,11 @@ Переименовать "{0}" в "{1}" + + Running code cleanup on fixed documents + Running code cleanup on fixed documents + + Solution does not contain specified reference Решение не содержит указанную ссылку. diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.tr.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.tr.xlf index a27884ea21b81..31523544c5c26 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.tr.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.tr.xlf @@ -127,6 +127,11 @@ '{0}' öğesini '{1}' olarak yeniden adlandır + + Running code cleanup on fixed documents + Running code cleanup on fixed documents + + Solution does not contain specified reference Çözüm belirtilen başvuruyu içermiyor diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hans.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hans.xlf index 282030d47ddc5..1a8ed118e7826 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hans.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hans.xlf @@ -127,6 +127,11 @@ 将“{0}” 重命名为“{1}” + + Running code cleanup on fixed documents + Running code cleanup on fixed documents + + Solution does not contain specified reference 解决方案不包含指定的引用 diff --git a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hant.xlf b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hant.xlf index fa3e4431600d7..9fd5e7497d743 100644 --- a/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hant.xlf +++ b/src/Workspaces/Core/Portable/xlf/WorkspacesResources.zh-Hant.xlf @@ -127,6 +127,11 @@ 將 '{0}' 重新命名為 '{1}' + + Running code cleanup on fixed documents + Running code cleanup on fixed documents + + Solution does not contain specified reference 解決方案未包含指定的參考 diff --git a/src/Workspaces/CoreTest/BatchFixAllProviderTests.cs b/src/Workspaces/CoreTest/BatchFixAllProviderTests.cs index 4c40c9a8b6565..f948925ecc2f0 100644 --- a/src/Workspaces/CoreTest/BatchFixAllProviderTests.cs +++ b/src/Workspaces/CoreTest/BatchFixAllProviderTests.cs @@ -107,7 +107,7 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context) if (_nested) { - fixes = [CodeAction.Create("Container", fixes.ToImmutableArray(), isInlinable: false)]; + fixes = [CodeAction.Create("Container", [.. fixes], isInlinable: false)]; } foreach (var fix in fixes) diff --git a/src/Workspaces/CoreTest/EditorConfigParsing/EditorConfigFileParserTests.cs b/src/Workspaces/CoreTest/EditorConfigParsing/EditorConfigFileParserTests.cs index c1a68021dae2b..4c80e4cd799c2 100644 --- a/src/Workspaces/CoreTest/EditorConfigParsing/EditorConfigFileParserTests.cs +++ b/src/Workspaces/CoreTest/EditorConfigParsing/EditorConfigFileParserTests.cs @@ -23,7 +23,7 @@ internal static EditorConfigFile CreateParseResults(string e list.Add(parseResult); } - return new EditorConfigFile(editorconfigFilePath, list.ToImmutableArray()); + return new EditorConfigFile(editorconfigFilePath, [.. list]); } [Fact] diff --git a/src/Workspaces/CoreTest/FindAllDeclarationsTests.TestSolutionsAndProject.cs b/src/Workspaces/CoreTest/FindAllDeclarationsTests.TestSolutionsAndProject.cs index 8f28f33ba1510..b5d5d44d9e1dd 100644 --- a/src/Workspaces/CoreTest/FindAllDeclarationsTests.TestSolutionsAndProject.cs +++ b/src/Workspaces/CoreTest/FindAllDeclarationsTests.TestSolutionsAndProject.cs @@ -62,7 +62,7 @@ private static void Verify(SolutionKind workspaceKind, IEnumerable decl private static void VerifyResults(IEnumerable declarations, string[] expectedResults) { declarations = declarations.OrderBy(d => d.ToString()); - expectedResults = expectedResults.OrderBy(r => r).ToArray(); + expectedResults = [.. expectedResults.OrderBy(r => r)]; for (var i = 0; i < expectedResults.Length; i++) { diff --git a/src/Workspaces/CoreTest/Formatter/FormatterTests.cs b/src/Workspaces/CoreTest/Formatter/FormatterTests.cs index abf74a8dd1a2b..f5f087ef9efda 100644 --- a/src/Workspaces/CoreTest/Formatter/FormatterTests.cs +++ b/src/Workspaces/CoreTest/Formatter/FormatterTests.cs @@ -59,8 +59,7 @@ public async Task FormatAsync_ForeignLanguageWithFormattingSupport() AssertEx.Equal(@"Formatted with options: LineFormattingOptions { UseTabs = False, TabSize = 4, IndentationSize = 4, NewLine = \r\n }", formattedText.ToString()); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public async Task FormatAsync_ForeignLanguageWithFormattingSupport_Options(bool passExplicitOptions) { var hostServices = s_composition.AddParts([typeof(NoCompilationLanguageService), typeof(TestFormattingService)]).GetHostServices(); diff --git a/src/Workspaces/CoreTest/Host/WorkspaceServices/TestTemporaryStorageServiceFactory.cs b/src/Workspaces/CoreTest/Host/WorkspaceServices/TestTemporaryStorageServiceFactory.cs deleted file mode 100644 index 21147f43736bd..0000000000000 --- a/src/Workspaces/CoreTest/Host/WorkspaceServices/TestTemporaryStorageServiceFactory.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Composition; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Host.Mef; - -namespace Microsoft.CodeAnalysis.UnitTests.Persistence -{ - [ExportWorkspaceServiceFactory(typeof(ITemporaryStorageServiceInternal), ServiceLayer.Test), Shared, PartNotDiscoverable] - internal sealed class TestTemporaryStorageServiceFactory : IWorkspaceServiceFactory - { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public TestTemporaryStorageServiceFactory() - { - } - - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => TrivialTemporaryStorageService.Instance; - } -} diff --git a/src/Workspaces/CoreTest/ObjectSerializationTests.cs b/src/Workspaces/CoreTest/ObjectSerializationTests.cs index 1afaa16289fda..91a75a4968f72 100644 --- a/src/Workspaces/CoreTest/ObjectSerializationTests.cs +++ b/src/Workspaces/CoreTest/ObjectSerializationTests.cs @@ -246,8 +246,7 @@ public void TestCompressedUInt() Assert.Throws(() => TestRoundTripCompressedUint(0xC0000000u)); // both high bits set not allowed } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public void TestByteSpan([CombinatorialValues(0, 1, 2, 3, 1000, 1000000)] int size) { var data = new byte[size]; @@ -597,7 +596,7 @@ private static void TestRoundTripChar(Char ch) [Fact] public void TestRoundTripGuid() { - void test(Guid guid) + static void test(Guid guid) { TestRoundTrip(guid, (w, v) => w.WriteGuid(v), r => r.ReadGuid()); } diff --git a/src/Workspaces/CoreTest/SerializationTests.cs b/src/Workspaces/CoreTest/SerializationTests.cs deleted file mode 100644 index ee8e276515d90..0000000000000 --- a/src/Workspaces/CoreTest/SerializationTests.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -#nullable disable - -using System.IO; -using Microsoft.CodeAnalysis.Test.Utilities; -using Roslyn.Test.Utilities; -using Roslyn.Utilities; -using Xunit; - -namespace Microsoft.CodeAnalysis.UnitTests -{ - [UseExportProvider] - public partial class SerializationTests : TestBase - { - [Fact] - public void VersionStamp_RoundTripText() - { - var versionStamp = VersionStamp.Create(); - - using var writerStream = new MemoryStream(); - - using (var writer = new ObjectWriter(writerStream, leaveOpen: true)) - { - versionStamp.WriteTo(writer); - } - - using var readerStream = new MemoryStream(writerStream.ToArray()); - using var reader = ObjectReader.TryGetReader(readerStream); - var deserializedVersionStamp = VersionStamp.ReadFrom(reader); - - Assert.Equal(versionStamp, deserializedVersionStamp); - } - } -} diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionTestHelpers.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionTestHelpers.cs index 0cdc9229c893f..fa04089a158a9 100644 --- a/src/Workspaces/CoreTest/SolutionTests/SolutionTestHelpers.cs +++ b/src/Workspaces/CoreTest/SolutionTests/SolutionTestHelpers.cs @@ -7,7 +7,6 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Remote.Testing; using Microsoft.CodeAnalysis.Test.Utilities; -using Microsoft.CodeAnalysis.UnitTests.Persistence; using Xunit; namespace Microsoft.CodeAnalysis.UnitTests @@ -17,17 +16,8 @@ internal static class SolutionTestHelpers public static Workspace CreateWorkspace(Type[]? additionalParts = null, TestHost testHost = TestHost.InProcess) => new AdhocWorkspace(FeaturesTestCompositions.Features.AddParts(additionalParts).WithTestHostParts(testHost).GetHostServices()); - public static Workspace CreateWorkspaceWithNormalText() - => CreateWorkspace( - [ - typeof(TestTemporaryStorageServiceFactory) - ]); - - public static Workspace CreateWorkspaceWithRecoverableText() - => CreateWorkspace(); - public static Workspace CreateWorkspaceWithPartialSemantics(TestHost testHost = TestHost.InProcess) - => WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics([typeof(TestTemporaryStorageServiceFactory)], testHost); + => WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics(testHost: testHost); #nullable disable diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs index db2774e7943e2..a98d7291ae18e 100644 --- a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs +++ b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs @@ -1227,8 +1227,7 @@ public void WithProjectCompilationOptionsExceptionHandling() Assert.Throws(() => solution.WithProjectCompilationOptions(ProjectId.CreateNewId(), options)); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public void WithProjectCompilationOptionsReplacesSyntaxTreeOptionProvider([CombinatorialValues(LanguageNames.CSharp, LanguageNames.VisualBasic)] string languageName) { var projectId = ProjectId.CreateNewId(); @@ -2216,7 +2215,8 @@ private static async Task ValidateSolutionAndCompilationsAsync(Solution solution { if (solution.ContainsProject(referenced.ProjectId)) { - var referencedMetadata = await solution.CompilationState.GetMetadataReferenceAsync(referenced, solution.GetProjectState(project.Id), CancellationToken.None); + var referencedMetadata = await solution.CompilationState.GetMetadataReferenceAsync( + referenced, solution.GetProjectState(project.Id), includeCrossLanguage: true, CancellationToken.None); Assert.NotNull(referencedMetadata); if (referencedMetadata is CompilationReference compilationReference) { @@ -2638,11 +2638,8 @@ public void TestUpdatingFilePathUpdatesSyntaxTree() } } -#if NETCOREAPP - [SupportedOSPlatform("windows")] -#endif [MethodImpl(MethodImplOptions.NoInlining)] - [Fact, WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/542736")] + [ConditionalFact(typeof(WindowsOnly)), WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/542736")] public void TestDocumentChangedOnDiskIsNotObserved() { var text1 = "public class A {}"; @@ -2651,7 +2648,7 @@ public void TestDocumentChangedOnDiskIsNotObserved() var file = Temp.CreateFile().WriteAllText(text1, Encoding.UTF8); // create a solution that evicts from the cache immediately. - using var workspace = CreateWorkspaceWithRecoverableText(); + using var workspace = CreateWorkspace(); var sol = workspace.CurrentSolution; var pid = ProjectId.CreateNewId(); @@ -2668,17 +2665,7 @@ public void TestDocumentChangedOnDiskIsNotObserved() Assert.Equal(text2, textOnDisk); // stop observing it and let GC reclaim it - if (PlatformInformation.IsWindows || PlatformInformation.IsRunningOnMono) - { - Assert.IsType(workspace.Services.GetService()); - observedText.AssertReleased(); - } - else - { - // If this assertion fails, it means a new target supports the true temporary storage service, and the - // condition above should be updated to ensure 'AssertReleased' is called for this target. - Assert.IsType(workspace.Services.GetService()); - } + observedText.AssertReleased(); // if we ask for the same text again we should get the original content var observedText2 = sol.GetDocument(did).GetTextAsync().Result; @@ -3714,8 +3701,7 @@ public async Task TestFrozenPartialSemanticsAfterSingleTextEdit() Assert.Contains(await frozenDocument.GetSyntaxTreeAsync(), (await frozenDocument.Project.GetCompilationAsync()).SyntaxTrees); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public async Task TestFrozenPartialSemanticsWithMulitipleUnrelatedEdits([CombinatorialValues(1, 2, 3)] int documentToFreeze) { using var workspace = CreateWorkspaceWithPartialSemantics(); @@ -3935,7 +3921,7 @@ public void TestUpdateDocumentsOrder() var pid = ProjectId.CreateNewId(); VersionStamp GetVersion() => solution.GetProject(pid).Version; - ImmutableArray GetDocumentIds() => solution.GetProject(pid).DocumentIds.ToImmutableArray(); + ImmutableArray GetDocumentIds() => [.. solution.GetProject(pid).DocumentIds]; ImmutableArray GetSyntaxTrees() { return solution.GetProject(pid).GetCompilationAsync().Result.SyntaxTrees.ToImmutableArray(); @@ -4035,8 +4021,7 @@ public void TestUpdateDocumentsOrderExceptions() Assert.Throws(() => solution = solution.WithProjectDocumentsOrder(pid, ImmutableList.CreateRange(new[] { did3, did2, did1 }))); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public async Task TestAddingEditorConfigFileWithDiagnosticSeverity([CombinatorialValues(LanguageNames.CSharp, LanguageNames.VisualBasic)] string languageName) { using var workspace = CreateWorkspace(); @@ -4072,8 +4057,7 @@ public async Task TestAddingEditorConfigFileWithDiagnosticSeverity([Combinatoria Assert.Equal(ReportDiagnostic.Error, severity); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public async Task TestAddingAndRemovingEditorConfigFileWithDiagnosticSeverity([CombinatorialValues(LanguageNames.CSharp, LanguageNames.VisualBasic)] string languageName) { using var workspace = CreateWorkspace(); @@ -4114,8 +4098,7 @@ public async Task TestAddingAndRemovingEditorConfigFileWithDiagnosticSeverity([C Assert.True(finalCompilation.ContainsSyntaxTree(syntaxTreeAfterRemovingEditorConfig)); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public async Task TestChangingAnEditorConfigFile([CombinatorialValues(LanguageNames.CSharp, LanguageNames.VisualBasic)] string languageName) { using var workspace = CreateWorkspace(); @@ -5079,5 +5062,62 @@ public async Task TestFrozenPartialSolutionOtherLanguage() var frozenCompilation = await frozenProject.GetCompilationAsync(); Assert.Null(frozenCompilation); } + + [Theory] + [InlineData(1000)] + [InlineData(2000)] + [InlineData(4000)] + [InlineData(8000)] + public async Task TestLargeLinkedFileChain(int intermediatePullCount) + { + using var workspace = CreateWorkspace(); + + var project1 = workspace.CurrentSolution + .AddProject($"Project1", $"Project1", LanguageNames.CSharp) + .WithParseOptions(CSharpParseOptions.Default.WithPreprocessorSymbols("DEBUG")) + .AddDocument($"Document", SourceText.From("class C { }"), filePath: @"c:\test\Document.cs").Project; + var documentId1 = project1.DocumentIds.Single(); + + // make another project, give a separate set of pp directives, so that we do *not* try to use the sibling + // root (from project1), but instead incrementally parse using the *contents* of the file in project1 again + // our actual tree. This used to stack overflow since we'd create a long chain of incremental parsing steps + // for each edit made to the sibling file. + var project2 = project1.Solution + .AddProject($"Project2", $"Project2", LanguageNames.CSharp) + .WithParseOptions(CSharpParseOptions.Default.WithPreprocessorSymbols("RELEASE")) + .AddDocument($"Document", SourceText.From("class C { }"), filePath: @"c:\test\Document.cs").Project; + var documentId2 = project2.DocumentIds.Single(); + + workspace.SetCurrentSolution( + _ => project2.Solution, + (_, _) => (WorkspaceChangeKind.SolutionAdded, null, null)); + + for (var i = 1; i <= 8000; i++) + { + var lastContents = $"#if true //{new string('.', i)}//"; + workspace.SetCurrentSolution( + old => old.WithDocumentText(documentId1, SourceText.From(lastContents)), + (_, _) => (WorkspaceChangeKind.DocumentChanged, documentId1.ProjectId, documentId1)); + + // Ensure that the first document is fine, and we're not stack overflowing on simply getting the tree + // from it. Do this on a disparate cadence from our pulls of the second document to ensure we are + // testing the case where we haven't necessarily immediately pulled on hte first doc before pulling on + // the second. + if (i % 33 == 0) + { + var document1 = workspace.CurrentSolution.GetRequiredDocument(documentId1); + await document1.GetSyntaxRootAsync(); + } + + if (i % intermediatePullCount == 0) + { + var document2 = workspace.CurrentSolution.GetRequiredDocument(documentId2); + + // Getting the second document should both be fine, and have contents equivalent to what is in the first document. + var root = await document2.GetSyntaxRootAsync(); + Assert.Equal(lastContents, root.ToFullString()); + } + } + } } } diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs index f8daa66a477e5..bb8a74e919e5a 100644 --- a/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs +++ b/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs @@ -70,7 +70,7 @@ public async Task WithReferencesMethodCorrectlyUpdatesWithEqualReferences(TestHo // reference with another reference that's equal, we correctly update generators. We'll have the underlying generators // be different since two AnalyzerFileReferences that are value equal but different instances would have their own generators as well. const string SharedPath = "Z:\\Generator.dll"; - ISourceGenerator CreateGenerator() => new SingleFileTestGenerator("// StaticContent", hintName: "generated"); + static ISourceGenerator CreateGenerator() => new SingleFileTestGenerator("// StaticContent", hintName: "generated"); var analyzerReference1 = new TestGeneratorReferenceWithFilePathEquality(CreateGenerator(), SharedPath); var analyzerReference2 = new TestGeneratorReferenceWithFilePathEquality(CreateGenerator(), SharedPath); diff --git a/src/Workspaces/CoreTest/SolutionTests/TryApplyChangesTests.cs b/src/Workspaces/CoreTest/SolutionTests/TryApplyChangesTests.cs index 63807101f0a66..b9e1680f9a148 100644 --- a/src/Workspaces/CoreTest/SolutionTests/TryApplyChangesTests.cs +++ b/src/Workspaces/CoreTest/SolutionTests/TryApplyChangesTests.cs @@ -31,7 +31,7 @@ public CustomizedCanApplyWorkspace(ApplyChangesKind[] allowedKinds, Func? canApplyCompilationOptions = null) : base(Host.Mef.MefHostServices.DefaultHost, workspaceKind: nameof(CustomizedCanApplyWorkspace)) { - _allowedKinds = allowedKinds.ToImmutableArray(); + _allowedKinds = [.. allowedKinds]; _canApplyParseOptions = canApplyParseOptions; _canApplyCompilationOptions = canApplyCompilationOptions; diff --git a/src/Workspaces/CoreTest/SyntaxReferenceTests.cs b/src/Workspaces/CoreTest/SyntaxReferenceTests.cs index 2ce0a35a4bb70..6614d2e7f9b26 100644 --- a/src/Workspaces/CoreTest/SyntaxReferenceTests.cs +++ b/src/Workspaces/CoreTest/SyntaxReferenceTests.cs @@ -2,34 +2,26 @@ // 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.Linq; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; -using Microsoft.CodeAnalysis.UnitTests.Persistence; using Roslyn.Test.Utilities; using Xunit; +using Microsoft.CodeAnalysis.Shared.Extensions; using CS = Microsoft.CodeAnalysis.CSharp; using VB = Microsoft.CodeAnalysis.VisualBasic; +using System.Threading.Tasks; +using System.Threading; namespace Microsoft.CodeAnalysis.UnitTests { [UseExportProvider] [Trait(Traits.Feature, Traits.Features.Workspace)] - public class SyntaxReferenceTests : TestBase + public sealed class SyntaxReferenceTests : TestBase { - private static Workspace CreateWorkspace(Type[] additionalParts = null) - => new AdhocWorkspace(FeaturesTestCompositions.Features.AddParts(additionalParts).GetHostServices()); - - private static Workspace CreateWorkspaceWithRecoverableSyntaxTrees() - => CreateWorkspace( - [ - typeof(TestTemporaryStorageServiceFactory) - ]); + private static Workspace CreateWorkspace() + => new AdhocWorkspace(FeaturesTestCompositions.Features.GetHostServices()); private static Solution AddSingleFileCSharpProject(Solution solution, string source) { @@ -52,16 +44,16 @@ private static Solution AddSingleFileVisualBasicProject(Solution solution, strin } [Fact] - public void TestCSharpReferenceToZeroWidthNode() + public async Task TestCSharpReferenceToZeroWidthNode() { - using var workspace = CreateWorkspaceWithRecoverableSyntaxTrees(); + using var workspace = CreateWorkspace(); var solution = AddSingleFileCSharpProject(workspace.CurrentSolution, @" public class C<> { } "); - var tree = solution.Projects.First().Documents.First().GetSyntaxTreeAsync().Result; + var tree = await solution.Projects.First().Documents.First().GetRequiredSyntaxTreeAsync(CancellationToken.None); // this is an expected TypeParameterSyntax with a missing identifier token (it is zero-length w/ an error attached to it) var node = tree.GetRoot().DescendantNodes().OfType().Single(); @@ -75,15 +67,15 @@ public class C<> } [Fact] - public void TestVisualBasicReferenceToZeroWidthNode() + public async Task TestVisualBasicReferenceToZeroWidthNode() { - using var workspace = CreateWorkspaceWithRecoverableSyntaxTrees(); + using var workspace = CreateWorkspace(); var solution = AddSingleFileVisualBasicProject(workspace.CurrentSolution, @" Public Class C(Of ) End Class "); - var tree = solution.Projects.First().Documents.First().GetSyntaxTreeAsync().Result; + var tree = await solution.Projects.First().Documents.First().GetRequiredSyntaxTreeAsync(CancellationToken.None); // this is an expected TypeParameterSyntax with a missing identifier token (it is zero-length w/ an error attached to it) var node = tree.GetRoot().DescendantNodes().OfType().Single(); @@ -97,9 +89,9 @@ End Class } [Fact] - public void TestCSharpReferenceToNodeInStructuredTrivia() + public async Task TestCSharpReferenceToNodeInStructuredTrivia() { - using var workspace = CreateWorkspaceWithRecoverableSyntaxTrees(); + using var workspace = CreateWorkspace(); var solution = AddSingleFileCSharpProject(workspace.CurrentSolution, @" #if true || true public class C @@ -107,7 +99,7 @@ public class C } #endif "); - var tree = solution.Projects.First().Documents.First().GetSyntaxTreeAsync().Result; + var tree = await solution.Projects.First().Documents.First().GetRequiredSyntaxTreeAsync(CancellationToken.None); // find binary node that is part of #if directive var node = tree.GetRoot().DescendantNodes(descendIntoTrivia: true).OfType().First(); @@ -120,9 +112,9 @@ public class C } [Fact] - public void TestVisualBasicReferenceToNodeInStructuredTrivia() + public async Task TestVisualBasicReferenceToNodeInStructuredTrivia() { - using var workspace = CreateWorkspaceWithRecoverableSyntaxTrees(); + using var workspace = CreateWorkspace(); var solution = AddSingleFileVisualBasicProject(workspace.CurrentSolution, @" #If True Or True Then Public Class C @@ -130,7 +122,7 @@ End Class #End If "); - var tree = solution.Projects.First().Documents.First().GetSyntaxTreeAsync().Result; + var tree = await solution.Projects.First().Documents.First().GetRequiredSyntaxTreeAsync(CancellationToken.None); // find binary node that is part of #if directive var node = tree.GetRoot().DescendantNodes(descendIntoTrivia: true).OfType().First(); @@ -143,9 +135,9 @@ End Class } [Fact] - public void TestCSharpReferenceToZeroWidthNodeInStructuredTrivia() + public async Task TestCSharpReferenceToZeroWidthNodeInStructuredTrivia() { - using var workspace = CreateWorkspaceWithRecoverableSyntaxTrees(); + using var workspace = CreateWorkspace(); var solution = AddSingleFileCSharpProject(workspace.CurrentSolution, @" #if true || public class C @@ -154,7 +146,7 @@ public class C #endif "); - var tree = solution.Projects.First().Documents.First().GetSyntaxTreeAsync().Result; + var tree = await solution.Projects.First().Documents.First().GetRequiredSyntaxTreeAsync(CancellationToken.None); // find binary node that is part of #if directive var binary = tree.GetRoot().DescendantNodes(descendIntoTrivia: true).OfType().First(); @@ -173,7 +165,7 @@ public class C [Fact] public async System.Threading.Tasks.Task TestVisualBasicReferenceToZeroWidthNodeInStructuredTriviaAsync() { - using var workspace = CreateWorkspaceWithRecoverableSyntaxTrees(); + using var workspace = CreateWorkspace(); var solution = AddSingleFileVisualBasicProject(workspace.CurrentSolution, @" #If (True Or ) Then Public Class C @@ -181,7 +173,7 @@ End Class #End If "); - var tree = await solution.Projects.First().Documents.First().GetSyntaxTreeAsync(); + var tree = await solution.Projects.First().Documents.First().GetRequiredSyntaxTreeAsync(CancellationToken.None); // find binary node that is part of #if directive var binary = tree.GetRoot().DescendantNodes(descendIntoTrivia: true).OfType().First(); diff --git a/src/Workspaces/CoreTest/TestCompositionTests.cs b/src/Workspaces/CoreTest/TestCompositionTests.cs index 13237a0dcb699..24535fa007888 100644 --- a/src/Workspaces/CoreTest/TestCompositionTests.cs +++ b/src/Workspaces/CoreTest/TestCompositionTests.cs @@ -2,21 +2,21 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Collections.Immutable; using Microsoft.CodeAnalysis.Test.Utilities; -using Microsoft.CodeAnalysis.UnitTests.Persistence; using Roslyn.Test.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.UnitTests { + using static SourceGeneratorTelemetryCollectorWorkspaceServiceTests; + public class TestCompositionTests { [Fact] public void FactoryReuse() { - var composition1 = FeaturesTestCompositions.Features.AddParts(typeof(TestErrorReportingService), typeof(TestTemporaryStorageServiceFactory)); - var composition2 = FeaturesTestCompositions.Features.AddParts(typeof(TestTemporaryStorageServiceFactory), typeof(TestErrorReportingService)); + var composition1 = FeaturesTestCompositions.Features.AddParts(typeof(TestErrorReportingService), typeof(TestSourceGeneratorTelemetryCollectorWorkspaceServiceFactory)); + var composition2 = FeaturesTestCompositions.Features.AddParts(typeof(TestSourceGeneratorTelemetryCollectorWorkspaceServiceFactory), typeof(TestErrorReportingService)); Assert.Same(composition1.ExportProviderFactory, composition2.ExportProviderFactory); } diff --git a/src/Workspaces/CoreTest/UtilityTest/AsyncLazyTests.cs b/src/Workspaces/CoreTest/UtilityTest/AsyncLazyTests.cs index c8f23e41194a1..ff97ddfac212b 100644 --- a/src/Workspaces/CoreTest/UtilityTest/AsyncLazyTests.cs +++ b/src/Workspaces/CoreTest/UtilityTest/AsyncLazyTests.cs @@ -220,8 +220,7 @@ public void GetValueAsyncThatIsCancelledReturnsTaskCancelledWithCorrectToken() } } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] private static void CancellationDuringInlinedComputationFromGetValueOrGetValueAsyncStillCachesResult(bool includeSynchronousComputation) { var computations = 0; @@ -272,8 +271,7 @@ public void SynchronousRequestShouldCacheValueWithAsynchronousComputeFunction() Assert.Same(secondRequestResult, firstRequestResult); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public async Task AwaitingProducesCorrectException(bool producerAsync, bool consumerAsync) { var exception = new ArgumentException(); diff --git a/src/Workspaces/CoreTest/WorkspaceServiceTests/GlobalOptionServiceTests.cs b/src/Workspaces/CoreTest/WorkspaceServiceTests/GlobalOptionServiceTests.cs index cf34c47dcdd13..5a50bd52ddc6a 100644 --- a/src/Workspaces/CoreTest/WorkspaceServiceTests/GlobalOptionServiceTests.cs +++ b/src/Workspaces/CoreTest/WorkspaceServiceTests/GlobalOptionServiceTests.cs @@ -50,8 +50,7 @@ public void LegacyGlobalOptions_SetGet() Assert.Equal(3, optionService.GetExternallyDefinedOption(optionKey)); } - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] public void ExternallyDefinedOption(bool subclass) { using var workspace1 = new AdhocWorkspace(); diff --git a/src/Workspaces/CoreTest/WorkspaceServiceTests/TemporaryStorageServiceTests.cs b/src/Workspaces/CoreTest/WorkspaceServiceTests/TemporaryStorageServiceTests.cs index bba69b5e4b499..053cad8235405 100644 --- a/src/Workspaces/CoreTest/WorkspaceServiceTests/TemporaryStorageServiceTests.cs +++ b/src/Workspaces/CoreTest/WorkspaceServiceTests/TemporaryStorageServiceTests.cs @@ -18,6 +18,8 @@ namespace Microsoft.CodeAnalysis.UnitTests { + using static TemporaryStorageService; + [UseExportProvider] #if NETCOREAPP [SupportedOSPlatform("windows")] @@ -51,7 +53,6 @@ public void TestTemporaryStorageStream() using var workspace = new AdhocWorkspace(); var textFactory = Assert.IsType(workspace.Services.GetService()); var service = Assert.IsType(workspace.Services.GetRequiredService()); - var temporaryStorage = service.CreateTemporaryStreamStorage(); using var data = SerializableBytes.CreateWritableStream(); for (var i = 0; i < SharedPools.ByteBufferSize; i++) @@ -59,9 +60,9 @@ public void TestTemporaryStorageStream() data.WriteByte((byte)(i % 2)); } - data.Position = 0; - temporaryStorage.WriteStreamAsync(data).Wait(); - using var result = temporaryStorage.ReadStreamAsync().Result; + var handle = service.WriteToTemporaryStorage(data, CancellationToken.None); + + using var result = handle.ReadFromTemporaryStorage(CancellationToken.None); Assert.Equal(data.Length, result.Length); for (var i = 0; i < SharedPools.ByteBufferSize; i++) @@ -72,65 +73,15 @@ public void TestTemporaryStorageStream() private static void TestTemporaryStorage(ITemporaryStorageServiceInternal temporaryStorageService, SourceText text) { - // create a temporary storage location - var temporaryStorage = temporaryStorageService.CreateTemporaryTextStorage(); - // write text into it - temporaryStorage.WriteTextAsync(text).Wait(); + var handle = temporaryStorageService.WriteToTemporaryStorage(text, CancellationToken.None); // read text back from it - var text2 = temporaryStorage.ReadTextAsync().Result; + var text2 = handle.ReadFromTemporaryStorage(CancellationToken.None); Assert.NotSame(text, text2); Assert.Equal(text.ToString(), text2.ToString()); Assert.Equal(text.Encoding, text2.Encoding); - - temporaryStorage.Dispose(); - } - - [ConditionalFact(typeof(WindowsOnly))] - public void TestTemporaryTextStorageExceptions() - { - using var workspace = new AdhocWorkspace(); - var textFactory = Assert.IsType(workspace.Services.GetService()); - var service = Assert.IsType(workspace.Services.GetRequiredService()); - var storage = service.CreateTemporaryTextStorage(); - - // Nothing has been written yet - Assert.Throws(() => storage.ReadText()); - Assert.Throws(() => storage.ReadTextAsync().Result); - - // write a normal string - var text = SourceText.From(new string(' ', 4096) + "public class A {}"); - storage.WriteTextAsync(text).Wait(); - - // Writing multiple times is not allowed - Assert.Throws(() => storage.WriteText(text)); - Assert.Throws(() => storage.WriteTextAsync(text).Wait()); - } - - [ConditionalFact(typeof(WindowsOnly))] - public void TestTemporaryStreamStorageExceptions() - { - using var workspace = new AdhocWorkspace(); - var textFactory = Assert.IsType(workspace.Services.GetService()); - var service = Assert.IsType(workspace.Services.GetRequiredService()); - var storage = service.CreateTemporaryStreamStorage(); - - // Nothing has been written yet - Assert.Throws(() => storage.ReadStream(CancellationToken.None)); - Assert.Throws(() => storage.ReadStreamAsync().Result); - - // write a normal stream - var stream = new MemoryStream(); - stream.Write([42], 0, 1); - stream.Position = 0; - storage.WriteStreamAsync(stream).Wait(); - - // Writing multiple times is not allowed - // These should also throw before ever getting to the point where they would look at the null stream arg. - Assert.Throws(() => storage.WriteStream(null!)); - Assert.Throws(() => storage.WriteStreamAsync(null!).Wait()); } [ConditionalFact(typeof(WindowsOnly))] @@ -139,15 +90,15 @@ public void TestZeroLengthStreams() using var workspace = new AdhocWorkspace(); var textFactory = Assert.IsType(workspace.Services.GetService()); var service = Assert.IsType(workspace.Services.GetRequiredService()); - var storage = service.CreateTemporaryStreamStorage(); // 0 length streams are allowed + TemporaryStorageStreamHandle handle; using (var stream1 = new MemoryStream()) { - storage.WriteStream(stream1); + handle = service.WriteToTemporaryStorage(stream1, CancellationToken.None); } - using (var stream2 = storage.ReadStream(CancellationToken.None)) + using (var stream2 = handle.ReadFromTemporaryStorage(CancellationToken.None)) { Assert.Equal(0, stream2.Length); } @@ -170,19 +121,15 @@ public void TestTemporaryStorageMemoryMappedFileManagement() { for (var j = 1; j < 5; j++) { - using ITemporaryStreamStorageInternal storage1 = service.CreateTemporaryStreamStorage(), - storage2 = service.CreateTemporaryStreamStorage(); - var storage3 = service.CreateTemporaryStreamStorage(); // let the finalizer run for this instance - - storage1.WriteStream(new MemoryStream(buffer.GetBuffer(), 0, 1024 * i - 1)); - storage2.WriteStream(new MemoryStream(buffer.GetBuffer(), 0, 1024 * i)); - storage3.WriteStream(new MemoryStream(buffer.GetBuffer(), 0, 1024 * i + 1)); + var handle1 = service.WriteToTemporaryStorage(new MemoryStream(buffer.GetBuffer(), 0, 1024 * i - 1), CancellationToken.None); + var handle2 = service.WriteToTemporaryStorage(new MemoryStream(buffer.GetBuffer(), 0, 1024 * i), CancellationToken.None); + var handle3 = service.WriteToTemporaryStorage(new MemoryStream(buffer.GetBuffer(), 0, 1024 * i + 1), CancellationToken.None); await Task.Yield(); - using Stream s1 = storage1.ReadStream(), - s2 = storage2.ReadStream(), - s3 = storage3.ReadStream(CancellationToken.None); + using var s1 = handle1.ReadFromTemporaryStorage(CancellationToken.None); + using var s2 = handle2.ReadFromTemporaryStorage(CancellationToken.None); + using var s3 = handle3.ReadFromTemporaryStorage(CancellationToken.None); Assert.Equal(1024 * i - 1, s1.Length); Assert.Equal(1024 * i, s2.Length); Assert.Equal(1024 * i + 1, s3.Length); @@ -214,20 +161,17 @@ public void TestTemporaryStorageScaling() // Create 4GB of memory mapped files var fileCount = (int)((long)4 * 1024 * 1024 * 1024 / data.Length); - var storageHandles = new List(fileCount); + var storageHandles = new List(fileCount); for (var i = 0; i < fileCount; i++) { - var s = service.CreateTemporaryStreamStorage(); - storageHandles.Add(s); - data.Position = 0; - s.WriteStreamAsync(data).Wait(); + var handle = service.WriteToTemporaryStorage(data, CancellationToken.None); + storageHandles.Add(handle); } for (var i = 0; i < 1024 * 5; i++) { - using var s = storageHandles[i].ReadStreamAsync().Result; + using var s = storageHandles[i].ReadFromTemporaryStorage(CancellationToken.None); Assert.Equal(1, s.ReadByte()); - storageHandles[i].Dispose(); } } } @@ -238,7 +182,6 @@ public void StreamTest1() using var workspace = new AdhocWorkspace(); var textFactory = Assert.IsType(workspace.Services.GetService()); var service = Assert.IsType(workspace.Services.GetRequiredService()); - var storage = service.CreateTemporaryStreamStorage(); using var expected = new MemoryStream(); for (var i = 0; i < 10000; i++) @@ -246,11 +189,10 @@ public void StreamTest1() expected.WriteByte((byte)(i % byte.MaxValue)); } - expected.Position = 0; - storage.WriteStream(expected); + var handle = service.WriteToTemporaryStorage(expected, CancellationToken.None); expected.Position = 0; - using var stream = storage.ReadStream(CancellationToken.None); + using var stream = handle.ReadFromTemporaryStorage(CancellationToken.None); Assert.Equal(expected.Length, stream.Length); for (var i = 0; i < expected.Length; i++) @@ -265,7 +207,6 @@ public void StreamTest2() using var workspace = new AdhocWorkspace(); var textFactory = Assert.IsType(workspace.Services.GetService()); var service = Assert.IsType(workspace.Services.GetRequiredService()); - var storage = service.CreateTemporaryStreamStorage(); using var expected = new MemoryStream(); for (var i = 0; i < 10000; i++) @@ -273,11 +214,10 @@ public void StreamTest2() expected.WriteByte((byte)(i % byte.MaxValue)); } - expected.Position = 0; - storage.WriteStream(expected); + var handle = service.WriteToTemporaryStorage(expected, CancellationToken.None); expected.Position = 0; - using var stream = storage.ReadStream(CancellationToken.None); + using var stream = handle.ReadFromTemporaryStorage(CancellationToken.None); Assert.Equal(expected.Length, stream.Length); var index = 0; @@ -302,7 +242,6 @@ public void StreamTest3() using var workspace = new AdhocWorkspace(); var textFactory = Assert.IsType(workspace.Services.GetService()); var service = Assert.IsType(workspace.Services.GetRequiredService()); - var storage = service.CreateTemporaryStreamStorage(); using var expected = new MemoryStream(); var random = new Random(Environment.TickCount); @@ -315,11 +254,10 @@ public void StreamTest3() expected.WriteByte(value); } - expected.Position = 0; - storage.WriteStream(expected); + var handle = service.WriteToTemporaryStorage(expected, CancellationToken.None); expected.Position = 0; - using var stream = storage.ReadStream(CancellationToken.None); + using var stream = handle.ReadFromTemporaryStorage(CancellationToken.None); Assert.Equal(expected.Length, stream.Length); for (var i = 0; i < expected.Length; i++) diff --git a/src/Workspaces/CoreTestUtilities/Fakes/SimpleAssetSource.cs b/src/Workspaces/CoreTestUtilities/Fakes/SimpleAssetSource.cs index ab8478a29d58e..e9e24ba94cab1 100644 --- a/src/Workspaces/CoreTestUtilities/Fakes/SimpleAssetSource.cs +++ b/src/Workspaces/CoreTestUtilities/Fakes/SimpleAssetSource.cs @@ -26,11 +26,10 @@ public ValueTask GetAssetsAsync( Contract.ThrowIfFalse(map.TryGetValue(checksum, out var data)); using var stream = new MemoryStream(); - using var context = new SolutionReplicationContext(); using (var writer = new ObjectWriter(stream, leaveOpen: true)) { - serializerService.Serialize(data, writer, context, cancellationToken); + serializerService.Serialize(data, writer, cancellationToken); } stream.Position = 0; diff --git a/src/Workspaces/CoreTestUtilities/MEF/TestComposition.cs b/src/Workspaces/CoreTestUtilities/MEF/TestComposition.cs index da2266620118d..95f12c9ac6ba3 100644 --- a/src/Workspaces/CoreTestUtilities/MEF/TestComposition.cs +++ b/src/Workspaces/CoreTestUtilities/MEF/TestComposition.cs @@ -33,9 +33,9 @@ public sealed class TestComposition public CacheKey(ImmutableHashSet assemblies, ImmutableHashSet parts, ImmutableHashSet excludedPartTypes) { - _assemblies = assemblies.OrderBy((a, b) => string.CompareOrdinal(a.FullName, b.FullName)).ToImmutableArray(); - _parts = parts.OrderBy((a, b) => string.CompareOrdinal(a.FullName, b.FullName)).ToImmutableArray(); - _excludedPartTypes = excludedPartTypes.OrderBy((a, b) => string.CompareOrdinal(a.FullName, b.FullName)).ToImmutableArray(); + _assemblies = [.. assemblies.OrderBy((a, b) => string.CompareOrdinal(a.FullName, b.FullName))]; + _parts = [.. parts.OrderBy((a, b) => string.CompareOrdinal(a.FullName, b.FullName))]; + _excludedPartTypes = [.. excludedPartTypes.OrderBy((a, b) => string.CompareOrdinal(a.FullName, b.FullName))]; } public override bool Equals(object? obj) diff --git a/src/Workspaces/CoreTestUtilities/Remote/InProcRemostHostClient.cs b/src/Workspaces/CoreTestUtilities/Remote/InProcRemostHostClient.cs index 8808a28046312..532018f46e517 100644 --- a/src/Workspaces/CoreTestUtilities/Remote/InProcRemostHostClient.cs +++ b/src/Workspaces/CoreTestUtilities/Remote/InProcRemostHostClient.cs @@ -184,36 +184,36 @@ public InProcRemoteServices(SolutionServices workspaceServices, TraceListener? t RegisterInProcBrokeredService(SolutionAssetProvider.ServiceDescriptor, () => new SolutionAssetProvider(workspaceServices)); RegisterRemoteBrokeredService(new RemoteAssetSynchronizationService.Factory()); RegisterRemoteBrokeredService(new RemoteAsynchronousOperationListenerService.Factory()); - RegisterRemoteBrokeredService(new RemoteSymbolSearchUpdateService.Factory()); + RegisterRemoteBrokeredService(new RemoteCodeLensReferencesService.Factory()); + RegisterRemoteBrokeredService(new RemoteConvertTupleToStructCodeRefactoringService.Factory()); + RegisterRemoteBrokeredService(new RemoteDependentTypeFinderService.Factory()); RegisterRemoteBrokeredService(new RemoteDesignerAttributeDiscoveryService.Factory()); - RegisterRemoteBrokeredService(new RemoteTaskListService.Factory()); RegisterRemoteBrokeredService(new RemoteDiagnosticAnalyzerService.Factory()); - RegisterRemoteBrokeredService(new RemoteSemanticClassificationService.Factory()); RegisterRemoteBrokeredService(new RemoteDocumentHighlightsService.Factory()); + RegisterRemoteBrokeredService(new RemoteEditAndContinueService.Factory()); RegisterRemoteBrokeredService(new RemoteEncapsulateFieldService.Factory()); - RegisterRemoteBrokeredService(new RemoteKeepAliveService.Factory()); - RegisterRemoteBrokeredService(new RemoteRenamerService.Factory()); - RegisterRemoteBrokeredService(new RemoteConvertTupleToStructCodeRefactoringService.Factory()); + RegisterRemoteBrokeredService(new RemoteExtensionMethodImportCompletionService.Factory()); RegisterRemoteBrokeredService(new RemoteFindUsagesService.Factory()); RegisterRemoteBrokeredService(new RemoteFullyQualifyService.Factory()); - RegisterRemoteBrokeredService(new RemoteSymbolFinderService.Factory()); - RegisterRemoteBrokeredService(new RemoteNavigateToSearchService.Factory()); - RegisterRemoteBrokeredService(new RemoteNavigationBarItemService.Factory()); - RegisterRemoteBrokeredService(new RemoteMissingImportDiscoveryService.Factory()); - RegisterRemoteBrokeredService(new RemoteExtensionMethodImportCompletionService.Factory()); - RegisterRemoteBrokeredService(new RemoteDependentTypeFinderService.Factory()); RegisterRemoteBrokeredService(new RemoteGlobalNotificationDeliveryService.Factory()); - RegisterRemoteBrokeredService(new RemoteCodeLensReferencesService.Factory()); - RegisterRemoteBrokeredService(new RemoteEditAndContinueService.Factory()); - RegisterRemoteBrokeredService(new RemoteValueTrackingService.Factory()); RegisterRemoteBrokeredService(new RemoteInheritanceMarginService.Factory()); - RegisterRemoteBrokeredService(new RemoteUnusedReferenceAnalysisService.Factory()); + RegisterRemoteBrokeredService(new RemoteKeepAliveService.Factory()); RegisterRemoteBrokeredService(new RemoteLegacySolutionEventsAggregationService.Factory()); + RegisterRemoteBrokeredService(new RemoteMissingImportDiscoveryService.Factory()); + RegisterRemoteBrokeredService(new RemoteNavigateToSearchService.Factory()); + RegisterRemoteBrokeredService(new RemoteNavigationBarItemService.Factory()); + RegisterRemoteBrokeredService(new RemoteProcessTelemetryService.Factory()); + RegisterRemoteBrokeredService(new RemoteRenamerService.Factory()); + RegisterRemoteBrokeredService(new RemoteSemanticClassificationService.Factory()); + RegisterRemoteBrokeredService(new RemoteSemanticSearchService.Factory()); + RegisterRemoteBrokeredService(new RemoteSourceGenerationService.Factory()); RegisterRemoteBrokeredService(new RemoteStackTraceExplorerService.Factory()); + RegisterRemoteBrokeredService(new RemoteSymbolFinderService.Factory()); + RegisterRemoteBrokeredService(new RemoteSymbolSearchUpdateService.Factory()); + RegisterRemoteBrokeredService(new RemoteTaskListService.Factory()); RegisterRemoteBrokeredService(new RemoteUnitTestingSearchService.Factory()); - RegisterRemoteBrokeredService(new RemoteSourceGenerationService.Factory()); - RegisterRemoteBrokeredService(new RemoteSemanticSearchService.Factory()); - RegisterRemoteBrokeredService(new RemoteProcessTelemetryService.Factory()); + RegisterRemoteBrokeredService(new RemoteUnusedReferenceAnalysisService.Factory()); + RegisterRemoteBrokeredService(new RemoteValueTrackingService.Factory()); } public void Dispose() diff --git a/src/Workspaces/CoreTestUtilities/Remote/InProcRemoteHostClientProvider.cs b/src/Workspaces/CoreTestUtilities/Remote/InProcRemoteHostClientProvider.cs index 7f6cfb699835d..e216af1473e25 100644 --- a/src/Workspaces/CoreTestUtilities/Remote/InProcRemoteHostClientProvider.cs +++ b/src/Workspaces/CoreTestUtilities/Remote/InProcRemoteHostClientProvider.cs @@ -17,6 +17,7 @@ namespace Microsoft.CodeAnalysis.Remote.Testing { +#pragma warning disable CA1416 // Validate platform compatibility internal sealed class InProcRemoteHostClientProvider : IRemoteHostClientProvider, IDisposable { [ExportWorkspaceServiceFactory(typeof(IRemoteHostClientProvider), ServiceLayer.Test), Shared, PartNotDiscoverable] @@ -99,4 +100,5 @@ public void Dispose() public Task TryGetRemoteHostClientAsync(CancellationToken cancellationToken) => Task.FromResult(_lazyClient.Value); } +#pragma warning restore CA1416 // Validate platform compatibility } diff --git a/src/Workspaces/CoreTestUtilities/Remote/TestSerializerService.cs b/src/Workspaces/CoreTestUtilities/Remote/TestSerializerService.cs index 8c42582dd37cc..3ce81ef9f9c1c 100644 --- a/src/Workspaces/CoreTestUtilities/Remote/TestSerializerService.cs +++ b/src/Workspaces/CoreTestUtilities/Remote/TestSerializerService.cs @@ -10,6 +10,7 @@ using System.Collections.Immutable; using System.Composition; using System.Linq; +using System.Runtime.Versioning; using System.Threading; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host; @@ -19,8 +20,13 @@ using Roslyn.Utilities; using ReferenceEqualityComparer = Roslyn.Utilities.ReferenceEqualityComparer; +#pragma warning disable CA1416 // Validate platform compatibility + namespace Microsoft.CodeAnalysis.UnitTests.Remote { +#if NETCOREAPP + [SupportedOSPlatform("windows")] +#endif internal sealed class TestSerializerService : SerializerService { private static readonly ImmutableDictionary s_wellKnownReferenceNames = ImmutableDictionary.Create(ReferenceEqualityComparer.Instance) @@ -41,7 +47,7 @@ public TestSerializerService(ConcurrentDictionary _sharedTestGeneratorReferences = sharedTestGeneratorReferences; } - public override void WriteMetadataReferenceTo(MetadataReference reference, ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken) + public override void WriteMetadataReferenceTo(MetadataReference reference, ObjectWriter writer, CancellationToken cancellationToken) { var wellKnownReferenceName = s_wellKnownReferenceNames.GetValueOrDefault(reference, null); if (wellKnownReferenceName is not null) @@ -52,7 +58,7 @@ public override void WriteMetadataReferenceTo(MetadataReference reference, Objec else { writer.WriteBoolean(false); - base.WriteMetadataReferenceTo(reference, writer, context, cancellationToken); + base.WriteMetadataReferenceTo(reference, writer, cancellationToken); } } diff --git a/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace_XmlConsumption.cs b/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace_XmlConsumption.cs index 2ae90f43f3550..9bddc611e2816 100644 --- a/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace_XmlConsumption.cs +++ b/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace_XmlConsumption.cs @@ -902,7 +902,7 @@ private IList CreateCommonReferences(XElement element) ((bool?)netcore30).HasValue && ((bool?)netcore30).Value) { - references = NetCoreApp.References.ToList(); + references = [.. NetCoreApp.References]; } var netstandard20 = element.Attribute(CommonReferencesNetStandard20Name); @@ -910,7 +910,7 @@ private IList CreateCommonReferences(XElement element) ((bool?)netstandard20).HasValue && ((bool?)netstandard20).Value) { - references = TargetFrameworkUtil.NetStandard20References.ToList(); + references = [.. TargetFrameworkUtil.NetStandard20References]; } var net6 = element.Attribute(CommonReferencesNet6Name); @@ -918,7 +918,7 @@ private IList CreateCommonReferences(XElement element) ((bool?)net6).HasValue && ((bool?)net6).Value) { - references = TargetFrameworkUtil.GetReferences(TargetFramework.Net60).ToList(); + references = [.. TargetFrameworkUtil.GetReferences(TargetFramework.Net60)]; } var net7 = element.Attribute(CommonReferencesNet7Name); @@ -926,7 +926,7 @@ private IList CreateCommonReferences(XElement element) ((bool?)net7).HasValue && ((bool?)net7).Value) { - references = TargetFrameworkUtil.GetReferences(TargetFramework.Net70).ToList(); + references = [.. TargetFrameworkUtil.GetReferences(TargetFramework.Net70)]; } var net8 = element.Attribute(CommonReferencesNet8Name); @@ -934,7 +934,7 @@ private IList CreateCommonReferences(XElement element) ((bool?)net8).HasValue && ((bool?)net8).Value) { - references = TargetFrameworkUtil.GetReferences(TargetFramework.Net80).ToList(); + references = [.. TargetFrameworkUtil.GetReferences(TargetFramework.Net80)]; } var mincorlib = element.Attribute(CommonReferencesMinCorlibName); diff --git a/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace`1.cs b/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace`1.cs index 309159decf2be..877bff6a16c33 100644 --- a/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace`1.cs +++ b/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace`1.cs @@ -54,8 +54,6 @@ public abstract partial class TestWorkspace : Wo internal override bool IgnoreUnchangeableDocumentsWhenApplyingChanges { get; } - private readonly IMetadataAsSourceFileService? _metadataAsSourceFileService; - private readonly string _workspaceKind; private readonly bool _supportsLspMutation; @@ -115,8 +113,6 @@ internal TestWorkspace( throw new InvalidOperationException($"{severityText} {fullMessage}"); }; } - - _metadataAsSourceFileService = ExportProvider.GetExportedValues().FirstOrDefault(); } private static HostServices GetHostServices([NotNull] ref TestComposition? composition, bool hasWorkspaceConfigurationOptions) @@ -202,12 +198,6 @@ public TDocument DocumentWithCursor public new void RegisterText(SourceTextContainer text) => base.RegisterText(text); - protected override void Dispose(bool finalize) - { - _metadataAsSourceFileService?.CleanupGeneratedFiles(); - base.Dispose(finalize); - } - internal void AddTestSolution(TSolution solution) => this.OnSolutionAdded(SolutionInfo.Create(solution.Id, solution.Version, solution.FilePath, projects: solution.Projects.Select(p => p.ToProjectInfo()))); @@ -638,7 +628,7 @@ internal void InitializeDocuments( Documents.Add(submission.Documents.Single()); } - var solution = CreateSolution(projectNameToTestHostProject.Values.ToArray()); + var solution = CreateSolution([.. projectNameToTestHostProject.Values]); AddTestSolution(solution); foreach (var projectElement in workspaceElement.Elements(ProjectElementName)) diff --git a/src/Workspaces/MSBuildTest/VisualStudioMSBuildWorkspaceTests.cs b/src/Workspaces/MSBuildTest/VisualStudioMSBuildWorkspaceTests.cs index 732ff37083097..230b95db8ef1b 100644 --- a/src/Workspaces/MSBuildTest/VisualStudioMSBuildWorkspaceTests.cs +++ b/src/Workspaces/MSBuildTest/VisualStudioMSBuildWorkspaceTests.cs @@ -2432,7 +2432,7 @@ public async Task TestProjectReferenceWithExternAlias() } [ConditionalFact(typeof(VisualStudioMSBuildInstalled))] - public async Task TestProjectReferenceWithReferenceOutputAssemblyFalse() + public async Task TestProjectReferenceWithReferenceOutputAssemblyFalse_SolutionRoot() { var files = GetProjectReferenceSolutionFiles(); files = VisitProjectReferences( @@ -2451,6 +2451,29 @@ public async Task TestProjectReferenceWithReferenceOutputAssemblyFalse() } } + [ConditionalFact(typeof(VisualStudioMSBuildInstalled))] + public async Task TestProjectReferenceWithReferenceOutputAssemblyFalse_ProjectRoot() + { + var files = GetProjectReferenceSolutionFiles(); + files = VisitProjectReferences( + files, + r => r.Add(new XElement(XName.Get("ReferenceOutputAssembly", MSBuildNamespace), "false"))); + + CreateFiles(files); + + var referencingProjectPath = GetSolutionFileName(@"CSharpProject\CSharpProject_ProjectReference.csproj"); + var referencedProjectPath = GetSolutionFileName(@"CSharpProject\CSharpProject.csproj"); + + using var workspace = CreateMSBuildWorkspace(); + var project = await workspace.OpenProjectAsync(referencingProjectPath); + + Assert.Empty(project.ProjectReferences); + + // Project referenced through ProjectReference with ReferenceOutputAssembly=false + // should be present in the solution. + Assert.NotNull(project.Solution.GetProjectsByName("CSharpProject").SingleOrDefault()); + } + private static FileSet VisitProjectReferences(FileSet files, Action visitProjectReference) { var result = new List<(string, object)>(); diff --git a/src/Workspaces/Remote/Core/AbstractAssetProvider.cs b/src/Workspaces/Remote/Core/AbstractAssetProvider.cs index 6a3c7c067f3ad..ac97eb85341e4 100644 --- a/src/Workspaces/Remote/Core/AbstractAssetProvider.cs +++ b/src/Workspaces/Remote/Core/AbstractAssetProvider.cs @@ -142,8 +142,7 @@ public async Task CreateDocumentInfoAsync( var attributes = await GetAssetAsync(new(AssetPathKind.DocumentAttributes, documentId), attributeChecksum, cancellationToken).ConfigureAwait(false); var serializableSourceText = await GetAssetAsync(new(AssetPathKind.DocumentText, documentId), textChecksum, cancellationToken).ConfigureAwait(false); - var text = await serializableSourceText.GetTextAsync(cancellationToken).ConfigureAwait(false); - var textLoader = TextLoader.From(TextAndVersion.Create(text, VersionStamp.Create(), attributes.FilePath)); + var textLoader = serializableSourceText.ToTextLoader(attributes.FilePath); // TODO: do we need version? return new DocumentInfo(attributes, textLoader, documentServiceProvider: null); @@ -190,23 +189,41 @@ public static async Task GetAssetsAsync( await assetProvider.GetAssetsAsync(assetPath, checksumSet, callback, arg, cancellationToken).ConfigureAwait(false); } + /// + /// Returns an array of assets, corresponding to all the checksums found in the given . + /// The assets will be returned in the order corresponding to their checksum in . + /// public static async Task> GetAssetsArrayAsync( this AbstractAssetProvider assetProvider, AssetPath assetPath, ChecksumCollection checksums, CancellationToken cancellationToken) where T : class { + // Note: nothing stops 'checksums' from having multiple identical checksums in it. First, collapse this down to + // a set so we're only asking about unique checksums. using var _1 = PooledHashSet.GetInstance(out var checksumSet); #if NET checksumSet.EnsureCapacity(checksums.Children.Length); #endif checksumSet.AddAll(checksums.Children); - using var _ = ArrayBuilder.GetInstance(checksumSet.Count, out var builder); + using var _2 = PooledDictionary.GetInstance(out var checksumToAsset); await assetProvider.GetAssetHelper().GetAssetsAsync( assetPath, checksumSet, - static (checksum, asset, builder) => builder.Add(asset), - builder, + // Calling .Add here is safe. As checksum-set is a unique set of checksums, we'll never have collions here. + static (checksum, asset, checksumToAsset) => checksumToAsset.Add(checksum, asset), + checksumToAsset, cancellationToken).ConfigureAwait(false); - return builder.ToImmutableAndClear(); + // Note: GetAssetsAsync will only succeed if we actually found all our assets (it crashes otherwise). So we can + // just safely assume we can index into checksumToAsset here. + Contract.ThrowIfTrue(checksumToAsset.Count != checksumSet.Count); + + // The result of GetAssetsArrayAsync wants the returned assets to be in the exact order of the checksums that + // were in 'checksums'. So now fetch the assets in that order, even if we found them in an entirely different + // order. + var result = new FixedSizeArrayBuilder(checksums.Children.Length); + foreach (var checksum in checksums.Children) + result.Add(checksumToAsset[checksum]); + + return result.MoveToImmutable(); } } diff --git a/src/Workspaces/Remote/Core/BrokeredServiceDescriptors.cs b/src/Workspaces/Remote/Core/BrokeredServiceDescriptors.cs index 100a69202bfb6..d66c72ff84200 100644 --- a/src/Workspaces/Remote/Core/BrokeredServiceDescriptors.cs +++ b/src/Workspaces/Remote/Core/BrokeredServiceDescriptors.cs @@ -74,7 +74,8 @@ protected override JsonRpcConnection CreateConnection(JsonRpc jsonRpc) public static readonly ServiceRpcDescriptor HotReloadLoggerService = CreateDebuggerServiceDescriptor("HotReloadLogger", new Version(0, 1)); public static readonly ServiceRpcDescriptor HotReloadSessionNotificationService = CreateDebuggerServiceDescriptor("HotReloadSessionNotificationService", new Version(0, 1)); public static readonly ServiceRpcDescriptor ManagedHotReloadAgentManagerService = CreateDebuggerServiceDescriptor("ManagedHotReloadAgentManagerService", new Version(0, 1)); - public static readonly ServiceRpcDescriptor MauiLaunchCustomizerServiceDescriptor = CreateMauiServiceDescriptor("MauiLaunchCustomizerService", new Version(0, 1)); + public static readonly ServiceRpcDescriptor HotReloadOptionService = CreateDebuggerClientServiceDescriptor("HotReloadOptionService", new Version(0, 1)); + public static readonly ServiceRpcDescriptor MauiLaunchCustomizerService = CreateMauiServiceDescriptor("MauiLaunchCustomizerService", new Version(0, 1)); public static ServiceMoniker CreateMoniker(string namespaceName, string componentName, string serviceName, Version? version) => new(namespaceName + "." + componentName + "." + serviceName, version); @@ -98,6 +99,13 @@ public static ServiceJsonRpcDescriptor CreateServerServiceDescriptor(string serv public static ServiceJsonRpcDescriptor CreateDebuggerServiceDescriptor(string serviceName, Version? version = null) => CreateDescriptor(CreateMoniker(VisualStudioComponentNamespace, DebuggerComponentName, serviceName, version)); + /// + /// Descriptor for services proferred by the debugger server (implemented in TypeScript). + /// + public static ServiceJsonRpcDescriptor CreateDebuggerClientServiceDescriptor(string serviceName, Version? version = null) + => new ClientServiceDescriptor(CreateMoniker(VisualStudioComponentNamespace, DebuggerComponentName, serviceName, version), clientInterface: null) + .WithExceptionStrategy(ExceptionProcessing.ISerializable); + /// /// Descriptor for services proferred by the MAUI extension (implemented in TypeScript). /// diff --git a/src/Workspaces/Remote/Core/RemoteHostAssetWriter.cs b/src/Workspaces/Remote/Core/RemoteHostAssetWriter.cs index ca05cccdec70f..f1548eb3fbcb6 100644 --- a/src/Workspaces/Remote/Core/RemoteHostAssetWriter.cs +++ b/src/Workspaces/Remote/Core/RemoteHostAssetWriter.cs @@ -4,12 +4,13 @@ using System; using System.Buffers.Binary; +using System.Collections.Generic; using System.IO.Pipelines; using System.Threading; -using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Serialization; +using Microsoft.CodeAnalysis.Shared.Utilities; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Remote; @@ -61,79 +62,29 @@ internal readonly struct RemoteHostAssetWriter( private static readonly ObjectPool s_streamPool = new(() => new()); - private static readonly UnboundedChannelOptions s_channelOptions = new() - { - // We have a single task reading the data from the channel and writing it to the pipe. This option allows the - // channel to operate in a more efficient manner knowing it won't have to synchronize data for multiple readers. - SingleReader = true, - - // Currently we only have a single writer writing to the channel when we call _scope.FindAssetsAsync. However, - // we could change this in the future to allow the search to happen in parallel. - SingleWriter = true, - }; - private readonly PipeWriter _pipeWriter = pipeWriter; private readonly Scope _scope = scope; private readonly AssetPath _assetPath = assetPath; private readonly ReadOnlyMemory _checksums = checksums; private readonly ISerializerService _serializer = serializer; - public async ValueTask WriteDataAsync(CancellationToken cancellationToken) - { - // Create a channel to communicate between the searching and writing tasks. This allows the searching task to - // find items, add them to the channel synchronously, and immediately continue searching for more items. - // Concurrently, the writing task can read from the channel and write the items to the pipe-writer. - var channel = Channel.CreateUnbounded(s_channelOptions); - - // When cancellation happens, attempt to close the channel. That will unblock the task writing the assets to - // the pipe. Capture-free version is only available on netcore unfortunately. - using var _ = cancellationToken.Register( -#if NET - static (obj, cancellationToken) => ((Channel)obj!).Writer.TryComplete(new OperationCanceledException(cancellationToken)), - state: channel); -#else - () => channel.Writer.TryComplete(new OperationCanceledException(cancellationToken))); -#endif - - // Spin up a task to go find all the requested checksums, adding results to the channel. - // Spin up a task to read from the channel, writing out the assets to the pipe-writer. - await Task.WhenAll( - FindAssetsFromScopeAndWriteToChannelAsync(channel.Writer, cancellationToken), - ReadAssetsFromChannelAndWriteToPipeAsync(channel.Reader, cancellationToken)).ConfigureAwait(false); - } - - private async Task FindAssetsFromScopeAndWriteToChannelAsync(ChannelWriter channelWriter, CancellationToken cancellationToken) - { - Exception? exception = null; - try - { - await Task.Yield(); - - await _scope.FindAssetsAsync( - _assetPath, _checksums, - // It's ok to use TryWrite here. TryWrite always succeeds unless the channel is completed. And the - // channel is only ever completed by us (after FindAssetsAsync completes or throws an exception) or if - // the cancellationToken is triggered above in WriteDataAsync. In that latter case, it's ok for writing - // to the channel to do nothing as we no longer need to write out those assets to the pipe. - static (checksum, asset, channelWriter) => channelWriter.TryWrite((checksum, asset)), - channelWriter, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) when ((exception = ex) == null) - { - throw ExceptionUtilities.Unreachable(); - } - finally - { - // No matter what path we take (exceptional or non-exceptional), always complete the channel so the - // writing task knows it's done. - channelWriter.TryComplete(exception); - } - } - - private async Task ReadAssetsFromChannelAndWriteToPipeAsync(ChannelReader channelReader, CancellationToken cancellationToken) + public Task WriteDataAsync(CancellationToken cancellationToken) + => ProducerConsumer.RunAsync( + ProducerConsumerOptions.SingleReaderWriterOptions, + produceItems: static (onItemFound, @this, cancellationToken) => @this.FindAssetsAsync(onItemFound, cancellationToken), + consumeItems: static (items, @this, cancellationToken) => @this.WriteBatchToPipeAsync(items, cancellationToken), + args: this, + cancellationToken); + + private Task FindAssetsAsync(Action onItemFound, CancellationToken cancellationToken) + => _scope.FindAssetsAsync( + _assetPath, _checksums, + static (checksum, asset, onItemFound) => onItemFound((checksum, asset)), + onItemFound, cancellationToken); + + private async Task WriteBatchToPipeAsync( + IAsyncEnumerable checksumsAndAssets, CancellationToken cancellationToken) { - await Task.Yield(); - // Get the in-memory buffer and object-writer we'll use to serialize the assets into. Don't write any // validation bytes at this point in time. We'll write them between each asset we write out. Using a single // object writer across all assets means we get the benefit of string deduplication across all assets we write @@ -150,14 +101,11 @@ private async Task ReadAssetsFromChannelAndWriteToPipeAsync(ChannelReader - /// Will be disposed from when the last ref-count to this scope goes - /// away. - /// - public readonly SolutionReplicationContext ReplicationContext = new(); - /// /// Only safe to read write while is held. /// diff --git a/src/Workspaces/Remote/Core/SolutionAssetStorage.cs b/src/Workspaces/Remote/Core/SolutionAssetStorage.cs index 8e87958c46eb3..d5f80f5030c92 100644 --- a/src/Workspaces/Remote/Core/SolutionAssetStorage.cs +++ b/src/Workspaces/Remote/Core/SolutionAssetStorage.cs @@ -111,8 +111,6 @@ private void DecreaseScopeRefCount(Scope scope) // Last ref went away, update our maps while under the lock, then cleanup its context data outside of the lock. _checksumToScope.Remove(solutionChecksum); } - - scope.ReplicationContext.Dispose(); } internal TestAccessor GetTestAccessor() diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs index 84b0ea0dd5915..5c23f42a6faa0 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs @@ -617,13 +617,14 @@ private async Task UpdateDocumentAsync( { var serializableSourceText = await _assetProvider.GetAssetAsync( assetPath: document.Id, newDocumentChecksums.textChecksum, cancellationToken).ConfigureAwait(false); - var sourceText = await serializableSourceText.GetTextAsync(cancellationToken).ConfigureAwait(false); + var loader = serializableSourceText.ToTextLoader(document.FilePath); + var mode = PreservationMode.PreserveValue; document = document.Kind switch { - TextDocumentKind.Document => document.Project.Solution.WithDocumentText(document.Id, sourceText).GetDocument(document.Id)!, - TextDocumentKind.AnalyzerConfigDocument => document.Project.Solution.WithAnalyzerConfigDocumentText(document.Id, sourceText).GetAnalyzerConfigDocument(document.Id)!, - TextDocumentKind.AdditionalDocument => document.Project.Solution.WithAdditionalDocumentText(document.Id, sourceText).GetAdditionalDocument(document.Id)!, + TextDocumentKind.Document => document.Project.Solution.WithDocumentTextLoader(document.Id, loader, mode).GetRequiredDocument(document.Id), + TextDocumentKind.AnalyzerConfigDocument => document.Project.Solution.WithAnalyzerConfigDocumentTextLoader(document.Id, loader, mode).GetRequiredAnalyzerConfigDocument(document.Id), + TextDocumentKind.AdditionalDocument => document.Project.Solution.WithAdditionalDocumentTextLoader(document.Id, loader, mode).GetRequiredAdditionalDocument(document.Id), _ => throw ExceptionUtilities.UnexpectedValue(document.Kind), }; } @@ -684,7 +685,7 @@ private async Task ValidateChecksumAsync( var workspace = new AdhocWorkspace(_hostServices); workspace.AddSolution(solutionInfo); - await TestUtils.AssertChecksumsAsync(_assetProvider, checksumFromRequest, workspace.CurrentSolution, incrementalSolutionBuilt).ConfigureAwait(false); + await TestUtils.AssertChecksumsAsync(_assetProvider, checksumFromRequest, workspace.CurrentSolution, incrementalSolutionBuilt, projectConeId).ConfigureAwait(false); } #endif } diff --git a/src/Workspaces/Remote/ServiceHub/Host/SolutionAssetCache.cs b/src/Workspaces/Remote/ServiceHub/Host/SolutionAssetCache.cs index 11067ca427033..b409b3b8e28db 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/SolutionAssetCache.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/SolutionAssetCache.cs @@ -267,5 +267,14 @@ public Entry(object @object) Object = @object; } } + + public TestAccessor GetTestAccessor() + => new(this); + + public readonly struct TestAccessor(SolutionAssetCache cache) + { + public void Clear() + => cache._assets.Clear(); + } } diff --git a/src/Workspaces/Remote/ServiceHub/Host/TestUtils.cs b/src/Workspaces/Remote/ServiceHub/Host/TestUtils.cs index 27047d2158e5b..ada4f5f1be1c1 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/TestUtils.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/TestUtils.cs @@ -40,43 +40,44 @@ internal static async Task AssertChecksumsAsync( AssetProvider assetService, Checksum checksumFromRequest, Solution solutionFromScratch, - Solution incrementalSolutionBuilt) + Solution incrementalSolutionBuilt, + ProjectId? projectConeId) { #if DEBUG var sb = new StringBuilder(); var allChecksumsFromRequest = await GetAllChildrenChecksumsAsync(checksumFromRequest).ConfigureAwait(false); - var assetMapFromNewSolution = await solutionFromScratch.GetAssetMapAsync(CancellationToken.None).ConfigureAwait(false); - var assetMapFromIncrementalSolution = await incrementalSolutionBuilt.GetAssetMapAsync(CancellationToken.None).ConfigureAwait(false); + var assetMapFromNewSolution = await solutionFromScratch.GetAssetMapAsync(projectConeId, CancellationToken.None).ConfigureAwait(false); + var assetMapFromIncrementalSolution = await incrementalSolutionBuilt.GetAssetMapAsync(projectConeId, CancellationToken.None).ConfigureAwait(false); // check 4 things // 1. first see if we create new solution from scratch, it works as expected (indicating a bug in incremental update) var mismatch1 = assetMapFromNewSolution.Where(p => !allChecksumsFromRequest.Contains(p.Key)).ToList(); - AppendMismatch(mismatch1, "assets only in new solutoin but not in the request", sb); + AppendMismatch(mismatch1, "Assets only in new solution but not in the request", sb); // 2. second check what items is mismatching for incremental solution var mismatch2 = assetMapFromIncrementalSolution.Where(p => !allChecksumsFromRequest.Contains(p.Key)).ToList(); - AppendMismatch(mismatch2, "assets only in the incremental solution but not in the request", sb); + AppendMismatch(mismatch2, "Assets only in the incremental solution but not in the request", sb); // 3. check whether solution created from scratch and incremental one have any mismatch var mismatch3 = assetMapFromNewSolution.Where(p => !assetMapFromIncrementalSolution.ContainsKey(p.Key)).ToList(); - AppendMismatch(mismatch3, "assets only in new solution but not in incremental solution", sb); + AppendMismatch(mismatch3, "Assets only in new solution but not in incremental solution", sb); var mismatch4 = assetMapFromIncrementalSolution.Where(p => !assetMapFromNewSolution.ContainsKey(p.Key)).ToList(); - AppendMismatch(mismatch4, "assets only in incremental solution but not in new solution", sb); + AppendMismatch(mismatch4, "Assets only in incremental solution but not in new solution", sb); // 4. see what item is missing from request var mismatch5 = await GetAssetFromAssetServiceAsync(allChecksumsFromRequest.Except(assetMapFromNewSolution.Keys)).ConfigureAwait(false); - AppendMismatch(mismatch5, "assets only in the request but not in new solution", sb); + AppendMismatch(mismatch5, "Assets only in the request but not in new solution", sb); var mismatch6 = await GetAssetFromAssetServiceAsync(allChecksumsFromRequest.Except(assetMapFromIncrementalSolution.Keys)).ConfigureAwait(false); - AppendMismatch(mismatch6, "assets only in the request but not in incremental solution", sb); + AppendMismatch(mismatch6, "Assets only in the request but not in incremental solution", sb); var result = sb.ToString(); if (result.Length > 0) { Logger.Log(FunctionId.SolutionCreator_AssetDifferences, result); - Debug.Fail("Differences detected in solution checksum: " + result); + Debug.Fail($"Differences detected in solution checksum (ProjectId={projectConeId}):\r\n{result}"); } return; @@ -154,10 +155,10 @@ private static void AddAllTo(DocumentStateChecksums documentStateChecksums, Hash /// create checksum to corresponding object map from solution this map should contain every parts of solution /// that can be used to re-create the solution back /// - public static async Task> GetAssetMapAsync(this Solution solution, CancellationToken cancellationToken) + public static async Task> GetAssetMapAsync(this Solution solution, ProjectId? projectConeId, CancellationToken cancellationToken) { var map = new Dictionary(); - await solution.AppendAssetMapAsync(map, cancellationToken).ConfigureAwait(false); + await solution.AppendAssetMapAsync(map, projectConeId, cancellationToken).ConfigureAwait(false); return map; } diff --git a/src/Workspaces/Remote/ServiceHub/Services/AssetSynchronization/RemoteAssetSynchronizationService.cs b/src/Workspaces/Remote/ServiceHub/Services/AssetSynchronization/RemoteAssetSynchronizationService.cs index f46c290d40e5d..c86c7482b169d 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/AssetSynchronization/RemoteAssetSynchronizationService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/AssetSynchronization/RemoteAssetSynchronizationService.cs @@ -56,8 +56,6 @@ public ValueTask SynchronizeTextAsync(DocumentId documentId, Checksum baseTextCh using (RoslynLogger.LogBlock(FunctionId.RemoteHostService_SynchronizeTextAsync, Checksum.GetChecksumLogInfo, baseTextChecksum, cancellationToken)) { - var serializer = workspace.Services.GetRequiredService(); - // Try to get the text associated with baseTextChecksum var text = await TryGetSourceTextAsync(WorkspaceManager, workspace, documentId, baseTextChecksum, cancellationToken).ConfigureAwait(false); if (text == null) @@ -72,9 +70,8 @@ public ValueTask SynchronizeTextAsync(DocumentId documentId, Checksum baseTextCh // the entire document. var newText = text.WithChanges(textChanges); var newSerializableText = new SerializableSourceText(newText, newText.GetContentHash()); - var newChecksum = serializer.CreateChecksum(newSerializableText, cancellationToken); - WorkspaceManager.SolutionAssetCache.GetOrAdd(newChecksum, newSerializableText); + WorkspaceManager.SolutionAssetCache.GetOrAdd(newSerializableText.ContentChecksum, newSerializableText); } return; diff --git a/src/Workspaces/Remote/ServiceHub/Services/AsynchronousOperationListener/RemoteAsynchronousOperationListenerService.cs b/src/Workspaces/Remote/ServiceHub/Services/AsynchronousOperationListener/RemoteAsynchronousOperationListenerService.cs index 0b331f550e052..1b405b7c66a44 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/AsynchronousOperationListener/RemoteAsynchronousOperationListenerService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/AsynchronousOperationListener/RemoteAsynchronousOperationListenerService.cs @@ -41,7 +41,7 @@ public ValueTask IsCompletedAsync(ImmutableArray featureNames, Can var exportProvider = workspace.Services.SolutionServices.ExportProvider; var listenerProvider = exportProvider.GetExports().Single().Value; - return new ValueTask(!listenerProvider.HasPendingWaiter(featureNames.ToArray())); + return new ValueTask(!listenerProvider.HasPendingWaiter([.. featureNames])); }, cancellationToken); } @@ -53,7 +53,7 @@ public ValueTask ExpeditedWaitAsync(ImmutableArray featureNames, Cancell var exportProvider = workspace.Services.SolutionServices.ExportProvider; var listenerProvider = exportProvider.GetExports().Single().Value; - await listenerProvider.WaitAllAsync(workspace, featureNames.ToArray()).ConfigureAwait(false); + await listenerProvider.WaitAllAsync(workspace, [.. featureNames]).ConfigureAwait(false); }, cancellationToken); } } diff --git a/src/Workspaces/Remote/ServiceHub/Services/NavigateToSearch/RemoteNavigateToSearchService.cs b/src/Workspaces/Remote/ServiceHub/Services/NavigateToSearch/RemoteNavigateToSearchService.cs index e267243f8c32d..27ea35c52107e 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/NavigateToSearch/RemoteNavigateToSearchService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/NavigateToSearch/RemoteNavigateToSearchService.cs @@ -13,7 +13,10 @@ namespace Microsoft.CodeAnalysis.Remote { - internal sealed class RemoteNavigateToSearchService : BrokeredServiceBase, IRemoteNavigateToSearchService + internal sealed class RemoteNavigateToSearchService( + in BrokeredServiceBase.ServiceConstructionArguments arguments, + RemoteCallback callback) + : BrokeredServiceBase(arguments), IRemoteNavigateToSearchService { internal sealed class Factory : FactoryBase { @@ -22,26 +25,14 @@ protected override IRemoteNavigateToSearchService CreateService( => new RemoteNavigateToSearchService(arguments, callback); } - private readonly RemoteCallback _callback; + private readonly RemoteCallback _callback = callback; - public RemoteNavigateToSearchService(in ServiceConstructionArguments arguments, RemoteCallback callback) - : base(arguments) - { - _callback = callback; - } - - private (Func onItemFound, Func onProjectCompleted) GetCallbacks( + private (Func, VoidResult, CancellationToken, Task> onItemsFound, Func onProjectCompleted) GetCallbacks( RemoteServiceCallbackId callbackId, CancellationToken cancellationToken) { - Func onItemFound = async i => await _callback.InvokeAsync((callback, _) => - callback.OnResultFoundAsync(callbackId, i), - cancellationToken).ConfigureAwait(false); - - Func onProjectCompleted = async () => await _callback.InvokeAsync((callback, _) => - callback.OnProjectCompletedAsync(callbackId), - cancellationToken).ConfigureAwait(false); - - return (onItemFound, onProjectCompleted); + return ( + async (array, _, cancellationToken) => await _callback.InvokeAsync((callback, cancellationToken) => callback.OnItemsFoundAsync(callbackId, array), cancellationToken).ConfigureAwait(false), + async () => await _callback.InvokeAsync((callback, cancellationToken) => callback.OnProjectCompletedAsync(callbackId), cancellationToken).ConfigureAwait(false)); } public ValueTask HydrateAsync(Checksum solutionChecksum, CancellationToken cancellationToken) @@ -64,10 +55,10 @@ public ValueTask SearchDocumentAsync( return RunServiceAsync(solutionChecksum, async solution => { var document = solution.GetRequiredDocument(documentId); - var (onItemFound, onProjectCompleted) = GetCallbacks(callbackId, cancellationToken); + var (onItemsFound, onProjectCompleted) = GetCallbacks(callbackId, cancellationToken); await AbstractNavigateToSearchService.SearchDocumentInCurrentProcessAsync( - document, searchPattern, kinds.ToImmutableHashSet(), onItemFound, cancellationToken).ConfigureAwait(false); + document, searchPattern, kinds.ToImmutableHashSet(), onItemsFound, cancellationToken).ConfigureAwait(false); }, cancellationToken); } @@ -83,12 +74,12 @@ public ValueTask SearchProjectsAsync( return RunServiceAsync(solutionChecksum, async solution => { var projects = projectIds.SelectAsArray(solution.GetRequiredProject); - var (onItemFound, onProjectCompleted) = GetCallbacks(callbackId, cancellationToken); + var (onItemsFound, onProjectCompleted) = GetCallbacks(callbackId, cancellationToken); var priorityDocuments = priorityDocumentIds.SelectAsArray(d => solution.GetRequiredDocument(d)); await AbstractNavigateToSearchService.SearchProjectsInCurrentProcessAsync( - projects, priorityDocuments, searchPattern, kinds.ToImmutableHashSet(), onItemFound, onProjectCompleted, cancellationToken).ConfigureAwait(false); + projects, priorityDocuments, searchPattern, kinds.ToImmutableHashSet(), onItemsFound, onProjectCompleted, cancellationToken).ConfigureAwait(false); }, cancellationToken); } @@ -103,10 +94,10 @@ public ValueTask SearchGeneratedDocumentsAsync( return RunServiceAsync(solutionChecksum, async solution => { var projects = projectIds.SelectAsArray(solution.GetRequiredProject); - var (onItemFound, onProjectCompleted) = GetCallbacks(callbackId, cancellationToken); + var (onItemsFound, onProjectCompleted) = GetCallbacks(callbackId, cancellationToken); await AbstractNavigateToSearchService.SearchGeneratedDocumentsInCurrentProcessAsync( - projects, searchPattern, kinds.ToImmutableHashSet(), onItemFound, onProjectCompleted, cancellationToken).ConfigureAwait(false); + projects, searchPattern, kinds.ToImmutableHashSet(), onItemsFound, onProjectCompleted, cancellationToken).ConfigureAwait(false); }, cancellationToken); } @@ -123,10 +114,10 @@ public ValueTask SearchCachedDocumentsAsync( // Intentionally do not call GetSolutionAsync here. We do not want the cost of // synchronizing the solution over to the remote side. Instead, we just directly // check whatever cached data we have from the previous vs session. - var (onItemFound, onProjectCompleted) = GetCallbacks(callbackId, cancellationToken); + var (onItemsFound, onProjectCompleted) = GetCallbacks(callbackId, cancellationToken); var storageService = GetWorkspaceServices().GetPersistentStorageService(); await AbstractNavigateToSearchService.SearchCachedDocumentsInCurrentProcessAsync( - storageService, documentKeys, priorityDocumentKeys, searchPattern, kinds.ToImmutableHashSet(), onItemFound, onProjectCompleted, cancellationToken).ConfigureAwait(false); + storageService, documentKeys, priorityDocumentKeys, searchPattern, kinds.ToImmutableHashSet(), onItemsFound, onProjectCompleted, cancellationToken).ConfigureAwait(false); }, cancellationToken); } } diff --git a/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.Caching.cs b/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.Caching.cs index 5db02e1df8e57..6bb8113b40e18 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.Caching.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.Caching.cs @@ -117,7 +117,6 @@ private static async Task CacheClassificationsAsync( var persistenceService = solution.Services.GetPersistentStorageService(); var storage = await persistenceService.GetStorageAsync(SolutionKey.ToSolutionKey(solution), cancellationToken).ConfigureAwait(false); - await using var _1 = storage.ConfigureAwait(false); if (storage == null) return; @@ -280,7 +279,6 @@ private async Task> TryReadCachedSemanticClassifi { var persistenceService = GetWorkspaceServices().GetPersistentStorageService(); var storage = await persistenceService.GetStorageAsync(documentKey.Project.Solution, cancellationToken).ConfigureAwait(false); - await using var _ = storage.ConfigureAwait(false); if (storage == null) return default; diff --git a/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.cs b/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.cs index 2db59acf92969..e593b1d8928de 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.cs @@ -52,7 +52,7 @@ await AbstractClassificationService.AddClassificationsInCurrentProcessAsync( _workQueue.AddWork((document, type, options)); } - return SerializableClassifiedSpans.Dehydrate(temp.ToImmutableArray()); + return SerializableClassifiedSpans.Dehydrate([.. temp]); }, cancellationToken); } } diff --git a/src/Workspaces/Remote/ServiceHub/Services/SymbolFinder/RemoteSymbolFinderService.cs b/src/Workspaces/Remote/ServiceHub/Services/SymbolFinder/RemoteSymbolFinderService.cs index 43be04c3b336c..83aef0424bc2b 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/SymbolFinder/RemoteSymbolFinderService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/SymbolFinder/RemoteSymbolFinderService.cs @@ -218,12 +218,6 @@ public ValueTask OnStartedAsync(CancellationToken cancellationToken) public ValueTask OnCompletedAsync(CancellationToken cancellationToken) => _callback.InvokeAsync((callback, cancellationToken) => callback.OnCompletedAsync(_callbackId, cancellationToken), cancellationToken); - public ValueTask OnFindInDocumentStartedAsync(Document document, CancellationToken cancellationToken) - => _callback.InvokeAsync((callback, cancellationToken) => callback.OnFindInDocumentStartedAsync(_callbackId, document.Id, cancellationToken), cancellationToken); - - public ValueTask OnFindInDocumentCompletedAsync(Document document, CancellationToken cancellationToken) - => _callback.InvokeAsync((callback, cancellationToken) => callback.OnFindInDocumentCompletedAsync(_callbackId, document.Id, cancellationToken), cancellationToken); - public ValueTask OnDefinitionFoundAsync(SymbolGroup group, CancellationToken cancellationToken) { var dehydratedGroup = SerializableSymbolGroup.Dehydrate(_solution, group, cancellationToken); @@ -231,15 +225,18 @@ public ValueTask OnDefinitionFoundAsync(SymbolGroup group, CancellationToken can (callback, cancellationToken) => callback.OnDefinitionFoundAsync(_callbackId, dehydratedGroup, cancellationToken), cancellationToken); } - public ValueTask OnReferenceFoundAsync(SymbolGroup group, ISymbol definition, ReferenceLocation reference, CancellationToken cancellationToken) + public ValueTask OnReferencesFoundAsync( + ImmutableArray<(SymbolGroup group, ISymbol symbol, ReferenceLocation location)> references, + CancellationToken cancellationToken) { - var dehydratedGroup = SerializableSymbolGroup.Dehydrate(_solution, group, cancellationToken); - var dehydratedDefinition = SerializableSymbolAndProjectId.Dehydrate(_solution, definition, cancellationToken); - var dehydratedReference = SerializableReferenceLocation.Dehydrate(reference, cancellationToken); + var dehydrated = references.SelectAsArray(t => + (SerializableSymbolGroup.Dehydrate(_solution, t.group, cancellationToken), + SerializableSymbolAndProjectId.Dehydrate(_solution, t.symbol, cancellationToken), + SerializableReferenceLocation.Dehydrate(t.location, cancellationToken))); return _callback.InvokeAsync( - (callback, cancellationToken) => callback.OnReferenceFoundAsync( - _callbackId, dehydratedGroup, dehydratedDefinition, dehydratedReference, cancellationToken), cancellationToken); + (callback, cancellationToken) => callback.OnReferencesFoundAsync( + _callbackId, dehydrated, cancellationToken), cancellationToken); } public ValueTask AddItemsAsync(int count, CancellationToken cancellationToken) diff --git a/src/Workspaces/Remote/ServiceHubTest/TelemetryLoggerTests.cs b/src/Workspaces/Remote/ServiceHubTest/TelemetryLoggerTests.cs index 8f4b733cd8b9d..c44212acc8ada 100644 --- a/src/Workspaces/Remote/ServiceHubTest/TelemetryLoggerTests.cs +++ b/src/Workspaces/Remote/ServiceHubTest/TelemetryLoggerTests.cs @@ -74,8 +74,7 @@ private static string InspectPropertyValue(object? value) _ => value.ToString()! }; - [Theory] - [CombinatorialData] + [Theory, CombinatorialData] internal void IgnoredSeverity(LogLevel level) { var logger = new TestLogger(); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Indentation/CSharpSmartTokenFormatter.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Indentation/CSharpSmartTokenFormatter.cs index e18cb1274e9d3..2e538e86dd901 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Indentation/CSharpSmartTokenFormatter.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Indentation/CSharpSmartTokenFormatter.cs @@ -58,8 +58,7 @@ public IList FormatRange( // Exception 2: Similar behavior for do-while if (common.ContainsDiagnostics && !CloseBraceOfTryOrDoBlock(endToken)) { - smartTokenformattingRules = ImmutableArray.Empty.Add( - new NoLineChangeFormattingRule()).AddRange(_formattingRules); + smartTokenformattingRules = [new NoLineChangeFormattingRule(), .. _formattingRules]; } var formatter = CSharpSyntaxFormatting.Instance; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/INamedTypeSymbolExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/INamedTypeSymbolExtensions.cs index e1509de06a403..da3c86ea8f721 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/INamedTypeSymbolExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/INamedTypeSymbolExtensions.cs @@ -393,7 +393,7 @@ private static ImmutableArray GetInterfacesToImplement( cancellationToken.ThrowIfCancellationRequested(); interfacesToImplement.RemoveRange(alreadyImplementedInterfaces); - return interfacesToImplement.ToImmutableArray(); + return [.. interfacesToImplement]; } private static ImmutableArray GetUnimplementedMembers( @@ -560,7 +560,7 @@ public static ImmutableArray GetOverridableMembers( RemoveNonOverriddableMembers(result, containingType, cancellationToken); } - return result.Keys.OrderBy(s => result[s]).ToImmutableArray(); + return [.. result.Keys.OrderBy(s => result[s])]; static void RemoveOverriddenMembers( Dictionary result, INamedTypeSymbol containingType, CancellationToken cancellationToken) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.AnalysisData.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.AnalysisData.cs index c5cf608238719..fa6a76cf05a04 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.AnalysisData.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.AnalysisData.cs @@ -86,7 +86,7 @@ protected AnalysisData() /// public SymbolUsageResult ToResult() => new(SymbolsWriteBuilder.ToImmutableDictionary(), - SymbolsReadBuilder.ToImmutableHashSet()); + [.. SymbolsReadBuilder]); public BasicBlockAnalysisData AnalyzeLocalFunctionInvocation(IMethodSymbol localFunction, CancellationToken cancellationToken) { diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.DataFlowAnalyzer.FlowGraphAnalysisData.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.DataFlowAnalyzer.FlowGraphAnalysisData.cs index 1a25bc7db5577..8b0b6e1b3b97a 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.DataFlowAnalyzer.FlowGraphAnalysisData.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.DataFlowAnalyzer.FlowGraphAnalysisData.cs @@ -196,7 +196,7 @@ private static ImmutableHashSet GetCapturedLocals(ControlFlowGraph } } - return builder.ToImmutableHashSet(); + return [.. builder]; } public BasicBlockAnalysisData GetBlockAnalysisData(BasicBlock basicBlock) @@ -600,7 +600,7 @@ public override bool TryGetDelegateInvocationTargets(IOperation write, out Immut // Attempts to return potential lamba/local function delegate invocation targets for the given write. if (_reachingDelegateCreationTargets.TryGetValue(write, out var targetsBuilder)) { - targets = targetsBuilder.ToImmutableHashSet(); + targets = [.. targetsBuilder]; return true; } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Log/FunctionId.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Log/FunctionId.cs index e9a169c4ce9e8..b99371005305b 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Log/FunctionId.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Log/FunctionId.cs @@ -626,5 +626,10 @@ internal enum FunctionId // 800-850 for Copilot performance logging. Copilot_Suggestion_Dismissed = 800, + Copilot_On_The_Fly_Docs_Showed_Link = 810, + Copilot_On_The_Fly_Docs_Loading_State_Entered = 811, + Copilot_On_The_Fly_Docs_Results_Displayed = 812, + Copilot_On_The_Fly_Docs_Error_Displayed = 813, + Copilot_On_The_Fly_Docs_Results_Canceled = 814, Copilot_Rename = 851 } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/FileBannerFacts/AbstractFileBannerFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/FileBannerFacts/AbstractFileBannerFacts.cs index 99aeca8efb44c..4bf5a87ee261d 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/FileBannerFacts/AbstractFileBannerFacts.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/FileBannerFacts/AbstractFileBannerFacts.cs @@ -157,7 +157,7 @@ public TSyntaxNode GetNodeWithoutLeadingBannerAndPreprocessorDirectives [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNull([NotNull] T value, [CallerLineNumber] int lineNumber = 0) where T : class? + public static void ThrowIfNull([NotNull] T value, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) where T : class? { if (value is null) { - Fail("Unexpected null", lineNumber); + Fail("Unexpected null", lineNumber, filePath); } } @@ -35,11 +36,11 @@ public static void ThrowIfNull([NotNull] T value, [CallerLineNumber] int line /// all builds /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNull([NotNull] T? value, [CallerLineNumber] int lineNumber = 0) where T : struct + public static void ThrowIfNull([NotNull] T? value, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) where T : struct { if (value is null) { - Fail("Unexpected null", lineNumber); + Fail("Unexpected null", lineNumber, filePath); } } @@ -48,11 +49,11 @@ public static void ThrowIfNull([NotNull] T? value, [CallerLineNumber] int lin /// all builds /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNull([NotNull] T value, string message, [CallerLineNumber] int lineNumber = 0) + public static void ThrowIfNull([NotNull] T value, string message, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) { if (value is null) { - Fail(message, lineNumber); + Fail(message, lineNumber, filePath); } } @@ -61,11 +62,11 @@ public static void ThrowIfNull([NotNull] T value, string message, [CallerLine /// all builds /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNull([NotNull] T value, [InterpolatedStringHandlerArgument("value")] ThrowIfNullInterpolatedStringHandler message, [CallerLineNumber] int lineNumber = 0) + public static void ThrowIfNull([NotNull] T value, [InterpolatedStringHandlerArgument("value")] ThrowIfNullInterpolatedStringHandler message, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) { if (value is null) { - Fail(message.GetFormattedText(), lineNumber); + Fail(message.GetFormattedText(), lineNumber, filePath); } } @@ -74,11 +75,11 @@ public static void ThrowIfNull([NotNull] T value, [InterpolatedStringHandlerA /// in all builds /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool condition, [CallerLineNumber] int lineNumber = 0) + public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool condition, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) { if (!condition) { - Fail("Unexpected false", lineNumber); + Fail("Unexpected false", lineNumber, filePath); } } @@ -87,11 +88,11 @@ public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool co /// in all builds /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool condition, string message, [CallerLineNumber] int lineNumber = 0) + public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool condition, string message, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) { if (!condition) { - Fail(message, lineNumber); + Fail(message, lineNumber, filePath); } } @@ -100,11 +101,11 @@ public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool co /// in all builds /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool condition, [InterpolatedStringHandlerArgument("condition")] ThrowIfFalseInterpolatedStringHandler message, [CallerLineNumber] int lineNumber = 0) + public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool condition, [InterpolatedStringHandlerArgument("condition")] ThrowIfFalseInterpolatedStringHandler message, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) { if (!condition) { - Fail(message.GetFormattedText(), lineNumber); + Fail(message.GetFormattedText(), lineNumber, filePath); } } @@ -113,11 +114,11 @@ public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool co /// all builds. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool condition, [CallerLineNumber] int lineNumber = 0) + public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool condition, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) { if (condition) { - Fail("Unexpected true", lineNumber); + Fail("Unexpected true", lineNumber, filePath); } } @@ -126,11 +127,11 @@ public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool cond /// all builds. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool condition, string message, [CallerLineNumber] int lineNumber = 0) + public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool condition, string message, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) { if (condition) { - Fail(message, lineNumber); + Fail(message, lineNumber, filePath); } } @@ -139,17 +140,20 @@ public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool cond /// all builds. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool condition, [InterpolatedStringHandlerArgument("condition")] ThrowIfTrueInterpolatedStringHandler message, [CallerLineNumber] int lineNumber = 0) + public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool condition, [InterpolatedStringHandlerArgument("condition")] ThrowIfTrueInterpolatedStringHandler message, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) { if (condition) { - Fail(message.GetFormattedText(), lineNumber); + Fail(message.GetFormattedText(), lineNumber, filePath); } } [DebuggerHidden] [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static void Fail(string message = "Unexpected", [CallerLineNumber] int lineNumber = 0) - => throw new InvalidOperationException($"{message} - line {lineNumber}"); + public static void Fail(string message = "Unexpected", [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) + { + var fileName = filePath is null ? null : Path.GetFileName(filePath); + throw new InvalidOperationException($"{message} - file {fileName} line {lineNumber}"); + } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/PathMetadataUtilities.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/PathMetadataUtilities.cs index 07dbf18c1be31..5754d73ddfbc0 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/PathMetadataUtilities.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/PathMetadataUtilities.cs @@ -71,6 +71,6 @@ public static ImmutableArray BuildFoldersFromNamespace(string? @namespac } var parts = @namespace.Split(NamespaceSeparatorArray, options: StringSplitOptions.RemoveEmptyEntries); - return parts.ToImmutableArray(); + return [.. parts]; } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/RestrictedInternalsVisibleToAttribute.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/RestrictedInternalsVisibleToAttribute.cs index 9886c1975190c..83d4ad3474067 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/RestrictedInternalsVisibleToAttribute.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/RestrictedInternalsVisibleToAttribute.cs @@ -10,5 +10,5 @@ namespace System.Runtime.CompilerServices; internal sealed class RestrictedInternalsVisibleToAttribute(string assemblyName, params string[] allowedNamespaces) : Attribute { public string AssemblyName { get; } = assemblyName; - public ImmutableArray AllowedNamespaces { get; } = allowedNamespaces.ToImmutableArray(); + public ImmutableArray AllowedNamespaces { get; } = [.. allowedNamespaces]; } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/CodeGeneration/CSharpCodeGenerationService.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/CodeGeneration/CSharpCodeGenerationService.cs index d70dc269c854c..8df5ceefaca7f 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/CodeGeneration/CSharpCodeGenerationService.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/CodeGeneration/CSharpCodeGenerationService.cs @@ -501,7 +501,7 @@ public override TDeclarationNode AddStatements( } else if (destinationMember is AccessorDeclarationSyntax accessorDeclaration) { - return (accessorDeclaration.Body == null) ? destinationMember : Cast(accessorDeclaration.AddBodyStatements(StatementGenerator.GenerateStatements(statements).ToArray())); + return (accessorDeclaration.Body == null) ? destinationMember : Cast(accessorDeclaration.AddBodyStatements([.. StatementGenerator.GenerateStatements(statements)])); } else if (destinationMember is CompilationUnitSyntax compilationUnit && info.Context.BestLocation is null) { @@ -520,7 +520,7 @@ public override TDeclarationNode AddStatements( // statement container. If the global statement is not already a block, create a block which can hold // both the original statement and any new statements we are adding to it. var block = statement as BlockSyntax ?? Block(statement); - return Cast(block.AddStatements(StatementGenerator.GenerateStatements(statements).ToArray())); + return Cast(block.AddStatements([.. StatementGenerator.GenerateStatements(statements)])); } else { diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/CodeGenerationHelpers.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/CodeGenerationHelpers.cs index 09b58b5e6a7fe..3839eb370ebbc 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/CodeGenerationHelpers.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/CodeGenerationHelpers.cs @@ -71,7 +71,7 @@ public static void GetNameAndInnermostNamespace( break; } - name = string.Join(".", names.ToArray()); + name = string.Join(".", [.. names]); } else { @@ -336,7 +336,7 @@ public static int GetInsertionIndex( // The list was grouped (by type, staticness, accessibility). Try to find a location // to put the new declaration into. - var result = Array.BinarySearch(declarationList.ToArray(), declaration, comparerWithoutNameCheck); + var result = Array.BinarySearch([.. declarationList], declaration, comparerWithoutNameCheck); var desiredGroupIndex = result < 0 ? ~result : result; Debug.Assert(desiredGroupIndex >= 0); Debug.Assert(desiredGroupIndex <= declarationList.Count); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractMethodSymbol.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractMethodSymbol.cs index 5d525a7b31c10..45c058621bda5 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractMethodSymbol.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractMethodSymbol.cs @@ -109,7 +109,7 @@ public virtual ImmutableArray ReturnTypeCustomModifiers public ImmutableArray UnmanagedCallingConventionTypes => []; public IMethodSymbol Construct(params ITypeSymbol[] typeArguments) - => new CodeGenerationConstructedMethodSymbol(this, typeArguments.ToImmutableArray()); + => new CodeGenerationConstructedMethodSymbol(this, [.. typeArguments]); public IMethodSymbol Construct(ImmutableArray typeArguments, ImmutableArray typeArgumentNullableAnnotations) => new CodeGenerationConstructedMethodSymbol(this, typeArguments); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractNamedTypeSymbol.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractNamedTypeSymbol.cs index 5b06942521e2d..445328ff50423 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractNamedTypeSymbol.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractNamedTypeSymbol.cs @@ -63,7 +63,7 @@ public INamedTypeSymbol Construct(params ITypeSymbol[] typeArguments) } return new CodeGenerationConstructedNamedTypeSymbol( - ConstructedFrom, typeArguments.ToImmutableArray(), this.TypeMembers); + ConstructedFrom, [.. typeArguments], this.TypeMembers); } public INamedTypeSymbol Construct(ImmutableArray typeArguments, ImmutableArray typeArgumentNullableAnnotations) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Helpers/MefHostServicesHelpers.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Helpers/MefHostServicesHelpers.cs index fcf86c82b58ca..bc4197cc204c1 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Helpers/MefHostServicesHelpers.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Helpers/MefHostServicesHelpers.cs @@ -27,7 +27,7 @@ public static ImmutableArray LoadNearbyAssemblies(IEnumerable } } - return assemblies.ToImmutableArray(); + return [.. assemblies]; } private static Assembly TryLoadNearbyAssembly(string assemblySimpleName)