diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8b5fb362b36..b38d2fbce0e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -8,7 +8,8 @@ /eng/common/ @dotnet/razor-tooling @dotnet/razor-compiler /eng/Versions.props @dotnet/razor-tooling @dotnet/razor-compiler /eng/Version.Details.xml @dotnet/razor-tooling @dotnet/razor-compiler -/eng/SourceBuild* @dotnet/source-build-internal +/eng/DotNetBuild.props @dotnet/product-construction +/eng/SourceBuild* @dotnet/source-build /src/Razor @dotnet/razor-tooling /src/Compiler @dotnet/razor-compiler /src/Shared @dotnet/razor-tooling @dotnet/razor-compiler diff --git a/Directory.Build.props b/Directory.Build.props index 69a3cd1524c..3556b512a08 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -22,9 +22,9 @@ true true true - true + true $(MSBuildThisFileDirectory) - true + true true true @@ -40,15 +40,9 @@ false - true + true net472 - - - false - + $(NetCurrent) $(DefaultNetCoreTargetFramework);$(NetPrevious) @@ -71,7 +65,7 @@ - + $(NetCurrent) $(DefaultNetCoreTargetFramework) diff --git a/Directory.Packages.props b/Directory.Packages.props index 19c6deb856d..02c9a790a75 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -63,7 +63,6 @@ - diff --git a/NuGet.config b/NuGet.config index 7a818f5f188..b5e83791aff 100644 --- a/NuGet.config +++ b/NuGet.config @@ -67,7 +67,6 @@ - diff --git a/Razor.Slim.slnf b/Razor.Slim.slnf index 0563c409be2..0b7ce028aee 100644 --- a/Razor.Slim.slnf +++ b/Razor.Slim.slnf @@ -15,7 +15,6 @@ "src\\Compiler\\Microsoft.Net.Compilers.Razor.Toolset\\Microsoft.Net.Compilers.Razor.Toolset.csproj", "src\\Compiler\\test\\Microsoft.AspNetCore.Razor.Test.MvcShim.Version1_X\\Microsoft.AspNetCore.Razor.Test.MvcShim.Version1_X.Compiler.csproj", "src\\Compiler\\test\\Microsoft.AspNetCore.Razor.Test.MvcShim.Version2_X\\Microsoft.AspNetCore.Razor.Test.MvcShim.Version2_X.Compiler.csproj", - "src\\Compiler\\test\\Microsoft.AspNetCore.Razor.Test.MvcShim\\Microsoft.AspNetCore.Razor.Test.MvcShim.Compiler.csproj", "src\\Compiler\\test\\Microsoft.NET.Sdk.Razor.SourceGenerators.Tests\\Microsoft.NET.Sdk.Razor.SourceGenerators.Test.csproj", "src\\Compiler\\tools\\Microsoft.AspNetCore.Mvc.Razor.Extensions.Tooling.Internal\\Microsoft.AspNetCore.Mvc.Razor.Extensions.Tooling.Internal.csproj", "src\\Compiler\\tools\\Microsoft.CodeAnalysis.Razor.Tooling.Internal\\Microsoft.CodeAnalysis.Razor.Tooling.Internal.csproj", diff --git a/Razor.sln b/Razor.sln index c9338fafeb3..9e4df49add7 100644 --- a/Razor.sln +++ b/Razor.sln @@ -100,10 +100,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.NET.Sdk.Razor.Sou EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.NET.Sdk.Razor.SourceGenerators.Test", "src\Compiler\test\Microsoft.NET.Sdk.Razor.SourceGenerators.Tests\Microsoft.NET.Sdk.Razor.SourceGenerators.Test.csproj", "{BD96BB0F-84DE-4A5F-8832-C8EADA36F43A}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Test.MvcShim.Compiler", "src\Compiler\test\Microsoft.AspNetCore.Razor.Test.MvcShim\Microsoft.AspNetCore.Razor.Test.MvcShim.Compiler.csproj", "{2CB7D554-49CE-45AC-97DF-7F8C597BDFA7}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib.Compiler", "src\Compiler\test\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib\Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib.Compiler.csproj", "{9FE4A38F-F0E7-45BD-94C1-1DC6FA55BB4A}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Test.MvcShim.Version1_X.Compiler", "src\Compiler\test\Microsoft.AspNetCore.Razor.Test.MvcShim.Version1_X\Microsoft.AspNetCore.Razor.Test.MvcShim.Version1_X.Compiler.csproj", "{A0867F6B-3DBB-4743-B241-F59878BFA15D}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Test.MvcShim.Version2_X.Compiler", "src\Compiler\test\Microsoft.AspNetCore.Razor.Test.MvcShim.Version2_X\Microsoft.AspNetCore.Razor.Test.MvcShim.Version2_X.Compiler.csproj", "{B040B919-D8E3-4656-BD85-88A541AA893D}" @@ -410,22 +406,6 @@ Global {BD96BB0F-84DE-4A5F-8832-C8EADA36F43A}.Release|Any CPU.Build.0 = Release|Any CPU {BD96BB0F-84DE-4A5F-8832-C8EADA36F43A}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU {BD96BB0F-84DE-4A5F-8832-C8EADA36F43A}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU - {2CB7D554-49CE-45AC-97DF-7F8C597BDFA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2CB7D554-49CE-45AC-97DF-7F8C597BDFA7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2CB7D554-49CE-45AC-97DF-7F8C597BDFA7}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU - {2CB7D554-49CE-45AC-97DF-7F8C597BDFA7}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU - {2CB7D554-49CE-45AC-97DF-7F8C597BDFA7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2CB7D554-49CE-45AC-97DF-7F8C597BDFA7}.Release|Any CPU.Build.0 = Release|Any CPU - {2CB7D554-49CE-45AC-97DF-7F8C597BDFA7}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU - {2CB7D554-49CE-45AC-97DF-7F8C597BDFA7}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU - {9FE4A38F-F0E7-45BD-94C1-1DC6FA55BB4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9FE4A38F-F0E7-45BD-94C1-1DC6FA55BB4A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9FE4A38F-F0E7-45BD-94C1-1DC6FA55BB4A}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU - {9FE4A38F-F0E7-45BD-94C1-1DC6FA55BB4A}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU - {9FE4A38F-F0E7-45BD-94C1-1DC6FA55BB4A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9FE4A38F-F0E7-45BD-94C1-1DC6FA55BB4A}.Release|Any CPU.Build.0 = Release|Any CPU - {9FE4A38F-F0E7-45BD-94C1-1DC6FA55BB4A}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU - {9FE4A38F-F0E7-45BD-94C1-1DC6FA55BB4A}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU {A0867F6B-3DBB-4743-B241-F59878BFA15D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A0867F6B-3DBB-4743-B241-F59878BFA15D}.Debug|Any CPU.Build.0 = Debug|Any CPU {A0867F6B-3DBB-4743-B241-F59878BFA15D}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU @@ -660,8 +640,6 @@ Global {FB7C870E-A173-4F75-BE63-4EF39C79A759} = {5B60F564-4AD7-4B70-A887-7D91496799A2} {F5017FD5-EA78-4CD2-A1B6-F534910683F8} = {AA4EE974-E765-4B97-AF35-F734BF9830F6} {BD96BB0F-84DE-4A5F-8832-C8EADA36F43A} = {AA4EE974-E765-4B97-AF35-F734BF9830F6} - {2CB7D554-49CE-45AC-97DF-7F8C597BDFA7} = {A5E2E4FA-6087-4C16-BB7A-89E23AA0F4E3} - {9FE4A38F-F0E7-45BD-94C1-1DC6FA55BB4A} = {A5E2E4FA-6087-4C16-BB7A-89E23AA0F4E3} {A0867F6B-3DBB-4743-B241-F59878BFA15D} = {A5E2E4FA-6087-4C16-BB7A-89E23AA0F4E3} {B040B919-D8E3-4656-BD85-88A541AA893D} = {A5E2E4FA-6087-4C16-BB7A-89E23AA0F4E3} {907EDA43-B4D9-40DA-BA07-8E00DD89FA33} = {FB7C870E-A173-4F75-BE63-4EF39C79A759} diff --git a/azure-pipelines-official.yml b/azure-pipelines-official.yml index 17079041941..a9a7d5fd76a 100644 --- a/azure-pipelines-official.yml +++ b/azure-pipelines-official.yml @@ -66,6 +66,8 @@ extends: name: NetCore1ESPool-Svc-Internal image: 1es-windows-2022 os: windows + policheck: + enabled: true tsa: enabled: true configFile: '$(Build.SourcesDirectory)/eng/TSAConfig.gdntsa' diff --git a/docs/CollectionBestPractices.md b/docs/CollectionBestPractices.md new file mode 100644 index 00000000000..65e702dbbb6 --- /dev/null +++ b/docs/CollectionBestPractices.md @@ -0,0 +1,406 @@ +# Collection Best Practices in Razor + +- [Imperative Collections](#imperative-collections) +- [Immutable Collections](#immutable-collections) + - [Using Builders](#using-builders) + - [`ImmutableArray`](#immutablearrayt) + - [Using `ImmutableArray.Builder`](#using-immutablearraytbuilder) + - [Frozen Collections](#frozen-collections) +- [Array Pools](#array-pools) +- [Object Pools](#object-pools) +- [✨It’s Magic! `PooledArrayBuilder`](#its-magic-pooledarraybuildert) +- [Using LINQ](#using-linq) + - [Best Practices](#best-practices) +- [Meta Tips](#meta-tips) + +# Imperative Collections +- .NET provides many collection types with different characteristics for different purposes. +- The collections from the System.Collections namespace should be avoided. Never use these unless in some legacy scenario. +- The collections in System.Collections.Generic are considered the “work horse” collection types for .NET and are + suitable for most purposes. They have years of hardening that make them highly efficient choices for most work. +- Popular imperative collection types include the ones we all use on a regular basis `List`, `HashSet`, + `Dictionary`, `Stack`. +- System.Collections.Concurrent contains collections that are designed for use when thread-safety is needed. + In general, these should only be used in particular situations. + +> [!WARNING] +> **Beware of collection growth** +> +> The imperative collections generally have more internal storage than needed to allow more items to be added. (This is +> what is meant by "capacity” vs. “count”). When enough items are added, the internal storage will need to grow. This +> requires creating larger storage, releasing the previous storage for garbage collection, and copying the existing +> contents into it, which consumes CPU time. For a larger collection, this can potential happen many times, so it’s +> important to set the capacity up front to avoid unnecessary internal storage growth. + +> [!WARNING] +> **Avoid exposing collection interfaces** +> +> Avoid exposing collections directly via interfaces, such as `IReadOnlyList` and +> `IReadOnlyDictionary`. The primary reason for this is that these interfaces can result in allocations +> when they are foreach’d. In general, collections provide a struct enumerator that can be used to foreach that +> collection without allocating an `IEnumertor` on the heap. However, when going through a collection interface, +> there isn’t a struct enumerator, so an allocation is likely required to foreach. In fact, many collections, such as +> `List`, are implemented to just return their struct enumerator when accessed via collection interfaces, resulting +> in an allocation when the struct enumerator is boxed. +> - If exposing a collection is necessary, consider whether it might be better to expose a more optimal read-only +> collection. Instead of `IReadOnlyList`, consider [`ImmutableArray`](#immutablearrayt). +> - There aren’t many other options when an API calls for exposing an `IReadOnlyDictionary`. In these +> cases, consider whether it might be better to just avoid exposing the collection altogether and provide APIs that +> access it. Or, in some cases, it might be necessary to create entirely new collection types. (This is why Razor +> `TagHelperDescriptors` expose a `MetadataCollection`.) + +> [!WARNING] +> **Be mindful of ToArray()** +> +> Calling `ToArray()` on a collection will create a new array and copy content from the collection into it. So, when +> the exact capacity is known up front, it is an anti-pattern to create a `List` without that capacity, fill it +> with items and then call `ToArray()` at the end. This results in extra allocations that could be avoided by creating +> an array and filling it. + +# Immutable Collections +- The .NET immutable collections are provided by the System.Collections.Immutable NuGet package, which provides + implementations for .NET, .NET Framework, and .NET Standard 2.0. +- The collections in the System.Collections.Immutable namespace have a very specific purpose. They are intended to be + *persistent* data structures; that is, a data structure that always preserves the previous version of itself when it + is modified. Such data structures are effectively immutable, but in hindsight, maybe it would have been better for + this namespace to have been called, System.Collections.Persistent? + - The term “persistent data structure” was introduced by the 1986 paper, + “Making Data Structures Persistent” ([PDF](https://www.cs.cmu.edu/~sleator/papers/making-data-structures-persistent.pdf)). + - A highly influential book in the area of persistent data structures is “Purely Functional Data Structures” (1999) + by Chris Okasaki ([Amazon](https://www.amazon.com/Purely-Functional-Data-Structures-Okasaki/dp/0521663504)). + Okasaki’s original dissertation is available from CMU’s website ([PDF](https://www.cs.cmu.edu/~rwh/students/okasaki.pdf)). +- Because of their persistency, nearly all of the immutable collections have very different implementations than their + imperative counterparts. For example, `List` is implemented using an array, while `ImmutableList` is implemented + using a binary tree. +- Mutating methods on an immutable collection perform “non-destructive mutation”. Instead, of mutating the underlying + object, a mutating method like `Add` produces a new instance of the immutable collection. This is similar to how the + `String.Replace(...)` API is used. +- Significant effort has been made to ensure that immutable collections are as efficient as they can be. However, the + cost of persistence means that immutable collections are generally assumed to be slower than imperative counterparts. + +> [!CAUTION] +> +> Because the immutable collections are often implemented using binary trees to achieve persistence, the asymptotic +> complexity of standard operations can be very surprising. For example, `ImmutableDictionary` access is +> O(log n) rather than the usual O(1) that would be expected when accessing a hash table data structure, such as +> `Dictionary`. A similar difference in performance characteristics exists across the various collection +> types. The following table shows the complexity of accessing a few popular collections types using their indexer. +> +> | Immutable collection type | Complexity | Imperative collection type | Complexity | +> | ----------------------------------------- | ---------- | -------------------------------- | ---------- | +> | `ImmutableDictionary` | O(log n) | `Dictionary` | O(1) | +> | `ImmutableHashSet` | O(log n) | `HashSet` | O(1) | +> | `ImmutableList` | O(log n) | `List` | O(1) | +> | `ImmutableSortedDictionary` | O(log n) | `SortedDictionary` | O(log n) | + +> [!CAUTION] +> **ToImmutableX() extension methods are not “freeze” methods!** +> +> The System.Immutable.Collections package provides several extension methods that produce an immutable collection from +> an existing collection or sequence. These methods aren’t optimized to reuse the internal storage of other collections +> in any way. Because of this, the following code is an anti-pattern. In this example, each element is added to a +> `HashSet` and then the elements of that set are added to a new `ImmutableHashSet`. +> +> ```C# +> var array = new[] { "One", "Two", "Two", "One", "Three" }; +> var set = new HashSet(array).ToImmutableHashSet(); +> ``` + +## Using Builders +- When creating an immutable collection with a lot of mutation, use a builder. Builders are optimized to populate the + internal storage of an immutable collection. +- The following code achieves the expected result but inefficiently creates several intermediate `ImmutableList` + instances. + +```C# +ImmutableList CreateList() +{ + var list = ImmutableList.Empty; + for (var i = 0; i < 10; i++) + { + list = list.Add(i); + } + + return list; +} +``` + +- The version below populates an `ImmutableList.Builder` and creates just a single `ImmutableList` instance + at the end. + +```C# +ImmutableList CreateList() +{ + var builder = ImmutableList.CreateBuilder(); + + for (var i = 0; i < 10; i++) + { + builder.Add(i); + } + + return builder.ToImmutable(); +} +``` + +## `ImmutableArray` +- `ImmutableArray` is very different than the other immutable collections. It is the only struct collection type, + and is not optimized for persistence. (In hindsight, perhaps a more appropriate name would have been + `FrozenArray`?) +- `ImmutableArray` is a relatively simple struct that provides read-only access to an internal array. + +> [!WARNING] +> **Be aware of copies!** +> +> In order to maintain its immutability semantics, `ImmutableArray` *always* creates a copy of the array it is +> wrapping internally. If it didn’t, external changes to the array would be reflected in the `ImmutableArray`. +> +> Because a new array copy is created for every `ImmutableArray` it is important to be mindful of chaining methods +> that produce immutable arrays to avoid unnecessary intermediate array copies. +> +> In addition, as of System.Immutable.Collections 8.0.0, there is a new `ImmutableCollectionsMarshal` class that can +> provide access to the internal array of an `ImmutableArray` or to create an new `ImmutableArray` that wraps an +> existing array without copying. These can be used in high performance scenarios, but should be employed carefully to +> avoid introducing subtle bugs. + +- Because `ImmutableArray` is a struct that wraps a single field of a reference type, it is essentially free to copy + at runtime. However, this also leaves a bit of a usability wart because, as a struct, an `ImmutableArray` reference + can never be null, but it can has its default, zeroed-out value where the internal array reference is null. For this + reason, an `IsDefault` property is provided to check if an `ImmutableArray` is actually wrapping an array. +- `ImmutableArray` *can* be used as a persistent data structure via non-destructive mutation, but mutating methods + are generally implemented to copy the elements of the internal array. For example, `Add` will create a copy of the + internal array storage with an additional element and return it as an `ImmutableArray`. + +> [!NOTE] +> **A Little History** +> +> `ImmutableArray` was not part of System.Collections.Immutable when originally conceived. It was developed out of +> necessity by Roslyn to expose array data while avoiding the inherent problems of exposing an array. (At the time, +> .NET arrays didn’t even implement `IReadOnlyList`, which didn’t ship until .NET Framework 4.5.) +> System.Collections.Immutable itself was inspired by the many persistent data structures used internally by Roslyn and +> was intended to be used within Visual Studio for asynchronous code. However, the NuGet package became so popular that +> it was ultimately pulled into the .NET runtime. + +### Using `ImmutableArray.Builder` +- The Builder type for `ImmutableArray` provides a couple of features not provided by other immutable collection + builders. +- `ToImmutable()`: Like other builders, creates a new `ImmutableArray` that wraps a copy of the filled portion of + internal array buffer used by the builder. +- `MoveToImmutable()`: Creates a new `ImmutableArray` that wraps the internal array buffer used by the builder. Note + that this requires that the builder’s capacity is the same as its count. In other words, the builder’s internal array + buffer must be completely filled, or this will throw an `InvalidOperationException`. If the operation is successful, + the internal buffer is set to an empty array. +- `DrainToImmutable()`: This is sort of like a combination of `ToImmutable()` and `MoveToImmutable()`. This operation + “drains” the builder by checking if the capacity equals the count. If true, it returns a new `ImmutableArray` that + wraps the internal array buffer. If false, it returns a new `ImmutableArray` that wraps a copy of the filled + portion of the internal array buffer. In either case, the internal buffer is set to an empty array. + +> [!CAUTION] +> **Immutable collections as static data** +> +> Because of their performance characteristics, most of the immutable collections are *not* suitable for static +> collections. In fact, `ImmutableArray` is really the only immutable collection that should be used for static data, +> since accessing it is essentially the same as accessing an array. +> +> When creating a static lookup table it can be tempting to reach for an `ImmutableHashSet` or an +> `ImmutableDictionary`, but that temptation should be resisted! Lookup will always be slower than using +> he imperative counterpart because of the internal tree structures employed for immutable collections. +> +> There are several tricks that can be used to encapsulate imperative collections as static data. For example, a nested +> static class could hide a `HashSet` or `Dictionary` behind static methods that access the +> collections. However, a better solution available today is to use a [frozen collection](#Frozen-Collections). + +## Frozen Collections +- The System.Collections.Frozen namespace became available starting with version 8.0.0 of the + System.Collections.Immutable NuGet package. +- Currently, there are two frozen collection types: `FrozenSet` and `FrozenDictionary`. +- The frozen collections are not persistent; in fact, they can’t be mutated at all! Instead, frozen collections are + optimized for faster lookup operations — faster than their imperative counterparts. +- Frozen collections provide faster lookup by performing up-front analysis and selecting an optimal implementation for + the content. This means that they are much more expensive to create. +- Because of their higher creation cost and improved lookup performance, frozen collections are best suited for + static data. + +# Array Pools +- When a temporary array is needed to perform work and the lifetime of the array is bounded, consider acquiring a + pooled array. `ArrayPool` can be used to acquire an array of some minimum length that can be returned to the pool + when the work is done. + +> [!WARNING] +> **Be mindful of the array size!** +> +> The size of an array acquired from an `ArrayPool` is guaranteed to be at least as large as the minimum length that +> was requested. However, it is likely that a larger array will have been returned. So, care should be taken to avoid +> using the acquired array’s length, unless that’s what’s needed. + +- Razor provides a handful of helper extension methods that acquire pooled arrays and return them within the scope of a +using statement: + +```C# +var pool = ArrayPool.Shared; + +using (pool.GetPooledArray(minimumLength: 42, out var array) +{ + // When using array but be careful that array.Length >= minimumLength. +} + +using (pool.GetPooledArraySpan(minimumLength: 42, out var span) +{ + // span is array.AsSpan(0, minimumLength) to help avoid subtle bugs. +} +``` + +# Object Pools +- Razor provides object pooling facilities based on + [Microsoft.Extensions.ObjectPool](https://www.nuget.org/packages/Microsoft.Extensions.ObjectPool/) (which was + originally based on Roslyn’s `ObjectPool`) along with several premade pools for many collection types in the + [Microsoft.AspNetCore.Razor.PooledObjects](https://github.com/dotnet/razor/tree/5c0677ad275e64300b897de0f6e8856ebe13f07b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects) + namespace. These can be used to acquire temporary collections to use for work and return when finished. + +```C# +using var _ = ListPool.GetPooledObject(out var list); + +// Use list here. It'll be returned to the pool at the end of the using +// statement's scope. +``` + +- Pooled collections provide a couple of benefits. + 1. Pooled collections decrease pressure on the garbage collector by reusing collection instances. + 2. Pooled collections avoid growing a collection’s internal storage. For example, when the `List` acquired from + `ListPool` in the code sample above is returned to the pool, it will be cleared. However, the capacity of its + internal storage will only be trimmed if it is larger than 512. So, lists acquired from the pool are likely to + already have a larger capacity than needed for most work. + +> [!WARNING] +> **Don't allow pooled objects to escape their scope!** +> +> Consider the following code: +> +> ```C# +> List M() +> { +> using var _ = ListPool.GetPooledObject(out var list); +> +> // use list... +> +> return list; +> } +> ``` +> +> The compiler won't complain if a pooled `List` escapes its scope. In the code above, the `List` will be +> returned to the pool at the end of the using statement's scope but is returned from the method. This results +> several problems: +> +> 1. The list will be cleared when returned to the pool. So, the caller will find it to be empty. +> 2. If the caller adds items to the list, other code acquiring a pooled list might receive the mutated list! +> 3. Likewise, if the caller holds onto the list, other code acquiring a pooled list might receive the same list and +> mutate it! +> +> In essence, a pooled object that escapes its scope can corrupt the pool in came from. + +# ✨It’s Magic! `PooledArrayBuilder` + +- Razor’s [`PooledArrayBuilder`](https://github.com/dotnet/razor/blob/5c0677ad275e64300b897de0f6e8856ebe13f07b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder%601.cs) + is heavily inspired by Roslyn’s [`TemporaryArray`](https://github.com/dotnet/roslyn/blob/d176f9b5a7220cd95a6d5811ba1c49ac392a2fdc/src/Compilers/Core/Portable/Collections/TemporaryArray%601.cs). +- The important feature of this type (and the reason we’ve started using it all over Razor) is that it stores the first + 4 elements of the array being built inline as fields. After 4 elements have been added, it will acquire a pooled + `ImmutableArray.Builder`. This makes it extremely cheap to use for small arrays and reduces pressure on the object + pools. +- Because `PooledArrayBuilder` is a struct, it must be passed by-reference. Otherwise, any elements added by a method + it’s passed to won’t be reflected back at the call-site. +- To avoid writing buggy code that accidentally copies a `PooledArrayBuilder`, it is marked with a `[NonCopyable]` + attribute. A Roslyn analyzer tracks types decorated with that attribute and ensures that instances are never copied. +- Because `PooledArrayBuilder` _may_ acquire a pooled `ImmutableArray.Builder`, it is disposable and should + generally be created within a using statement. However, that makes it a bit more awkward to pass by reference, so a + special `AsRef()` extension method is provided. +- In the following code example, an `ImmutableArray.Builder` will never be acquired from the pool because the + `PooledArrayBuilder` only ever contains three elements. + +```C# +ImmutableArray BuildStrings() +{ + using var builder = new PooledArrayBuilder(); + AddElements(ref builder.AsRef()); + + return builder.DrainToImmutable(); +} + +void AddElements(ref PooledArrayBuilder builder) +{ + builder.Add("One"); + builder.Add("Two"); + builder.Add("Three"); +} +``` + +# Using LINQ +- LINQ (that is, LINQ to Objects) is a bit of a tricky subject. It has been used extensively throughout Razor for a long + time. It’s certainly not off limits but should be used with an understanding of the hidden costs: + - Every lambda expression represents at least one allocation — the delegate that holds it. + - A lambda that accesses variables or instance data from an outer scope will result in a closure being allocated each + time the delegate is invoked. + - Many LINQ methods allocate an iterator instance. + - Because Razor tooling runs in Visual Studio, it runs on .NET Framework and doesn’t benefit from many LINQ + optimizations made in modern .NET. + - Because LINQ methods target `IEnumerable` instances, they can trigger additional allocations depending on how + `GetEnumerator()` is implemented. For example, a simple call like `Queue.Any()` might seem innocuous—it doesn’t + even have a lambda! However, the implementation of + [`Enumerable.Any()`](https://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,1288) on .NET + Framework doesn’t have any fast paths and simply calls `GetEnumerator()`. So, `Any()` boxes `Queue`’s struct + enumerator, resulting an allocation every time it’s called. In a tight loop, that could be disastrous! + - LINQ can obfuscate algorithmic complexity. It can be hard to see that introducing a LINQ expression has made an + algorithm O(n^2). + +## Best Practices +- Consider whether LINQ could have a negative performance impact for a particular scenario. Is this a hot path? Is it + happening in a loop? +- Always try to use static lambdas to ensure closures aren’t created and delegates are cached and reused. +- What collection type is being targeted? Do we have specialized LINQ methods that could be used? Razor provides a few + for `ImmutableArray` and `IReadOnlyList`. + +# Using Collection Expressions +- C# 12 introduced collection expressions as a language-level abstraction to generate collection-based code. It is a + goal of collection expressions to produce efficient code. +- Collection expressions are generally very good. They are especially helpful for combining collections or even query + expressions. + +```C# +int[] Combine(List list, HashSet set) +{ + return [..list, ..set]; +} + +int[] Squares(List list, HashSet set) +{ + return [ + ..from x in list select x * x, + ..from x in set select x * x + ]; +} +``` + +> [!WARNING] +> **Considerations when using collection expressions** +> +> - Sometimes, a collection expression might create a new temporary collection instance, such as a `List`. However, +> it will not acquire a temporary collection from Razor’s object pools ([SharpLab](https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0ATEBqAPgAQAYACfARhQG4BYAKHwGZSAmYgYWIG87jfSmAlgDsALgG0AusQCyACnIMAPMJEA+YgGcYARwCuMIWBgBKLjz4X8AdmJiAdHc079hmBJq0LAXzpegA=)). +> - There are pathological collection expressions to be avoided. For example, never use a collection expression to +> replace a call to `ImmutableArray.Builder.ToImmutable()` ([SharpLab](https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0ATEBqAPgAQAYACfARgDoBhCAG1pjABcBLCAOwGcKBJAWz4BXJgENgDANwBYAFD4AzKQBMxKsQDes4ttKL+Q0eJgBBKFBEBPADwt2TAHzEAsgApbTANoBdYiLOWASg0tHVCANz9iYEEWWgwYKGIAXmJ9YTEGU3MLalgRJhgAIRi4hJs7excA6RlQ0OjY+KgKYwwMACURdgBzGBc/bOqQuuJhuvwAdmIPCgookqavGtCAX1kVoA=)). +> When using a collection expression in a new scenario or with an uncommon type, it's a good idea to try it out on +> https://sharplab.io first. + +- Empty collection expression generally produces very efficient code and can be used without concern ([SharpLab](https://sharplab.io/#v2:EYLgtghglgdgPgAQEwEYCwAoBAGABAlAOgGEB7AG3IFMBjAFylJgGcBuTHfFAFnazwIkK1eoxaEAkmDABXOhGDU+nQQBlYAR2UCiAJRkwGYKkLAAHKNQBOAZSpWAblBpU2mDgGZ8SXMVwBvTFxg/C8EblwAWQAKAEoAoJCkgDkUaIBRGBljKwVqQnTzOgBPAB5YOgA+ONi+JJS0gEErXOKCorKK6tjaxPrcVOiAbQBdXox+4OSkYbG6yeSPWfGkgF8+3A2EMIjBgg9yw0rcZioNGSoYF3jAiZD1u+CtnYGZqVl5RSpm1sOq3AgLQgxRuGweSWe+F2S2IMmYdFIYF0VAgABMAPIwcjFdTw3DkKDw0GPXAPB6YIZkSi0BhMABCMksqPs0RKZiopAAZtFYfDEci0Zjsbi6LEADS4ABExCsKLoVElsRGnhOKOoqO8vjhCKRKIxWJxhLo0TMgIgYGYuAF+uxNlNMD+xwcEHIF2Y8RAuAk1qFhvhjswtySZisUGd8twsrRTGxuAqo1wAH1na7XLgALy4FNuwgAFVIP2BcT4kIquDoAAtCUMy7BmQAPEYZ47Jl1umswBsjebBbZxwy+UgGOjNpPZ1yEVSXADmlZLJL7Eky2XsEARVkduAA4lQ6MucmvSFY4hsg5NOUeUTQK7hos6rFm21R+2On+6Nkkz5MQgQUPgAOyPqmPb1OC9zuCSS5ZAe65cB4hA7nu0GruuJ4kkk6bHIh+4oUexYQUkfYEAAbFqfK6oKBoir4UbytEPoGnaEAOl0Jz2rEH4hJhuAwFQADu0TMOxfCrEAA=)). +- It is expected that collection expressions will improve over time. At the time of writing, there are + [several open issues](https://github.com/dotnet/roslyn/issues?q=is%3Aissue+is%3Aopen+%22collection+expression%22+label%3AArea-Compilers+label%3A%22Code+Gen+Quality%22) + tracking collection expression enhancements. + +# Meta Tips + +- Always be aware of the memory layout, features, and performance characteristics of the data structure you are using. +- If you have an implementation question for a .NET collection type, check out the source code using the + [.NET Source Browser](https://source.dot.net/) for modern .NET, or the + [.NET Framework Reference Source](https://referencesource.microsoft.com/). And of course, the .NET runtime repo is + available at [dotnet/runtime](https://github.com/dotnet/runtime). +- Several reflection-based tools exist for exploring .NET assemblies, such as + [ILSpy](https://github.com/icsharpcode/ILSpy) or dotPeek (from JetBrains). +- Use https://sharplab.io to see how code will be compiled. This can be especially useful for collection expressions, + which are usually very efficient do have pathological cases to avoid. \ No newline at end of file diff --git a/docs/Compiler Breaking Changes - DotNet 8.md b/docs/Compiler Breaking Changes - DotNet 8.md new file mode 100644 index 00000000000..c2c31f9bcf3 --- /dev/null +++ b/docs/Compiler Breaking Changes - DotNet 8.md @@ -0,0 +1,76 @@ +# This document lists known breaking changes in Razor after .NET 8 all the way to .NET 9. + +## Parsing of `@` identifiers was unified + +***Introduced in VS 17.10 and .NET 8.0.300*** + +In https://github.com/dotnet/razor/pull/10232, we adjusted the behavior of how an identifier is parsed following an `@` to be more consistent across Razor. +This resulted in a few scenarios that have different behavior, listed below. + +### Verbatim interpolated strings + +Strings of the form `@$"ticket-{i}.png"` are no longer recognized. This will be fixed in a later release by changing to a new lexer; until then, use `$@` to work around the issue. + +### C# preprocessor directives followed by HTML are not parsed correctly + +1. The preprocessor directive is directly before HTML. This flavor looks something like this: +```razor +@{ + #region R +

@ViewData["Title"]

+ #endregion +} +``` +2. There is valid C# between the preprocessor directive and the html, but it doesn't have a character that tells the parser to end parsing before the HTML. This is a variation of 1, and can occur with things like `switch` statements: +```razor +@{ + switch (true) + { + #region R + case true: +
@(1 + 1)
+ break; + } +} +``` + +Previously, C# preprocessor directives followed by HTML would sometimes be parsed correctly if the HTML had an `@` transition in it. It is now consistently parsed +incorrectly. This will be resolved in a later release by changing to a new lexer. Until then, there are available workarounds to get this to compile. + +#### Surround the HTML in a block + +The HTML can be surrounded with braces. + +```razor +@{ + #if DEBUG + { +

@ViewData["Title"]

+ } + #endif +} +``` + +#### Add a semicolon to the directive + +Directives such as `#region` and `#endregion` allow putting a semicolon after the directive. This will effectively work around the issue. + +```razor +@{ + #region R ; +

@ViewData["Title"]

+ #endregion +} +``` + +#### Add a semicolon after the directive + +Directives such as `#if` and `#endif` do not allow semicolons after the directive condition, but one can be placed on the next line to make an empty statement. + +```razor +@{ + #if DEBUG + ; +

@ViewData["Title"]

+ #endif +} diff --git a/docs/Parsing.md b/docs/Parsing.md index a5208c9e525..ead3c502d21 100644 --- a/docs/Parsing.md +++ b/docs/Parsing.md @@ -58,4 +58,6 @@ CSharpCode Literal: ^^^\r\n ``` -In this way we keep the whitespace as belonging to the overall CSharpCode node, but don't make it part of the directive itself, ensuring the editor sees the correct length for the directive. \ No newline at end of file +In this way we keep the whitespace as belonging to the overall CSharpCode node, but don't make it part of the directive itself, ensuring the editor sees the correct length for the directive. + +We apply a very similar fix to `@using` directives, to ensure that the newline is treated as metacode of the overall block, rather than being a part of the `using` itself. \ No newline at end of file diff --git a/docs/ProjectsAndLayering.md b/docs/ProjectsAndLayering.md index 29c85730d64..e95f1b8efbe 100644 --- a/docs/ProjectsAndLayering.md +++ b/docs/ProjectsAndLayering.md @@ -119,7 +119,7 @@ target the broadest set of frameworks. - Microsoft.AspNetCore.Razor.Test.MvcShim (`net8.0`;`net472`) - Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib (`netstandard2.0`) - Microsoft.AspNetCore.Razor.Test.MvcShim.Version1_X (`net8.0`;`net472`) -- Microsoft.AspNetCore.Razor.Test.MvcShim.Version2_X (`net8.0`;`net4720`) +- Microsoft.AspNetCore.Razor.Test.MvcShim.Version2_X (`net8.0`;`net472`) ### Tooling Core Tests diff --git a/docs/contributing/Roslyn-Debugging.md b/docs/contributing/Roslyn-Debugging.md index a3cd1492461..e316428ac12 100644 --- a/docs/contributing/Roslyn-Debugging.md +++ b/docs/contributing/Roslyn-Debugging.md @@ -8,18 +8,24 @@ Sometimes it may be necessary to make changes in [`dotnet/roslyn`](https://githu 2. `./Restore.cmd` 3. Make the desired changes in `dotnet/roslyn`. 4. `./Build.cmd -pack`. The `-pack` option causes the creation of NuGet packages. -5. You should see the generated packages in the `\artifacts\packages\Debug\Debug` directory. Take note of the package versions (ie. `Microsoft.CodeAnalysis.Workspaces.Common.3.8.0-dev.nupkg` => `3.8.0-dev`). -6. Open `NuGet.config` and add the local package source `` and package source below under the `packageSourceMapping` tag: +5. You should see the generated packages in the `\artifacts\packages\Debug` directory. Take note of the package versions (ie. `Microsoft.CodeAnalysis.Workspaces.Common.3.8.0-dev.nupkg` => `3.8.0-dev`). +6. In the Razor repo, open `NuGet.config` and add two local package sources: + * `` + * `` +7. Add the package source mappings below under the `packageSourceMapping` tag: ```xml - + + + + - ``` -7. Open `eng/Versions.props` and update `RoslynPackageVersion` to the version noted in step 5. -8. To get the end-to-end local debugging working, running `./Build.cmd -deploy` script from roslyn repository. this will copy over the right binaries from roslyn to the shared local roslyn/razor hive. +7. Open `eng/Versions.props` and find the `MicrosoftCodeAnalysisExternalAccessRazorPackageVersion` property. +8. Grab the value of that property, and replace all instances of that value in the file to be the version noted in step 5. +9. To get the end-to-end local debugging working, running `./Build.cmd -deployExtensions` script from roslyn repository. this will copy over the right binaries from roslyn to the shared local roslyn/razor hive. ## Troubleshooting diff --git a/eng/AfterSigning.targets b/eng/AfterSigning.targets index e334d9498ec..68095f7bcf0 100644 --- a/eng/AfterSigning.targets +++ b/eng/AfterSigning.targets @@ -1,6 +1,6 @@  - + $(ArtifactsDir)VSSetup\ Microsoft.VisualStudio.RazorExtension.vsix @@ -27,7 +27,7 @@ AfterTargets="GenerateVisualStudioInsertionManifests" Inputs="$(_RazorAssemblyVersion)" Outputs="$(_DependentAssemblyVersionsFile)" - Condition="'$(OS)'=='WINDOWS_NT' AND '$(ArcadeBuildFromSource)' != 'true'"> + Condition="'$(OS)'=='WINDOWS_NT' AND '$(DotNetBuildSourceOnly)' != 'true'"> <_AssemblyVersionEntry Include="RazorRuntimeAssembly" /> <_AssemblyVersionEntry Include="RazorToolingAssembly" /> diff --git a/eng/SourceBuild.props b/eng/DotNetBuild.props similarity index 86% rename from eng/SourceBuild.props rename to eng/DotNetBuild.props index 7c3d9e2731d..d06a5255433 100644 --- a/eng/SourceBuild.props +++ b/eng/DotNetBuild.props @@ -1,4 +1,4 @@ - + diff --git a/eng/Publishing.props b/eng/Publishing.props index 3df1479c419..9344f044ddc 100644 --- a/eng/Publishing.props +++ b/eng/Publishing.props @@ -27,7 +27,7 @@ - + false diff --git a/eng/SourceBuildPrebuiltBaseline.xml b/eng/SourceBuildPrebuiltBaseline.xml index 4f2766f12ec..5c7ee51b9cd 100644 --- a/eng/SourceBuildPrebuiltBaseline.xml +++ b/eng/SourceBuildPrebuiltBaseline.xml @@ -1,4 +1,4 @@ - + diff --git a/eng/TSAConfig.gdntsa b/eng/TSAConfig.gdntsa index d2035a27979..d8d1c1cdc11 100644 --- a/eng/TSAConfig.gdntsa +++ b/eng/TSAConfig.gdntsa @@ -11,8 +11,5 @@ "projectName": "DevDiv", "areaPath": "DevDiv\\NET Developer Experience\\Razor Tooling", "iterationPath": "DevDiv", - "tools": [ - "APIScan", - "BinSkim" - ] + "allTools": true } diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 7ea417466f4..2df6540542f 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -6,87 +6,87 @@ 839cdfb0ecca5e0be3dbccd926e7651ef50fdf10 - + https://github.com/dotnet/source-build-reference-packages - ad3c9aa85596f42c6a483233c50fab8cee8c412a + 4660d88cf953fbbd14192c787053a20246ce1aeb - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 - + https://github.com/dotnet/roslyn - 9f86520c46f67d2a8a59af189f8fd87e35c574bb + 7b7951aa13c50ad768538e58ed3805898b058928 @@ -96,14 +96,14 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 3a25a7f1cc446b60678ed25c9d829420d6321eba - + https://github.com/dotnet/arcade - dd332f2d4e21daa8b79f84251ab156af9a0b11b2 + 3c393bbd85ae16ddddba20d0b75035b0c6f1a52d - + https://github.com/dotnet/arcade - dd332f2d4e21daa8b79f84251ab156af9a0b11b2 + 3c393bbd85ae16ddddba20d0b75035b0c6f1a52d diff --git a/eng/Versions.props b/eng/Versions.props index 707ebbb49ad..f8a2092014e 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -30,8 +30,8 @@ imported. This OK because we want to just have an obvious salt for a local build. --> - 17.12.3 - 17.12 + 17.13.1 + 17.13 $(AddinMajorVersion) $(AddinVersion).$(OfficialBuildId) $(AddinVersion).42424242.42 @@ -49,29 +49,29 @@ 6.0.2-servicing.22064.6 6.0.1 - 10.0.0-alpha.1.24455.1 - 9.0.0-beta.24453.1 + 10.0.0-alpha.1.24515.1 + 9.0.0-beta.24516.2 1.0.0-beta.23475.1 1.0.0-beta.23475.1 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 - 4.12.0-3.24454.5 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 + 4.12.0-3.24466.4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - View component '{0}' must have exactly one public method named '{1}' or '{2}'. - - - Method '{0}' of view component '{1}' should be declared to return {2}&lt;T&gt;. - - - Could not find an '{0}' or '{1}' method for the view component '{2}'. - - - Method '{0}' of view component '{1}' cannot return a {2}. - - - Method '{0}' of view component '{1}' should be declared to return a value. - - \ No newline at end of file diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperConventions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperConventions.cs deleted file mode 100644 index 058c20868e3..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperConventions.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable disable - -namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X; - -public static class ViewComponentTagHelperConventions -{ - public static readonly string Kind = "MVC.ViewComponent"; -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperIntermediateNode.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperIntermediateNode.cs deleted file mode 100644 index 5f08ef6be68..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperIntermediateNode.cs +++ /dev/null @@ -1,60 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable disable - -using System; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.CodeGeneration; -using Microsoft.AspNetCore.Razor.Language.Intermediate; - -namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X; - -public sealed class ViewComponentTagHelperIntermediateNode : ExtensionIntermediateNode -{ - public override IntermediateNodeCollection Children { get; } = IntermediateNodeCollection.ReadOnly; - - public string ClassName { get; set; } - - public TagHelperDescriptor TagHelper { get; set; } - - public override void Accept(IntermediateNodeVisitor visitor) - { - if (visitor == null) - { - throw new ArgumentNullException(nameof(visitor)); - } - - AcceptExtensionNode(this, visitor); - } - - public override void WriteNode(CodeTarget target, CodeRenderingContext context) - { - if (target == null) - { - throw new ArgumentNullException(nameof(target)); - } - - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - var extension = target.GetExtension(); - if (extension == null) - { - ReportMissingCodeTargetExtension(context); - return; - } - - extension.WriteViewComponentTagHelper(context, this); - } - - public override void FormatNode(IntermediateNodeFormatter formatter) - { - formatter.WriteContent(ClassName); - - formatter.WriteProperty(nameof(ClassName), ClassName); - formatter.WriteProperty(nameof(TagHelper), TagHelper?.DisplayName); - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperMetadata.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperMetadata.cs deleted file mode 100644 index 88d92b8cc7b..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperMetadata.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable disable - -namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X; - -public static class ViewComponentTagHelperMetadata -{ - /// - /// The key in a containing - /// the short name of a view component. - /// - public static readonly string Name = "MVC.ViewComponent.Name"; -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperPass.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperPass.cs deleted file mode 100644 index cbacadea315..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperPass.cs +++ /dev/null @@ -1,205 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable disable - -using System.Collections.Generic; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.Extensions; -using Microsoft.AspNetCore.Razor.Language.Intermediate; - -namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X; - -public class ViewComponentTagHelperPass : IntermediateNodePassBase, IRazorOptimizationPass -{ - // Run after the default taghelper pass - public override int Order => IntermediateNodePassBase.DefaultFeatureOrder + 2000; - - protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) - { - var @namespace = documentNode.FindPrimaryNamespace(); - var @class = documentNode.FindPrimaryClass(); - if (@namespace == null || @class == null) - { - // Nothing to do, bail. We can't function without the standard structure. - return; - } - - var context = new Context(@namespace, @class); - - // For each VCTH *usage* we need to rewrite the tag helper node to use the tag helper runtime to construct - // and set properties on the the correct field, and using the name of the type we will generate. - var nodes = documentNode.FindDescendantNodes(); - for (var i = 0; i < nodes.Count; i++) - { - var node = nodes[i]; - foreach (var tagHelper in node.TagHelpers) - { - RewriteUsage(context, node, tagHelper); - } - } - - // Then for each VCTH *definition* that we've seen we need to generate the class that implements - // ITagHelper and the field that will hold it. - foreach (var tagHelper in context.TagHelpers) - { - AddField(context, tagHelper); - AddTagHelperClass(context, tagHelper); - } - } - - private void RewriteUsage(Context context, TagHelperIntermediateNode node, TagHelperDescriptor tagHelper) - { - if (!tagHelper.IsViewComponentKind()) - { - return; - } - - context.Add(tagHelper); - - // Now we need to insert a create node using the default tag helper runtime. This is similar to - // code in DefaultTagHelperOptimizationPass. - // - // Find the body node. - var i = 0; - while (i < node.Children.Count && node.Children[i] is TagHelperBodyIntermediateNode) - { - i++; - } - while (i < node.Children.Count && node.Children[i] is DefaultTagHelperBodyIntermediateNode) - { - i++; - } - - // Now find the last create node. - while (i < node.Children.Count && node.Children[i] is DefaultTagHelperCreateIntermediateNode) - { - i++; - } - - // Now i has the right insertion point. - node.Children.Insert(i, new DefaultTagHelperCreateIntermediateNode() - { - FieldName = context.GetFieldName(tagHelper), - TagHelper = tagHelper, - TypeName = context.GetFullyQualifiedName(tagHelper), - }); - - // Now we need to rewrite any set property nodes to use the default runtime. - for (i = 0; i < node.Children.Count; i++) - { - if (node.Children[i] is TagHelperPropertyIntermediateNode propertyNode && - propertyNode.TagHelper == tagHelper) - { - // This is a set property for this VCTH - we need to replace it with a node - // that will use our field and property name. - node.Children[i] = new DefaultTagHelperPropertyIntermediateNode(propertyNode) - { - FieldName = context.GetFieldName(tagHelper), - PropertyName = propertyNode.BoundAttribute.GetPropertyName(), - }; - } - } - } - - private void AddField(Context context, TagHelperDescriptor tagHelper) - { - // We need to insert a node for the field that will hold the tag helper. We've already generated a field name - // at this time and use it for all uses of the same tag helper type. - // - // We also want to preserve the ordering of the nodes for testability. So insert at the end of any existing - // field nodes. - var i = 0; - while (i < context.Class.Children.Count && context.Class.Children[i] is DefaultTagHelperRuntimeIntermediateNode) - { - i++; - } - - while (i < context.Class.Children.Count && context.Class.Children[i] is FieldDeclarationIntermediateNode) - { - i++; - } - - context.Class.Children.Insert(i, new FieldDeclarationIntermediateNode() - { - Annotations = - { - { CommonAnnotations.DefaultTagHelperExtension.TagHelperField, bool.TrueString }, - }, - Modifiers = - { - "private", - }, - FieldName = context.GetFieldName(tagHelper), - FieldType = "global::" + context.GetFullyQualifiedName(tagHelper), - }); - } - - private void AddTagHelperClass(Context context, TagHelperDescriptor tagHelper) - { - var node = new ViewComponentTagHelperIntermediateNode() - { - ClassName = context.GetClassName(tagHelper), - TagHelper = tagHelper - }; - - context.Class.Children.Add(node); - } - - private struct Context - { - private readonly Dictionary _tagHelpers; - - public Context(NamespaceDeclarationIntermediateNode @namespace, ClassDeclarationIntermediateNode @class) - { - Namespace = @namespace; - Class = @class; - - _tagHelpers = new Dictionary(); - } - - public ClassDeclarationIntermediateNode Class { get; } - - public NamespaceDeclarationIntermediateNode Namespace { get; } - - - public IEnumerable TagHelpers => _tagHelpers.Keys; - - public bool Add(TagHelperDescriptor tagHelper) - { - if (_tagHelpers.ContainsKey(tagHelper)) - { - return false; - } - - var className = $"__Generated__{tagHelper.GetViewComponentName()}ViewComponentTagHelper"; - var namespaceSeparator = string.IsNullOrEmpty(Namespace.Content) ? string.Empty : "."; - var fullyQualifiedName = $"{Namespace.Content}{namespaceSeparator}{Class.ClassName}.{className}"; - var fieldName = GenerateFieldName(tagHelper); - - _tagHelpers.Add(tagHelper, (className, fullyQualifiedName, fieldName)); - - return true; - } - - public string GetClassName(TagHelperDescriptor taghelper) - { - return _tagHelpers[taghelper].className; - } - - public string GetFullyQualifiedName(TagHelperDescriptor taghelper) - { - return _tagHelpers[taghelper].fullyQualifiedName; - } - - public string GetFieldName(TagHelperDescriptor taghelper) - { - return _tagHelpers[taghelper].fieldName; - } - - private static string GenerateFieldName(TagHelperDescriptor tagHelper) - { - return $"__{tagHelper.GetViewComponentName()}ViewComponentTagHelper"; - } - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperTargetExtension.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperTargetExtension.cs index 8d63ca7aa11..3d8f4117fdd 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperTargetExtension.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTagHelperTargetExtension.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X; @@ -65,7 +66,7 @@ public void WriteViewComponentTagHelper(CodeRenderingContext context, ViewCompon using (context.CodeWriter.BuildClassDeclaration( PublicModifiers, node.ClassName, - TagHelperTypeName, + new BaseTypeWithModel(TagHelperTypeName), interfaces: null, typeParameters: null, context)) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTypeVisitor.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTypeVisitor.cs deleted file mode 100644 index 7e2b8240fb5..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTypeVisitor.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using Microsoft.CodeAnalysis; - -namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X; - -internal sealed class ViewComponentTypeVisitor : SymbolVisitor -{ - private readonly INamedTypeSymbol _viewComponentAttribute; - private readonly INamedTypeSymbol? _nonViewComponentAttribute; - private readonly List _results; - - public ViewComponentTypeVisitor( - INamedTypeSymbol viewComponentAttribute, - INamedTypeSymbol? nonViewComponentAttribute, - List results) - { - _viewComponentAttribute = viewComponentAttribute; - _nonViewComponentAttribute = nonViewComponentAttribute; - _results = results; - } - - public override void VisitNamedType(INamedTypeSymbol symbol) - { - if (IsViewComponent(symbol)) - { - _results.Add(symbol); - } - - if (symbol.DeclaredAccessibility != Accessibility.Public) - { - return; - } - - foreach (var member in symbol.GetTypeMembers()) - { - Visit(member); - } - } - - public override void VisitNamespace(INamespaceSymbol symbol) - { - foreach (var member in symbol.GetMembers()) - { - Visit(member); - } - } - - internal bool IsViewComponent(INamedTypeSymbol symbol) - { - if (_viewComponentAttribute == null) - { - return false; - } - - if (symbol.DeclaredAccessibility != Accessibility.Public || - symbol.IsAbstract || - symbol.IsGenericType || - AttributeIsDefined(symbol, _nonViewComponentAttribute)) - { - return false; - } - - return symbol.Name.EndsWith(ViewComponentTypes.ViewComponentSuffix, StringComparison.Ordinal) || - AttributeIsDefined(symbol, _viewComponentAttribute); - } - - private static bool AttributeIsDefined(INamedTypeSymbol? type, INamedTypeSymbol? queryAttribute) - { - if (type == null || queryAttribute == null) - { - return false; - } - - foreach (var attribute in type.GetAttributes()) - { - if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, queryAttribute)) - { - return true; - } - } - - return AttributeIsDefined(type.BaseType, queryAttribute); - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTypes.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTypes.cs deleted file mode 100644 index c333ca62aee..00000000000 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc.Version2_X/ViewComponentTypes.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable disable - -using System; - -namespace Microsoft.AspNetCore.Mvc.Razor.Extensions.Version2_X; - -internal static class ViewComponentTypes -{ - public const string Assembly = "Microsoft.AspNetCore.Mvc.ViewFeatures"; - - public static readonly Version AssemblyVersion = new Version(1, 1, 0, 0); - - public const string ViewComponentSuffix = "ViewComponent"; - - public const string ViewComponentAttribute = "Microsoft.AspNetCore.Mvc.ViewComponentAttribute"; - - public const string NonViewComponentAttribute = "Microsoft.AspNetCore.Mvc.NonViewComponentAttribute"; - - public const string GenericTask = "System.Threading.Tasks.Task`1"; - - public const string Task = "System.Threading.Tasks.Task"; - - public const string IDictionary = "System.Collections.Generic.IDictionary`2"; - - public const string AsyncMethodName = "InvokeAsync"; - - public const string SyncMethodName = "Invoke"; - - public static class ViewComponent - { - public const string Name = "Name"; - } -} diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectDirective.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectDirective.cs index 15ed4fc0327..583286b24dc 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectDirective.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectDirective.cs @@ -27,7 +27,7 @@ public static class InjectDirective builder.Description = RazorExtensionsResources.InjectDirective_Description; }); - public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder) + public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder builder, bool considerNullabilityEnforcement) { if (builder == null) { @@ -36,7 +36,7 @@ public static RazorProjectEngineBuilder Register(RazorProjectEngineBuilder build builder.AddDirective(Directive); builder.Features.Add(new Pass()); - builder.AddTargetExtension(new InjectTargetExtension()); + builder.AddTargetExtension(new InjectTargetExtension(considerNullabilityEnforcement)); return builder; } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectTargetExtension.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectTargetExtension.cs index 46e83e4cae9..0ea1c5ac9b0 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectTargetExtension.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/InjectTargetExtension.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions; -public class InjectTargetExtension : IInjectTargetExtension +public class InjectTargetExtension(bool considerNullabilityEnforcement) : IInjectTargetExtension { private const string RazorInjectAttribute = "[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]"; @@ -42,7 +42,7 @@ public void WriteInjectProperty(CodeRenderingContext context, InjectIntermediate else if (!node.IsMalformed) { var property = $"public {node.TypeName} {node.MemberName} {{ get; private set; }}"; - if (!context.Options.SuppressNullabilityEnforcement) + if (considerNullabilityEnforcement && !context.Options.SuppressNullabilityEnforcement) { property += " = default!;"; } @@ -61,7 +61,7 @@ public void WriteInjectProperty(CodeRenderingContext context, InjectIntermediate void WriteProperty() { - if (!context.Options.SuppressNullabilityEnforcement) + if (considerNullabilityEnforcement && !context.Options.SuppressNullabilityEnforcement) { context.CodeWriter.WriteLine("#nullable restore"); } @@ -70,7 +70,7 @@ void WriteProperty() .WriteLine(RazorInjectAttribute) .WriteLine(property); - if (!context.Options.SuppressNullabilityEnforcement) + if (considerNullabilityEnforcement && !context.Options.SuppressNullabilityEnforcement) { context.CodeWriter.WriteLine("#nullable disable"); } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/ModelDirective.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/ModelDirective.cs index 8d8e6338ea4..206d2d7efc0 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/ModelDirective.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/ModelDirective.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.AspNetCore.Razor.Language.Intermediate; namespace Microsoft.AspNetCore.Mvc.Razor.Extensions; @@ -43,10 +44,10 @@ public static string GetModelType(DocumentIntermediateNode document) } var visitor = new Visitor(); - return GetModelType(document, visitor); + return GetModelType(document, visitor).Content; } - private static string GetModelType(DocumentIntermediateNode document, Visitor visitor) + private static IntermediateToken GetModelType(DocumentIntermediateNode document, Visitor visitor) { visitor.Visit(document); @@ -57,17 +58,17 @@ private static string GetModelType(DocumentIntermediateNode document, Visitor vi var tokens = directive.Tokens.ToArray(); if (tokens.Length >= 1) { - return tokens[0].Content; + return IntermediateToken.CreateCSharpToken(tokens[0].Content, tokens[0].Source); } } if (document.DocumentKind == RazorPageDocumentClassifierPass.RazorPageDocumentKind) { - return visitor.Class.ClassName; + return IntermediateToken.CreateCSharpToken(visitor.Class.ClassName); } else { - return "dynamic"; + return IntermediateToken.CreateCSharpToken("dynamic"); } } @@ -99,10 +100,13 @@ protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentInte }; visitor.Namespace?.Children.Insert(0, usingNode); + modelType.Source = null; } - var baseType = visitor.Class?.BaseType?.Replace("", "<" + modelType + ">"); - visitor.Class.BaseType = baseType; + if (visitor.Class?.BaseType is BaseTypeWithModel { ModelType: not null } existingBaseType) + { + existingBaseType.ModelType = modelType; + } } } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/MvcViewDocumentClassifierPass.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/MvcViewDocumentClassifierPass.cs index 4ddbe268963..fd80aa0b4ff 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/MvcViewDocumentClassifierPass.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/MvcViewDocumentClassifierPass.cs @@ -4,6 +4,7 @@ #nullable disable using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.AspNetCore.Razor.Language.Intermediate; namespace Microsoft.AspNetCore.Mvc.Razor.Extensions; @@ -53,7 +54,7 @@ protected override void OnDocumentStructureCreated( { @class.ClassName = className; } - @class.BaseType = "global::Microsoft.AspNetCore.Mvc.Razor.RazorPage"; + @class.BaseType = new BaseTypeWithModel("global::Microsoft.AspNetCore.Mvc.Razor.RazorPage", location: null); @class.Modifiers.Clear(); if (_useConsolidatedMvcViews) { diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/RazorExtensions.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/RazorExtensions.cs index fa19e49ba40..317a5f387b0 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/RazorExtensions.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/RazorExtensions.cs @@ -22,7 +22,7 @@ public static void Register(RazorProjectEngineBuilder builder) throw new ArgumentNullException(nameof(builder)); } - InjectDirective.Register(builder); + InjectDirective.Register(builder, considerNullabilityEnforcement: true); ModelDirective.Register(builder); PageDirective.Register(builder); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/RazorPageDocumentClassifierPass.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/RazorPageDocumentClassifierPass.cs index 1706d408e53..0cab4df6007 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/RazorPageDocumentClassifierPass.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/RazorPageDocumentClassifierPass.cs @@ -79,7 +79,7 @@ protected override void OnDocumentStructureCreated( @class.ClassName = className; } - @class.BaseType = "global::Microsoft.AspNetCore.Mvc.RazorPages.Page"; + @class.BaseType = new BaseTypeWithModel("global::Microsoft.AspNetCore.Mvc.RazorPages.Page"); @class.Modifiers.Clear(); if (_useConsolidatedMvcViews) { diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/ViewComponentTagHelperTargetExtension.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/ViewComponentTagHelperTargetExtension.cs index f8c3e34e3c7..212bdd87437 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/ViewComponentTagHelperTargetExtension.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/ViewComponentTagHelperTargetExtension.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.CodeGeneration; +using Microsoft.AspNetCore.Razor.Language.Intermediate; namespace Microsoft.AspNetCore.Mvc.Razor.Extensions; @@ -68,7 +69,7 @@ public void WriteViewComponentTagHelper(CodeRenderingContext context, ViewCompon using (context.CodeWriter.BuildClassDeclaration( PublicModifiers, node.ClassName, - TagHelperTypeName, + new BaseTypeWithModel(TagHelperTypeName), interfaces: null, typeParameters: null, context)) diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs index e7399de064b..29c72c13cf4 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.Helpers.cs @@ -87,8 +87,7 @@ private static StaticCompilationTagHelperFeature GetStaticTagHelperFeature(Compi private static SourceGeneratorProjectEngine GetGenerationProjectEngine( SourceGeneratorProjectItem item, IEnumerable imports, - RazorSourceGenerationOptions razorSourceGeneratorOptions, - bool isAddComponentParameterAvailable) + RazorSourceGenerationOptions razorSourceGeneratorOptions) { var fileSystem = new VirtualRazorProjectFileSystem(); fileSystem.Add(item); @@ -107,7 +106,7 @@ private static SourceGeneratorProjectEngine GetGenerationProjectEngine( options.SuppressMetadataSourceChecksumAttributes = !razorSourceGeneratorOptions.GenerateMetadataSourceChecksumAttributes; options.SupportLocalizedComponentNames = razorSourceGeneratorOptions.SupportLocalizedComponentNames; options.SuppressUniqueIds = razorSourceGeneratorOptions.TestSuppressUniqueIds; - options.SuppressAddComponentParameter = !isAddComponentParameterAvailable; + options.SuppressAddComponentParameter = razorSourceGeneratorOptions.Configuration.SuppressAddComponentParameter; })); b.Features.Add(new ConfigureRazorParserOptions(razorSourceGeneratorOptions.UseRoslynTokenizer, razorSourceGeneratorOptions.CSharpParseOptions)); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs index 6f1cfa303b4..73d6a8a0b4e 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.RazorProviders.cs @@ -2,21 +2,24 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Immutable; using System.IO; +using System.Linq; using System.Text; using System.Threading; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Razor.Compiler.CSharp; namespace Microsoft.NET.Sdk.Razor.SourceGenerators { public partial class RazorSourceGenerator { - private (RazorSourceGenerationOptions?, Diagnostic?) ComputeRazorSourceGeneratorOptions(((AnalyzerConfigOptionsProvider, ParseOptions), bool) pair, CancellationToken ct) + private (RazorSourceGenerationOptions?, Diagnostic?) ComputeRazorSourceGeneratorOptions((((AnalyzerConfigOptionsProvider, ParseOptions), ImmutableArray), bool) pair, CancellationToken ct) { - var ((options, parseOptions), isSuppressed) = pair; + var (((options, parseOptions), references), isSuppressed) = pair; var globalOptions = options.GlobalOptions; if (isSuppressed) @@ -42,7 +45,15 @@ public partial class RazorSourceGenerator razorLanguageVersion = RazorLanguageVersion.Latest; } - var razorConfiguration = new RazorConfiguration(razorLanguageVersion, configurationName ?? "default", Extensions: [], UseConsolidatedMvcViews: true); + var minimalReferences = references + .Where(r => r.Display is { } display && display.EndsWith("Microsoft.AspNetCore.Components.dll", StringComparison.Ordinal)) + .ToImmutableArray(); + + var isComponentParameterSupported = minimalReferences.Length == 0 + ? false + : CSharpCompilation.Create("components", references: minimalReferences).HasAddComponentParameter(); + + var razorConfiguration = new RazorConfiguration(razorLanguageVersion, configurationName ?? "default", Extensions: [], UseConsolidatedMvcViews: true, SuppressAddComponentParameter: !isComponentParameterSupported); // We use the new tokenizer by default var useRazorTokenizer = !parseOptions.Features.TryGetValue("use-razor-tokenizer", out var useRazorTokenizerValue) @@ -86,7 +97,7 @@ private static (SourceGeneratorProjectItem?, Diagnostic?) ComputeProjectItems((A .Replace(Path.DirectorySeparatorChar, '/') .Replace("//", "/"), relativePhysicalPath: relativePath, - fileKind: additionalText.Path.EndsWith(".razor", StringComparison.OrdinalIgnoreCase) ? FileKinds.Component : FileKinds.Legacy, + fileKind: FileKinds.GetFileKindFromFilePath(additionalText.Path), additionalText: additionalText, cssScope: cssScope); return (projectItem, null); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs index 0fe65e3eb60..9d3eff80c26 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/SourceGenerators/RazorSourceGenerator.cs @@ -6,7 +6,6 @@ using System.IO; using System.Linq; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -44,6 +43,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var razorSourceGeneratorOptions = analyzerConfigOptions .Combine(parseOptions) + .Combine(metadataRefs.Collect()) .SuppressIfNeeded(isGeneratorSuppressed) .Select(ComputeRazorSourceGeneratorOptions) .ReportDiagnostics(context); @@ -235,30 +235,16 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var razorHostOutputsEnabled = analyzerConfigOptions.CheckGlobalFlagSet("EnableRazorHostOutputs"); var withOptionsDesignTime = withOptions.EmptyOrCachedWhen(razorHostOutputsEnabled, false); - var isAddComponentParameterAvailable = metadataRefs - .Where(r => r.Display is { } display && display.EndsWith("Microsoft.AspNetCore.Components.dll", StringComparison.Ordinal)) - .Collect() - .Select((refs, _) => - { - var compilation = CSharpCompilation.Create("components", references: refs); - return compilation.GetTypesByMetadataName("Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder") - .Any(static t => - t.DeclaredAccessibility == Accessibility.Public && - t.GetMembers("AddComponentParameter") - .Any(static m => m.DeclaredAccessibility == Accessibility.Public)); - }); - IncrementalValuesProvider<(string, SourceGeneratorRazorCodeDocument)> processed(bool designTime) { return (designTime ? withOptionsDesignTime : withOptions) - .Combine(isAddComponentParameterAvailable) .Select((pair, _) => { - var (((sourceItem, imports), razorSourceGeneratorOptions), isAddComponentParameterAvailable) = pair; + var ((sourceItem, imports), razorSourceGeneratorOptions) = pair; RazorSourceGeneratorEventSource.Log.ParseRazorDocumentStart(sourceItem.RelativePhysicalPath); - var projectEngine = GetGenerationProjectEngine(sourceItem, imports, razorSourceGeneratorOptions, isAddComponentParameterAvailable); + var projectEngine = GetGenerationProjectEngine(sourceItem, imports, razorSourceGeneratorOptions); var document = projectEngine.ProcessInitialParse(sourceItem, designTime); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/BaseTagHelperDescriptorProviderTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/BaseTagHelperDescriptorProviderTest.cs index afc107b1499..13404d95a58 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/BaseTagHelperDescriptorProviderTest.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/BaseTagHelperDescriptorProviderTest.cs @@ -7,24 +7,39 @@ using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Test.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.Razor; public abstract class TagHelperDescriptorProviderTestBase { - static TagHelperDescriptorProviderTestBase() + protected TagHelperDescriptorProviderTestBase(string additionalCodeOpt = null) { - BaseCompilation = TestCompilation.Create(typeof(ComponentTagHelperDescriptorProviderTest).Assembly); CSharpParseOptions = new CSharpParseOptions(LanguageVersion.CSharp7_3); + var testTagHelpers = CSharpCompilation.Create( + assemblyName: AssemblyName, + syntaxTrees: + [ + Parse(TagHelperDescriptorFactoryTagHelpers.Code), + ..(additionalCodeOpt != null ? [Parse(additionalCodeOpt)] : Enumerable.Empty()), + ], + references: ReferenceUtil.AspNetLatestAll, + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + BaseCompilation = TestCompilation.Create( + syntaxTrees: [], + references: [testTagHelpers.VerifyDiagnostics().EmitToImageReference()]); } - protected static Compilation BaseCompilation { get; } + protected Compilation BaseCompilation { get; } - protected static CSharpParseOptions CSharpParseOptions { get; } + protected CSharpParseOptions CSharpParseOptions { get; } - protected static CSharpSyntaxTree Parse(string text) + protected static string AssemblyName { get; } = "Microsoft.CodeAnalysis.Razor.Test"; + + protected CSharpSyntaxTree Parse(string text) { return (CSharpSyntaxTree)CSharpSyntaxTree.ParseText(text, CSharpParseOptions); } @@ -35,9 +50,7 @@ protected static TagHelperDescriptor[] ExcludeBuiltInComponents(TagHelperDescrip { var results = context.Results - .Where(c => c.AssemblyName != "Microsoft.AspNetCore.Razor.Test.ComponentShim") - .Where(c => !c.DisplayName.StartsWith("Microsoft.AspNetCore.Components.Web", StringComparison.Ordinal)) - .Where(c => c.GetTypeName() != "Microsoft.AspNetCore.Components.Bind") + .Where(c => !c.DisplayName.StartsWith("Microsoft.AspNetCore.Components.", StringComparison.Ordinal)) .OrderBy(c => c.Name) .ToArray(); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/CompilationTagHelperFeatureTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/CompilationTagHelperFeatureTest.cs index ce7156f1c60..2b93c3a4bf2 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/CompilationTagHelperFeatureTest.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/CompilationTagHelperFeatureTest.cs @@ -5,7 +5,7 @@ using System.Linq; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis.CSharp; using Microsoft.NET.Sdk.Razor.SourceGenerators; using Moq; @@ -21,8 +21,8 @@ public void IsValidCompilation_ReturnsTrueIfTagHelperInterfaceCannotBeFound() // Arrange var references = new[] { - MetadataReference.CreateFromFile(typeof(string).Assembly.Location), - }; + ReferenceUtil.NetLatestSystemRuntime, + }; var compilation = CSharpCompilation.Create("Test", references: references); // Act @@ -38,8 +38,8 @@ public void IsValidCompilation_ReturnsFalseIfSystemStringCannotBeFound() // Arrange var references = new[] { - MetadataReference.CreateFromFile(typeof(ITagHelper).Assembly.Location), - }; + ReferenceUtil.AspNetLatestRazor, + }; var compilation = CSharpCompilation.Create("Test", references: references); // Act @@ -55,9 +55,9 @@ public void IsValidCompilation_ReturnsTrueIfWellKnownTypesAreFound() // Arrange var references = new[] { - MetadataReference.CreateFromFile(typeof(string).Assembly.Location), - MetadataReference.CreateFromFile(typeof(ITagHelper).Assembly.Location), - }; + ReferenceUtil.NetLatestSystemRuntime, + ReferenceUtil.AspNetLatestRazor, + }; var compilation = CSharpCompilation.Create("Test", references: references); // Act @@ -106,9 +106,9 @@ public void GetDescriptors_SetsCompilation_IfCompilationIsValid() var references = new[] { - MetadataReference.CreateFromFile(typeof(string).Assembly.Location), - MetadataReference.CreateFromFile(typeof(ITagHelper).Assembly.Location), - }; + ReferenceUtil.NetLatestSystemRuntime, + ReferenceUtil.AspNetLatestRazor, + }; var engine = RazorProjectEngine.Create( configure => diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperDescriptorProviderTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperDescriptorProviderTest.cs index 3a37a47cd1b..b1549968c4d 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperDescriptorProviderTest.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/ComponentTagHelperDescriptorProviderTest.cs @@ -1620,7 +1620,7 @@ public Task SetParametersAsync(ParameterView parameters) Assert.Empty(compilation.GetDiagnostics()); var targetSymbol = (IAssemblySymbol)compilation.GetAssemblyOrModuleSymbol( - compilation.References.First(static r => r.Display.Contains("Microsoft.CodeAnalysis.Razor.Test.dll"))); + compilation.References.First(static r => r.Display.Contains("Microsoft.CodeAnalysis.Razor.Test"))); var context = new TagHelperDescriptorProviderContext(compilation, targetSymbol); var provider = new ComponentTagHelperDescriptorProvider(); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/DefaultTagHelperDescriptorFactoryTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/DefaultTagHelperDescriptorFactoryTest.cs index d76829adccb..f1a6bea4038 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/DefaultTagHelperDescriptorFactoryTest.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/DefaultTagHelperDescriptorFactoryTest.cs @@ -7,26 +7,21 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Reflection; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Razor.Workspaces.Test; using Xunit; using static Microsoft.AspNetCore.Razor.Language.CommonMetadata; namespace Microsoft.CodeAnalysis.Razor.Workspaces; -public class DefaultTagHelperDescriptorFactoryTest +public class DefaultTagHelperDescriptorFactoryTest : TagHelperDescriptorProviderTestBase { - private static readonly Assembly _assembly = typeof(DefaultTagHelperDescriptorFactoryTest).GetTypeInfo().Assembly; - - protected static readonly AssemblyName TagHelperDescriptorFactoryTestAssembly = _assembly.GetName(); - - protected static readonly string AssemblyName = TagHelperDescriptorFactoryTestAssembly.Name; + public DefaultTagHelperDescriptorFactoryTest() : base(AdditionalCode) + { + Compilation = BaseCompilation; + } - private static Compilation Compilation { get; } = TestCompilation.Create(_assembly); + private Compilation Compilation { get; } public static TheoryData RequiredAttributeParserErrorData { @@ -324,66 +319,66 @@ public static TheoryData IsEnumData get { // tagHelperType, expectedDescriptor - return new TheoryData + return new TheoryData { { - typeof(EnumTagHelper), - TagHelperDescriptorBuilder.Create(typeof(EnumTagHelper).FullName, AssemblyName) - .Metadata(GetMetadata()) + "TestNamespace.EnumTagHelper", + TagHelperDescriptorBuilder.Create("TestNamespace.EnumTagHelper", AssemblyName) + .Metadata(GetMetadata("TestNamespace", "EnumTagHelper")) .TagMatchingRuleDescriptor(ruleBuilder => ruleBuilder.RequireTagName("enum")) .BoundAttributeDescriptor(builder => builder .Name("non-enum-property") - .Metadata(PropertyName(nameof(EnumTagHelper.NonEnumProperty))) + .Metadata(PropertyName("NonEnumProperty")) .TypeName(typeof(int).FullName)) .BoundAttributeDescriptor(builder => builder .Name("enum-property") - .Metadata(PropertyName(nameof(EnumTagHelper.EnumProperty))) - .TypeName(typeof(CustomEnum).FullName) + .Metadata(PropertyName("EnumProperty")) + .TypeName("TestNamespace.CustomEnum") .AsEnum()) .Build() }, { - typeof(MultiEnumTagHelper), - TagHelperDescriptorBuilder.Create(typeof(MultiEnumTagHelper).FullName, AssemblyName) - .Metadata(GetMetadata()) + "TestNamespace.MultiEnumTagHelper", + TagHelperDescriptorBuilder.Create("TestNamespace.MultiEnumTagHelper", AssemblyName) + .Metadata(GetMetadata("TestNamespace", "MultiEnumTagHelper")) .TagMatchingRuleDescriptor(ruleBuilder => ruleBuilder.RequireTagName("p")) .TagMatchingRuleDescriptor(ruleBuilder => ruleBuilder.RequireTagName("input")) .BoundAttributeDescriptor(builder => builder .Name("non-enum-property") - .Metadata(PropertyName(nameof(MultiEnumTagHelper.NonEnumProperty))) + .Metadata(PropertyName("NonEnumProperty")) .TypeName(typeof(int).FullName)) .BoundAttributeDescriptor(builder => builder .Name("enum-property") - .Metadata(PropertyName(nameof(MultiEnumTagHelper.EnumProperty))) - .TypeName(typeof(CustomEnum).FullName) + .Metadata(PropertyName("EnumProperty")) + .TypeName("TestNamespace.CustomEnum") .AsEnum()) .Build() }, { - typeof(NestedEnumTagHelper), - TagHelperDescriptorBuilder.Create(typeof(NestedEnumTagHelper).FullName, AssemblyName) - .Metadata(GetMetadata()) + "TestNamespace.NestedEnumTagHelper", + TagHelperDescriptorBuilder.Create("TestNamespace.NestedEnumTagHelper", AssemblyName) + .Metadata(GetMetadata("TestNamespace", "NestedEnumTagHelper")) .TagMatchingRuleDescriptor(ruleBuilder => ruleBuilder.RequireTagName("nested-enum")) .BoundAttributeDescriptor(builder => builder .Name("nested-enum-property") - .Metadata(PropertyName(nameof(NestedEnumTagHelper.NestedEnumProperty))) - .TypeName($"{typeof(NestedEnumTagHelper).FullName}.{nameof(NestedEnumTagHelper.NestedEnum)}") + .Metadata(PropertyName("NestedEnumProperty")) + .TypeName("TestNamespace.NestedEnumTagHelper.NestedEnum") .AsEnum()) .BoundAttributeDescriptor(builder => builder .Name("non-enum-property") - .Metadata(PropertyName(nameof(NestedEnumTagHelper.NonEnumProperty))) + .Metadata(PropertyName("NonEnumProperty")) .TypeName(typeof(int).FullName)) .BoundAttributeDescriptor(builder => builder .Name("enum-property") - .Metadata(PropertyName(nameof(NestedEnumTagHelper.EnumProperty))) - .TypeName(typeof(CustomEnum).FullName) + .Metadata(PropertyName("EnumProperty")) + .TypeName("TestNamespace.CustomEnum") .AsEnum()) .Build() }, @@ -394,12 +389,12 @@ public static TheoryData IsEnumData [Theory] [MemberData(nameof(IsEnumData))] public void CreateDescriptor_IsEnumIsSetCorrectly( - Type tagHelperType, + string tagHelperTypeFullName, TagHelperDescriptor expectedDescriptor) { // Arrange var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeFullName); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -413,27 +408,27 @@ public static TheoryData RequiredParentData get { // tagHelperType, expectedDescriptor - return new TheoryData + return new TheoryData { { - typeof(RequiredParentTagHelper), - TagHelperDescriptorBuilder.Create(typeof(RequiredParentTagHelper).FullName, AssemblyName) - .Metadata(GetMetadata()) + "TestNamespace.RequiredParentTagHelper", + TagHelperDescriptorBuilder.Create("TestNamespace.RequiredParentTagHelper", AssemblyName) + .Metadata(GetMetadata("TestNamespace", "RequiredParentTagHelper")) .TagMatchingRuleDescriptor(builder => builder.RequireTagName("input").RequireParentTag("div")) .Build() }, { - typeof(MultiSpecifiedRequiredParentTagHelper), - TagHelperDescriptorBuilder.Create(typeof(MultiSpecifiedRequiredParentTagHelper).FullName, AssemblyName) - .Metadata(GetMetadata()) + "TestNamespace.MultiSpecifiedRequiredParentTagHelper", + TagHelperDescriptorBuilder.Create("TestNamespace.MultiSpecifiedRequiredParentTagHelper", AssemblyName) + .Metadata(GetMetadata("TestNamespace", "MultiSpecifiedRequiredParentTagHelper")) .TagMatchingRuleDescriptor(builder => builder.RequireTagName("p").RequireParentTag("div")) .TagMatchingRuleDescriptor(builder => builder.RequireTagName("input").RequireParentTag("section")) .Build() }, { - typeof(MultiWithUnspecifiedRequiredParentTagHelper), - TagHelperDescriptorBuilder.Create(typeof(MultiWithUnspecifiedRequiredParentTagHelper).FullName, AssemblyName) - .Metadata(GetMetadata()) + "TestNamespace.MultiWithUnspecifiedRequiredParentTagHelper", + TagHelperDescriptorBuilder.Create("TestNamespace.MultiWithUnspecifiedRequiredParentTagHelper", AssemblyName) + .Metadata(GetMetadata("TestNamespace", "MultiWithUnspecifiedRequiredParentTagHelper")) .TagMatchingRuleDescriptor(builder => builder.RequireTagName("p")) .TagMatchingRuleDescriptor(builder => builder.RequireTagName("input").RequireParentTag("div")) .Build() @@ -445,12 +440,12 @@ public static TheoryData RequiredParentData [Theory] [MemberData(nameof(RequiredParentData))] public void CreateDescriptor_CreatesDesignTimeDescriptorsWithRequiredParent( - Type tagHelperType, + string tagHelperTypeFullName, TagHelperDescriptor expectedDescriptor) { // Arrange var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeFullName); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -459,16 +454,14 @@ public void CreateDescriptor_CreatesDesignTimeDescriptorsWithRequiredParent( Assert.Equal(expectedDescriptor, descriptor); } - private static KeyValuePair[] GetMetadata() + private static KeyValuePair[] GetMetadata(string @namespace, string name) { - var type = typeof(T); - var name = type.Name; - var fullName = type.FullName; + var fullName = $"{@namespace}.{name}"; return new[] { TypeName(fullName), - TypeNamespace(fullName[..(fullName.Length - name.Length - 1)]), + TypeNamespace(@namespace), TypeNameIdentifier(name) }; } @@ -478,29 +471,29 @@ public static TheoryData RestrictChildrenData get { // tagHelperType, expectedDescriptor - return new TheoryData + return new TheoryData { { - typeof(RestrictChildrenTagHelper), - TagHelperDescriptorBuilder.Create(typeof(RestrictChildrenTagHelper).FullName, AssemblyName) - .Metadata(GetMetadata()) + "TestNamespace.RestrictChildrenTagHelper", + TagHelperDescriptorBuilder.Create("TestNamespace.RestrictChildrenTagHelper", AssemblyName) + .Metadata(GetMetadata("TestNamespace", "RestrictChildrenTagHelper")) .TagMatchingRuleDescriptor(builder => builder.RequireTagName("restrict-children")) .AllowChildTag("p") .Build() }, { - typeof(DoubleRestrictChildrenTagHelper), - TagHelperDescriptorBuilder.Create(typeof(DoubleRestrictChildrenTagHelper).FullName, AssemblyName) - .Metadata(GetMetadata()) + "TestNamespace.DoubleRestrictChildrenTagHelper", + TagHelperDescriptorBuilder.Create("TestNamespace.DoubleRestrictChildrenTagHelper", AssemblyName) + .Metadata(GetMetadata("TestNamespace", "DoubleRestrictChildrenTagHelper")) .TagMatchingRuleDescriptor(builder => builder.RequireTagName("double-restrict-children")) .AllowChildTag("p") .AllowChildTag("strong") .Build() }, { - typeof(MultiTargetRestrictChildrenTagHelper), - TagHelperDescriptorBuilder.Create(typeof(MultiTargetRestrictChildrenTagHelper).FullName, AssemblyName) - .Metadata(GetMetadata()) + "TestNamespace.MultiTargetRestrictChildrenTagHelper", + TagHelperDescriptorBuilder.Create("TestNamespace.MultiTargetRestrictChildrenTagHelper", AssemblyName) + .Metadata(GetMetadata("TestNamespace", "MultiTargetRestrictChildrenTagHelper")) .TagMatchingRuleDescriptor(builder => builder.RequireTagName("p")) .TagMatchingRuleDescriptor(builder => builder.RequireTagName("div")) .AllowChildTag("p") @@ -515,12 +508,12 @@ public static TheoryData RestrictChildrenData [Theory] [MemberData(nameof(RestrictChildrenData))] public void CreateDescriptor_CreatesDescriptorsWithAllowedChildren( - Type tagHelperType, + string tagHelperTypeFullName, TagHelperDescriptor expectedDescriptor) { // Arrange var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeFullName); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -534,21 +527,21 @@ public static TheoryData TagStructureData get { // tagHelperType, expectedDescriptor - return new TheoryData + return new TheoryData { { - typeof(TagStructureTagHelper), - TagHelperDescriptorBuilder.Create(typeof(TagStructureTagHelper).FullName, AssemblyName) - .Metadata(GetMetadata()) + "TestNamespace.TagStructureTagHelper", + TagHelperDescriptorBuilder.Create("TestNamespace.TagStructureTagHelper", AssemblyName) + .Metadata(GetMetadata("TestNamespace", "TagStructureTagHelper")) .TagMatchingRuleDescriptor(builder => builder .RequireTagName("input") .RequireTagStructure(TagStructure.WithoutEndTag)) .Build() }, { - typeof(MultiSpecifiedTagStructureTagHelper), - TagHelperDescriptorBuilder.Create(typeof(MultiSpecifiedTagStructureTagHelper).FullName, AssemblyName) - .Metadata(GetMetadata()) + "TestNamespace.MultiSpecifiedTagStructureTagHelper", + TagHelperDescriptorBuilder.Create("TestNamespace.MultiSpecifiedTagStructureTagHelper", AssemblyName) + .Metadata(GetMetadata("TestNamespace", "MultiSpecifiedTagStructureTagHelper")) .TagMatchingRuleDescriptor(builder => builder .RequireTagName("p") .RequireTagStructure(TagStructure.NormalOrSelfClosing)) @@ -558,9 +551,9 @@ public static TheoryData TagStructureData .Build() }, { - typeof(MultiWithUnspecifiedTagStructureTagHelper), - TagHelperDescriptorBuilder.Create(typeof(MultiWithUnspecifiedTagStructureTagHelper).FullName, AssemblyName) - .Metadata(GetMetadata()) + "TestNamespace.MultiWithUnspecifiedTagStructureTagHelper", + TagHelperDescriptorBuilder.Create("TestNamespace.MultiWithUnspecifiedTagStructureTagHelper", AssemblyName) + .Metadata(GetMetadata("TestNamespace", "MultiWithUnspecifiedTagStructureTagHelper")) .TagMatchingRuleDescriptor(builder => builder .RequireTagName("p")) .TagMatchingRuleDescriptor(builder => builder @@ -575,12 +568,12 @@ public static TheoryData TagStructureData [Theory] [MemberData(nameof(TagStructureData))] public void CreateDescriptor_CreatesDesignTimeDescriptorsWithTagStructure( - Type tagHelperType, + string tagHelperTypeFullName, TagHelperDescriptor expectedDescriptor) { // Arrange var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeFullName); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -594,174 +587,174 @@ public static TheoryData EditorBrowsableData get { // tagHelperType, designTime, expectedDescriptor - return new TheoryData + return new TheoryData { { - typeof(InheritedEditorBrowsableTagHelper), + "TestNamespace.InheritedEditorBrowsableTagHelper", true, CreateTagHelperDescriptor( tagName: "inherited-editor-browsable", - typeName: typeof(InheritedEditorBrowsableTagHelper).FullName, + typeName: "TestNamespace.InheritedEditorBrowsableTagHelper", assemblyName: AssemblyName, - typeNamespace: typeof(InheritedEditorBrowsableTagHelper).FullName.Substring(0, typeof(InheritedEditorBrowsableTagHelper).FullName.Length - nameof(InheritedEditorBrowsableTagHelper).Length -1), - typeNameIdentifier: nameof(InheritedEditorBrowsableTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "InheritedEditorBrowsableTagHelper", attributes: new Action[] { builder => builder .Name("property") - .Metadata(PropertyName(nameof(InheritedEditorBrowsableTagHelper.Property))) + .Metadata(PropertyName("Property")) .TypeName(typeof(int).FullName), }) }, - { typeof(EditorBrowsableTagHelper), true, null }, + { "TestNamespace.EditorBrowsableTagHelper", true, null }, { - typeof(EditorBrowsableTagHelper), + "TestNamespace.EditorBrowsableTagHelper", false, CreateTagHelperDescriptor( tagName: "editor-browsable", - typeName: typeof(EditorBrowsableTagHelper).FullName, + typeName: "TestNamespace.EditorBrowsableTagHelper", assemblyName: AssemblyName, - typeNamespace: typeof(EditorBrowsableTagHelper).FullName.Substring(0, typeof(EditorBrowsableTagHelper).FullName.Length - nameof(EditorBrowsableTagHelper).Length -1), - typeNameIdentifier: nameof(EditorBrowsableTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "EditorBrowsableTagHelper", attributes: new Action[] { builder => builder .Name("property") - .Metadata(PropertyName(nameof(EditorBrowsableTagHelper.Property))) + .Metadata(PropertyName("Property")) .TypeName(typeof(int).FullName), }) }, { - typeof(HiddenPropertyEditorBrowsableTagHelper), + "TestNamespace.HiddenPropertyEditorBrowsableTagHelper", true, CreateTagHelperDescriptor( tagName: "hidden-property-editor-browsable", - typeName: typeof(HiddenPropertyEditorBrowsableTagHelper).FullName, - typeNamespace: typeof(HiddenPropertyEditorBrowsableTagHelper).FullName.Substring(0, typeof(HiddenPropertyEditorBrowsableTagHelper).FullName.Length - nameof(HiddenPropertyEditorBrowsableTagHelper).Length -1), - typeNameIdentifier: nameof(HiddenPropertyEditorBrowsableTagHelper), + typeName: "TestNamespace.HiddenPropertyEditorBrowsableTagHelper", + typeNamespace: "TestNamespace", + typeNameIdentifier: "HiddenPropertyEditorBrowsableTagHelper", assemblyName: AssemblyName) }, { - typeof(HiddenPropertyEditorBrowsableTagHelper), + "TestNamespace.HiddenPropertyEditorBrowsableTagHelper", false, CreateTagHelperDescriptor( tagName: "hidden-property-editor-browsable", - typeName: typeof(HiddenPropertyEditorBrowsableTagHelper).FullName, - typeNamespace: typeof(HiddenPropertyEditorBrowsableTagHelper).FullName.Substring(0, typeof(HiddenPropertyEditorBrowsableTagHelper).FullName.Length - nameof(HiddenPropertyEditorBrowsableTagHelper).Length -1), - typeNameIdentifier: nameof(HiddenPropertyEditorBrowsableTagHelper), + typeName: "TestNamespace.HiddenPropertyEditorBrowsableTagHelper", + typeNamespace: "TestNamespace", + typeNameIdentifier: "HiddenPropertyEditorBrowsableTagHelper", assemblyName: AssemblyName, attributes: new Action[] { builder => builder .Name("property") - .Metadata(PropertyName(nameof(HiddenPropertyEditorBrowsableTagHelper.Property))) + .Metadata(PropertyName("Property")) .TypeName(typeof(int).FullName), }) }, { - typeof(OverriddenEditorBrowsableTagHelper), + "TestNamespace.OverriddenEditorBrowsableTagHelper", true, CreateTagHelperDescriptor( tagName: "overridden-editor-browsable", - typeName: typeof(OverriddenEditorBrowsableTagHelper).FullName, - typeNamespace: typeof(OverriddenEditorBrowsableTagHelper).FullName.Substring(0, typeof(OverriddenEditorBrowsableTagHelper).FullName.Length - nameof(OverriddenEditorBrowsableTagHelper).Length -1), - typeNameIdentifier: nameof(OverriddenEditorBrowsableTagHelper), + typeName: "TestNamespace.OverriddenEditorBrowsableTagHelper", + typeNamespace: "TestNamespace", + typeNameIdentifier: "OverriddenEditorBrowsableTagHelper", assemblyName: AssemblyName, attributes: new Action[] { builder => builder .Name("property") - .Metadata(PropertyName(nameof(OverriddenEditorBrowsableTagHelper.Property))) + .Metadata(PropertyName("Property")) .TypeName(typeof(int).FullName), }) }, { - typeof(MultiPropertyEditorBrowsableTagHelper), + "TestNamespace.MultiPropertyEditorBrowsableTagHelper", true, CreateTagHelperDescriptor( tagName: "multi-property-editor-browsable", - typeName: typeof(MultiPropertyEditorBrowsableTagHelper).FullName, - typeNamespace: typeof(MultiPropertyEditorBrowsableTagHelper).FullName.Substring(0, typeof(MultiPropertyEditorBrowsableTagHelper).FullName.Length - nameof(MultiPropertyEditorBrowsableTagHelper).Length -1), - typeNameIdentifier: nameof(MultiPropertyEditorBrowsableTagHelper), + typeName: "TestNamespace.MultiPropertyEditorBrowsableTagHelper", + typeNamespace: "TestNamespace", + typeNameIdentifier: "MultiPropertyEditorBrowsableTagHelper", assemblyName: AssemblyName, attributes: new Action[] { builder => builder .Name("property2") - .Metadata(PropertyName(nameof(MultiPropertyEditorBrowsableTagHelper.Property2))) + .Metadata(PropertyName("Property2")) .TypeName(typeof(int).FullName), }) }, { - typeof(MultiPropertyEditorBrowsableTagHelper), + "TestNamespace.MultiPropertyEditorBrowsableTagHelper", false, CreateTagHelperDescriptor( tagName: "multi-property-editor-browsable", - typeName: typeof(MultiPropertyEditorBrowsableTagHelper).FullName, - typeNamespace: typeof(MultiPropertyEditorBrowsableTagHelper).FullName.Substring(0, typeof(MultiPropertyEditorBrowsableTagHelper).FullName.Length - nameof(MultiPropertyEditorBrowsableTagHelper).Length -1), - typeNameIdentifier: nameof(MultiPropertyEditorBrowsableTagHelper), + typeName: "TestNamespace.MultiPropertyEditorBrowsableTagHelper", + typeNamespace: "TestNamespace", + typeNameIdentifier: "MultiPropertyEditorBrowsableTagHelper", assemblyName: AssemblyName, attributes: new Action[] { builder => builder .Name("property") - .Metadata(PropertyName(nameof(MultiPropertyEditorBrowsableTagHelper.Property))) + .Metadata(PropertyName("Property")) .TypeName(typeof(int).FullName), builder => builder .Name("property2") - .Metadata(PropertyName(nameof(MultiPropertyEditorBrowsableTagHelper.Property2))) + .Metadata(PropertyName("Property2")) .TypeName(typeof(int).FullName), }) }, { - typeof(OverriddenPropertyEditorBrowsableTagHelper), + "TestNamespace.OverriddenPropertyEditorBrowsableTagHelper", true, CreateTagHelperDescriptor( tagName: "overridden-property-editor-browsable", - typeName: typeof(OverriddenPropertyEditorBrowsableTagHelper).FullName, - typeNamespace: typeof(OverriddenPropertyEditorBrowsableTagHelper).FullName.Substring(0, typeof(OverriddenPropertyEditorBrowsableTagHelper).FullName.Length - nameof(OverriddenPropertyEditorBrowsableTagHelper).Length -1), - typeNameIdentifier: nameof(OverriddenPropertyEditorBrowsableTagHelper), + typeName: "TestNamespace.OverriddenPropertyEditorBrowsableTagHelper", + typeNamespace: "TestNamespace", + typeNameIdentifier: "OverriddenPropertyEditorBrowsableTagHelper", assemblyName: AssemblyName) }, { - typeof(OverriddenPropertyEditorBrowsableTagHelper), + "TestNamespace.OverriddenPropertyEditorBrowsableTagHelper", false, CreateTagHelperDescriptor( tagName: "overridden-property-editor-browsable", - typeName: typeof(OverriddenPropertyEditorBrowsableTagHelper).FullName, - typeNamespace: typeof(OverriddenPropertyEditorBrowsableTagHelper).FullName.Substring(0, typeof(OverriddenPropertyEditorBrowsableTagHelper).FullName.Length - nameof(OverriddenPropertyEditorBrowsableTagHelper).Length -1), - typeNameIdentifier: nameof(OverriddenPropertyEditorBrowsableTagHelper), + typeName: "TestNamespace.OverriddenPropertyEditorBrowsableTagHelper", + typeNamespace: "TestNamespace", + typeNameIdentifier: "OverriddenPropertyEditorBrowsableTagHelper", assemblyName: AssemblyName, attributes: new Action[] { builder => builder .Name("property2") - .Metadata(PropertyName(nameof(OverriddenPropertyEditorBrowsableTagHelper.Property2))) + .Metadata(PropertyName("Property2")) .TypeName(typeof(int).FullName), builder => builder .Name("property") - .Metadata(PropertyName(nameof(OverriddenPropertyEditorBrowsableTagHelper.Property))) + .Metadata(PropertyName("Property")) .TypeName(typeof(int).FullName), }) }, { - typeof(DefaultEditorBrowsableTagHelper), + "TestNamespace.DefaultEditorBrowsableTagHelper", true, CreateTagHelperDescriptor( tagName: "default-editor-browsable", - typeName: typeof(DefaultEditorBrowsableTagHelper).FullName, - typeNamespace: typeof(DefaultEditorBrowsableTagHelper).FullName.Substring(0, typeof(DefaultEditorBrowsableTagHelper).FullName.Length - nameof(DefaultEditorBrowsableTagHelper).Length -1), - typeNameIdentifier: nameof(DefaultEditorBrowsableTagHelper), + typeName: "TestNamespace.DefaultEditorBrowsableTagHelper", + typeNamespace: "TestNamespace", + typeNameIdentifier: "DefaultEditorBrowsableTagHelper", assemblyName: AssemblyName, attributes: new Action[] { builder => builder .Name("property") - .Metadata(PropertyName(nameof(DefaultEditorBrowsableTagHelper.Property))) + .Metadata(PropertyName("Property")) .TypeName(typeof(int).FullName), }) }, - { typeof(MultiEditorBrowsableTagHelper), true, null } + { "TestNamespace.MultiEditorBrowsableTagHelper", true, null } }; } } @@ -769,13 +762,13 @@ public static TheoryData EditorBrowsableData [Theory] [MemberData(nameof(EditorBrowsableData))] public void CreateDescriptor_UnderstandsEditorBrowsableAttribute( - Type tagHelperType, + string tagHelperTypeFullName, bool designTime, TagHelperDescriptor expectedDescriptor) { // Arrange var factory = new DefaultTagHelperDescriptorFactory(Compilation, designTime, designTime); - var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeFullName); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -791,29 +784,29 @@ public static TheoryData AttributeTargetData var attributes = Enumerable.Empty(); // tagHelperType, expectedDescriptor - return new TheoryData + return new TheoryData { { - typeof(AttributeTargetingTagHelper), + "TestNamespace.AttributeTargetingTagHelper", CreateTagHelperDescriptor( TagHelperMatchingConventions.ElementCatchAllName, - typeof(AttributeTargetingTagHelper).FullName, + "TestNamespace.AttributeTargetingTagHelper", AssemblyName, - typeof(AttributeTargetingTagHelper).FullName.Substring(0, typeof(AttributeTargetingTagHelper).FullName.Length - nameof(AttributeTargetingTagHelper).Length -1), - nameof(AttributeTargetingTagHelper), + "TestNamespace", + "AttributeTargetingTagHelper", ruleBuilders: new Action[] { builder => builder.RequireAttributeDescriptor(attribute => attribute.Name("class")), }) }, { - typeof(MultiAttributeTargetingTagHelper), + "TestNamespace.MultiAttributeTargetingTagHelper", CreateTagHelperDescriptor( TagHelperMatchingConventions.ElementCatchAllName, - typeof(MultiAttributeTargetingTagHelper).FullName, + "TestNamespace.MultiAttributeTargetingTagHelper", AssemblyName, - typeNamespace: typeof(MultiAttributeTargetingTagHelper).FullName.Substring(0, typeof(MultiAttributeTargetingTagHelper).FullName.Length - nameof(MultiAttributeTargetingTagHelper).Length -1), - typeNameIdentifier: nameof(MultiAttributeTargetingTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "MultiAttributeTargetingTagHelper", ruleBuilders: new Action[] { builder => @@ -825,13 +818,13 @@ public static TheoryData AttributeTargetData }) }, { - typeof(MultiAttributeAttributeTargetingTagHelper), + "TestNamespace.MultiAttributeAttributeTargetingTagHelper", CreateTagHelperDescriptor( TagHelperMatchingConventions.ElementCatchAllName, - typeof(MultiAttributeAttributeTargetingTagHelper).FullName, + "TestNamespace.MultiAttributeAttributeTargetingTagHelper", AssemblyName, - typeNamespace: typeof(MultiAttributeAttributeTargetingTagHelper).FullName.Substring(0, typeof(MultiAttributeAttributeTargetingTagHelper).FullName.Length - nameof(MultiAttributeAttributeTargetingTagHelper).Length -1), - typeNameIdentifier: nameof(MultiAttributeAttributeTargetingTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "MultiAttributeAttributeTargetingTagHelper", ruleBuilders: new Action[] { builder => builder.RequireAttributeDescriptor(attribute => attribute.Name("custom")), @@ -844,52 +837,52 @@ public static TheoryData AttributeTargetData }) }, { - typeof(InheritedAttributeTargetingTagHelper), + "TestNamespace.InheritedAttributeTargetingTagHelper", CreateTagHelperDescriptor( TagHelperMatchingConventions.ElementCatchAllName, - typeof(InheritedAttributeTargetingTagHelper).FullName, + "TestNamespace.InheritedAttributeTargetingTagHelper", AssemblyName, - typeNamespace: typeof(InheritedAttributeTargetingTagHelper).FullName.Substring(0, typeof(InheritedAttributeTargetingTagHelper).FullName.Length - nameof(InheritedAttributeTargetingTagHelper).Length -1), - typeNameIdentifier: nameof(InheritedAttributeTargetingTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "InheritedAttributeTargetingTagHelper", ruleBuilders: new Action[] { builder => builder.RequireAttributeDescriptor(attribute => attribute.Name("style")), }) }, { - typeof(RequiredAttributeTagHelper), + "TestNamespace.RequiredAttributeTagHelper", CreateTagHelperDescriptor( "input", - typeof(RequiredAttributeTagHelper).FullName, + "TestNamespace.RequiredAttributeTagHelper", AssemblyName, - typeNamespace: typeof(RequiredAttributeTagHelper).FullName.Substring(0, typeof(RequiredAttributeTagHelper).FullName.Length - nameof(RequiredAttributeTagHelper).Length -1), - typeNameIdentifier: nameof(RequiredAttributeTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "RequiredAttributeTagHelper", ruleBuilders: new Action[] { builder => builder.RequireAttributeDescriptor(attribute => attribute.Name("class")), }) }, { - typeof(InheritedRequiredAttributeTagHelper), + "TestNamespace.InheritedRequiredAttributeTagHelper", CreateTagHelperDescriptor( "div", - typeof(InheritedRequiredAttributeTagHelper).FullName, + "TestNamespace.InheritedRequiredAttributeTagHelper", AssemblyName, - typeNamespace: typeof(InheritedRequiredAttributeTagHelper).FullName.Substring(0, typeof(InheritedRequiredAttributeTagHelper).FullName.Length - nameof(InheritedRequiredAttributeTagHelper).Length -1), - typeNameIdentifier: nameof(InheritedRequiredAttributeTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "InheritedRequiredAttributeTagHelper", ruleBuilders: new Action[] { builder => builder.RequireAttributeDescriptor(attribute => attribute.Name("class")), }) }, { - typeof(MultiAttributeRequiredAttributeTagHelper), + "TestNamespace.MultiAttributeRequiredAttributeTagHelper", CreateTagHelperDescriptor( "div", - typeof(MultiAttributeRequiredAttributeTagHelper).FullName, + "TestNamespace.MultiAttributeRequiredAttributeTagHelper", AssemblyName, - typeNamespace: typeof(MultiAttributeRequiredAttributeTagHelper).FullName.Substring(0, typeof(MultiAttributeRequiredAttributeTagHelper).FullName.Length - nameof(MultiAttributeRequiredAttributeTagHelper).Length -1), - typeNameIdentifier: nameof(MultiAttributeRequiredAttributeTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "MultiAttributeRequiredAttributeTagHelper", ruleBuilders: new Action[] { builder => builder @@ -901,13 +894,13 @@ public static TheoryData AttributeTargetData }) }, { - typeof(MultiAttributeSameTagRequiredAttributeTagHelper), + "TestNamespace.MultiAttributeSameTagRequiredAttributeTagHelper", CreateTagHelperDescriptor( "input", - typeof(MultiAttributeSameTagRequiredAttributeTagHelper).FullName, + "TestNamespace.MultiAttributeSameTagRequiredAttributeTagHelper", AssemblyName, - typeNamespace: typeof(MultiAttributeSameTagRequiredAttributeTagHelper).FullName.Substring(0, typeof(MultiAttributeSameTagRequiredAttributeTagHelper).FullName.Length - nameof(MultiAttributeSameTagRequiredAttributeTagHelper).Length -1), - typeNameIdentifier: nameof(MultiAttributeSameTagRequiredAttributeTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "MultiAttributeSameTagRequiredAttributeTagHelper", ruleBuilders: new Action[] { builder => builder.RequireAttributeDescriptor(attribute => attribute.Name("style")), @@ -915,13 +908,13 @@ public static TheoryData AttributeTargetData }) }, { - typeof(MultiRequiredAttributeTagHelper), + "TestNamespace.MultiRequiredAttributeTagHelper", CreateTagHelperDescriptor( "input", - typeof(MultiRequiredAttributeTagHelper).FullName, + "TestNamespace.MultiRequiredAttributeTagHelper", AssemblyName, - typeNamespace: typeof(MultiRequiredAttributeTagHelper).FullName.Substring(0, typeof(MultiRequiredAttributeTagHelper).FullName.Length - nameof(MultiRequiredAttributeTagHelper).Length -1), - typeNameIdentifier: nameof(MultiRequiredAttributeTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "MultiRequiredAttributeTagHelper", ruleBuilders: new Action[] { builder => builder @@ -930,13 +923,13 @@ public static TheoryData AttributeTargetData }) }, { - typeof(MultiTagMultiRequiredAttributeTagHelper), + "TestNamespace.MultiTagMultiRequiredAttributeTagHelper", CreateTagHelperDescriptor( "div", - typeof(MultiTagMultiRequiredAttributeTagHelper).FullName, + "TestNamespace.MultiTagMultiRequiredAttributeTagHelper", AssemblyName, - typeNamespace: typeof(MultiTagMultiRequiredAttributeTagHelper).FullName.Substring(0, typeof(MultiTagMultiRequiredAttributeTagHelper).FullName.Length - nameof(MultiTagMultiRequiredAttributeTagHelper).Length -1), - typeNameIdentifier: nameof(MultiTagMultiRequiredAttributeTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "MultiTagMultiRequiredAttributeTagHelper", ruleBuilders: new Action[] { builder => builder @@ -950,13 +943,13 @@ public static TheoryData AttributeTargetData }) }, { - typeof(AttributeWildcardTargetingTagHelper), + "TestNamespace.AttributeWildcardTargetingTagHelper", CreateTagHelperDescriptor( TagHelperMatchingConventions.ElementCatchAllName, - typeof(AttributeWildcardTargetingTagHelper).FullName, + "TestNamespace.AttributeWildcardTargetingTagHelper", AssemblyName, - typeNamespace: typeof(AttributeWildcardTargetingTagHelper).FullName.Substring(0, typeof(AttributeWildcardTargetingTagHelper).FullName.Length - nameof(AttributeWildcardTargetingTagHelper).Length -1), - typeNameIdentifier: nameof(AttributeWildcardTargetingTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "AttributeWildcardTargetingTagHelper", ruleBuilders: new Action[] { builder => builder @@ -966,13 +959,13 @@ public static TheoryData AttributeTargetData }) }, { - typeof(MultiAttributeWildcardTargetingTagHelper), + "TestNamespace.MultiAttributeWildcardTargetingTagHelper", CreateTagHelperDescriptor( TagHelperMatchingConventions.ElementCatchAllName, - typeof(MultiAttributeWildcardTargetingTagHelper).FullName, + "TestNamespace.MultiAttributeWildcardTargetingTagHelper", AssemblyName, - typeNamespace: typeof(MultiAttributeWildcardTargetingTagHelper).FullName.Substring(0, typeof(MultiAttributeWildcardTargetingTagHelper).FullName.Length - nameof(MultiAttributeWildcardTargetingTagHelper).Length -1), - typeNameIdentifier: nameof(MultiAttributeWildcardTargetingTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "MultiAttributeWildcardTargetingTagHelper", ruleBuilders: new Action[] { builder => builder @@ -991,12 +984,12 @@ public static TheoryData AttributeTargetData [Theory] [MemberData(nameof(AttributeTargetData))] public void CreateDescriptor_ReturnsExpectedDescriptors( - Type tagHelperType, + string tagHelperTypeFullName, TagHelperDescriptor expectedDescriptor) { // Arrange var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeFullName); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -1010,16 +1003,16 @@ public static TheoryData HtmlCaseData get { // tagHelperType, expectedTagName, expectedAttributeName - return new TheoryData + return new TheoryData { - { typeof(SingleAttributeTagHelper), "single-attribute", "int-attribute" }, - { typeof(ALLCAPSTAGHELPER), "allcaps", "allcapsattribute" }, - { typeof(CAPSOnOUTSIDETagHelper), "caps-on-outside", "caps-on-outsideattribute" }, - { typeof(capsONInsideTagHelper), "caps-on-inside", "caps-on-insideattribute" }, - { typeof(One1Two2Three3TagHelper), "one1-two2-three3", "one1-two2-three3-attribute" }, - { typeof(ONE1TWO2THREE3TagHelper), "one1two2three3", "one1two2three3-attribute" }, - { typeof(First_Second_ThirdHiTagHelper), "first_second_third-hi", "first_second_third-attribute" }, - { typeof(UNSuffixedCLASS), "un-suffixed-class", "un-suffixed-attribute" }, + { "TestNamespace.SingleAttributeTagHelper", "single-attribute", "int-attribute" }, + { "TestNamespace.ALLCAPSTAGHELPER", "allcaps", "allcapsattribute" }, + { "TestNamespace.CAPSOnOUTSIDETagHelper", "caps-on-outside", "caps-on-outsideattribute" }, + { "TestNamespace.capsONInsideTagHelper", "caps-on-inside", "caps-on-insideattribute" }, + { "TestNamespace.One1Two2Three3TagHelper", "one1-two2-three3", "one1-two2-three3-attribute" }, + { "TestNamespace.ONE1TWO2THREE3TagHelper", "one1two2three3", "one1two2three3-attribute" }, + { "TestNamespace.First_Second_ThirdHiTagHelper", "first_second_third-hi", "first_second_third-attribute" }, + { "TestNamespace.UNSuffixedCLASS", "un-suffixed-class", "un-suffixed-attribute" }, }; } } @@ -1027,13 +1020,13 @@ public static TheoryData HtmlCaseData [Theory] [MemberData(nameof(HtmlCaseData))] public void CreateDescriptor_HtmlCasesTagNameAndAttributeName( - Type tagHelperType, + string tagHelperTypeFullName, string expectedTagName, string expectedAttributeName) { // Arrange var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeFullName); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -1049,30 +1042,26 @@ public void CreateDescriptor_HtmlCasesTagNameAndAttributeName( public void CreateDescriptor_OverridesAttributeNameFromAttribute() { // Arrange - var validProperty1 = typeof(OverriddenAttributeTagHelper).GetProperty( - nameof(OverriddenAttributeTagHelper.ValidAttribute1)); - var validProperty2 = typeof(OverriddenAttributeTagHelper).GetProperty( - nameof(OverriddenAttributeTagHelper.ValidAttribute2)); var expectedDescriptor = CreateTagHelperDescriptor( "overridden-attribute", - typeof(OverriddenAttributeTagHelper).FullName, + "TestNamespace.OverriddenAttributeTagHelper", AssemblyName, - typeNamespace: typeof(OverriddenAttributeTagHelper).FullName.Substring(0, typeof(OverriddenAttributeTagHelper).FullName.Length - nameof(OverriddenAttributeTagHelper).Length -1), - typeNameIdentifier: nameof(OverriddenAttributeTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "OverriddenAttributeTagHelper", new Action[] { builder => builder .Name("SomethingElse") - .Metadata(PropertyName(validProperty1.Name)) - .TypeName(validProperty1.PropertyType.FullName), + .Metadata(PropertyName("ValidAttribute1")) + .TypeName(typeof(string).FullName), builder => builder .Name("Something-Else") - .Metadata(PropertyName(validProperty2.Name)) - .TypeName(validProperty2.PropertyType.FullName), + .Metadata(PropertyName("ValidAttribute2")) + .TypeName(typeof(string).FullName), }); var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(typeof(OverriddenAttributeTagHelper).FullName); + var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.OverriddenAttributeTagHelper"); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -1085,30 +1074,26 @@ public void CreateDescriptor_OverridesAttributeNameFromAttribute() public void CreateDescriptor_DoesNotInheritOverridenAttributeName() { // Arrange - var validProperty1 = typeof(InheritedOverriddenAttributeTagHelper).GetProperty( - nameof(InheritedOverriddenAttributeTagHelper.ValidAttribute1)); - var validProperty2 = typeof(InheritedOverriddenAttributeTagHelper).GetProperty( - nameof(InheritedOverriddenAttributeTagHelper.ValidAttribute2)); var expectedDescriptor = CreateTagHelperDescriptor( "inherited-overridden-attribute", - typeof(InheritedOverriddenAttributeTagHelper).FullName, + "TestNamespace.InheritedOverriddenAttributeTagHelper", AssemblyName, - typeNamespace: typeof(InheritedOverriddenAttributeTagHelper).FullName.Substring(0, typeof(InheritedOverriddenAttributeTagHelper).FullName.Length - nameof(InheritedOverriddenAttributeTagHelper).Length -1), - typeNameIdentifier: nameof(InheritedOverriddenAttributeTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "InheritedOverriddenAttributeTagHelper", new Action[] { builder => builder .Name("valid-attribute1") - .Metadata(PropertyName(validProperty1.Name)) - .TypeName(validProperty1.PropertyType.FullName), + .Metadata(PropertyName("ValidAttribute1")) + .TypeName(typeof(string).FullName), builder => builder .Name("Something-Else") - .Metadata(PropertyName(validProperty2.Name)) - .TypeName(validProperty2.PropertyType.FullName), + .Metadata(PropertyName("ValidAttribute2")) + .TypeName(typeof(string).FullName), }); var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(typeof(InheritedOverriddenAttributeTagHelper).FullName); + var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.InheritedOverriddenAttributeTagHelper"); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -1121,30 +1106,25 @@ public void CreateDescriptor_DoesNotInheritOverridenAttributeName() public void CreateDescriptor_AllowsOverriddenAttributeNameOnUnimplementedVirtual() { // Arrange - var validProperty1 = typeof(InheritedNotOverriddenAttributeTagHelper).GetProperty( - nameof(InheritedNotOverriddenAttributeTagHelper.ValidAttribute1)); - var validProperty2 = typeof(InheritedNotOverriddenAttributeTagHelper).GetProperty( - nameof(InheritedNotOverriddenAttributeTagHelper.ValidAttribute2)); - var expectedDescriptor = CreateTagHelperDescriptor( "inherited-not-overridden-attribute", - typeof(InheritedNotOverriddenAttributeTagHelper).FullName, + "TestNamespace.InheritedNotOverriddenAttributeTagHelper", AssemblyName, - typeNamespace: typeof(InheritedNotOverriddenAttributeTagHelper).FullName.Substring(0, typeof(InheritedNotOverriddenAttributeTagHelper).FullName.Length - nameof(InheritedNotOverriddenAttributeTagHelper).Length -1), - typeNameIdentifier: nameof(InheritedNotOverriddenAttributeTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "InheritedNotOverriddenAttributeTagHelper", new Action[] { builder => builder .Name("SomethingElse") - .Metadata(PropertyName(validProperty1.Name)) - .TypeName(validProperty1.PropertyType.FullName), + .Metadata(PropertyName("ValidAttribute1")) + .TypeName(typeof(string).FullName), builder => builder .Name("Something-Else") - .Metadata(PropertyName(validProperty2.Name)) - .TypeName(validProperty2.PropertyType.FullName), + .Metadata(PropertyName("ValidAttribute2")) + .TypeName(typeof(string).FullName), }); var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(typeof(InheritedNotOverriddenAttributeTagHelper).FullName); + var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.InheritedNotOverriddenAttributeTagHelper"); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -1159,19 +1139,19 @@ public void CreateDescriptor_BuildsDescriptorsWithInheritedProperties() // Arrange var expectedDescriptor = CreateTagHelperDescriptor( "inherited-single-attribute", - typeof(InheritedSingleAttributeTagHelper).FullName, + "TestNamespace.InheritedSingleAttributeTagHelper", AssemblyName, - typeNamespace: typeof(InheritedSingleAttributeTagHelper).FullName.Substring(0, typeof(InheritedSingleAttributeTagHelper).FullName.Length - nameof(InheritedSingleAttributeTagHelper).Length -1), - typeNameIdentifier: nameof(InheritedSingleAttributeTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "InheritedSingleAttributeTagHelper", new Action[] { builder => builder .Name("int-attribute") - .Metadata(PropertyName(nameof(InheritedSingleAttributeTagHelper.IntAttribute))) + .Metadata(PropertyName("IntAttribute")) .TypeName(typeof(int).FullName) }); var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(typeof(InheritedSingleAttributeTagHelper).FullName); + var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.InheritedSingleAttributeTagHelper"); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -1184,22 +1164,21 @@ public void CreateDescriptor_BuildsDescriptorsWithInheritedProperties() public void CreateDescriptor_BuildsDescriptorsWithConventionNames() { // Arrange - var intProperty = typeof(SingleAttributeTagHelper).GetProperty(nameof(SingleAttributeTagHelper.IntAttribute)); var expectedDescriptor = CreateTagHelperDescriptor( "single-attribute", - typeof(SingleAttributeTagHelper).FullName, + "TestNamespace.SingleAttributeTagHelper", AssemblyName, - typeNamespace: typeof(SingleAttributeTagHelper).FullName.Substring(0, typeof(SingleAttributeTagHelper).FullName.Length - nameof(SingleAttributeTagHelper).Length -1), - typeNameIdentifier: nameof(SingleAttributeTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "SingleAttributeTagHelper", new Action[] { builder => builder .Name("int-attribute") - .Metadata(PropertyName(intProperty.Name)) - .TypeName(intProperty.PropertyType.FullName) + .Metadata(PropertyName("IntAttribute")) + .TypeName(typeof(int).FullName) }); var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(typeof(SingleAttributeTagHelper).FullName); + var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.SingleAttributeTagHelper"); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -1212,23 +1191,21 @@ public void CreateDescriptor_BuildsDescriptorsWithConventionNames() public void CreateDescriptor_OnlyAcceptsPropertiesWithGetAndSet() { // Arrange - var validProperty = typeof(MissingAccessorTagHelper).GetProperty( - nameof(MissingAccessorTagHelper.ValidAttribute)); var expectedDescriptor = CreateTagHelperDescriptor( "missing-accessor", - typeof(MissingAccessorTagHelper).FullName, + "TestNamespace.MissingAccessorTagHelper", AssemblyName, - typeNamespace: typeof(MissingAccessorTagHelper).FullName.Substring(0, typeof(MissingAccessorTagHelper).FullName.Length - nameof(MissingAccessorTagHelper).Length -1), - typeNameIdentifier: nameof(MissingAccessorTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "MissingAccessorTagHelper", new Action[] { builder => builder .Name("valid-attribute") - .Metadata(PropertyName(validProperty.Name)) - .TypeName(validProperty.PropertyType.FullName) + .Metadata(PropertyName("ValidAttribute")) + .TypeName(typeof(string).FullName) }); var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(typeof(MissingAccessorTagHelper).FullName); + var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.MissingAccessorTagHelper"); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -1241,23 +1218,21 @@ public void CreateDescriptor_OnlyAcceptsPropertiesWithGetAndSet() public void CreateDescriptor_OnlyAcceptsPropertiesWithPublicGetAndSet() { // Arrange - var validProperty = typeof(NonPublicAccessorTagHelper).GetProperty( - nameof(NonPublicAccessorTagHelper.ValidAttribute)); var expectedDescriptor = CreateTagHelperDescriptor( "non-public-accessor", - typeof(NonPublicAccessorTagHelper).FullName, + "TestNamespace.NonPublicAccessorTagHelper", AssemblyName, - typeNamespace: typeof(NonPublicAccessorTagHelper).FullName.Substring(0, typeof(NonPublicAccessorTagHelper).FullName.Length - nameof(NonPublicAccessorTagHelper).Length -1), - typeNameIdentifier: nameof(NonPublicAccessorTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "NonPublicAccessorTagHelper", new Action[] { builder => builder .Name("valid-attribute") - .Metadata(PropertyName(validProperty.Name)) - .TypeName(validProperty.PropertyType.FullName) + .Metadata(PropertyName("ValidAttribute")) + .TypeName(typeof(string).FullName) }); var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(typeof(NonPublicAccessorTagHelper).FullName); + var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.NonPublicAccessorTagHelper"); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -1272,19 +1247,19 @@ public void CreateDescriptor_DoesNotIncludePropertiesWithNotBound() // Arrange var expectedDescriptor = CreateTagHelperDescriptor( "not-bound-attribute", - typeof(NotBoundAttributeTagHelper).FullName, + "TestNamespace.NotBoundAttributeTagHelper", AssemblyName, - typeNamespace: typeof(NotBoundAttributeTagHelper).FullName.Substring(0, typeof(NotBoundAttributeTagHelper).FullName.Length - nameof(NotBoundAttributeTagHelper).Length -1), - typeNameIdentifier: nameof(NotBoundAttributeTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "NotBoundAttributeTagHelper", new Action[] { builder => builder .Name("bound-property") - .Metadata(PropertyName(nameof(NotBoundAttributeTagHelper.BoundProperty))) + .Metadata(PropertyName("BoundProperty")) .TypeName(typeof(object).FullName) }); var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(typeof(NotBoundAttributeTagHelper).FullName); + var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.NotBoundAttributeTagHelper"); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -1300,15 +1275,15 @@ public void CreateDescriptor_ResolvesMultipleTagHelperDescriptorsFromSingleType( var expectedDescriptor = CreateTagHelperDescriptor( string.Empty, - typeof(MultiTagTagHelper).FullName, + "TestNamespace.MultiTagTagHelper", AssemblyName, - typeNamespace: typeof(MultiTagTagHelper).FullName.Substring(0, typeof(MultiTagTagHelper).FullName.Length - nameof(MultiTagTagHelper).Length -1), - typeNameIdentifier: nameof(MultiTagTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "MultiTagTagHelper", new Action[] { builder => builder .Name("valid-attribute") - .Metadata(PropertyName(nameof(MultiTagTagHelper.ValidAttribute))) + .Metadata(PropertyName("ValidAttribute")) .TypeName(typeof(string).FullName), }, new Action[] @@ -1317,7 +1292,7 @@ public void CreateDescriptor_ResolvesMultipleTagHelperDescriptorsFromSingleType( builder => builder.RequireTagName("div"), }); var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(typeof(MultiTagTagHelper).FullName); + var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.MultiTagTagHelper"); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -1330,22 +1305,21 @@ public void CreateDescriptor_ResolvesMultipleTagHelperDescriptorsFromSingleType( public void CreateDescriptor_DoesNotResolveInheritedTagNames() { // Arrange - var validProp = typeof(InheritedMultiTagTagHelper).GetProperty(nameof(InheritedMultiTagTagHelper.ValidAttribute)); var expectedDescriptor = CreateTagHelperDescriptor( "inherited-multi-tag", - typeof(InheritedMultiTagTagHelper).FullName, + "TestNamespace.InheritedMultiTagTagHelper", AssemblyName, - typeNamespace: typeof(InheritedMultiTagTagHelper).FullName.Substring(0, typeof(InheritedMultiTagTagHelper).FullName.Length - nameof(InheritedMultiTagTagHelper).Length -1), - typeNameIdentifier: nameof(InheritedMultiTagTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "InheritedMultiTagTagHelper", new Action[] { builder => builder .Name("valid-attribute") - .Metadata(PropertyName(validProp.Name)) - .TypeName(validProp.PropertyType.FullName), + .Metadata(PropertyName("ValidAttribute")) + .TypeName(typeof(string).FullName), }); var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(typeof(InheritedMultiTagTagHelper).FullName); + var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.InheritedMultiTagTagHelper"); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -1360,10 +1334,10 @@ public void CreateDescriptor_IgnoresDuplicateTagNamesFromAttribute() // Arrange var expectedDescriptor = CreateTagHelperDescriptor( string.Empty, - typeof(DuplicateTagNameTagHelper).FullName, + "TestNamespace.DuplicateTagNameTagHelper", AssemblyName, - typeNamespace: typeof(DuplicateTagNameTagHelper).FullName.Substring(0, typeof(DuplicateTagNameTagHelper).FullName.Length - nameof(DuplicateTagNameTagHelper).Length -1), - typeNameIdentifier: nameof(DuplicateTagNameTagHelper), + typeNamespace: "TestNamespace", + typeNameIdentifier: "DuplicateTagNameTagHelper", ruleBuilders: new Action[] { builder => builder.RequireTagName("p"), @@ -1371,7 +1345,7 @@ public void CreateDescriptor_IgnoresDuplicateTagNamesFromAttribute() }); var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(typeof(DuplicateTagNameTagHelper).FullName); + var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.DuplicateTagNameTagHelper"); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -1387,12 +1361,12 @@ public void CreateDescriptor_OverridesTagNameFromAttribute() var expectedDescriptor = CreateTagHelperDescriptor( "data-condition", - typeof(OverrideNameTagHelper).FullName, + "TestNamespace.OverrideNameTagHelper", AssemblyName, - typeNamespace: typeof(OverrideNameTagHelper).FullName.Substring(0, typeof(OverrideNameTagHelper).FullName.Length - nameof(OverrideNameTagHelper).Length -1), - typeNameIdentifier: nameof(OverrideNameTagHelper)); + typeNamespace: "TestNamespace", + typeNameIdentifier: "OverrideNameTagHelper"); var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(typeof(OverrideNameTagHelper).FullName); + var typeSymbol = Compilation.GetTypeByMetadataName("TestNamespace.OverrideNameTagHelper"); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -1425,13 +1399,13 @@ public void CreateDescriptor_CreatesErrorOnInvalidNames( // Arrange name = name.Replace("\n", "\\n").Replace("\r", "\\r").Replace("\"", "\\\""); var text = $$""" - [{{typeof(AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute).FullName}}("{{name}}")] - public class DynamicTestTagHelper : {{typeof(AspNetCore.Razor.TagHelpers.TagHelper).FullName}} + [Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("{{name}}")] + public class DynamicTestTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper { } """; - var syntaxTree = CSharpSyntaxTree.ParseText(text); - var compilation = TestCompilation.Create(_assembly, syntaxTree); + var syntaxTree = Parse(text); + var compilation = Compilation.AddSyntaxTrees(syntaxTree); var tagHelperType = compilation.GetTypeByMetadataName("DynamicTestTagHelper"); var attribute = tagHelperType.GetAttributes().Single(); var factory = new DefaultTagHelperDescriptorFactory(compilation, includeDocumentation: false, excludeHidden: false); @@ -1477,80 +1451,80 @@ public static TheoryData InvalidTagHelperAttributeDescriptorData { get { - var invalidBoundAttributeBuilder = new TagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, nameof(InvalidBoundAttribute), "Test"); - invalidBoundAttributeBuilder.Metadata(TypeName(typeof(InvalidBoundAttribute).FullName)); + var invalidBoundAttributeBuilder = new TagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, "InvalidBoundAttribute", "Test"); + invalidBoundAttributeBuilder.Metadata(TypeName("TestNamespace.InvalidBoundAttribute")); // type, expectedAttributeDescriptors - return new TheoryData> + return new TheoryData> { { - typeof(InvalidBoundAttribute), + "TestNamespace.InvalidBoundAttribute", new[] { - CreateAttributeFor(typeof(InvalidBoundAttribute), attribute => + CreateAttributeFor("TestNamespace.InvalidBoundAttribute", attribute => { attribute .Name("data-something") - .Metadata(PropertyName(nameof(InvalidBoundAttribute.DataSomething))) + .Metadata(PropertyName("DataSomething")) .TypeName(typeof(string).FullName); }), } }, { - typeof(InvalidBoundAttributeWithValid), + "TestNamespace.InvalidBoundAttributeWithValid", new[] { - CreateAttributeFor(typeof(InvalidBoundAttributeWithValid), attribute => + CreateAttributeFor("TestNamespace.InvalidBoundAttributeWithValid", attribute => { attribute .Name("data-something") - .Metadata(PropertyName(nameof(InvalidBoundAttributeWithValid.DataSomething))) + .Metadata(PropertyName("DataSomething")) .TypeName(typeof(string).FullName); ; }), - CreateAttributeFor(typeof(InvalidBoundAttributeWithValid), attribute => + CreateAttributeFor("TestNamespace.InvalidBoundAttributeWithValid", attribute => { attribute .Name("int-attribute") - .Metadata(PropertyName(nameof(InvalidBoundAttributeWithValid.IntAttribute))) + .Metadata(PropertyName("IntAttribute")) .TypeName(typeof(int).FullName); }), } }, { - typeof(OverriddenInvalidBoundAttributeWithValid), + "TestNamespace.OverriddenInvalidBoundAttributeWithValid", new[] { - CreateAttributeFor(typeof(OverriddenInvalidBoundAttributeWithValid), attribute => + CreateAttributeFor("TestNamespace.OverriddenInvalidBoundAttributeWithValid", attribute => { attribute .Name("valid-something") - .Metadata(PropertyName(nameof(OverriddenInvalidBoundAttributeWithValid.DataSomething))) + .Metadata(PropertyName("DataSomething")) .TypeName(typeof(string).FullName); }), } }, { - typeof(OverriddenValidBoundAttributeWithInvalid), + "TestNamespace.OverriddenValidBoundAttributeWithInvalid", new[] { - CreateAttributeFor(typeof(OverriddenValidBoundAttributeWithInvalid), attribute => + CreateAttributeFor("TestNamespace.OverriddenValidBoundAttributeWithInvalid", attribute => { attribute .Name("data-something") - .Metadata(PropertyName(nameof(OverriddenValidBoundAttributeWithInvalid.ValidSomething))) + .Metadata(PropertyName("ValidSomething")) .TypeName(typeof(string).FullName); }), } }, { - typeof(OverriddenValidBoundAttributeWithInvalidUpperCase), + "TestNamespace.OverriddenValidBoundAttributeWithInvalidUpperCase", new[] { - CreateAttributeFor(typeof(OverriddenValidBoundAttributeWithInvalidUpperCase), attribute => + CreateAttributeFor("TestNamespace.OverriddenValidBoundAttributeWithInvalidUpperCase", attribute => { attribute .Name("DATA-SOMETHING") - .Metadata(PropertyName(nameof(OverriddenValidBoundAttributeWithInvalidUpperCase.ValidSomething))) + .Metadata(PropertyName("ValidSomething")) .TypeName(typeof(string).FullName); }), } @@ -1562,12 +1536,12 @@ public static TheoryData InvalidTagHelperAttributeDescriptorData [Theory] [MemberData(nameof(InvalidTagHelperAttributeDescriptorData))] public void CreateDescriptor_DoesNotAllowDataDashAttributes( - Type type, + string typeFullName, IEnumerable expectedAttributeDescriptors) { // Arrange var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(type.FullName); + var typeSymbol = Compilation.GetTypeByMetadataName(typeFullName); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -1605,14 +1579,14 @@ public void CreateDescriptor_WithValidAttributeName_HasNoErrors(string name) { // Arrange var text = $$""" - public class DynamicTestTagHelper : {{typeof(AspNetCore.Razor.TagHelpers.TagHelper).FullName}} + public class DynamicTestTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper { - [{{typeof(AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute).FullName}}("{{name}}")] + [Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("{{name}}")] public string SomeAttribute { get; set; } } """; - var syntaxTree = CSharpSyntaxTree.ParseText(text); - var compilation = TestCompilation.Create(_assembly, syntaxTree); + var syntaxTree = Parse(text); + var compilation = Compilation.AddSyntaxTrees(syntaxTree); var tagHelperType = compilation.GetTypeByMetadataName("DynamicTestTagHelper"); var factory = new DefaultTagHelperDescriptorFactory(compilation, includeDocumentation: false, excludeHidden: false); @@ -1646,14 +1620,14 @@ public void CreateDescriptor_WithValidAttributePrefix_HasNoErrors(string prefix) { // Arrange var text = $$""" - public class DynamicTestTagHelper : {{typeof(AspNetCore.Razor.TagHelpers.TagHelper).FullName}} + public class DynamicTestTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper { - [{{typeof(AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute).FullName}}({{nameof(AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute.DictionaryAttributePrefix)}} = "{{prefix}}")] + [Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute(DictionaryAttributePrefix = "{{prefix}}")] public System.Collections.Generic.IDictionary SomeAttribute { get; set; } } """; - var syntaxTree = CSharpSyntaxTree.ParseText(text); - var compilation = TestCompilation.Create(_assembly, syntaxTree); + var syntaxTree = Parse(text); + var compilation = Compilation.AddSyntaxTrees(syntaxTree); var tagHelperType = compilation.GetTypeByMetadataName("DynamicTestTagHelper"); var factory = new DefaultTagHelperDescriptorFactory(compilation, includeDocumentation: false, excludeHidden: false); @@ -1690,14 +1664,14 @@ public void CreateDescriptor_WithInvalidAttributeName_HasErrors(string name, str // Arrange name = name.Replace("\n", "\\n").Replace("\r", "\\r").Replace("\"", "\\\""); var text = $$""" - public class DynamicTestTagHelper : {{typeof(AspNetCore.Razor.TagHelpers.TagHelper).FullName}} + public class DynamicTestTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper { - [{{typeof(AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute).FullName}}("{{name}}")] + [Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("{{name}}")] public string InvalidProperty { get; set; } } """; - var syntaxTree = CSharpSyntaxTree.ParseText(text); - var compilation = TestCompilation.Create(_assembly, syntaxTree); + var syntaxTree = Parse(text); + var compilation = Compilation.AddSyntaxTrees(syntaxTree); var tagHelperType = compilation.GetTypeByMetadataName("DynamicTestTagHelper"); var factory = new DefaultTagHelperDescriptorFactory(compilation, includeDocumentation: false, excludeHidden: false); @@ -1737,14 +1711,14 @@ public void CreateDescriptor_WithInvalidAttributePrefix_HasErrors(string prefix, // Arrange prefix = prefix.Replace("\n", "\\n").Replace("\r", "\\r").Replace("\"", "\\\""); var text = $$""" - public class DynamicTestTagHelper : {{typeof(AspNetCore.Razor.TagHelpers.TagHelper).FullName}} + public class DynamicTestTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper { - [{{typeof(AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute).FullName}}({{nameof(AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute.DictionaryAttributePrefix)}} = "{{prefix}}")] + [Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute(DictionaryAttributePrefix = "{{prefix}}")] public System.Collections.Generic.IDictionary InvalidProperty { get; set; } } """; - var syntaxTree = CSharpSyntaxTree.ParseText(text); - var compilation = TestCompilation.Create(_assembly, syntaxTree); + var syntaxTree = Parse(text); + var compilation = Compilation.AddSyntaxTrees(syntaxTree); var tagHelperType = compilation.GetTypeByMetadataName("DynamicTestTagHelper"); var factory = new DefaultTagHelperDescriptorFactory(compilation, includeDocumentation: false, excludeHidden: false); @@ -1781,13 +1755,13 @@ public void CreateDescriptor_WithInvalidAllowedChildren_HasErrors(string name, s // Arrange name = name.Replace("\n", "\\n").Replace("\r", "\\r").Replace("\"", "\\\""); var text = $$""" - [{{typeof(AspNetCore.Razor.TagHelpers.RestrictChildrenAttribute).FullName}}("{{name}}")] - public class DynamicTestTagHelper : {{typeof(AspNetCore.Razor.TagHelpers.TagHelper).FullName}} + [Microsoft.AspNetCore.Razor.TagHelpers.RestrictChildrenAttribute("{{name}}")] + public class DynamicTestTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper { } """; - var syntaxTree = CSharpSyntaxTree.ParseText(text); - var compilation = TestCompilation.Create(_assembly, syntaxTree); + var syntaxTree = Parse(text); + var compilation = Compilation.AddSyntaxTrees(syntaxTree); var tagHelperType = compilation.GetTypeByMetadataName("DynamicTestTagHelper"); var factory = new DefaultTagHelperDescriptorFactory(compilation, includeDocumentation: false, excludeHidden: false); @@ -1823,13 +1797,13 @@ public void CreateDescriptor_WithInvalidParentTag_HasErrors(string name, string[ // Arrange name = name.Replace("\n", "\\n").Replace("\r", "\\r").Replace("\"", "\\\""); var text = $$""" - [{{typeof(AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute).FullName}}({{nameof(AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute.ParentTag)}} = "{{name}}")] - public class DynamicTestTagHelper : {{typeof(AspNetCore.Razor.TagHelpers.TagHelper).FullName}} + [Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute(ParentTag = "{{name}}")] + public class DynamicTestTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper { } """; - var syntaxTree = CSharpSyntaxTree.ParseText(text); - var compilation = TestCompilation.Create(_assembly, syntaxTree); + var syntaxTree = Parse(text); + var compilation = Compilation.AddSyntaxTrees(syntaxTree); var tagHelperType = compilation.GetTypeByMetadataName("DynamicTestTagHelper"); var factory = new DefaultTagHelperDescriptorFactory(compilation, includeDocumentation: false, excludeHidden: false); @@ -1870,17 +1844,17 @@ public static TheoryData TagHelperWithPrefixData dictionaryNamespace = dictionaryNamespace.Substring(0, dictionaryNamespace.IndexOf('`')); // tagHelperType, expectedAttributeDescriptors, expectedDiagnostics - return new TheoryData, IEnumerable> + return new TheoryData, IEnumerable> { { - typeof(DefaultValidHtmlAttributePrefix), + "TestNamespace.DefaultValidHtmlAttributePrefix", new[] { - CreateAttributeFor(typeof(DefaultValidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.DefaultValidHtmlAttributePrefix", attribute => { attribute .Name("dictionary-property") - .Metadata(PropertyName(nameof(DefaultValidHtmlAttributePrefix.DictionaryProperty))) + .Metadata(PropertyName("DictionaryProperty")) .TypeName($"{dictionaryNamespace}") .AsDictionaryAttribute("dictionary-property-", typeof(string).FullName); }), @@ -1888,14 +1862,14 @@ public static TheoryData TagHelperWithPrefixData Enumerable.Empty() }, { - typeof(SingleValidHtmlAttributePrefix), + "TestNamespace.SingleValidHtmlAttributePrefix", new[] { - CreateAttributeFor(typeof(SingleValidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.SingleValidHtmlAttributePrefix", attribute => { attribute .Name("valid-name") - .Metadata(PropertyName(nameof(SingleValidHtmlAttributePrefix.DictionaryProperty))) + .Metadata(PropertyName("DictionaryProperty")) .TypeName($"{dictionaryNamespace}") .AsDictionaryAttribute("valid-name-", typeof(string).FullName); }), @@ -1903,67 +1877,67 @@ public static TheoryData TagHelperWithPrefixData Enumerable.Empty() }, { - typeof(MultipleValidHtmlAttributePrefix), + "TestNamespace.MultipleValidHtmlAttributePrefix", new[] { - CreateAttributeFor(typeof(MultipleValidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.MultipleValidHtmlAttributePrefix", attribute => { attribute .Name("valid-name1") - .Metadata(PropertyName(nameof(MultipleValidHtmlAttributePrefix.DictionaryProperty))) + .Metadata(PropertyName("DictionaryProperty")) .TypeName($"{typeof(Dictionary<,>).Namespace}.Dictionary") .AsDictionaryAttribute("valid-prefix1-", typeof(object).FullName); }), - CreateAttributeFor(typeof(MultipleValidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.MultipleValidHtmlAttributePrefix", attribute => { attribute .Name("valid-name2") - .Metadata(PropertyName(nameof(MultipleValidHtmlAttributePrefix.DictionarySubclassProperty))) - .TypeName(typeof(DictionarySubclass).FullName) + .Metadata(PropertyName("DictionarySubclassProperty")) + .TypeName("TestNamespace.DictionarySubclass") .AsDictionaryAttribute("valid-prefix2-", typeof(string).FullName); }), - CreateAttributeFor(typeof(MultipleValidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.MultipleValidHtmlAttributePrefix", attribute => { attribute .Name("valid-name3") - .Metadata(PropertyName(nameof(MultipleValidHtmlAttributePrefix.DictionaryWithoutParameterlessConstructorProperty))) - .TypeName(typeof(DictionaryWithoutParameterlessConstructor).FullName) + .Metadata(PropertyName("DictionaryWithoutParameterlessConstructorProperty")) + .TypeName("TestNamespace.DictionaryWithoutParameterlessConstructor") .AsDictionaryAttribute("valid-prefix3-", typeof(string).FullName); }), - CreateAttributeFor(typeof(MultipleValidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.MultipleValidHtmlAttributePrefix", attribute => { attribute .Name("valid-name4") - .Metadata(PropertyName(nameof(MultipleValidHtmlAttributePrefix.GenericDictionarySubclassProperty))) - .TypeName(typeof(GenericDictionarySubclass).Namespace + ".GenericDictionarySubclass") + .Metadata(PropertyName("GenericDictionarySubclassProperty")) + .TypeName("TestNamespace.GenericDictionarySubclass") .AsDictionaryAttribute("valid-prefix4-", typeof(object).FullName); }), - CreateAttributeFor(typeof(MultipleValidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.MultipleValidHtmlAttributePrefix", attribute => { attribute .Name("valid-name5") - .Metadata(PropertyName(nameof(MultipleValidHtmlAttributePrefix.SortedDictionaryProperty))) + .Metadata(PropertyName("SortedDictionaryProperty")) .TypeName(typeof(SortedDictionary).Namespace + ".SortedDictionary") .AsDictionaryAttribute("valid-prefix5-", typeof(int).FullName); }), - CreateAttributeFor(typeof(MultipleValidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.MultipleValidHtmlAttributePrefix", attribute => { attribute .Name("valid-name6") - .Metadata(PropertyName(nameof(MultipleValidHtmlAttributePrefix.StringProperty))) + .Metadata(PropertyName("StringProperty")) .TypeName(typeof(string).FullName); }), - CreateAttributeFor(typeof(MultipleValidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.MultipleValidHtmlAttributePrefix", attribute => { attribute - .Metadata(PropertyName(nameof(MultipleValidHtmlAttributePrefix.GetOnlyDictionaryProperty))) + .Metadata(PropertyName("GetOnlyDictionaryProperty")) .TypeName($"{dictionaryNamespace}") .AsDictionaryAttribute("get-only-dictionary-property-", typeof(int).FullName); }), - CreateAttributeFor(typeof(MultipleValidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.MultipleValidHtmlAttributePrefix", attribute => { attribute - .Metadata(PropertyName(nameof(MultipleValidHtmlAttributePrefix.GetOnlyDictionaryPropertyWithAttributePrefix))) + .Metadata(PropertyName("GetOnlyDictionaryPropertyWithAttributePrefix")) .TypeName($"{dictionaryNamespace}") .AsDictionaryAttribute("valid-prefix6", typeof(string).FullName); }), @@ -1971,127 +1945,127 @@ public static TheoryData TagHelperWithPrefixData Enumerable.Empty() }, { - typeof(SingleInvalidHtmlAttributePrefix), + "TestNamespace.SingleInvalidHtmlAttributePrefix", new[] { - CreateAttributeFor(typeof(SingleInvalidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.SingleInvalidHtmlAttributePrefix", attribute => { attribute .Name("valid-name") - .Metadata(PropertyName(nameof(SingleInvalidHtmlAttributePrefix.StringProperty))) + .Metadata(PropertyName("StringProperty")) .TypeName(typeof(string).FullName) .AddDiagnostic(RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull( - typeof(SingleInvalidHtmlAttributePrefix).FullName, - nameof(SingleInvalidHtmlAttributePrefix.StringProperty))); + "TestNamespace.SingleInvalidHtmlAttributePrefix", + "StringProperty")); }), }, new[] { RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull( - typeof(SingleInvalidHtmlAttributePrefix).FullName, - nameof(SingleInvalidHtmlAttributePrefix.StringProperty)) + "TestNamespace.SingleInvalidHtmlAttributePrefix", + "StringProperty") } }, { - typeof(MultipleInvalidHtmlAttributePrefix), + "TestNamespace.MultipleInvalidHtmlAttributePrefix", new[] { - CreateAttributeFor(typeof(MultipleInvalidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.MultipleInvalidHtmlAttributePrefix", attribute => { attribute .Name("valid-name1") - .Metadata(PropertyName(nameof(MultipleInvalidHtmlAttributePrefix.LongProperty))) + .Metadata(PropertyName("LongProperty")) .TypeName(typeof(long).FullName); }), - CreateAttributeFor(typeof(MultipleInvalidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.MultipleInvalidHtmlAttributePrefix", attribute => { attribute .Name("valid-name2") - .Metadata(PropertyName(nameof(MultipleInvalidHtmlAttributePrefix.DictionaryOfIntProperty))) + .Metadata(PropertyName("DictionaryOfIntProperty")) .TypeName($"{typeof(Dictionary<,>).Namespace}.Dictionary") .AsDictionaryAttribute("valid-prefix2-", typeof(string).FullName) .AddDiagnostic( RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull( - typeof(MultipleInvalidHtmlAttributePrefix).FullName, - nameof(MultipleInvalidHtmlAttributePrefix.DictionaryOfIntProperty))); + "TestNamespace.MultipleInvalidHtmlAttributePrefix", + "DictionaryOfIntProperty")); }), - CreateAttributeFor(typeof(MultipleInvalidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.MultipleInvalidHtmlAttributePrefix", attribute => { attribute .Name("valid-name3") - .Metadata(PropertyName(nameof(MultipleInvalidHtmlAttributePrefix.ReadOnlyDictionaryProperty))) + .Metadata(PropertyName("ReadOnlyDictionaryProperty")) .TypeName($"{typeof(IReadOnlyDictionary<,>).Namespace}.IReadOnlyDictionary") .AddDiagnostic( RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull( - typeof(MultipleInvalidHtmlAttributePrefix).FullName, - nameof(MultipleInvalidHtmlAttributePrefix.ReadOnlyDictionaryProperty))); + "TestNamespace.MultipleInvalidHtmlAttributePrefix", + "ReadOnlyDictionaryProperty")); }), - CreateAttributeFor(typeof(MultipleInvalidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.MultipleInvalidHtmlAttributePrefix", attribute => { attribute .Name("valid-name4") - .Metadata(PropertyName(nameof(MultipleInvalidHtmlAttributePrefix.IntProperty))) + .Metadata(PropertyName("IntProperty")) .TypeName(typeof(int).FullName) .AddDiagnostic( RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull( - typeof(MultipleInvalidHtmlAttributePrefix).FullName, - nameof(MultipleInvalidHtmlAttributePrefix.IntProperty))); + "TestNamespace.MultipleInvalidHtmlAttributePrefix", + "IntProperty")); }), - CreateAttributeFor(typeof(MultipleInvalidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.MultipleInvalidHtmlAttributePrefix", attribute => { attribute .Name("valid-name5") - .Metadata(PropertyName(nameof(MultipleInvalidHtmlAttributePrefix.DictionaryOfIntSubclassProperty))) - .TypeName(typeof(DictionaryOfIntSubclass).FullName) + .Metadata(PropertyName("DictionaryOfIntSubclassProperty")) + .TypeName("TestNamespace.DictionaryOfIntSubclass") .AsDictionaryAttribute("valid-prefix5-", typeof(string).FullName) .AddDiagnostic( RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull( - typeof(MultipleInvalidHtmlAttributePrefix).FullName, - nameof(MultipleInvalidHtmlAttributePrefix.DictionaryOfIntSubclassProperty))); + "TestNamespace.MultipleInvalidHtmlAttributePrefix", + "DictionaryOfIntSubclassProperty")); }), - CreateAttributeFor(typeof(MultipleInvalidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.MultipleInvalidHtmlAttributePrefix", attribute => { attribute - .Metadata(PropertyName(nameof(MultipleInvalidHtmlAttributePrefix.GetOnlyDictionaryAttributePrefix))) + .Metadata(PropertyName("GetOnlyDictionaryAttributePrefix")) .TypeName($"{dictionaryNamespace}") .AsDictionaryAttribute("valid-prefix6", typeof(string).FullName) .AddDiagnostic( RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull( - typeof(MultipleInvalidHtmlAttributePrefix).FullName, - nameof(MultipleInvalidHtmlAttributePrefix.GetOnlyDictionaryAttributePrefix))); + "TestNamespace.MultipleInvalidHtmlAttributePrefix", + "GetOnlyDictionaryAttributePrefix")); }), - CreateAttributeFor(typeof(MultipleInvalidHtmlAttributePrefix), attribute => + CreateAttributeFor("TestNamespace.MultipleInvalidHtmlAttributePrefix", attribute => { attribute - .Metadata(PropertyName(nameof(MultipleInvalidHtmlAttributePrefix.GetOnlyDictionaryPropertyWithAttributeName))) + .Metadata(PropertyName("GetOnlyDictionaryPropertyWithAttributeName")) .TypeName($"{dictionaryNamespace}") .AsDictionaryAttribute("invalid-name7-", typeof(object).FullName) .AddDiagnostic( RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNull( - typeof(MultipleInvalidHtmlAttributePrefix).FullName, - nameof(MultipleInvalidHtmlAttributePrefix.GetOnlyDictionaryPropertyWithAttributeName))); + "TestNamespace.MultipleInvalidHtmlAttributePrefix", + "GetOnlyDictionaryPropertyWithAttributeName")); }), }, new[] { RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull( - typeof(MultipleInvalidHtmlAttributePrefix).FullName, - nameof(MultipleInvalidHtmlAttributePrefix.DictionaryOfIntProperty)), + "TestNamespace.MultipleInvalidHtmlAttributePrefix", + "DictionaryOfIntProperty"), RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull( - typeof(MultipleInvalidHtmlAttributePrefix).FullName, - nameof(MultipleInvalidHtmlAttributePrefix.ReadOnlyDictionaryProperty)), + "TestNamespace.MultipleInvalidHtmlAttributePrefix", + "ReadOnlyDictionaryProperty"), RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull( - typeof(MultipleInvalidHtmlAttributePrefix).FullName, - nameof(MultipleInvalidHtmlAttributePrefix.IntProperty)), + "TestNamespace.MultipleInvalidHtmlAttributePrefix", + "IntProperty"), RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull( - typeof(MultipleInvalidHtmlAttributePrefix).FullName, - nameof(MultipleInvalidHtmlAttributePrefix.DictionaryOfIntSubclassProperty)), + "TestNamespace.MultipleInvalidHtmlAttributePrefix", + "DictionaryOfIntSubclassProperty"), RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNotNull( - typeof(MultipleInvalidHtmlAttributePrefix).FullName, - nameof(MultipleInvalidHtmlAttributePrefix.GetOnlyDictionaryAttributePrefix)), + "TestNamespace.MultipleInvalidHtmlAttributePrefix", + "GetOnlyDictionaryAttributePrefix"), RazorDiagnosticFactory.CreateTagHelper_InvalidAttributePrefixNull( - typeof(MultipleInvalidHtmlAttributePrefix).FullName, - nameof(MultipleInvalidHtmlAttributePrefix.GetOnlyDictionaryPropertyWithAttributeName)), + "TestNamespace.MultipleInvalidHtmlAttributePrefix", + "GetOnlyDictionaryPropertyWithAttributeName"), } }, }; @@ -2101,13 +2075,13 @@ public static TheoryData TagHelperWithPrefixData [Theory] [MemberData(nameof(TagHelperWithPrefixData))] public void CreateDescriptor_WithPrefixes_ReturnsExpectedAttributeDescriptors( - Type tagHelperType, + string tagHelperTypeFullName, IEnumerable expectedAttributeDescriptors, IEnumerable expectedDiagnostics) { // Arrange var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeFullName); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -2122,36 +2096,36 @@ public static TheoryData TagOutputHintData get { // tagHelperType, expectedDescriptor - return new TheoryData + return new TheoryData { { - typeof(MultipleDescriptorTagHelperWithOutputElementHint), - TagHelperDescriptorBuilder.Create(typeof(MultipleDescriptorTagHelperWithOutputElementHint).FullName, AssemblyName) - .Metadata(GetMetadata()) + "TestNamespace.MultipleDescriptorTagHelperWithOutputElementHint", + TagHelperDescriptorBuilder.Create("TestNamespace.MultipleDescriptorTagHelperWithOutputElementHint", AssemblyName) + .Metadata(GetMetadata("TestNamespace", "MultipleDescriptorTagHelperWithOutputElementHint")) .TagMatchingRuleDescriptor(builder => builder.RequireTagName("a")) .TagMatchingRuleDescriptor(builder => builder.RequireTagName("p")) .TagOutputHint("div") .Build() }, { - typeof(InheritedOutputElementHintTagHelper), - TagHelperDescriptorBuilder.Create(typeof(InheritedOutputElementHintTagHelper).FullName, AssemblyName) - .Metadata(GetMetadata()) + "TestNamespace2.InheritedOutputElementHintTagHelper", + TagHelperDescriptorBuilder.Create("TestNamespace2.InheritedOutputElementHintTagHelper", AssemblyName) + .Metadata(GetMetadata("TestNamespace2", "InheritedOutputElementHintTagHelper")) .TagMatchingRuleDescriptor(builder => builder.RequireTagName("inherited-output-element-hint")) .Build() }, { - typeof(OutputElementHintTagHelper), - TagHelperDescriptorBuilder.Create(typeof(OutputElementHintTagHelper).FullName, AssemblyName) - .Metadata(GetMetadata()) + "TestNamespace2.OutputElementHintTagHelper", + TagHelperDescriptorBuilder.Create("TestNamespace2.OutputElementHintTagHelper", AssemblyName) + .Metadata(GetMetadata("TestNamespace2", "OutputElementHintTagHelper")) .TagMatchingRuleDescriptor(builder => builder.RequireTagName("output-element-hint")) .TagOutputHint("hinted-value") .Build() }, { - typeof(OverriddenOutputElementHintTagHelper), - TagHelperDescriptorBuilder.Create(typeof(OverriddenOutputElementHintTagHelper).FullName, AssemblyName) - .Metadata(GetMetadata()) + "TestNamespace2.OverriddenOutputElementHintTagHelper", + TagHelperDescriptorBuilder.Create("TestNamespace2.OverriddenOutputElementHintTagHelper", AssemblyName) + .Metadata(GetMetadata("TestNamespace2", "OverriddenOutputElementHintTagHelper")) .TagMatchingRuleDescriptor(builder => builder.RequireTagName("overridden-output-element-hint")) .TagOutputHint("overridden") .Build() @@ -2163,12 +2137,12 @@ public static TheoryData TagOutputHintData [Theory] [MemberData(nameof(TagOutputHintData))] public void CreateDescriptor_CreatesDescriptorsWithOutputElementHint( - Type tagHelperType, + string tagHelperTypeFullName, TagHelperDescriptor expectedDescriptor) { // Arrange var factory = new DefaultTagHelperDescriptorFactory(Compilation, includeDocumentation: false, excludeHidden: false); - var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperType.FullName); + var typeSymbol = Compilation.GetTypeByMetadataName(tagHelperTypeFullName); // Act var descriptor = factory.CreateDescriptor(typeSymbol); @@ -2181,7 +2155,7 @@ public void CreateDescriptor_CreatesDescriptorsWithOutputElementHint( public void CreateDescriptor_CapturesDocumentationOnTagHelperClass() { // Arrange - var syntaxTree = CSharpSyntaxTree.ParseText(@" + var syntaxTree = Parse(@" using Microsoft.AspNetCore.Razor.TagHelpers; /// @@ -2190,10 +2164,10 @@ public void CreateDescriptor_CapturesDocumentationOnTagHelperClass() /// /// Inherits from . /// - public class DocumentedTagHelper : " + typeof(AspNetCore.Razor.TagHelpers.TagHelper).Name + @" + public class DocumentedTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper { }"); - var compilation = TestCompilation.Create(_assembly, syntaxTree); + var compilation = Compilation.AddSyntaxTrees(syntaxTree); var factory = new DefaultTagHelperDescriptorFactory(compilation, includeDocumentation: true, excludeHidden: false); var typeSymbol = compilation.GetTypeByMetadataName("DocumentedTagHelper"); var expectedDocumentation = @@ -2218,10 +2192,10 @@ public class DocumentedTagHelper : " + typeof(AspNetCore.Razor.TagHelpers.TagHel public void CreateDescriptor_CapturesDocumentationOnTagHelperProperties() { // Arrange - var syntaxTree = CSharpSyntaxTree.ParseText(@" + var syntaxTree = Parse(@" using System.Collections.Generic; - public class DocumentedTagHelper : " + typeof(AspNetCore.Razor.TagHelpers.TagHelper).FullName + @" + public class DocumentedTagHelper : Microsoft.spNetCore.Razor.TagHelpers.TagHelper { /// /// This is of type . @@ -2241,7 +2215,7 @@ public class DocumentedTagHelper : " + typeof(AspNetCore.Razor.TagHelpers.TagHel /// public List RemarksAndSummaryProperty { get; set; } }"); - var compilation = TestCompilation.Create(_assembly, syntaxTree); + var compilation = Compilation.AddSyntaxTrees(syntaxTree); var factory = new DefaultTagHelperDescriptorFactory(compilation, includeDocumentation: true, excludeHidden: false); var typeSymbol = compilation.GetTypeByMetadataName("DocumentedTagHelper"); var expectedDocumentations = new[] @@ -2453,27 +2427,33 @@ protected static TagHelperDescriptor CreateTagHelperDescriptor( return descriptor; } - private static BoundAttributeDescriptor CreateAttributeFor(Type tagHelperType, Action configure) + private static BoundAttributeDescriptor CreateAttributeFor(string tagHelperTypeFullName, Action configure) { - var tagHelperBuilder = new TagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, tagHelperType.Name, "Test"); - tagHelperBuilder.Metadata(TypeName(tagHelperType.FullName)); + var tagHelperBuilder = new TagHelperDescriptorBuilder(TagHelperConventions.DefaultKind, tagHelperTypeFullName.Split('.')[^1], "Test"); + tagHelperBuilder.Metadata(TypeName(tagHelperTypeFullName)); var attributeBuilder = new BoundAttributeDescriptorBuilder(tagHelperBuilder, TagHelperConventions.DefaultKind); configure(attributeBuilder); return attributeBuilder.Build(); } -} -[AspNetCore.Razor.TagHelpers.OutputElementHint("hinted-value")] -public class OutputElementHintTagHelper : AspNetCore.Razor.TagHelpers.TagHelper -{ -} + private const string AdditionalCode = + """ + namespace TestNamespace2 + { + [Microsoft.AspNetCore.Razor.TagHelpers.OutputElementHint("hinted-value")] + public class OutputElementHintTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper + { + } -public class InheritedOutputElementHintTagHelper : OutputElementHintTagHelper -{ -} + public class InheritedOutputElementHintTagHelper : OutputElementHintTagHelper + { + } -[AspNetCore.Razor.TagHelpers.OutputElementHint("overridden")] -public class OverriddenOutputElementHintTagHelper : OutputElementHintTagHelper -{ + [Microsoft.AspNetCore.Razor.TagHelpers.OutputElementHint("overridden")] + public class OverriddenOutputElementHintTagHelper : OutputElementHintTagHelper + { + } + } + """; } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/DefaultTagHelperDescriptorProviderTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/DefaultTagHelperDescriptorProviderTest.cs index f5d46a59aec..c85e90979b3 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/DefaultTagHelperDescriptorProviderTest.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/DefaultTagHelperDescriptorProviderTest.cs @@ -4,23 +4,19 @@ #nullable disable using System.Linq; -using System.Reflection; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis.CSharp; using Xunit; namespace Microsoft.CodeAnalysis.Razor; -public class DefaultTagHelperDescriptorProviderTest +public class DefaultTagHelperDescriptorProviderTest : TagHelperDescriptorProviderTestBase { - private static readonly Assembly _assembly = typeof(DefaultTagHelperDescriptorProviderTest).GetTypeInfo().Assembly; - [Fact] public void Execute_DoesNotAddEditorBrowsableNeverDescriptorsAtDesignTime() { // Arrange - var editorBrowsableTypeName = "Microsoft.CodeAnalysis.Razor.Workspaces.Test.EditorBrowsableTagHelper"; - var compilation = TestCompilation.Create(_assembly); + var editorBrowsableTypeName = "TestNamespace.EditorBrowsableTagHelper"; + var compilation = BaseCompilation; var descriptorProvider = new DefaultTagHelperDescriptorProvider(); var context = new TagHelperDescriptorProviderContext(compilation) @@ -44,7 +40,7 @@ public void Execute_WithDefaultDiscoversTagHelpersFromAssemblyAndReference() { // Arrange var testTagHelper = "TestAssembly.TestTagHelper"; - var enumTagHelper = "Microsoft.CodeAnalysis.Razor.Workspaces.Test.EnumTagHelper"; + var enumTagHelper = "TestNamespace.EnumTagHelper"; var csharp = @" using Microsoft.AspNetCore.Razor.TagHelpers; namespace TestAssembly @@ -54,7 +50,7 @@ public class TestTagHelper : TagHelper public override void Process(TagHelperContext context, TagHelperOutput output) {} } }"; - var compilation = TestCompilation.Create(_assembly, CSharpSyntaxTree.ParseText(csharp)); + var compilation = BaseCompilation.AddSyntaxTrees(Parse(csharp)); var descriptorProvider = new DefaultTagHelperDescriptorProvider(); var context = new TagHelperDescriptorProviderContext(compilation); @@ -74,7 +70,7 @@ public void Execute_WithTargetAssembly_Works() { // Arrange var testTagHelper = "TestAssembly.TestTagHelper"; - var enumTagHelper = "Microsoft.CodeAnalysis.Razor.Workspaces.Test.EnumTagHelper"; + var enumTagHelper = "TestNamespace.EnumTagHelper"; var csharp = @" using Microsoft.AspNetCore.Razor.TagHelpers; namespace TestAssembly @@ -84,11 +80,11 @@ public class TestTagHelper : TagHelper public override void Process(TagHelperContext context, TagHelperOutput output) {} } }"; - var compilation = TestCompilation.Create(_assembly, CSharpSyntaxTree.ParseText(csharp)); + var compilation = BaseCompilation.AddSyntaxTrees(Parse(csharp)); var descriptorProvider = new DefaultTagHelperDescriptorProvider(); var targetSymbol = (IAssemblySymbol)compilation.GetAssemblyOrModuleSymbol( - compilation.References.First(static r => r.Display.Contains("Microsoft.CodeAnalysis.Razor.Test.dll"))); + compilation.References.First(static r => r.Display.Contains("Microsoft.CodeAnalysis.Razor.Test"))); var context = new TagHelperDescriptorProviderContext(compilation, targetSymbol); diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/GenericTypeNameRewriterTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/GenericTypeNameRewriterTest.cs index 7cf9d14cc74..ded5dfa74ed 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/GenericTypeNameRewriterTest.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/GenericTypeNameRewriterTest.cs @@ -14,8 +14,8 @@ public class GenericTypeNameRewriterTest [Theory] [InlineData("TItem2", "Type2")] - // Unspecified argument -> System.Object - [InlineData("TItem3", "System.Object")] + // Unspecified argument -> object + [InlineData("TItem3", "object")] // Not a type parameter [InlineData("TItem4", "TItem4")] @@ -24,8 +24,8 @@ public class GenericTypeNameRewriterTest [InlineData("TItem1.TItem2", "TItem1.TItem2")] // Type parameters can't have type parameters - [InlineData("TItem1.TItem2", "TItem1.TItem2")] - [InlineData("TItem2, System.TItem2, RenderFragment>", "TItem2, System.TItem2, RenderFragment>")] + [InlineData("TItem1.TItem2", "TItem1.TItem2")] + [InlineData("TItem2, System.TItem2, RenderFragment>", "TItem2, System.TItem2, RenderFragment>")] // Tuples [InlineData("List<(TItem1 X, TItem2 Y)>", "List<(Type1 X, Type2 Y)>")] diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/Microsoft.CodeAnalysis.Razor.Test.csproj b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/Microsoft.CodeAnalysis.Razor.Test.csproj index fede476e87b..1b3c0be1035 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/Microsoft.CodeAnalysis.Razor.Test.csproj +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/Microsoft.CodeAnalysis.Razor.Test.csproj @@ -16,13 +16,11 @@ - - - + + + - - diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/TagHelperDescriptorFactoryTagHelpers.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/TagHelperDescriptorFactoryTagHelpers.cs index 325e4d4bd94..347654bd950 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/TagHelperDescriptorFactoryTagHelpers.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/TagHelperDescriptorFactoryTagHelpers.cs @@ -3,462 +3,470 @@ #nullable disable +namespace Microsoft.CodeAnalysis.Razor; + +internal static class TagHelperDescriptorFactoryTagHelpers +{ + public const string Code = """ using System.Collections.Generic; using System.ComponentModel; using Microsoft.AspNetCore.Razor.TagHelpers; -namespace Microsoft.CodeAnalysis.Razor.Workspaces.Test; - -public enum CustomEnum +namespace TestNamespace { - FirstValue, - SecondValue -} - -public class EnumTagHelper : TagHelper -{ - public int NonEnumProperty { get; set; } + public enum CustomEnum + { + FirstValue, + SecondValue + } - public CustomEnum EnumProperty { get; set; } -} + public class EnumTagHelper : TagHelper + { + public int NonEnumProperty { get; set; } -[HtmlTargetElement("p")] -[HtmlTargetElement("input")] -public class MultiEnumTagHelper : EnumTagHelper -{ -} + public CustomEnum EnumProperty { get; set; } + } -public class NestedEnumTagHelper : EnumTagHelper -{ - public NestedEnum NestedEnumProperty { get; set; } + [HtmlTargetElement("p")] + [HtmlTargetElement("input")] + public class MultiEnumTagHelper : EnumTagHelper + { + } - public enum NestedEnum + public class NestedEnumTagHelper : EnumTagHelper { - NestedOne, - NestedTwo + public NestedEnum NestedEnumProperty { get; set; } + + public enum NestedEnum + { + NestedOne, + NestedTwo + } } -} -[HtmlTargetElement("input", ParentTag = "div")] -public class RequiredParentTagHelper : TagHelper -{ -} + [HtmlTargetElement("input", ParentTag = "div")] + public class RequiredParentTagHelper : TagHelper + { + } -[HtmlTargetElement("p", ParentTag = "div")] -[HtmlTargetElement("input", ParentTag = "section")] -public class MultiSpecifiedRequiredParentTagHelper : TagHelper -{ -} + [HtmlTargetElement("p", ParentTag = "div")] + [HtmlTargetElement("input", ParentTag = "section")] + public class MultiSpecifiedRequiredParentTagHelper : TagHelper + { + } -[HtmlTargetElement("p")] -[HtmlTargetElement("input", ParentTag = "div")] -public class MultiWithUnspecifiedRequiredParentTagHelper : TagHelper -{ -} + [HtmlTargetElement("p")] + [HtmlTargetElement("input", ParentTag = "div")] + public class MultiWithUnspecifiedRequiredParentTagHelper : TagHelper + { + } -[RestrictChildren("p")] -public class RestrictChildrenTagHelper -{ -} + [RestrictChildren("p")] + public class RestrictChildrenTagHelper + { + } -[RestrictChildren("p", "strong")] -public class DoubleRestrictChildrenTagHelper -{ -} + [RestrictChildren("p", "strong")] + public class DoubleRestrictChildrenTagHelper + { + } -[HtmlTargetElement("p")] -[HtmlTargetElement("div")] -[RestrictChildren("p", "strong")] -public class MultiTargetRestrictChildrenTagHelper -{ -} + [HtmlTargetElement("p")] + [HtmlTargetElement("div")] + [RestrictChildren("p", "strong")] + public class MultiTargetRestrictChildrenTagHelper + { + } -[HtmlTargetElement("input", TagStructure = TagStructure.WithoutEndTag)] -public class TagStructureTagHelper : TagHelper -{ -} + [HtmlTargetElement("input", TagStructure = TagStructure.WithoutEndTag)] + public class TagStructureTagHelper : TagHelper + { + } -[HtmlTargetElement("p", TagStructure = TagStructure.NormalOrSelfClosing)] -[HtmlTargetElement("input", TagStructure = TagStructure.WithoutEndTag)] -public class MultiSpecifiedTagStructureTagHelper : TagHelper -{ -} + [HtmlTargetElement("p", TagStructure = TagStructure.NormalOrSelfClosing)] + [HtmlTargetElement("input", TagStructure = TagStructure.WithoutEndTag)] + public class MultiSpecifiedTagStructureTagHelper : TagHelper + { + } -[HtmlTargetElement("p")] -[HtmlTargetElement("input", TagStructure = TagStructure.WithoutEndTag)] -public class MultiWithUnspecifiedTagStructureTagHelper : TagHelper -{ -} + [HtmlTargetElement("p")] + [HtmlTargetElement("input", TagStructure = TagStructure.WithoutEndTag)] + public class MultiWithUnspecifiedTagStructureTagHelper : TagHelper + { + } -[EditorBrowsable(EditorBrowsableState.Always)] -public class DefaultEditorBrowsableTagHelper : TagHelper -{ [EditorBrowsable(EditorBrowsableState.Always)] - public int Property { get; set; } -} + public class DefaultEditorBrowsableTagHelper : TagHelper + { + [EditorBrowsable(EditorBrowsableState.Always)] + public int Property { get; set; } + } -public class HiddenPropertyEditorBrowsableTagHelper : TagHelper -{ - [EditorBrowsable(EditorBrowsableState.Never)] - public int Property { get; set; } -} + public class HiddenPropertyEditorBrowsableTagHelper : TagHelper + { + [EditorBrowsable(EditorBrowsableState.Never)] + public int Property { get; set; } + } -public class MultiPropertyEditorBrowsableTagHelper : TagHelper -{ - [EditorBrowsable(EditorBrowsableState.Never)] - public int Property { get; set; } + public class MultiPropertyEditorBrowsableTagHelper : TagHelper + { + [EditorBrowsable(EditorBrowsableState.Never)] + public int Property { get; set; } - public virtual int Property2 { get; set; } -} + public virtual int Property2 { get; set; } + } -public class OverriddenPropertyEditorBrowsableTagHelper : MultiPropertyEditorBrowsableTagHelper -{ - [EditorBrowsable(EditorBrowsableState.Never)] - public override int Property2 { get; set; } -} + public class OverriddenPropertyEditorBrowsableTagHelper : MultiPropertyEditorBrowsableTagHelper + { + [EditorBrowsable(EditorBrowsableState.Never)] + public override int Property2 { get; set; } + } -[EditorBrowsable(EditorBrowsableState.Never)] -public class EditorBrowsableTagHelper : TagHelper -{ [EditorBrowsable(EditorBrowsableState.Never)] - public virtual int Property { get; set; } -} + public class EditorBrowsableTagHelper : TagHelper + { + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual int Property { get; set; } + } -public class InheritedEditorBrowsableTagHelper : EditorBrowsableTagHelper -{ - public override int Property { get; set; } -} + public class InheritedEditorBrowsableTagHelper : EditorBrowsableTagHelper + { + public override int Property { get; set; } + } -[EditorBrowsable(EditorBrowsableState.Advanced)] -public class OverriddenEditorBrowsableTagHelper : EditorBrowsableTagHelper -{ [EditorBrowsable(EditorBrowsableState.Advanced)] - public override int Property { get; set; } -} + public class OverriddenEditorBrowsableTagHelper : EditorBrowsableTagHelper + { + [EditorBrowsable(EditorBrowsableState.Advanced)] + public override int Property { get; set; } + } -[HtmlTargetElement("p")] -[HtmlTargetElement("div")] -[EditorBrowsable(EditorBrowsableState.Never)] -public class MultiEditorBrowsableTagHelper : TagHelper -{ -} + [HtmlTargetElement("p")] + [HtmlTargetElement("div")] + [EditorBrowsable(EditorBrowsableState.Never)] + public class MultiEditorBrowsableTagHelper : TagHelper + { + } -[HtmlTargetElement(Attributes = "class*")] -public class AttributeWildcardTargetingTagHelper : TagHelper -{ -} + [HtmlTargetElement(Attributes = "class*")] + public class AttributeWildcardTargetingTagHelper : TagHelper + { + } -[HtmlTargetElement(Attributes = "class*,style*")] -public class MultiAttributeWildcardTargetingTagHelper : TagHelper -{ -} + [HtmlTargetElement(Attributes = "class*,style*")] + public class MultiAttributeWildcardTargetingTagHelper : TagHelper + { + } -[HtmlTargetElement(Attributes = "class")] -public class AttributeTargetingTagHelper : TagHelper -{ -} + [HtmlTargetElement(Attributes = "class")] + public class AttributeTargetingTagHelper : TagHelper + { + } -[HtmlTargetElement(Attributes = "class,style")] -public class MultiAttributeTargetingTagHelper : TagHelper -{ -} + [HtmlTargetElement(Attributes = "class,style")] + public class MultiAttributeTargetingTagHelper : TagHelper + { + } -[HtmlTargetElement(Attributes = "custom")] -[HtmlTargetElement(Attributes = "class,style")] -public class MultiAttributeAttributeTargetingTagHelper : TagHelper -{ -} + [HtmlTargetElement(Attributes = "custom")] + [HtmlTargetElement(Attributes = "class,style")] + public class MultiAttributeAttributeTargetingTagHelper : TagHelper + { + } -[HtmlTargetElement(Attributes = "style")] -public class InheritedAttributeTargetingTagHelper : AttributeTargetingTagHelper -{ -} + [HtmlTargetElement(Attributes = "style")] + public class InheritedAttributeTargetingTagHelper : AttributeTargetingTagHelper + { + } -[HtmlTargetElement("input", Attributes = "class")] -public class RequiredAttributeTagHelper : TagHelper -{ -} + [HtmlTargetElement("input", Attributes = "class")] + public class RequiredAttributeTagHelper : TagHelper + { + } -[HtmlTargetElement("div", Attributes = "class")] -public class InheritedRequiredAttributeTagHelper : RequiredAttributeTagHelper -{ -} + [HtmlTargetElement("div", Attributes = "class")] + public class InheritedRequiredAttributeTagHelper : RequiredAttributeTagHelper + { + } -[HtmlTargetElement("div", Attributes = "class")] -[HtmlTargetElement("input", Attributes = "class")] -public class MultiAttributeRequiredAttributeTagHelper : TagHelper -{ -} + [HtmlTargetElement("div", Attributes = "class")] + [HtmlTargetElement("input", Attributes = "class")] + public class MultiAttributeRequiredAttributeTagHelper : TagHelper + { + } -[HtmlTargetElement("input", Attributes = "style")] -[HtmlTargetElement("input", Attributes = "class")] -public class MultiAttributeSameTagRequiredAttributeTagHelper : TagHelper -{ -} + [HtmlTargetElement("input", Attributes = "style")] + [HtmlTargetElement("input", Attributes = "class")] + public class MultiAttributeSameTagRequiredAttributeTagHelper : TagHelper + { + } -[HtmlTargetElement("input", Attributes = "class,style")] -public class MultiRequiredAttributeTagHelper : TagHelper -{ -} + [HtmlTargetElement("input", Attributes = "class,style")] + public class MultiRequiredAttributeTagHelper : TagHelper + { + } -[HtmlTargetElement("div", Attributes = "style")] -public class InheritedMultiRequiredAttributeTagHelper : MultiRequiredAttributeTagHelper -{ -} + [HtmlTargetElement("div", Attributes = "style")] + public class InheritedMultiRequiredAttributeTagHelper : MultiRequiredAttributeTagHelper + { + } -[HtmlTargetElement("div", Attributes = "class,style")] -[HtmlTargetElement("input", Attributes = "class,style")] -public class MultiTagMultiRequiredAttributeTagHelper : TagHelper -{ -} + [HtmlTargetElement("div", Attributes = "class,style")] + [HtmlTargetElement("input", Attributes = "class,style")] + public class MultiTagMultiRequiredAttributeTagHelper : TagHelper + { + } -[HtmlTargetElement("p")] -[HtmlTargetElement("div")] -public class MultiTagTagHelper -{ - public string ValidAttribute { get; set; } -} + [HtmlTargetElement("p")] + [HtmlTargetElement("div")] + public class MultiTagTagHelper + { + public string ValidAttribute { get; set; } + } -public class InheritedMultiTagTagHelper : MultiTagTagHelper -{ -} + public class InheritedMultiTagTagHelper : MultiTagTagHelper + { + } -[HtmlTargetElement("p")] -[HtmlTargetElement("p")] -[HtmlTargetElement("div")] -[HtmlTargetElement("div")] -public class DuplicateTagNameTagHelper -{ -} + [HtmlTargetElement("p")] + [HtmlTargetElement("p")] + [HtmlTargetElement("div")] + [HtmlTargetElement("div")] + public class DuplicateTagNameTagHelper + { + } -[HtmlTargetElement("data-condition")] -public class OverrideNameTagHelper -{ -} + [HtmlTargetElement("data-condition")] + public class OverrideNameTagHelper + { + } -public class InheritedSingleAttributeTagHelper : SingleAttributeTagHelper -{ -} + public class InheritedSingleAttributeTagHelper : SingleAttributeTagHelper + { + } -public class DuplicateAttributeNameTagHelper -{ - public string MyNameIsLegion { get; set; } + public class DuplicateAttributeNameTagHelper + { + public string MyNameIsLegion { get; set; } - [HtmlAttributeName("my-name-is-legion")] - public string Fred { get; set; } -} + [HtmlAttributeName("my-name-is-legion")] + public string Fred { get; set; } + } -public class NotBoundAttributeTagHelper -{ - public object BoundProperty { get; set; } + public class NotBoundAttributeTagHelper + { + public object BoundProperty { get; set; } - [HtmlAttributeNotBound] - public string NotBoundProperty { get; set; } + [HtmlAttributeNotBound] + public string NotBoundProperty { get; set; } - [HtmlAttributeName("unused")] - [HtmlAttributeNotBound] - public string NamedNotBoundProperty { get; set; } -} + [HtmlAttributeName("unused")] + [HtmlAttributeNotBound] + public string NamedNotBoundProperty { get; set; } + } -public class OverriddenAttributeTagHelper -{ - [HtmlAttributeName("SomethingElse")] - public virtual string ValidAttribute1 { get; set; } + public class OverriddenAttributeTagHelper + { + [HtmlAttributeName("SomethingElse")] + public virtual string ValidAttribute1 { get; set; } - [HtmlAttributeName("Something-Else")] - public string ValidAttribute2 { get; set; } -} + [HtmlAttributeName("Something-Else")] + public string ValidAttribute2 { get; set; } + } -public class InheritedOverriddenAttributeTagHelper : OverriddenAttributeTagHelper -{ - public override string ValidAttribute1 { get; set; } -} + public class InheritedOverriddenAttributeTagHelper : OverriddenAttributeTagHelper + { + public override string ValidAttribute1 { get; set; } + } -public class InheritedNotOverriddenAttributeTagHelper : OverriddenAttributeTagHelper -{ -} + public class InheritedNotOverriddenAttributeTagHelper : OverriddenAttributeTagHelper + { + } -public class ALLCAPSTAGHELPER : TagHelper -{ - public int ALLCAPSATTRIBUTE { get; set; } -} + public class ALLCAPSTAGHELPER : TagHelper + { + public int ALLCAPSATTRIBUTE { get; set; } + } -public class CAPSOnOUTSIDETagHelper : TagHelper -{ - public int CAPSOnOUTSIDEATTRIBUTE { get; set; } -} + public class CAPSOnOUTSIDETagHelper : TagHelper + { + public int CAPSOnOUTSIDEATTRIBUTE { get; set; } + } -public class capsONInsideTagHelper : TagHelper -{ - public int capsONInsideattribute { get; set; } -} + public class capsONInsideTagHelper : TagHelper + { + public int capsONInsideattribute { get; set; } + } -public class One1Two2Three3TagHelper : TagHelper -{ - public int One1Two2Three3Attribute { get; set; } -} + public class One1Two2Three3TagHelper : TagHelper + { + public int One1Two2Three3Attribute { get; set; } + } -public class ONE1TWO2THREE3TagHelper : TagHelper -{ - public int ONE1TWO2THREE3Attribute { get; set; } -} + public class ONE1TWO2THREE3TagHelper : TagHelper + { + public int ONE1TWO2THREE3Attribute { get; set; } + } -public class First_Second_ThirdHiTagHelper : TagHelper -{ - public int First_Second_ThirdAttribute { get; set; } -} + public class First_Second_ThirdHiTagHelper : TagHelper + { + public int First_Second_ThirdAttribute { get; set; } + } -public class UNSuffixedCLASS : TagHelper -{ - public int UNSuffixedATTRIBUTE { get; set; } -} + public class UNSuffixedCLASS : TagHelper + { + public int UNSuffixedATTRIBUTE { get; set; } + } -public class InvalidBoundAttribute : TagHelper -{ - public string DataSomething { get; set; } -} + public class InvalidBoundAttribute : TagHelper + { + public string DataSomething { get; set; } + } -public class InvalidBoundAttributeWithValid : SingleAttributeTagHelper -{ - public string DataSomething { get; set; } -} + public class InvalidBoundAttributeWithValid : SingleAttributeTagHelper + { + public string DataSomething { get; set; } + } -public class OverriddenInvalidBoundAttributeWithValid : TagHelper -{ - [HtmlAttributeName("valid-something")] - public string DataSomething { get; set; } -} + public class OverriddenInvalidBoundAttributeWithValid : TagHelper + { + [HtmlAttributeName("valid-something")] + public string DataSomething { get; set; } + } -public class OverriddenValidBoundAttributeWithInvalid : TagHelper -{ - [HtmlAttributeName("data-something")] - public string ValidSomething { get; set; } -} + public class OverriddenValidBoundAttributeWithInvalid : TagHelper + { + [HtmlAttributeName("data-something")] + public string ValidSomething { get; set; } + } -public class OverriddenValidBoundAttributeWithInvalidUpperCase : TagHelper -{ - [HtmlAttributeName("DATA-SOMETHING")] - public string ValidSomething { get; set; } -} + public class OverriddenValidBoundAttributeWithInvalidUpperCase : TagHelper + { + [HtmlAttributeName("DATA-SOMETHING")] + public string ValidSomething { get; set; } + } -public class DefaultValidHtmlAttributePrefix : TagHelper -{ - public IDictionary DictionaryProperty { get; set; } -} + public class DefaultValidHtmlAttributePrefix : TagHelper + { + public IDictionary DictionaryProperty { get; set; } + } -public class SingleValidHtmlAttributePrefix : TagHelper -{ - [HtmlAttributeName("valid-name")] - public IDictionary DictionaryProperty { get; set; } -} + public class SingleValidHtmlAttributePrefix : TagHelper + { + [HtmlAttributeName("valid-name")] + public IDictionary DictionaryProperty { get; set; } + } -public class MultipleValidHtmlAttributePrefix : TagHelper -{ - [HtmlAttributeName("valid-name1", DictionaryAttributePrefix = "valid-prefix1-")] - public Dictionary DictionaryProperty { get; set; } + public class MultipleValidHtmlAttributePrefix : TagHelper + { + [HtmlAttributeName("valid-name1", DictionaryAttributePrefix = "valid-prefix1-")] + public Dictionary DictionaryProperty { get; set; } - [HtmlAttributeName("valid-name2", DictionaryAttributePrefix = "valid-prefix2-")] - public DictionarySubclass DictionarySubclassProperty { get; set; } + [HtmlAttributeName("valid-name2", DictionaryAttributePrefix = "valid-prefix2-")] + public DictionarySubclass DictionarySubclassProperty { get; set; } - [HtmlAttributeName("valid-name3", DictionaryAttributePrefix = "valid-prefix3-")] - public DictionaryWithoutParameterlessConstructor DictionaryWithoutParameterlessConstructorProperty { get; set; } + [HtmlAttributeName("valid-name3", DictionaryAttributePrefix = "valid-prefix3-")] + public DictionaryWithoutParameterlessConstructor DictionaryWithoutParameterlessConstructorProperty { get; set; } - [HtmlAttributeName("valid-name4", DictionaryAttributePrefix = "valid-prefix4-")] - public GenericDictionarySubclass GenericDictionarySubclassProperty { get; set; } + [HtmlAttributeName("valid-name4", DictionaryAttributePrefix = "valid-prefix4-")] + public GenericDictionarySubclass GenericDictionarySubclassProperty { get; set; } - [HtmlAttributeName("valid-name5", DictionaryAttributePrefix = "valid-prefix5-")] - public SortedDictionary SortedDictionaryProperty { get; set; } + [HtmlAttributeName("valid-name5", DictionaryAttributePrefix = "valid-prefix5-")] + public SortedDictionary SortedDictionaryProperty { get; set; } - [HtmlAttributeName("valid-name6")] - public string StringProperty { get; set; } + [HtmlAttributeName("valid-name6")] + public string StringProperty { get; set; } - public IDictionary GetOnlyDictionaryProperty { get; } + public IDictionary GetOnlyDictionaryProperty { get; } - [HtmlAttributeName(DictionaryAttributePrefix = "valid-prefix6")] - public IDictionary GetOnlyDictionaryPropertyWithAttributePrefix { get; } -} + [HtmlAttributeName(DictionaryAttributePrefix = "valid-prefix6")] + public IDictionary GetOnlyDictionaryPropertyWithAttributePrefix { get; } + } -public class SingleInvalidHtmlAttributePrefix : TagHelper -{ - [HtmlAttributeName("valid-name", DictionaryAttributePrefix = "valid-prefix")] - public string StringProperty { get; set; } -} + public class SingleInvalidHtmlAttributePrefix : TagHelper + { + [HtmlAttributeName("valid-name", DictionaryAttributePrefix = "valid-prefix")] + public string StringProperty { get; set; } + } -public class MultipleInvalidHtmlAttributePrefix : TagHelper -{ - [HtmlAttributeName("valid-name1")] - public long LongProperty { get; set; } + public class MultipleInvalidHtmlAttributePrefix : TagHelper + { + [HtmlAttributeName("valid-name1")] + public long LongProperty { get; set; } - [HtmlAttributeName("valid-name2", DictionaryAttributePrefix = "valid-prefix2-")] - public Dictionary DictionaryOfIntProperty { get; set; } + [HtmlAttributeName("valid-name2", DictionaryAttributePrefix = "valid-prefix2-")] + public Dictionary DictionaryOfIntProperty { get; set; } - [HtmlAttributeName("valid-name3", DictionaryAttributePrefix = "valid-prefix3-")] - public IReadOnlyDictionary ReadOnlyDictionaryProperty { get; set; } + [HtmlAttributeName("valid-name3", DictionaryAttributePrefix = "valid-prefix3-")] + public IReadOnlyDictionary ReadOnlyDictionaryProperty { get; set; } - [HtmlAttributeName("valid-name4", DictionaryAttributePrefix = "valid-prefix4-")] - public int IntProperty { get; set; } + [HtmlAttributeName("valid-name4", DictionaryAttributePrefix = "valid-prefix4-")] + public int IntProperty { get; set; } - [HtmlAttributeName("valid-name5", DictionaryAttributePrefix = "valid-prefix5-")] - public DictionaryOfIntSubclass DictionaryOfIntSubclassProperty { get; set; } + [HtmlAttributeName("valid-name5", DictionaryAttributePrefix = "valid-prefix5-")] + public DictionaryOfIntSubclass DictionaryOfIntSubclassProperty { get; set; } - [HtmlAttributeName(DictionaryAttributePrefix = "valid-prefix6")] - public IDictionary GetOnlyDictionaryAttributePrefix { get; } + [HtmlAttributeName(DictionaryAttributePrefix = "valid-prefix6")] + public IDictionary GetOnlyDictionaryAttributePrefix { get; } - [HtmlAttributeName("invalid-name7")] - public IDictionary GetOnlyDictionaryPropertyWithAttributeName { get; } -} + [HtmlAttributeName("invalid-name7")] + public IDictionary GetOnlyDictionaryPropertyWithAttributeName { get; } + } -public class DictionarySubclass : Dictionary -{ -} + public class DictionarySubclass : Dictionary + { + } -public class DictionaryWithoutParameterlessConstructor : Dictionary -{ - public DictionaryWithoutParameterlessConstructor(int count) - : base() + public class DictionaryWithoutParameterlessConstructor : Dictionary { + public DictionaryWithoutParameterlessConstructor(int count) + : base() + { + } } -} -public class DictionaryOfIntSubclass : Dictionary -{ -} + public class DictionaryOfIntSubclass : Dictionary + { + } -public class GenericDictionarySubclass : Dictionary -{ -} + public class GenericDictionarySubclass : Dictionary + { + } -[OutputElementHint("strong")] -public class OutputElementHintTagHelper : TagHelper -{ -} + [OutputElementHint("strong")] + public class OutputElementHintTagHelper : TagHelper + { + } -[HtmlTargetElement("a")] -[HtmlTargetElement("p")] -[OutputElementHint("div")] -public class MultipleDescriptorTagHelperWithOutputElementHint : TagHelper -{ -} + [HtmlTargetElement("a")] + [HtmlTargetElement("p")] + [OutputElementHint("div")] + public class MultipleDescriptorTagHelperWithOutputElementHint : TagHelper + { + } -public class NonPublicAccessorTagHelper : TagHelper -{ - public string ValidAttribute { get; set; } - public string InvalidPrivateSetAttribute { get; private set; } - public string InvalidPrivateGetAttribute { private get; set; } - protected string InvalidProtectedAttribute { get; set; } - internal string InvalidInternalAttribute { get; set; } - protected internal string InvalidProtectedInternalAttribute { get; set; } -} + public class NonPublicAccessorTagHelper : TagHelper + { + public string ValidAttribute { get; set; } + public string InvalidPrivateSetAttribute { get; private set; } + public string InvalidPrivateGetAttribute { private get; set; } + protected string InvalidProtectedAttribute { get; set; } + internal string InvalidInternalAttribute { get; set; } + protected internal string InvalidProtectedInternalAttribute { get; set; } + } -public class SingleAttributeTagHelper : TagHelper -{ - public int IntAttribute { get; set; } -} + public class SingleAttributeTagHelper : TagHelper + { + public int IntAttribute { get; set; } + } -public class MissingAccessorTagHelper : TagHelper -{ - public string ValidAttribute { get; set; } - public string InvalidNoGetAttribute { set { } } - public string InvalidNoSetAttribute { get { return string.Empty; } } + public class MissingAccessorTagHelper : TagHelper + { + public string ValidAttribute { get; set; } + public string InvalidNoGetAttribute { set { } } + public string InvalidNoSetAttribute { get { return string.Empty; } } + } +} +"""; } diff --git a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/TagHelperTypeVisitorTest.cs b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/TagHelperTypeVisitorTest.cs index 2f9eb46f588..b02afeda409 100644 --- a/src/Compiler/Microsoft.CodeAnalysis.Razor/test/TagHelperTypeVisitorTest.cs +++ b/src/Compiler/Microsoft.CodeAnalysis.Razor/test/TagHelperTypeVisitorTest.cs @@ -4,26 +4,28 @@ #nullable disable using System.Collections.Generic; -using System.Reflection; -using Microsoft.AspNetCore.Razor.TagHelpers; using Xunit; namespace Microsoft.CodeAnalysis.Razor.Workspaces; -public class TagHelperTypeVisitorTest +public class TagHelperTypeVisitorTest : TagHelperDescriptorProviderTestBase { - private static readonly Assembly _assembly = typeof(TagHelperTypeVisitorTest).GetTypeInfo().Assembly; + public TagHelperTypeVisitorTest() : base(AdditionalCode) + { + Compilation = BaseCompilation; + ITagHelperSymbol = Compilation.GetTypeByMetadataName(TagHelperTypes.ITagHelper); + } - private static Compilation Compilation { get; } = TestCompilation.Create(_assembly); + private Compilation Compilation { get; } - private static INamedTypeSymbol ITagHelperSymbol { get; } = Compilation.GetTypeByMetadataName(TagHelperTypes.ITagHelper); + private INamedTypeSymbol ITagHelperSymbol { get; } [Fact] public void IsTagHelper_PlainTagHelper_ReturnsTrue() { // Arrange var testVisitor = new TagHelperTypeVisitor(ITagHelperSymbol, new List()); - var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Valid_PlainTagHelper).FullName); + var tagHelperSymbol = Compilation.GetTypeByMetadataName("TestNamespace.Valid_PlainTagHelper"); // Act var isTagHelper = testVisitor.IsTagHelper(tagHelperSymbol); @@ -37,7 +39,7 @@ public void IsTagHelper_InheritedTagHelper_ReturnsTrue() { // Arrange var testVisitor = new TagHelperTypeVisitor(ITagHelperSymbol, new List()); - var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Valid_InheritedTagHelper).FullName); + var tagHelperSymbol = Compilation.GetTypeByMetadataName("TestNamespace.Valid_InheritedTagHelper"); // Act var isTagHelper = testVisitor.IsTagHelper(tagHelperSymbol); @@ -51,7 +53,7 @@ public void IsTagHelper_AbstractTagHelper_ReturnsFalse() { // Arrange var testVisitor = new TagHelperTypeVisitor(ITagHelperSymbol, new List()); - var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_AbstractTagHelper).FullName); + var tagHelperSymbol = Compilation.GetTypeByMetadataName("TestNamespace.Invalid_AbstractTagHelper"); // Act var isTagHelper = testVisitor.IsTagHelper(tagHelperSymbol); @@ -65,7 +67,7 @@ public void IsTagHelper_GenericTagHelper_ReturnsFalse() { // Arrange var testVisitor = new TagHelperTypeVisitor(ITagHelperSymbol, new List()); - var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_GenericTagHelper<>).FullName); + var tagHelperSymbol = Compilation.GetTypeByMetadataName("TestNamespace.Invalid_GenericTagHelper`1"); // Act var isTagHelper = testVisitor.IsTagHelper(tagHelperSymbol); @@ -79,7 +81,7 @@ public void IsTagHelper_InternalTagHelper_ReturnsFalse() { // Arrange var testVisitor = new TagHelperTypeVisitor(ITagHelperSymbol, new List()); - var tagHelperSymbol = Compilation.GetTypeByMetadataName(typeof(Invalid_InternalTagHelper).FullName); + var tagHelperSymbol = Compilation.GetTypeByMetadataName("TestNamespace.Invalid_InternalTagHelper"); // Act var isTagHelper = testVisitor.IsTagHelper(tagHelperSymbol); @@ -88,32 +90,40 @@ public void IsTagHelper_InternalTagHelper_ReturnsFalse() Assert.False(isTagHelper); } - public class Invalid_NestedPublicTagHelper : TagHelper - { - } - - public class Valid_NestedPublicViewComponent - { - public string Invoke(string foo) => null; - } -} - -public abstract class Invalid_AbstractTagHelper : TagHelper -{ -} - -public class Invalid_GenericTagHelper : TagHelper -{ -} - -internal class Invalid_InternalTagHelper : TagHelper -{ -} - -public class Valid_PlainTagHelper : TagHelper -{ -} - -public class Valid_InheritedTagHelper : Valid_PlainTagHelper -{ + private const string AdditionalCode = + """ + using Microsoft.AspNetCore.Razor.TagHelpers; + + namespace TestNamespace + { + public class Invalid_NestedPublicTagHelper : TagHelper + { + } + + public class Valid_NestedPublicViewComponent + { + public string Invoke(string foo) => null; + } + + public abstract class Invalid_AbstractTagHelper : TagHelper + { + } + + public class Invalid_GenericTagHelper : TagHelper + { + } + + internal class Invalid_InternalTagHelper : TagHelper + { + } + + public class Valid_PlainTagHelper : TagHelper + { + } + + public class Valid_InheritedTagHelper : Valid_PlainTagHelper + { + } + } + """; } diff --git a/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators.Transport/Microsoft.NET.Sdk.Razor.SourceGenerators.Transport.csproj b/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators.Transport/Microsoft.NET.Sdk.Razor.SourceGenerators.Transport.csproj index f439aa3e63b..423e64725fa 100644 --- a/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators.Transport/Microsoft.NET.Sdk.Razor.SourceGenerators.Transport.csproj +++ b/src/Compiler/Microsoft.NET.Sdk.Razor.SourceGenerators.Transport/Microsoft.NET.Sdk.Razor.SourceGenerators.Transport.csproj @@ -9,7 +9,7 @@ false true - false + false diff --git a/src/Compiler/Microsoft.Net.Compilers.Razor.Toolset/Microsoft.Net.Compilers.Razor.Toolset.csproj b/src/Compiler/Microsoft.Net.Compilers.Razor.Toolset/Microsoft.Net.Compilers.Razor.Toolset.csproj index 32fc1ff170c..5aeeccbf89b 100644 --- a/src/Compiler/Microsoft.Net.Compilers.Razor.Toolset/Microsoft.Net.Compilers.Razor.Toolset.csproj +++ b/src/Compiler/Microsoft.Net.Compilers.Razor.Toolset/Microsoft.Net.Compilers.Razor.Toolset.csproj @@ -11,7 +11,7 @@ true false true - true + true .NET Compilers Razor Toolset Package. Referencing this package will cause the project to be built using the Razor compilers and source generator contained in the package, as opposed to the version installed with the SDK. diff --git a/src/Compiler/perf/Microbenchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks.Compiler.csproj b/src/Compiler/perf/Microbenchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks.Compiler.csproj index ff25b1f3ace..879da42abbc 100644 --- a/src/Compiler/perf/Microbenchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks.Compiler.csproj +++ b/src/Compiler/perf/Microbenchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks.Compiler.csproj @@ -5,7 +5,7 @@ Exe true true - true + true false diff --git a/src/Compiler/perf/Microsoft.AspNetCore.Razor.Microbenchmarks.Generator/Microsoft.AspNetCore.Razor.Microbenchmarks.Generator.csproj b/src/Compiler/perf/Microsoft.AspNetCore.Razor.Microbenchmarks.Generator/Microsoft.AspNetCore.Razor.Microbenchmarks.Generator.csproj index aebe9633fef..501e4975322 100644 --- a/src/Compiler/perf/Microsoft.AspNetCore.Razor.Microbenchmarks.Generator/Microsoft.AspNetCore.Razor.Microbenchmarks.Generator.csproj +++ b/src/Compiler/perf/Microsoft.AspNetCore.Razor.Microbenchmarks.Generator/Microsoft.AspNetCore.Razor.Microbenchmarks.Generator.csproj @@ -21,10 +21,10 @@ - - - - + + + + diff --git a/src/Compiler/test/Directory.Build.props b/src/Compiler/test/Directory.Build.props index 7af57e829d4..838e522f0f9 100644 --- a/src/Compiler/test/Directory.Build.props +++ b/src/Compiler/test/Directory.Build.props @@ -8,6 +8,6 @@ --> true false - true + true diff --git a/src/Compiler/test/Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib/Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib.Compiler.csproj b/src/Compiler/test/Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib/Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib.Compiler.csproj deleted file mode 100644 index acb10650c86..00000000000 --- a/src/Compiler/test/Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib/Microsoft.AspNetCore.Razor.Test.MvcShim.ClassLib.Compiler.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - netstandard2.0 - - - - - - - - - - - - - - diff --git a/src/Compiler/test/Microsoft.AspNetCore.Razor.Test.MvcShim/Microsoft.AspNetCore.Razor.Test.MvcShim.Compiler.csproj b/src/Compiler/test/Microsoft.AspNetCore.Razor.Test.MvcShim/Microsoft.AspNetCore.Razor.Test.MvcShim.Compiler.csproj deleted file mode 100644 index faa0c341e56..00000000000 --- a/src/Compiler/test/Microsoft.AspNetCore.Razor.Test.MvcShim/Microsoft.AspNetCore.Razor.Test.MvcShim.Compiler.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - $(DefaultNetCoreTargetFrameworks);$(DefaultNetFxTargetFramework) - true - - - - - - - - - - diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Microsoft.NET.Sdk.Razor.SourceGenerators.Test.csproj b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Microsoft.NET.Sdk.Razor.SourceGenerators.Test.csproj index 4d1acf312b7..799ef5cd703 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Microsoft.NET.Sdk.Razor.SourceGenerators.Test.csproj +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/Microsoft.NET.Sdk.Razor.SourceGenerators.Test.csproj @@ -25,9 +25,9 @@ - - - + + + diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorComponentTests.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorComponentTests.cs index 03dfe7f65a8..1eb307d8484 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorComponentTests.cs +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorComponentTests.cs @@ -13,6 +13,91 @@ namespace Microsoft.NET.Sdk.Razor.SourceGenerators; public sealed class RazorSourceGeneratorComponentTests : RazorSourceGeneratorTestsBase { + [Fact, WorkItem("https://github.com/dotnet/razor/issues/10991")] + public async Task ImportsRazor() + { + // Arrange + var project = CreateTestProject(new() + { + ["Folder1/_Imports.razor"] = """ + @using MyApp.MyNamespace.AndAnother + """, + ["Folder1/Component1.razor"] = """ + @{ var c = new Class1(); } + """, + ["Folder2/Component2.razor"] = """ + @{ var c = new Class1(); } + """, + }, new() + { + ["Class1.cs"] = """ + namespace MyApp.MyNamespace.AndAnother; + + public class Class1 { } + """, + }); + var compilation = await project.GetCompilationAsync(); + var driver = await GetDriverAsync(project); + + // Act + var result = RunGenerator(compilation!, ref driver, + // Folder2/Component2.razor(1,16): error CS0246: The type or namespace name 'Class1' could not be found (are you missing a using directive or an assembly reference?) + // var c = new Class1(); + Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "Class1").WithArguments("Class1").WithLocation(1, 16)); + + // Assert + result.Diagnostics.Verify(); + Assert.Equal(3, result.GeneratedSources.Length); + result.VerifyOutputsMatchBaseline(); + } + + [Fact, WorkItem("https://github.com/dotnet/razor/issues/10991")] + public async Task ImportsRazor_WithMarkup() + { + // Arrange + var project = CreateTestProject(new() + { + ["_Imports.razor"] = """ + @using System.Net.Http +

test

+ """, + }); + var compilation = await project.GetCompilationAsync(); + var driver = await GetDriverAsync(project); + + // Act + var result = RunGenerator(compilation!, ref driver); + + // Assert + result.Diagnostics.Verify( + // _Imports.razor(2,1): error RZ10003: Markup, code and block directives are not valid in component imports. + Diagnostic("RZ10003").WithLocation(2, 1)); + Assert.Single(result.GeneratedSources); + result.VerifyOutputsMatchBaseline(); + } + + [Fact] + public async Task ImportsRazor_SystemInNamespace() + { + // Arrange + var project = CreateTestProject(new() + { + ["System/_Imports.razor"] = """ + @using global::System.Net.Http + """, + }); + var compilation = await project.GetCompilationAsync(); + var driver = await GetDriverAsync(project); + + // Act + var result = RunGenerator(compilation!, ref driver); + + // Assert + result.Diagnostics.Verify(); + Assert.Single(result.GeneratedSources); + result.VerifyOutputsMatchBaseline(); + } + [Fact, WorkItem("https://github.com/dotnet/razor/issues/8718")] public async Task PartialClass() { diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTagHelperTests.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTagHelperTests.cs index 821ff07ad91..cee068e87dc 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTagHelperTests.cs +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTagHelperTests.cs @@ -54,6 +54,139 @@ public override void Process(TagHelperContext context, TagHelperOutput output) await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index"); } + [Fact, WorkItem("https://github.com/dotnet/razor/issues/8460")] + public async Task VoidTagName() + { + // Arrange + var project = CreateTestProject(new() + { + ["Views/Home/Index.cshtml"] = """ + @addTagHelper *, TestProject + markup + @{ code } + """, + }, new() + { + ["ColTagHelper.cs"] = """ + using Microsoft.AspNetCore.Razor.TagHelpers; + public class ColTagHelper : TagHelper { } + """, + }); + var compilation = await project.GetCompilationAsync(); + var driver = await GetDriverAsync(project); + + // Act + var result = RunGenerator(compilation!, ref driver, out _, verify: static compilation => + { + // Malformed C# is generated due to everything after the tag being considered C#. + Assert.Contains(compilation.GetDiagnostics(), static d => d.Severity == DiagnosticSeverity.Error); + }); + + // Assert + result.Diagnostics.Verify( + // Views/Home/Index.cshtml(3,5): error RZ1042: Component or tag helper 'col' must be self-closing because it's named like an HTML void element. To use child content, qualify such components with their namespace or use prefixes with such tag helpers. + Diagnostic("RZ1042").WithLocation(3, 5)); + result.VerifyOutputsMatchBaseline(); + } + + [Fact, WorkItem("https://github.com/dotnet/razor/issues/8460")] + public async Task VoidTagName_Prefixed() + { + // Arrange + var project = CreateTestProject(new() + { + ["Views/Home/Index.cshtml"] = """ + @tagHelperPrefix th: + @addTagHelper *, TestProject + markup + @{ code } + """, + }, new() + { + ["ColTagHelper.cs"] = """ + using Microsoft.AspNetCore.Razor.TagHelpers; + public class ColTagHelper : TagHelper + { + public override void Process(TagHelperContext context, TagHelperOutput output) + { + output.TagName = "my-col"; + } + } + """, + }); + var compilation = await project.GetCompilationAsync(); + var driver = await GetDriverAsync(project); + + // Act + var result = RunGenerator(compilation!, ref driver, out compilation); + + // Assert + result.Diagnostics.Verify(); + await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index"); + } + + [Fact, WorkItem("https://github.com/dotnet/razor/issues/8460")] + public async Task VoidTagName_SelfClosing() + { + // Arrange + var project = CreateTestProject(new() + { + ["Views/Home/Index.cshtml"] = """ + @addTagHelper *, TestProject + + @{ } + """, + }, new() + { + ["ColTagHelper.cs"] = """ + using Microsoft.AspNetCore.Razor.TagHelpers; + public class ColTagHelper : TagHelper + { + public override void Process(TagHelperContext context, TagHelperOutput output) + { + output.TagName = "my-col"; + } + } + """, + }); + var compilation = await project.GetCompilationAsync(); + var driver = await GetDriverAsync(project); + + // Act + var result = RunGenerator(compilation!, ref driver, out compilation); + + // Assert + result.Diagnostics.Verify(); + await VerifyRazorPageMatchesBaselineAsync(compilation, "Views_Home_Index"); + } + + [Fact, WorkItem("https://github.com/dotnet/razor/issues/8460")] + public async Task VoidTagName_NoMatchingTagHelper() + { + // Arrange + var project = CreateTestProject(new() + { + ["Views/Home/Index.cshtml"] = """ + @addTagHelper *, TestProject + markup + @{ code } + """, + }); + var compilation = await project.GetCompilationAsync(); + var driver = await GetDriverAsync(project); + + // Act + var result = RunGenerator(compilation!, ref driver, out _, verify: static compilation => + { + // Malformed C# is generated due to everything after the tag being considered C#. + Assert.Contains(compilation.GetDiagnostics(), static d => d.Severity == DiagnosticSeverity.Error); + }); + + // Assert + result.Diagnostics.Verify(); + result.VerifyOutputsMatchBaseline(); + } + [Theory] [InlineData("Index")] [InlineData("CustomEncoder")] diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs index 9d5de9e89e6..b6d76601363 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTests.cs @@ -1059,7 +1059,7 @@ namespace MyApp.Pages using global::System.Threading.Tasks; using global::Microsoft.AspNetCore.Components; #nullable restore -#line (2,2)-(3,1) ""Pages/Index.razor"" +#line (2,2)-(2,33) ""Pages/Index.razor"" using SurveyPromptRootNamspace; #line default @@ -1132,7 +1132,7 @@ namespace MyApp.Pages using global::System.Threading.Tasks; using global::Microsoft.AspNetCore.Components; #nullable restore -#line (2,2)-(3,1) ""Pages/Index.razor"" +#line (2,2)-(2,33) ""Pages/Index.razor"" using SurveyPromptRootNamspace; #line default @@ -1160,6 +1160,7 @@ protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components. Assert.Equal(2, result.GeneratedSources.Length); Assert.Collection(eventListener.Events, + e => Assert.Equal("ComputeRazorSourceGeneratorOptions", e.EventName), e => Assert.Equal("DiscoverTagHelpersFromCompilationStart", e.EventName), e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName), e => Assert.Equal("DiscoverTagHelpersFromReferencesStart", e.EventName), @@ -3260,6 +3261,7 @@ public async Task IncrementalCompilation_OnlyCompilationRuns_When_MetadataRefere // reference causes the compilation to change so we re-run tag helper discovery there // but we didn't re-check the actual reference itself Assert.Collection(eventListener.Events, + e => Assert.Equal("ComputeRazorSourceGeneratorOptions", e.EventName), e => Assert.Equal("DiscoverTagHelpersFromCompilationStart", e.EventName), e => Assert.Equal("DiscoverTagHelpersFromCompilationStop", e.EventName)); } diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTestsBase.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTestsBase.cs index 6d9f8cdef27..12ebd58439a 100644 --- a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTestsBase.cs +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/RazorSourceGeneratorTestsBase.cs @@ -99,10 +99,15 @@ protected static GeneratorRunResult RunGenerator(Compilation compilation, ref Ge } protected static GeneratorRunResult RunGenerator(Compilation compilation, ref GeneratorDriver driver, out Compilation outputCompilation, params DiagnosticDescription[] expectedDiagnostics) + { + return RunGenerator(compilation, ref driver, out outputCompilation, c => c.VerifyDiagnostics(expectedDiagnostics)); + } + + protected static GeneratorRunResult RunGenerator(Compilation compilation, ref GeneratorDriver driver, out Compilation outputCompilation, Action verify) { driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out outputCompilation, out _); - outputCompilation.VerifyDiagnostics(expectedDiagnostics); + verify(outputCompilation); var result = driver.GetRunResult(); return result.Results.Single(); diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ImportsRazor/Folder1_Component1_razor.g.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ImportsRazor/Folder1_Component1_razor.g.cs new file mode 100644 index 00000000000..58840281074 --- /dev/null +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ImportsRazor/Folder1_Component1_razor.g.cs @@ -0,0 +1,39 @@ +#pragma checksum "Folder1/Component1.razor" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "6b035b1ceae4651124a6a654ee9943a6c99618b3" +// +#pragma warning disable 1591 +namespace MyApp.Folder1 +{ + #line default + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::System.Threading.Tasks; + using global::Microsoft.AspNetCore.Components; +#nullable restore +#line (1,2)-(1,36) "Folder1/_Imports.razor" +using MyApp.MyNamespace.AndAnother + +#line default +#line hidden +#nullable disable + ; + #nullable restore + public partial class Component1 : global::Microsoft.AspNetCore.Components.ComponentBase + #nullable disable + { + #pragma warning disable 1998 + protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) + { +#nullable restore +#line (1,3)-(1,26) "Folder1/Component1.razor" + var c = new Class1(); + +#line default +#line hidden +#nullable disable + + } + #pragma warning restore 1998 + } +} +#pragma warning restore 1591 diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ImportsRazor/Folder1__Imports_razor.g.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ImportsRazor/Folder1__Imports_razor.g.cs new file mode 100644 index 00000000000..3a32131235b --- /dev/null +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ImportsRazor/Folder1__Imports_razor.g.cs @@ -0,0 +1,31 @@ +#pragma checksum "Folder1/_Imports.razor" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "4b635952fa51f5f9709e03900447fd0d79a5bbf4" +// +#pragma warning disable 1591 +namespace MyApp.Folder1 +{ + #line default + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::System.Threading.Tasks; + using global::Microsoft.AspNetCore.Components; +#nullable restore +#line (1,2)-(1,36) "Folder1/_Imports.razor" +using MyApp.MyNamespace.AndAnother + +#line default +#line hidden +#nullable disable + ; + #nullable restore + public partial class _Imports : object + #nullable disable + { + #pragma warning disable 1998 + protected void Execute() + { + } + #pragma warning restore 1998 + } +} +#pragma warning restore 1591 diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ImportsRazor/Folder2_Component2_razor.g.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ImportsRazor/Folder2_Component2_razor.g.cs new file mode 100644 index 00000000000..fd2616847d5 --- /dev/null +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ImportsRazor/Folder2_Component2_razor.g.cs @@ -0,0 +1,33 @@ +#pragma checksum "Folder2/Component2.razor" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "6b035b1ceae4651124a6a654ee9943a6c99618b3" +// +#pragma warning disable 1591 +namespace MyApp.Folder2 +{ + #line default + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::System.Threading.Tasks; + using global::Microsoft.AspNetCore.Components; + #line default + #line hidden + #nullable restore + public partial class Component2 : global::Microsoft.AspNetCore.Components.ComponentBase + #nullable disable + { + #pragma warning disable 1998 + protected override void BuildRenderTree(global::Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder) + { +#nullable restore +#line (1,3)-(1,26) "Folder2/Component2.razor" + var c = new Class1(); + +#line default +#line hidden +#nullable disable + + } + #pragma warning restore 1998 + } +} +#pragma warning restore 1591 diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ImportsRazor_SystemInNamespace/System__Imports_razor.g.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ImportsRazor_SystemInNamespace/System__Imports_razor.g.cs new file mode 100644 index 00000000000..83a9570411e --- /dev/null +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ImportsRazor_SystemInNamespace/System__Imports_razor.g.cs @@ -0,0 +1,31 @@ +#pragma checksum "System/_Imports.razor" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "53b28fda6534c79c7b9a4da6dcc903c4b51cfab9" +// +#pragma warning disable 1591 +namespace MyApp.System +{ + #line default + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::System.Threading.Tasks; + using global::Microsoft.AspNetCore.Components; +#nullable restore +#line (1,2)-(1,31) "System/_Imports.razor" +using global::System.Net.Http + +#line default +#line hidden +#nullable disable + ; + #nullable restore + public partial class _Imports : object + #nullable disable + { + #pragma warning disable 1998 + protected void Execute() + { + } + #pragma warning restore 1998 + } +} +#pragma warning restore 1591 diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ImportsRazor_WithMarkup/_Imports_razor.g.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ImportsRazor_WithMarkup/_Imports_razor.g.cs new file mode 100644 index 00000000000..008aee425ac --- /dev/null +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorComponentTests/ImportsRazor_WithMarkup/_Imports_razor.g.cs @@ -0,0 +1,31 @@ +#pragma checksum "_Imports.razor" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "26a98239eb5029d034398cd0d9647d5b05517dc7" +// +#pragma warning disable 1591 +namespace MyApp +{ + #line default + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::System.Threading.Tasks; + using global::Microsoft.AspNetCore.Components; +#nullable restore +#line (1,2)-(1,23) "_Imports.razor" +using System.Net.Http + +#line default +#line hidden +#nullable disable + ; + #nullable restore + public partial class _Imports : object + #nullable disable + { + #pragma warning disable 1998 + protected void Execute() + { + } + #pragma warning restore 1998 + } +} +#pragma warning restore 1591 diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorTagHelperTests/VoidTagName/Views_Home_Index_cshtml.g.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorTagHelperTests/VoidTagName/Views_Home_Index_cshtml.g.cs new file mode 100644 index 00000000000..d5e24a19bb0 --- /dev/null +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorTagHelperTests/VoidTagName/Views_Home_Index_cshtml.g.cs @@ -0,0 +1,106 @@ +#pragma checksum "Views/Home/Index.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "7d86d5004a5ff098a5acce1b4775d9a15b2d114a" +// +#pragma warning disable 1591 +[assembly: global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute(typeof(AspNetCoreGeneratedDocument.Views_Home_Index), @"mvc.1.0.view", @"/Views/Home/Index.cshtml")] +namespace AspNetCoreGeneratedDocument +{ + #line default + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::System.Threading.Tasks; + using global::Microsoft.AspNetCore.Mvc; + using global::Microsoft.AspNetCore.Mvc.Rendering; + using global::Microsoft.AspNetCore.Mvc.ViewFeatures; + #line default + #line hidden + [global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemMetadataAttribute("Identifier", "/Views/Home/Index.cshtml")] + [global::System.Runtime.CompilerServices.CreateNewOnMetadataUpdateAttribute] + #nullable restore + internal sealed class Views_Home_Index : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage + #nullable disable + { + #line hidden + #pragma warning disable 0649 + private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperExecutionContext __tagHelperExecutionContext; + #pragma warning restore 0649 + private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner __tagHelperRunner = new global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner(); + #pragma warning disable 0169 + private string __tagHelperStringValueBuffer; + #pragma warning restore 0169 + private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager __backed__tagHelperScopeManager = null; + private global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager __tagHelperScopeManager + { + get + { + if (__backed__tagHelperScopeManager == null) + { + __backed__tagHelperScopeManager = new global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager(StartTagHelperWritingScope, EndTagHelperWritingScope); + } + return __backed__tagHelperScopeManager; + } + } + private global::ColTagHelper __ColTagHelper; + #pragma warning disable 1998 + public async override global::System.Threading.Tasks.Task ExecuteAsync() + { + __tagHelperExecutionContext = __tagHelperScopeManager.Begin("col", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => { + WriteLiteral("markup"); + } + ); + __ColTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__ColTagHelper); + await __tagHelperRunner.RunAsync(__tagHelperExecutionContext); + if (!__tagHelperExecutionContext.Output.IsContentModified) + { + await __tagHelperExecutionContext.SetOutputContentAsync(); + } + Write(__tagHelperExecutionContext.Output); + __tagHelperExecutionContext = __tagHelperScopeManager.End(); + WriteLiteral("\r\n"); + WriteLiteral(" "); + __tagHelperExecutionContext = __tagHelperScopeManager.Begin("col", global::Microsoft.AspNetCore.Razor.TagHelpers.TagMode.StartTagAndEndTag, "test", async() => { + } + ); + __ColTagHelper = CreateTagHelper(); + __tagHelperExecutionContext.Add(__ColTagHelper); + await __tagHelperRunner.RunAsync(__tagHelperExecutionContext); + if (!__tagHelperExecutionContext.Output.IsContentModified) + { + await __tagHelperExecutionContext.SetOutputContentAsync(); + } + Write(__tagHelperExecutionContext.Output); + __tagHelperExecutionContext = __tagHelperScopeManager.End(); +#nullable restore +#line (3,9)-(3,20) "Views/Home/Index.cshtml" +code + +#line default +#line hidden +#nullable disable + + } + #pragma warning restore 1998 + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } = default!; + #nullable disable + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } = default!; + #nullable disable + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } = default!; + #nullable disable + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } = default!; + #nullable disable + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html { get; private set; } = default!; + #nullable disable + } +} +#pragma warning restore 1591 diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorTagHelperTests/VoidTagName_NoMatchingTagHelper/Views_Home_Index_cshtml.g.cs b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorTagHelperTests/VoidTagName_NoMatchingTagHelper/Views_Home_Index_cshtml.g.cs new file mode 100644 index 00000000000..f1fdd3a93f6 --- /dev/null +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorTagHelperTests/VoidTagName_NoMatchingTagHelper/Views_Home_Index_cshtml.g.cs @@ -0,0 +1,60 @@ +#pragma checksum "Views/Home/Index.cshtml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "7d86d5004a5ff098a5acce1b4775d9a15b2d114a" +// +#pragma warning disable 1591 +[assembly: global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute(typeof(AspNetCoreGeneratedDocument.Views_Home_Index), @"mvc.1.0.view", @"/Views/Home/Index.cshtml")] +namespace AspNetCoreGeneratedDocument +{ + #line default + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::System.Threading.Tasks; + using global::Microsoft.AspNetCore.Mvc; + using global::Microsoft.AspNetCore.Mvc.Rendering; + using global::Microsoft.AspNetCore.Mvc.ViewFeatures; + #line default + #line hidden + [global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemMetadataAttribute("Identifier", "/Views/Home/Index.cshtml")] + [global::System.Runtime.CompilerServices.CreateNewOnMetadataUpdateAttribute] + #nullable restore + internal sealed class Views_Home_Index : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage + #nullable disable + { + #pragma warning disable 1998 + public async override global::System.Threading.Tasks.Task ExecuteAsync() + { + WriteLiteral("markup\r\n"); + WriteLiteral(" "); +#nullable restore +#line (3,9)-(3,20) "Views/Home/Index.cshtml" +code + +#line default +#line hidden +#nullable disable + + } + #pragma warning restore 1998 + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } = default!; + #nullable disable + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } = default!; + #nullable disable + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } = default!; + #nullable disable + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } = default!; + #nullable disable + #nullable restore + [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] + public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper Html { get; private set; } = default!; + #nullable disable + } +} +#pragma warning restore 1591 diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorTagHelperTests/VoidTagName_Prefixed/Views_Home_Index.html b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorTagHelperTests/VoidTagName_Prefixed/Views_Home_Index.html new file mode 100644 index 00000000000..a2cf137a7db --- /dev/null +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorTagHelperTests/VoidTagName_Prefixed/Views_Home_Index.html @@ -0,0 +1,2 @@ +markup + code \ No newline at end of file diff --git a/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorTagHelperTests/VoidTagName_SelfClosing/Views_Home_Index.html b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorTagHelperTests/VoidTagName_SelfClosing/Views_Home_Index.html new file mode 100644 index 00000000000..3926775d3b1 --- /dev/null +++ b/src/Compiler/test/Microsoft.NET.Sdk.Razor.SourceGenerators.Tests/TestFiles/RazorSourceGeneratorTagHelperTests/VoidTagName_SelfClosing/Views_Home_Index.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/Compiler/tools/Microsoft.AspNetCore.Mvc.Razor.Extensions.Tooling.Internal/Microsoft.AspNetCore.Mvc.Razor.Extensions.Tooling.Internal.csproj b/src/Compiler/tools/Microsoft.AspNetCore.Mvc.Razor.Extensions.Tooling.Internal/Microsoft.AspNetCore.Mvc.Razor.Extensions.Tooling.Internal.csproj index fc904636846..584c2711ab5 100644 --- a/src/Compiler/tools/Microsoft.AspNetCore.Mvc.Razor.Extensions.Tooling.Internal/Microsoft.AspNetCore.Mvc.Razor.Extensions.Tooling.Internal.csproj +++ b/src/Compiler/tools/Microsoft.AspNetCore.Mvc.Razor.Extensions.Tooling.Internal/Microsoft.AspNetCore.Mvc.Razor.Extensions.Tooling.Internal.csproj @@ -6,7 +6,7 @@ false false true - false + false diff --git a/src/Compiler/tools/Microsoft.CodeAnalysis.Razor.Tooling.Internal/Microsoft.CodeAnalysis.Razor.Tooling.Internal.csproj b/src/Compiler/tools/Microsoft.CodeAnalysis.Razor.Tooling.Internal/Microsoft.CodeAnalysis.Razor.Tooling.Internal.csproj index 582b2166ce9..45c3032e4ba 100644 --- a/src/Compiler/tools/Microsoft.CodeAnalysis.Razor.Tooling.Internal/Microsoft.CodeAnalysis.Razor.Tooling.Internal.csproj +++ b/src/Compiler/tools/Microsoft.CodeAnalysis.Razor.Tooling.Internal/Microsoft.CodeAnalysis.Razor.Tooling.Internal.csproj @@ -6,7 +6,7 @@ false false true - false + false true diff --git a/src/Compiler/tools/RazorSyntaxGenerator/RazorSyntaxGenerator.csproj b/src/Compiler/tools/RazorSyntaxGenerator/RazorSyntaxGenerator.csproj index 614004aed9a..ffe509a8e85 100644 --- a/src/Compiler/tools/RazorSyntaxGenerator/RazorSyntaxGenerator.csproj +++ b/src/Compiler/tools/RazorSyntaxGenerator/RazorSyntaxGenerator.csproj @@ -9,7 +9,7 @@ false false - true + true diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs index f79adf835b1..e713afe564d 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCSharpFormattingBenchmark.cs @@ -58,7 +58,7 @@ public async Task InitializeRazorCSharpFormattingAsync() DocumentUri = new Uri(_filePath); DocumentSnapshot = await GetDocumentSnapshotAsync(projectFilePath, _filePath, targetPath); - DocumentText = await DocumentSnapshot.GetTextAsync(); + DocumentText = await DocumentSnapshot.GetTextAsync(CancellationToken.None); } private static void WriteSampleFormattingFile(string filePath, bool preformatted, int blocks) diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCodeActionsBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCodeActionsBenchmark.cs index d1d21777c6f..11fc48f289e 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCodeActionsBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCodeActionsBenchmark.cs @@ -77,7 +77,7 @@ public async Task SetupAsync() DocumentUri = new Uri(_filePath); DocumentSnapshot = await GetDocumentSnapshotAsync(projectFilePath, _filePath, targetPath); - DocumentText = await DocumentSnapshot.GetTextAsync(); + DocumentText = await DocumentSnapshot.GetTextAsync(CancellationToken.None); RazorCodeActionRange = DocumentText.GetZeroWidthRange(razorCodeActionIndex); CSharpCodeActionRange = DocumentText.GetZeroWidthRange(csharpCodeActionIndex); diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCompletionBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCompletionBenchmark.cs index d35a168a36e..7fc0caba7bc 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCompletionBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCompletionBenchmark.cs @@ -75,7 +75,7 @@ public async Task SetupAsync() DocumentUri = new Uri(_filePath); DocumentSnapshot = await GetDocumentSnapshotAsync(projectFilePath, _filePath, targetPath); - DocumentText = await DocumentSnapshot.GetTextAsync(); + DocumentText = await DocumentSnapshot.GetTextAsync(CancellationToken.None); RazorPosition = DocumentText.GetPosition(razorCodeActionIndex); diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorDiagnosticsBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorDiagnosticsBenchmark.cs index 1733e5058fa..7071c1986f1 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorDiagnosticsBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorDiagnosticsBenchmark.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.CodeAnalysis.Razor.Diagnostics; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorDocumentMappingBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorDocumentMappingBenchmark.cs index c3371b04891..d656cfc4036 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorDocumentMappingBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorDocumentMappingBenchmark.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Microsoft.AspNetCore.Razor.Language; @@ -50,7 +51,7 @@ public async Task InitializeRazorCSharpFormattingAsync() DocumentSnapshot = await GetDocumentSnapshotAsync(projectFilePath, _filePath, targetPath); - var codeDocument = await DocumentSnapshot.GetGeneratedOutputAsync(); + var codeDocument = await DocumentSnapshot.GetGeneratedOutputAsync(CancellationToken.None); CSharpDocument = codeDocument.GetCSharpDocument(); } diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensScrollingBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensScrollingBenchmark.cs index 20bcd3f5505..216314a8876 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensScrollingBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorSemanticTokensScrollingBenchmark.cs @@ -51,7 +51,7 @@ public async Task InitializeRazorSemanticAsync() var documentSnapshot = await GetDocumentSnapshotAsync(ProjectFilePath, filePath, TargetPath); DocumentContext = new DocumentContext(documentUri, documentSnapshot, projectContext: null); - var text = await DocumentSnapshot.GetTextAsync().ConfigureAwait(false); + var text = await DocumentSnapshot.GetTextAsync(CancellationToken.None).ConfigureAwait(false); Range = VsLspFactory.CreateRange( start: (0, 0), end: (text.Lines.Count - 1, 0)); diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks.csproj b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks.csproj index 1010350dd29..0fae6de3c03 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks.csproj +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks.csproj @@ -7,7 +7,7 @@ true true false - true + true false @@ -43,6 +43,9 @@ TestServices\%(FileName)%(Extension) + + TestServices\%(FileName)%(Extension) + diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/ProjectSystem/BackgroundCodeGenerationBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/ProjectSystem/BackgroundCodeGenerationBenchmark.cs index 2d6dff5a590..e0d019e3bca 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/ProjectSystem/BackgroundCodeGenerationBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/ProjectSystem/BackgroundCodeGenerationBenchmark.cs @@ -59,6 +59,6 @@ private void SnapshotManager_Changed(object sender, ProjectChangeEventArgs e) var project = ProjectManager.GetLoadedProject(e.ProjectKey); var document = project.GetDocument(e.DocumentFilePath); - Tasks.Add(document.GetGeneratedOutputAsync()); + Tasks.Add(document.GetGeneratedOutputAsync(CancellationToken.None).AsTask()); } } diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Resources/Telerik/Kendo.Mvc.Examples.project.razor.json b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Resources/Telerik/Kendo.Mvc.Examples.project.razor.json index 00ca3271787..9104b499f89 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Resources/Telerik/Kendo.Mvc.Examples.project.razor.json +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Resources/Telerik/Kendo.Mvc.Examples.project.razor.json @@ -1,5 +1,5 @@ { - "__Version": 5, + "__Version": 6, "ProjectKey": "C:\\Users\\admin\\location\\Kendo.Mvc.Examples\\obj\\Debug\\net7.0\\", "FilePath": "C:\\Users\\admin\\location\\Kendo.Mvc.Examples\\Kendo.Mvc.Examples.csproj", "Configuration": { diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Resources/project.razor.json b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Resources/project.razor.json index 2521817d3c0..d1525d22f3a 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Resources/project.razor.json +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Resources/project.razor.json @@ -1,5 +1,5 @@ { - "__Version": 5, + "__Version": 6, "ProjectKey": "C:\\Users\\admin\\location\\blazorserver\\obj\\Debug\\net7.0\\", "FilePath": "C:\\Users\\admin\\location\\blazorserver\\blazorserver.csproj", "Configuration": { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace/RazorWorkspaceListenerBase.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace/RazorWorkspaceListenerBase.cs index 7ab3ecf2a4b..753ad07c46d 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace/RazorWorkspaceListenerBase.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace/RazorWorkspaceListenerBase.cs @@ -279,7 +279,7 @@ private static async Task ProcessWorkCoreAsync(ImmutableArray work, Stream private static async Task ReportUpdateProjectAsync(Stream stream, Project project, ILogger logger, CancellationToken cancellationToken) { logger.LogTrace("Serializing information for {projectId}", project.Id); - var projectInfo = await RazorProjectInfoFactory.ConvertAsync(project, logger, cancellationToken).ConfigureAwait(false); + var projectInfo = await RazorProjectInfoFactory.ConvertAsync(project, cancellationToken).ConfigureAwait(false); if (projectInfo is null) { logger.LogTrace("Skipped writing data for {projectId}", project.Id); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AdhocWorkspaceFactory.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AdhocWorkspaceFactory.cs deleted file mode 100644 index b279b5959db..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AdhocWorkspaceFactory.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Collections.Immutable; -using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Razor.Workspaces; - -namespace Microsoft.AspNetCore.Razor.LanguageServer; - -internal sealed class AdhocWorkspaceFactory(IHostServicesProvider hostServicesProvider) : IAdhocWorkspaceFactory -{ - public AdhocWorkspace Create(params IWorkspaceService[] workspaceServices) - { - workspaceServices ??= []; - - var fallbackServices = hostServicesProvider.GetServices(); - var services = AdhocServices.Create( - workspaceServices: workspaceServices.ToImmutableArray(), - languageServices: ImmutableArray.Empty, - fallbackServices); - - return new AdhocWorkspace(services); - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs index d71c507cb57..bdad71ad41b 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/AutoInsert/OnAutoInsertEndpoint.cs @@ -143,7 +143,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V // Therefore we are just going to no-op if the user has turned off on type formatting. Maybe one day we can make this // smarter, but at least the user can always turn the setting back on, type their "///", and turn it back off, without // having to restart VS. Not the worst compromise (hopefully!) - if (!_optionsMonitor.CurrentValue.FormatOnType) + if (!_optionsMonitor.CurrentValue.Formatting.IsOnTypeEnabled()) { Logger.LogInformation($"Formatting on type disabled, so auto insert is a no-op for C#."); return SpecializedTasks.Null(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CLaSPTelemetryService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CLaSPTelemetryService.cs new file mode 100644 index 00000000000..b0ad219152c --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CLaSPTelemetryService.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Diagnostics; +using Microsoft.AspNetCore.Razor.Telemetry; +using Microsoft.CommonLanguageServerProtocol.Framework; + +namespace Microsoft.AspNetCore.Razor.LanguageServer; + +internal sealed class CLaSPTelemetryService(ITelemetryReporter telemetryReporter) : AbstractTelemetryService +{ + public override AbstractRequestScope CreateRequestScope(string lspMethodName) + => new RequestTelemetryScope(lspMethodName, telemetryReporter); + + private sealed class RequestTelemetryScope : AbstractRequestScope + { + private readonly Stopwatch _stopWatch; + private readonly ITelemetryReporter _telemetryReporter; + private TelemetryResult _result = TelemetryResult.Succeeded; + private TimeSpan _queuedDuration; + + public RequestTelemetryScope(string lspMethodName, ITelemetryReporter telemetryReporter) : base(lspMethodName) + { + _stopWatch = Stopwatch.StartNew(); + _telemetryReporter = telemetryReporter; + } + + public override void Dispose() + { + var requestDuration = _stopWatch.Elapsed; + _telemetryReporter.ReportRequestTiming(Name, Language, _queuedDuration, requestDuration, _result); + } + + public override void RecordCancellation() + { + _result = TelemetryResult.Cancelled; + } + + public override void RecordException(Exception _) + { + _result = TelemetryResult.Failed; + } + + public override void RecordExecutionStart() + { + _queuedDuration = _stopWatch.Elapsed; + } + + public override void RecordWarning(string message) + { + _result = TelemetryResult.Failed; + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionProvider.cs index 429d8aff10d..92fc6052c54 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/DefaultCSharpCodeActionProvider.cs @@ -67,7 +67,7 @@ public Task> ProvideAsync( } var tree = context.CodeDocument.GetSyntaxTree(); - var node = tree.Root.FindInnermostNode(context.Location.AbsoluteIndex); + var node = tree.Root.FindInnermostNode(context.StartLocation.AbsoluteIndex); var isInImplicitExpression = node?.AncestorsAndSelf().Any(n => n is CSharpImplicitExpressionSyntax) ?? false; var allowList = isInImplicitExpression diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/TypeAccessibilityCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/TypeAccessibilityCodeActionProvider.cs index 679eb58c843..b8850127a6d 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/TypeAccessibilityCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/TypeAccessibilityCodeActionProvider.cs @@ -215,7 +215,7 @@ static bool TryGetOwner(RazorCodeActionContext context, [NotNullWhen(true)] out return false; } - owner = syntaxTree.Root.FindInnermostNode(context.Location.AbsoluteIndex); + owner = syntaxTree.Root.FindInnermostNode(context.StartLocation.AbsoluteIndex); if (owner is null) { Debug.Fail("Owner should never be null."); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/UnformattedRemappingCSharpCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/UnformattedRemappingCSharpCodeActionResolver.cs index 170b4cd3417..9514184521e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/UnformattedRemappingCSharpCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CSharp/UnformattedRemappingCSharpCodeActionResolver.cs @@ -70,7 +70,7 @@ public async override Task ResolveAsync( return codeAction; } - var codeDocument = await documentContext.Snapshot.GetGeneratedOutputAsync().ConfigureAwait(false); + var codeDocument = await documentContext.Snapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false); if (codeDocument.IsUnsupported()) { return codeAction; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CodeActionEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CodeActionEndpoint.cs index 1914b6f87d7..49375d1505c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CodeActionEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/CodeActionEndpoint.cs @@ -65,7 +65,7 @@ internal sealed class CodeActionEndpoint( return null; } - var razorCodeActionContext = await GenerateRazorCodeActionContextAsync(request, documentContext.Snapshot).ConfigureAwait(false); + var razorCodeActionContext = await GenerateRazorCodeActionContextAsync(request, documentContext.Snapshot, cancellationToken).ConfigureAwait(false); if (razorCodeActionContext is null) { return null; @@ -134,15 +134,18 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V } // internal for testing - internal async Task GenerateRazorCodeActionContextAsync(VSCodeActionParams request, IDocumentSnapshot documentSnapshot) + internal async Task GenerateRazorCodeActionContextAsync( + VSCodeActionParams request, + IDocumentSnapshot documentSnapshot, + CancellationToken cancellationToken) { - var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); + var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false); if (codeDocument.IsUnsupported()) { return null; } - var sourceText = await documentSnapshot.GetTextAsync().ConfigureAwait(false); + var sourceText = codeDocument.Source.Text; // VS Provides `CodeActionParams.Context.SelectionRange` in addition to // `CodeActionParams.Range`. The `SelectionRange` is relative to where the @@ -158,16 +161,22 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V request.Range = vsCodeActionContext.SelectionRange; } - if (!sourceText.TryGetSourceLocation(request.Range.Start, out var location)) + if (!sourceText.TryGetSourceLocation(request.Range.Start, out var startLocation)) { return null; } + if (!sourceText.TryGetSourceLocation(request.Range.End, out var endLocation)) + { + endLocation = startLocation; + } + var context = new RazorCodeActionContext( request, documentSnapshot, codeDocument, - location, + startLocation, + endLocation, sourceText, _languageServerFeatureOptions.SupportsFileManipulation, _supportsCodeActionResolve); @@ -177,7 +186,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V private async Task> GetDelegatedCodeActionsAsync(DocumentContext documentContext, RazorCodeActionContext context, Guid correlationId, CancellationToken cancellationToken) { - var languageKind = context.CodeDocument.GetLanguageKind(context.Location.AbsoluteIndex, rightAssociative: false); + var languageKind = context.CodeDocument.GetLanguageKind(context.StartLocation.AbsoluteIndex, rightAssociative: false); // No point delegating if we're in a Razor context if (languageKind == RazorLanguageKind.Razor) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionProvider.cs index b5d22fabb6c..16f7dd30960 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Html/DefaultHtmlCodeActionProvider.cs @@ -49,7 +49,7 @@ public static async Task RemapAndFixHtmlCodeActionEditAsync(IEditMappingService if (codeAction.Edit.TryGetTextDocumentEdits(out var documentEdits)) { - var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); + var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false); var htmlSourceText = codeDocument.GetHtmlSourceText(); foreach (var edit in documentEdits) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Models/ExtractToComponentCodeActionParams.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Models/ExtractToComponentCodeActionParams.cs new file mode 100644 index 00000000000..70ef4ca50b6 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Models/ExtractToComponentCodeActionParams.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text.Json.Serialization; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; + +internal sealed class ExtractToComponentCodeActionParams +{ + [JsonPropertyName("uri")] + public required Uri Uri { get; set; } + + [JsonPropertyName("start")] + public int Start { get; set; } + + [JsonPropertyName("end")] + public int End { get; set; } + + [JsonPropertyName("namespace")] + public required string Namespace { get; set; } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/AddUsingsCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/AddUsingsCodeActionResolver.cs index f0516a53585..65e6a300aa0 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/AddUsingsCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/AddUsingsCodeActionResolver.cs @@ -45,7 +45,7 @@ internal sealed class AddUsingsCodeActionResolver(IDocumentContextFactory docume var documentSnapshot = documentContext.Snapshot; - var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); + var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false); if (codeDocument.IsUnsupported()) { return null; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ComponentAccessibilityCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ComponentAccessibilityCodeActionProvider.cs index 8a551f300b3..cf88e1adaa4 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ComponentAccessibilityCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ComponentAccessibilityCodeActionProvider.cs @@ -27,7 +27,7 @@ internal sealed class ComponentAccessibilityCodeActionProvider : IRazorCodeActio public async Task> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken) { // Locate cursor - var node = context.CodeDocument.GetSyntaxTree().Root.FindInnermostNode(context.Location.AbsoluteIndex); + var node = context.CodeDocument.GetSyntaxTree().Root.FindInnermostNode(context.StartLocation.AbsoluteIndex); if (node is null) { return []; @@ -44,7 +44,7 @@ public async Task> ProvideAsync(RazorC return []; } - if (context.Location.AbsoluteIndex < startTag.SpanStart) + if (context.StartLocation.AbsoluteIndex < startTag.SpanStart) { // Cursor is before the start tag, so we shouldn't show a light bulb. This can happen // in cases where the cursor is in whitespace at the beginning of the document diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionProvider.cs index 297ac7c24c1..8631bfab4f5 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionProvider.cs @@ -42,7 +42,7 @@ public Task> ProvideAsync(RazorCodeAct return SpecializedTasks.EmptyImmutableArray(); } - var owner = syntaxTree.Root.FindInnermostNode(context.Location.AbsoluteIndex); + var owner = syntaxTree.Root.FindInnermostNode(context.StartLocation.AbsoluteIndex); if (owner is null) { _logger.LogWarning($"Owner should never be null."); @@ -84,7 +84,7 @@ public Task> ProvideAsync(RazorCodeAct } // Do not provide code action if the cursor is inside the code block - if (context.Location.AbsoluteIndex > csharpCodeBlockNode.SpanStart) + if (context.StartLocation.AbsoluteIndex > csharpCodeBlockNode.SpanStart) { return SpecializedTasks.EmptyImmutableArray(); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs index 74a832a0f2b..391e0340dd5 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs @@ -65,7 +65,7 @@ internal sealed class ExtractToCodeBehindCodeActionResolver( return null; } - var codeBehindPath = GenerateCodeBehindPath(path); + var codeBehindPath = FileUtilities.GenerateUniquePath(path, $"{Path.GetExtension(path)}.cs"); // VS Code in Windows expects path to start with '/' var updatedCodeBehindPath = _languageServerFeatureOptions.ReturnCodeActionAndRenamePathsWithPrefixedSlash && !codeBehindPath.StartsWith("/") @@ -111,33 +111,6 @@ internal sealed class ExtractToCodeBehindCodeActionResolver( }; } - /// - /// Generate a file path with adjacent to our input path that has the - /// correct codebehind extension, using numbers to differentiate from - /// any collisions. - /// - /// The origin file path. - /// A non-existent file path with the same base name and a codebehind extension. - private static string GenerateCodeBehindPath(string path) - { - var baseFileName = Path.GetFileNameWithoutExtension(path); - var extension = Path.GetExtension(path); - var directoryName = Path.GetDirectoryName(path).AssumeNotNull(); - - var n = 0; - string codeBehindPath; - do - { - var identifier = n > 0 ? n.ToString(CultureInfo.InvariantCulture) : string.Empty; // Make it look nice - - codeBehindPath = Path.Combine(directoryName, $"{baseFileName}{identifier}{extension}.cs"); - n++; - } - while (File.Exists(codeBehindPath)); - - return codeBehindPath; - } - private async Task GenerateCodeBehindClassAsync(IProjectSnapshot project, Uri codeBehindUri, string className, string namespaceName, string contents, RazorCodeDocument razorCodeDocument, CancellationToken cancellationToken) { using var _ = StringBuilderPool.GetPooledObject(out var builder); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToComponentCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToComponentCodeActionProvider.cs new file mode 100644 index 00000000000..f4c2ca7a5c2 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToComponentCodeActionProvider.cs @@ -0,0 +1,241 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ICSharpCode.Decompiler.CSharp.Syntax; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.AspNetCore.Razor.Threading; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor; + +internal sealed class ExtractToComponentCodeActionProvider() : IRazorCodeActionProvider +{ + public Task> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken) + { + if (!context.SupportsFileCreation) + { + return SpecializedTasks.EmptyImmutableArray(); + } + + if (!FileKinds.IsComponent(context.CodeDocument.GetFileKind())) + { + return SpecializedTasks.EmptyImmutableArray(); + } + + var syntaxTree = context.CodeDocument.GetSyntaxTree(); + if (syntaxTree?.Root is null) + { + return SpecializedTasks.EmptyImmutableArray(); + } + + if (!TryGetNamespace(context.CodeDocument, out var @namespace)) + { + return SpecializedTasks.EmptyImmutableArray(); + } + + var (startNode, endNode) = GetStartAndEndElements(context, syntaxTree); + if (startNode is null || endNode is null) + { + return SpecializedTasks.EmptyImmutableArray(); + } + + // If the selection begins in @code don't offer to extract. The inserted + // component would not be valid since it's inserted at the starting point + if (RazorSyntaxFacts.IsInCodeBlock(startNode)) + { + return SpecializedTasks.EmptyImmutableArray(); + } + + var actionParams = CreateActionParams(context, startNode, endNode, @namespace); + + var resolutionParams = new RazorCodeActionResolutionParams() + { + Action = LanguageServerConstants.CodeActions.ExtractToNewComponentAction, + Language = LanguageServerConstants.CodeActions.Languages.Razor, + Data = actionParams, + }; + + var codeAction = RazorCodeActionFactory.CreateExtractToComponent(resolutionParams); + return Task.FromResult>([codeAction]); + } + + private static (SyntaxNode? Start, SyntaxNode? End) GetStartAndEndElements(RazorCodeActionContext context, RazorSyntaxTree syntaxTree) + { + var owner = syntaxTree.Root.FindInnermostNode(context.StartLocation.AbsoluteIndex, includeWhitespace: true); + if (owner is null) + { + return (null, null); + } + + var startElementNode = owner.FirstAncestorOrSelf(IsBlockNode); + + if (startElementNode is null || LocationInvalid(context.StartLocation, startElementNode)) + { + return (null, null); + } + + var endElementNode = context.StartLocation == context.EndLocation + ? startElementNode + : GetEndElementNode(context, syntaxTree); + + return (startElementNode, endElementNode); + + static bool LocationInvalid(SourceLocation location, SyntaxNode node) + { + // Make sure to test for cases where selection + // is inside of a markup tag such as

hello$ there

+ if (node is MarkupElementSyntax markupElement) + { + return location.AbsoluteIndex > markupElement.StartTag.Span.End && + location.AbsoluteIndex < markupElement.EndTag.SpanStart; + } + + return !node.Span.Contains(location.AbsoluteIndex); + } + } + + private static SyntaxNode? GetEndElementNode(RazorCodeActionContext context, RazorSyntaxTree syntaxTree) + { + var endOwner = syntaxTree.Root.FindInnermostNode(context.EndLocation.AbsoluteIndex, includeWhitespace: true); + if (endOwner is null) + { + return null; + } + + // Correct selection to include the current node if the selection ends immediately after a closing tag. + if (endOwner is MarkupTextLiteralSyntax + && endOwner.ContainsOnlyWhitespace() + && endOwner.TryGetPreviousSibling(out var previousSibling)) + { + endOwner = previousSibling; + } + + return endOwner.FirstAncestorOrSelf(IsBlockNode); + } + + private static bool IsBlockNode(SyntaxNode node) + => node.Kind is + SyntaxKind.MarkupElement or + SyntaxKind.MarkupTagHelperElement or + SyntaxKind.CSharpCodeBlock; + + private static ExtractToComponentCodeActionParams CreateActionParams( + RazorCodeActionContext context, + SyntaxNode startNode, + SyntaxNode endNode, + string @namespace) + { + var selectionSpan = AreSiblings(startNode, endNode) + ? TextSpan.FromBounds(startNode.Span.Start, endNode.Span.End) + : GetEncompassingTextSpan(startNode, endNode); + + return new ExtractToComponentCodeActionParams + { + Uri = context.Request.TextDocument.Uri, + Start = selectionSpan.Start, + End = selectionSpan.End, + Namespace = @namespace + }; + } + + private static TextSpan GetEncompassingTextSpan(SyntaxNode startNode, SyntaxNode endNode) + { + // Find a valid node that encompasses both the start and the end to + // become the selection. + var commonAncestor = endNode.Span.Contains(startNode.Span) + ? endNode + : startNode; + + while (commonAncestor is MarkupElementSyntax or + MarkupTagHelperAttributeSyntax or + MarkupBlockSyntax) + { + if (commonAncestor.Span.Contains(startNode.Span) && + commonAncestor.Span.Contains(endNode.Span)) + { + break; + } + + commonAncestor = commonAncestor.Parent; + } + + // If walking up the tree was required then make sure to reduce + // selection back down to minimal nodes needed. + // For example: + //
+ // {|result: + // {|selection:

Some text

+ //
+ // + //

More text

+ //
+ // + // |}|} + //
+ if (commonAncestor != startNode && + commonAncestor != endNode) + { + SyntaxNode? modifiedStart = null, modifiedEnd = null; + foreach (var child in commonAncestor.ChildNodes().Where(static node => node.Kind == SyntaxKind.MarkupElement)) + { + if (child.Span.Contains(startNode.Span)) + { + modifiedStart = child; + if (modifiedEnd is not null) + break; // Exit if we've found both + } + + if (child.Span.Contains(endNode.Span)) + { + modifiedEnd = child; + if (modifiedStart is not null) + break; // Exit if we've found both + } + } + + if (modifiedStart is not null && modifiedEnd is not null) + { + return TextSpan.FromBounds(modifiedStart.Span.Start, modifiedEnd.Span.End); + } + } + + // Fallback to extracting the nearest common ancestor span + return commonAncestor.Span; + } + + private static bool AreSiblings(SyntaxNode? node1, SyntaxNode? node2) + { + if (node1 is null) + { + return false; + } + + if (node2 is null) + { + return false; + } + + return node1.Parent == node2.Parent; + } + + private static bool TryGetNamespace(RazorCodeDocument codeDocument, [NotNullWhen(returnValue: true)] out string? @namespace) + // If the compiler can't provide a computed namespace it will fallback to "__GeneratedComponent" or + // similar for the NamespaceNode. This would end up with extracting to a wrong namespace + // and causing compiler errors. Avoid offering this refactoring if we can't accurately get a + // good namespace to extract to + => codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out @namespace); +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToComponentCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToComponentCodeActionResolver.cs new file mode 100644 index 00000000000..f4a43384c2f --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToComponentCodeActionResolver.cs @@ -0,0 +1,136 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.AspNetCore.Razor.Utilities; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.Formatting; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor; + +internal sealed class ExtractToComponentCodeActionResolver( + IDocumentContextFactory documentContextFactory, + LanguageServerFeatureOptions languageServerFeatureOptions) : IRazorCodeActionResolver +{ + private readonly IDocumentContextFactory _documentContextFactory = documentContextFactory; + private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; + + public string Action => LanguageServerConstants.CodeActions.ExtractToNewComponentAction; + + public async Task ResolveAsync(JsonElement data, CancellationToken cancellationToken) + { + if (data.ValueKind == JsonValueKind.Undefined) + { + return null; + } + + var actionParams = JsonSerializer.Deserialize(data.GetRawText()); + if (actionParams is null) + { + return null; + } + + if (!_documentContextFactory.TryCreate(actionParams.Uri, out var documentContext)) + { + return null; + } + + var componentDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + if (componentDocument.IsUnsupported()) + { + return null; + } + + var text = componentDocument.Source.Text; + var path = FilePathNormalizer.Normalize(actionParams.Uri.GetAbsoluteOrUNCPath()); + var directoryName = Path.GetDirectoryName(path).AssumeNotNull(); + var templatePath = Path.Combine(directoryName, "Component.razor"); + var componentPath = FileUtilities.GenerateUniquePath(templatePath, ".razor"); + var componentName = Path.GetFileNameWithoutExtension(componentPath); + + // VS Code in Windows expects path to start with '/' + componentPath = _languageServerFeatureOptions.ReturnCodeActionAndRenamePathsWithPrefixedSlash && !componentPath.StartsWith('/') + ? '/' + componentPath + : componentPath; + + var newComponentUri = new UriBuilder + { + Scheme = Uri.UriSchemeFile, + Path = componentPath, + Host = string.Empty, + }.Uri; + + using var _ = StringBuilderPool.GetPooledObject(out var builder); + + var syntaxTree = componentDocument.GetSyntaxTree(); + + // Right now this includes all the usings in the original document. + // https://github.com/dotnet/razor/issues/11025 tracks reducing to only the required set. + var usingDirectives = syntaxTree.GetUsingDirectives(); + foreach (var usingDirective in usingDirectives) + { + builder.AppendLine(usingDirective.ToFullString()); + } + + // If any using directives were added, add a newline before the extracted content. + if (usingDirectives.Length > 0) + { + builder.AppendLine(); + } + + var span = TextSpan.FromBounds(actionParams.Start, actionParams.End); + FormattingUtilities.NaivelyUnindentSubstring(text, span, builder); + + var removeRange = text.GetRange(actionParams.Start, actionParams.End); + + var documentChanges = new SumType[] + { + new CreateFile { Uri = newComponentUri }, + new TextDocumentEdit + { + TextDocument = new OptionalVersionedTextDocumentIdentifier { Uri = actionParams.Uri }, + Edits = + [ + new TextEdit + { + NewText = $"<{componentName} />", + Range = removeRange, + } + ], + }, + new TextDocumentEdit + { + TextDocument = new OptionalVersionedTextDocumentIdentifier { Uri = newComponentUri }, + Edits = + [ + new TextEdit + { + NewText = builder.ToString(), + Range = VsLspFactory.DefaultRange, + } + ], + } + }; + + return new WorkspaceEdit + { + DocumentChanges = documentChanges, + }; + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionProvider.cs index 366b544d783..bd048ecfc5c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionProvider.cs @@ -30,7 +30,7 @@ public Task> ProvideAsync(RazorCodeAct } var syntaxTree = context.CodeDocument.GetSyntaxTree(); - var owner = syntaxTree.Root.FindToken(context.Location.AbsoluteIndex).Parent.AssumeNotNull(); + var owner = syntaxTree.Root.FindToken(context.StartLocation.AbsoluteIndex).Parent.AssumeNotNull(); if (IsGenerateEventHandlerValid(owner, out var methodName, out var eventName)) { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs index 026cf9c36ec..8df517e9037 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/GenerateMethodCodeActionResolver.cs @@ -215,7 +215,7 @@ private async Task GenerateMethodInCodeBlockAsync( var formattedChange = await _razorFormattingService.TryGetCSharpCodeActionEditAsync( documentContext, - result.SelectAsArray(code.Source.Text.GetTextChange), + result.SelectAsArray(code.GetCSharpSourceText().GetTextChange), formattingOptions, cancellationToken).ConfigureAwait(false); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/RazorCodeActionFactory.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/RazorCodeActionFactory.cs index 832f3c2a351..f9f4a94cd27 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/RazorCodeActionFactory.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/RazorCodeActionFactory.cs @@ -14,6 +14,7 @@ internal static class RazorCodeActionFactory private readonly static Guid s_fullyQualifyComponentTelemetryId = new("3d9abe36-7d10-4e08-8c18-ad88baa9a923"); private readonly static Guid s_createComponentFromTagTelemetryId = new("a28e0baa-a4d5-4953-a817-1db586035841"); private readonly static Guid s_createExtractToCodeBehindTelemetryId = new("f63167f7-fdc6-450f-8b7b-b240892f4a27"); + private readonly static Guid s_createExtractToComponentTelemetryId = new("af67b0a3-f84b-4808-97a7-b53e85b22c64"); private readonly static Guid s_generateMethodTelemetryId = new("c14fa003-c752-45fc-bb29-3a123ae5ecef"); private readonly static Guid s_generateAsyncMethodTelemetryId = new("9058ca47-98e2-4f11-bf7c-a16a444dd939"); @@ -67,6 +68,19 @@ public static RazorVSInternalCodeAction CreateExtractToCodeBehind(RazorCodeActio return codeAction; } + public static RazorVSInternalCodeAction CreateExtractToComponent(RazorCodeActionResolutionParams resolutionParams) + { + var title = SR.ExtractTo_Component_Title; + var data = JsonSerializer.SerializeToElement(resolutionParams); + var codeAction = new RazorVSInternalCodeAction() + { + Title = title, + Data = data, + TelemetryId = s_createExtractToComponentTelemetryId, + }; + return codeAction; + } + public static RazorVSInternalCodeAction CreateGenerateMethod(Uri uri, string methodName, string eventName) { var @params = new GenerateMethodCodeActionParams diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/RazorCodeActionContext.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/RazorCodeActionContext.cs index 0fc4c9e384f..26160e318b1 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/RazorCodeActionContext.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/RazorCodeActionContext.cs @@ -13,7 +13,8 @@ internal sealed record class RazorCodeActionContext( VSCodeActionParams Request, IDocumentSnapshot DocumentSnapshot, RazorCodeDocument CodeDocument, - SourceLocation Location, + SourceLocation StartLocation, + SourceLocation EndLocation, SourceText SourceText, bool SupportsFileCreation, bool SupportsCodeActionResolve); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs index 35ffb1fba6a..900b1fcc39b 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionListProvider.cs @@ -39,7 +39,7 @@ public DelegatedCompletionListProvider( IClientConnection clientConnection, CompletionListCache completionListCache) { - _responseRewriters = responseRewriters.OrderBy(rewriter => rewriter.Order).ToImmutableArray(); + _responseRewriters = responseRewriters.OrderByAsArray(static x => x.Order); _documentMappingService = documentMappingService; _clientConnection = clientConnection; _completionListCache = completionListCache; @@ -57,7 +57,9 @@ public DelegatedCompletionListProvider( Guid correlationId, CancellationToken cancellationToken) { - var positionInfo = await _documentMappingService.GetPositionInfoAsync(documentContext, absoluteIndex, cancellationToken).ConfigureAwait(false); + var positionInfo = await _documentMappingService + .GetPositionInfoAsync(documentContext, absoluteIndex, cancellationToken) + .ConfigureAwait(false); if (positionInfo.LanguageKind == RazorLanguageKind.Razor) { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionEndpoint.cs index fea284d3d97..298e83b4bb7 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionEndpoint.cs @@ -36,6 +36,11 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V { ResolveProvider = true, TriggerCharacters = _completionListProvider.AggregateTriggerCharacters.ToArray(), + // This is the intersection of C# and HTML commit characters. + // We need to specify it so that platform can correctly calculate ApplicableToSpan in + // https://devdiv.visualstudio.com/DevDiv/_git/VSLanguageServerClient?path=/src/product/RemoteLanguage/Impl/Features/Completion/AsyncCompletionSource.cs&version=GBdevelop&line=855&lineEnd=855&lineStartColumn=9&lineEndColumn=49&lineStyle=plain&_a=contents + // This is needed to fix https://github.com/dotnet/razor/issues/10787 in particular + AllCommitCharacters = [" ", ">", ";", "="] }; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionItemResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionItemResolver.cs index 6cc6fd21f6e..3e292cb856a 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionItemResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionItemResolver.cs @@ -5,26 +5,19 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.Tooltip; +using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.Completion; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Tooltip; using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.VisualStudio.Text.Adornments; namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; -internal class RazorCompletionItemResolver : CompletionItemResolver +internal class RazorCompletionItemResolver(IProjectSnapshotManager projectManager) : CompletionItemResolver { - private readonly LSPTagHelperTooltipFactory _lspTagHelperTooltipFactory; - private readonly VSLSPTagHelperTooltipFactory _vsLspTagHelperTooltipFactory; - - public RazorCompletionItemResolver( - LSPTagHelperTooltipFactory lspTagHelperTooltipFactory, - VSLSPTagHelperTooltipFactory vsLspTagHelperTooltipFactory) - { - _lspTagHelperTooltipFactory = lspTagHelperTooltipFactory; - _vsLspTagHelperTooltipFactory = vsLspTagHelperTooltipFactory; - } + private readonly IProjectSnapshotManager _projectManager = projectManager; public override async Task ResolveAsync( VSInternalCompletionItem completionItem, @@ -109,11 +102,11 @@ public RazorCompletionItemResolver( if (useDescriptionProperty) { - _vsLspTagHelperTooltipFactory.TryCreateTooltip(descriptionInfo, out tagHelperClassifiedTextTooltip); + ClassifiedTagHelperTooltipFactory.TryCreateTooltip(descriptionInfo, out tagHelperClassifiedTextTooltip); } else { - _lspTagHelperTooltipFactory.TryCreateTooltip(descriptionInfo, documentationKind, out tagHelperMarkupTooltip); + MarkupTagHelperTooltipFactory.TryCreateTooltip(descriptionInfo, documentationKind, out tagHelperMarkupTooltip); } break; @@ -128,11 +121,15 @@ public RazorCompletionItemResolver( if (useDescriptionProperty) { - tagHelperClassifiedTextTooltip = await _vsLspTagHelperTooltipFactory.TryCreateTooltipAsync(razorCompletionResolveContext.FilePath, descriptionInfo, cancellationToken).ConfigureAwait(false); + tagHelperClassifiedTextTooltip = await ClassifiedTagHelperTooltipFactory + .TryCreateTooltipAsync(razorCompletionResolveContext.FilePath, descriptionInfo, _projectManager.GetQueryOperations(), cancellationToken) + .ConfigureAwait(false); } else { - tagHelperMarkupTooltip = await _lspTagHelperTooltipFactory.TryCreateTooltipAsync(razorCompletionResolveContext.FilePath, descriptionInfo, documentationKind, cancellationToken).ConfigureAwait(false); + tagHelperMarkupTooltip = await MarkupTagHelperTooltipFactory + .TryCreateTooltipAsync(razorCompletionResolveContext.FilePath, descriptionInfo, _projectManager.GetQueryOperations(), documentationKind, cancellationToken) + .ConfigureAwait(false); } break; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultHostServicesProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultHostServicesProvider.cs index 355e9afe62b..95642353d8e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultHostServicesProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultHostServicesProvider.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Razor.Workspaces; namespace Microsoft.AspNetCore.Razor.LanguageServer; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultRazorConfigurationService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultRazorConfigurationService.cs index 6010dde3ebd..d7d3abcab15 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultRazorConfigurationService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DefaultRazorConfigurationService.cs @@ -96,10 +96,10 @@ internal RazorLSPOptions BuildOptions(JsonObject[] result) } else { - ExtractVSCodeOptions(result, out var enableFormatting, out var autoClosingTags, out var commitElementsWithSpace, out var codeBlockBraceOnNextLine); + ExtractVSCodeOptions(result, out var formatting, out var autoClosingTags, out var commitElementsWithSpace, out var codeBlockBraceOnNextLine); return RazorLSPOptions.Default with { - EnableFormatting = enableFormatting, + Formatting = formatting, AutoClosingTags = autoClosingTags, CommitElementsWithSpace = commitElementsWithSpace, CodeBlockBraceOnNextLine = codeBlockBraceOnNextLine @@ -109,7 +109,7 @@ internal RazorLSPOptions BuildOptions(JsonObject[] result) private void ExtractVSCodeOptions( JsonObject[] result, - out bool enableFormatting, + out FormattingFlags formatting, out bool autoClosingTags, out bool commitElementsWithSpace, out bool codeBlockBraceOnNextLine) @@ -117,7 +117,7 @@ private void ExtractVSCodeOptions( var razor = result[0]; var html = result[1]; - enableFormatting = RazorLSPOptions.Default.EnableFormatting; + formatting = RazorLSPOptions.Default.Formatting; autoClosingTags = RazorLSPOptions.Default.AutoClosingTags; codeBlockBraceOnNextLine = RazorLSPOptions.Default.CodeBlockBraceOnNextLine; // Deliberately not using the "default" here because we want a different default for VS Code, as @@ -130,7 +130,15 @@ private void ExtractVSCodeOptions( if (parsedFormat.TryGetPropertyValue("enable", out var parsedEnableFormatting) && parsedEnableFormatting is not null) { - enableFormatting = GetObjectOrDefault(parsedEnableFormatting, enableFormatting); + var formattingEnabled = GetObjectOrDefault(parsedEnableFormatting, formatting.IsEnabled()); + if (formattingEnabled) + { + formatting |= FormattingFlags.Enabled; + } + else + { + formatting = FormattingFlags.Disabled; + } } if (parsedFormat.TryGetPropertyValue("codeBlockBraceOnNextLine", out var parsedCodeBlockBraceOnNextLine) && diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Definition/DefinitionEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Definition/DefinitionEndpoint.cs index dd22142c0ef..44014b47563 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Definition/DefinitionEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Definition/DefinitionEndpoint.cs @@ -5,17 +5,19 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; using Microsoft.AspNetCore.Razor.Threading; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.GoToDefinition; using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.VisualStudio.LanguageServer.Protocol; -using DefinitionResult = Microsoft.VisualStudio.LanguageServer.Protocol.SumType< +using DefinitionResult = System.Nullable; + Microsoft.VisualStudio.LanguageServer.Protocol.DocumentLink[]>>; namespace Microsoft.AspNetCore.Razor.LanguageServer.Definition; @@ -23,10 +25,11 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Definition; internal sealed class DefinitionEndpoint( IRazorComponentDefinitionService componentDefinitionService, IDocumentMappingService documentMappingService, + IProjectSnapshotManager projectManager, LanguageServerFeatureOptions languageServerFeatureOptions, IClientConnection clientConnection, ILoggerFactory loggerFactory) - : AbstractRazorDelegatingEndpoint( + : AbstractRazorDelegatingEndpoint( languageServerFeatureOptions, documentMappingService, clientConnection, @@ -34,6 +37,7 @@ internal sealed class DefinitionEndpoint( { private readonly IRazorComponentDefinitionService _componentDefinitionService = componentDefinitionService; private readonly IDocumentMappingService _documentMappingService = documentMappingService; + private readonly IProjectSnapshotManager _projectManager = projectManager; protected override bool PreferCSharpOverHtmlIfPossible => true; @@ -46,7 +50,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V serverCapabilities.DefinitionProvider = new DefinitionOptions(); } - protected async override Task TryHandleAsync( + protected async override Task TryHandleAsync( TextDocumentPositionParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, @@ -62,7 +66,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V // If single server support is on, then we ignore attributes, as they are better handled by delegating to Roslyn return await _componentDefinitionService - .GetDefinitionAsync(documentContext.Snapshot, positionInfo, ignoreAttributes: SingleServerSupport, cancellationToken) + .GetDefinitionAsync(documentContext.Snapshot, positionInfo, _projectManager.GetQueryOperations(), ignoreAttributes: SingleServerSupport, cancellationToken) .ConfigureAwait(false); } @@ -84,30 +88,28 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V positionInfo.LanguageKind)); } - protected async override Task HandleDelegatedResponseAsync( - DefinitionResult? response, + protected async override Task HandleDelegatedResponseAsync( + DefinitionResult response, TextDocumentPositionParams originalRequest, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) { - if (response is not DefinitionResult result) - { - return null; - } + var result = response.GetValueOrDefault().Value; - if (result.TryGetFirst(out var location)) + // Not using .TryGetXXX because this does the null check for us too + if (result is Location location) { (location.Uri, location.Range) = await _documentMappingService.MapToHostDocumentUriAndRangeAsync(location.Uri, location.Range, cancellationToken).ConfigureAwait(false); } - else if (result.TryGetSecond(out var locations)) + else if (result is Location[] locations) { foreach (var loc in locations) { (loc.Uri, loc.Range) = await _documentMappingService.MapToHostDocumentUriAndRangeAsync(loc.Uri, loc.Range, cancellationToken).ConfigureAwait(false); } } - else if (result.TryGetThird(out var links)) + else if (result is DocumentLink[] links) { foreach (var link in links) { @@ -118,6 +120,6 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V } } - return result; + return response; } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/DocumentPullDiagnosticsEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/DocumentPullDiagnosticsEndpoint.cs index eba0c7da011..ae6dbff95e5 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/DocumentPullDiagnosticsEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/DocumentPullDiagnosticsEndpoint.cs @@ -3,13 +3,18 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Components; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.AspNetCore.Razor.Telemetry; +using Microsoft.CodeAnalysis.Razor.Diagnostics; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Protocol.Diagnostics; @@ -25,6 +30,7 @@ internal class DocumentPullDiagnosticsEndpoint : IRazorRequestHandler _lastReporedProjectTagHelperCount = ImmutableDictionary.Empty; public DocumentPullDiagnosticsEndpoint( LanguageServerFeatureOptions languageServerFeatureOptions, @@ -72,7 +78,11 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalDocumentDiagno return null; } - var razorDiagnostics = await GetRazorDiagnosticsAsync(documentContext, cancellationToken).ConfigureAwait(false); + var documentSnapshot = documentContext.Snapshot; + + var razorDiagnostics = await GetRazorDiagnosticsAsync(documentSnapshot, cancellationToken).ConfigureAwait(false); + + await ReportRZ10012TelemetryAsync(documentContext, razorDiagnostics, cancellationToken).ConfigureAwait(false); var (csharpDiagnostics, htmlDiagnostics) = await GetHtmlCSharpDiagnosticsAsync(documentContext, correlationId, cancellationToken).ConfigureAwait(false); @@ -96,7 +106,9 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalDocumentDiagno { if (report.Diagnostics is not null) { - var mappedDiagnostics = await _translateDiagnosticsService.TranslateAsync(RazorLanguageKind.CSharp, report.Diagnostics, documentContext, cancellationToken).ConfigureAwait(false); + var mappedDiagnostics = await _translateDiagnosticsService + .TranslateAsync(RazorLanguageKind.CSharp, report.Diagnostics, documentSnapshot, cancellationToken) + .ConfigureAwait(false); report.Diagnostics = mappedDiagnostics; } @@ -110,7 +122,9 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalDocumentDiagno { if (report.Diagnostics is not null) { - var mappedDiagnostics = await _translateDiagnosticsService.TranslateAsync(RazorLanguageKind.Html, report.Diagnostics, documentContext, cancellationToken).ConfigureAwait(false); + var mappedDiagnostics = await _translateDiagnosticsService + .TranslateAsync(RazorLanguageKind.Html, report.Diagnostics, documentSnapshot, cancellationToken) + .ConfigureAwait(false); report.Diagnostics = mappedDiagnostics; } @@ -121,10 +135,10 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalDocumentDiagno return allDiagnostics.ToArray(); } - private static async Task GetRazorDiagnosticsAsync(DocumentContext documentContext, CancellationToken cancellationToken) + private static async Task GetRazorDiagnosticsAsync(IDocumentSnapshot documentSnapshot, CancellationToken cancellationToken) { - var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false); + var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false); + var sourceText = codeDocument.Source.Text; var csharpDocument = codeDocument.GetCSharpDocument(); var diagnostics = csharpDocument.Diagnostics; @@ -133,7 +147,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalDocumentDiagno return null; } - var convertedDiagnostics = RazorDiagnosticConverter.Convert(diagnostics, sourceText, documentContext.Snapshot); + var convertedDiagnostics = RazorDiagnosticConverter.Convert(diagnostics, sourceText, documentSnapshot); return [ @@ -160,4 +174,59 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalDocumentDiagno return (delegatedResponse.CSharpDiagnostics, delegatedResponse.HtmlDiagnostics); } + + /// + /// Reports telemetry for RZ10012 "Found markup element with unexpected name" to help track down potential issues + /// with taghelpers being discovered (or lack thereof) + /// + private async ValueTask ReportRZ10012TelemetryAsync(DocumentContext documentContext, VSInternalDiagnosticReport[]? razorDiagnostics, CancellationToken cancellationToken) + { + if (razorDiagnostics is null) + { + return; + } + + if (_telemetryReporter is null) + { + return; + } + + var relevantDiagnosticsCount = razorDiagnostics.Sum(CountDiagnostics); + if (relevantDiagnosticsCount == 0) + { + return; + } + + var tagHelpers = await documentContext.Project.GetTagHelpersAsync(cancellationToken).ConfigureAwait(false); + var tagHelperCount = tagHelpers.Count(); + var shouldReport = false; + + ImmutableInterlocked.AddOrUpdate( + ref _lastReporedProjectTagHelperCount, + documentContext.Project.Key, + (k) => + { + shouldReport = true; + return tagHelperCount; + }, + (k, currentValue) => + { + shouldReport = currentValue != tagHelperCount; + return tagHelperCount; + }); + + if (shouldReport) + { + _telemetryReporter.ReportEvent( + "RZ10012", + Severity.Low, + new("tagHelpers", tagHelperCount), + new("RZ10012errors", relevantDiagnosticsCount), + new("project", documentContext.Project.Key.Id)); + } + + static int CountDiagnostics(VSInternalDiagnosticReport report) + => report.Diagnostics?.Count(d => d.Code == ComponentDiagnosticFactory.UnexpectedMarkupElement.Id) + ?? 0; + } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/RazorDiagnosticsPublisher.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/RazorDiagnosticsPublisher.cs index 1f3320294e5..61925ca772e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/RazorDiagnosticsPublisher.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/RazorDiagnosticsPublisher.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.AspNetCore.Razor.Utilities; using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.Diagnostics; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; @@ -112,10 +113,10 @@ private async ValueTask ProcessBatchAsync(ImmutableArray item } } - private async Task PublishDiagnosticsAsync(IDocumentSnapshot document, CancellationToken token) + private async Task PublishDiagnosticsAsync(IDocumentSnapshot document, CancellationToken cancellationToken) { - var result = await document.GetGeneratedOutputAsync().ConfigureAwait(false); - var csharpDiagnostics = await GetCSharpDiagnosticsAsync(document, token).ConfigureAwait(false); + var result = await document.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false); + var csharpDiagnostics = await GetCSharpDiagnosticsAsync(document, cancellationToken).ConfigureAwait(false); var razorDiagnostics = result.GetCSharpDocument().Diagnostics; lock (_publishedDiagnostics) @@ -187,7 +188,7 @@ .. csharpDiagnostics ?? [] if (_documentContextFactory.Value.TryCreate(delegatedParams.TextDocument.Uri, projectContext: null, out var documentContext)) { return await _translateDiagnosticsService.Value - .TranslateAsync(RazorLanguageKind.CSharp, fullDiagnostics.Items, documentContext, token) + .TranslateAsync(RazorLanguageKind.CSharp, fullDiagnostics.Items, documentContext.Snapshot, cancellationToken) .ConfigureAwait(false); } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DocumentContextFactory.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DocumentContextFactory.cs index 68755ea2ae8..9b6262cb683 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DocumentContextFactory.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DocumentContextFactory.cs @@ -2,16 +2,12 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; using Microsoft.AspNetCore.Razor.Utilities; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer; @@ -38,13 +34,6 @@ public bool TryCreate( return false; } - if (documentSnapshot is null) - { - Debug.Fail($"Document snapshot should never be null here for '{filePath}'. This indicates that our acquisition of documents / versions did not behave as expected."); - context = null; - return false; - } - context = new DocumentContext(documentUri, documentSnapshot, projectContext); return true; } @@ -60,9 +49,8 @@ private bool TryResolveDocument( } if (_projectManager.TryGetLoadedProject(projectContext.ToProjectKey(), out var project) && - project.GetDocument(filePath) is { } document) + project.TryGetDocument(filePath, out documentSnapshot)) { - documentSnapshot = document; return true; } @@ -71,10 +59,9 @@ private bool TryResolveDocument( // move it to the real project when/if we find out about it. var miscellaneousProject = _projectManager.GetMiscellaneousProject(); var normalizedDocumentPath = FilePathNormalizer.Normalize(filePath); - if (miscellaneousProject.GetDocument(normalizedDocumentPath) is { } miscDocument) + if (miscellaneousProject.TryGetDocument(normalizedDocumentPath, out documentSnapshot)) { _logger.LogDebug($"Found document {filePath} in the misc files project, but was asked for project context {projectContext.Id}"); - documentSnapshot = miscDocument; return true; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DocumentSnapshotTextLoader.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DocumentSnapshotTextLoader.cs index fe7330a70f8..a9963165966 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DocumentSnapshotTextLoader.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DocumentSnapshotTextLoader.cs @@ -9,23 +9,13 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer; -internal class DocumentSnapshotTextLoader : TextLoader +internal class DocumentSnapshotTextLoader(IDocumentSnapshot documentSnapshot) : TextLoader { - private readonly IDocumentSnapshot _documentSnapshot; - - public DocumentSnapshotTextLoader(IDocumentSnapshot documentSnapshot) - { - if (documentSnapshot is null) - { - throw new ArgumentNullException(nameof(documentSnapshot)); - } - - _documentSnapshot = documentSnapshot; - } + private readonly IDocumentSnapshot _documentSnapshot = documentSnapshot; public override async Task LoadTextAndVersionAsync(LoadTextOptions options, CancellationToken cancellationToken) { - var sourceText = await _documentSnapshot.GetTextAsync().ConfigureAwait(false); + var sourceText = await _documentSnapshot.GetTextAsync(cancellationToken).ConfigureAwait(false); var textAndVersion = TextAndVersion.Create(sourceText, VersionStamp.Default); return textAndVersion; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DocumentSynchronization/DocumentDidChangeEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DocumentSynchronization/DocumentDidChangeEndpoint.cs index 73c66dab4b9..b0b320d3baf 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DocumentSynchronization/DocumentDidChangeEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DocumentSynchronization/DocumentDidChangeEndpoint.cs @@ -51,9 +51,9 @@ public async Task HandleNotificationAsync(DidChangeTextDocumentParams request, R var documentContext = requestContext.DocumentContext; if (documentContext is null) { - _logger.LogError($"Could not find a document context for didChange on '{request.TextDocument.Uri}'"); + _logger.LogWarning($"Could not find a document context for didChange on '{request.TextDocument.Uri}'"); Debug.Fail($"Could not find a document context for didChange on '{request.TextDocument.Uri}'"); - throw new InvalidOperationException($"Could not find a document context for didChange on '{request.TextDocument.Uri}'"); + return; } var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index 8f12aff65d8..985d4395b83 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -19,9 +19,9 @@ using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; using Microsoft.AspNetCore.Razor.LanguageServer.Semantic; using Microsoft.AspNetCore.Razor.LanguageServer.SpellCheck; -using Microsoft.AspNetCore.Razor.LanguageServer.Tooltip; using Microsoft.AspNetCore.Razor.ProjectEngineHost; using Microsoft.CodeAnalysis.Razor.Completion; +using Microsoft.CodeAnalysis.Razor.Diagnostics; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -104,6 +104,7 @@ public static void AddDiagnosticServices(this IServiceCollection services) services.AddHandlerWithCapabilities(); services.AddSingleton(); services.AddSingleton(sp => new Lazy(sp.GetRequiredService)); + services.AddSingleton(); } public static void AddHoverServices(this IServiceCollection services) @@ -145,6 +146,8 @@ public static void AddCodeActionsServices(this IServiceCollection services) // Razor Code actions services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -219,13 +222,9 @@ public static void AddDocumentManagementServices(this IServiceCollection service services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - // Add project snapshot manager services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(sp => (LspProjectSnapshotManager)sp.GetRequiredService()); } public static void AddHandlerWithCapabilities(this IServiceCollection services) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/FindAllReferences/FindAllReferencesEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/FindAllReferences/FindAllReferencesEndpoint.cs index ccd2e6e93a1..3c7d9c45058 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/FindAllReferences/FindAllReferencesEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/FindAllReferences/FindAllReferencesEndpoint.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.FindAllReferences; [RazorLanguageServerEndpoint(Methods.TextDocumentReferencesName)] -internal sealed class FindAllReferencesEndpoint : AbstractRazorDelegatingEndpoint, ICapabilitiesProvider +internal sealed class FindAllReferencesEndpoint : AbstractRazorDelegatingEndpoint, ICapabilitiesProvider { private readonly IFilePathService _filePathService; private readonly IDocumentMappingService _documentMappingService; @@ -72,8 +72,18 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V positionInfo.LanguageKind)); } - protected override async Task HandleDelegatedResponseAsync(VSInternalReferenceItem[] delegatedResponse, ReferenceParams originalRequest, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) + protected override async Task HandleDelegatedResponseAsync( + VSInternalReferenceItem[]? delegatedResponse, + ReferenceParams originalRequest, + RazorRequestContext requestContext, + DocumentPositionInfo positionInfo, + CancellationToken cancellationToken) { + if (delegatedResponse is null) + { + return null; + } + using var remappedLocations = new PooledArrayBuilder(); foreach (var referenceItem in delegatedResponse) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs index 1564a8d4129..6358afb3c34 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentFormattingEndpoint.cs @@ -37,7 +37,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentFormattingParams public async Task HandleRequestAsync(DocumentFormattingParams request, RazorRequestContext requestContext, CancellationToken cancellationToken) { - if (!_optionsMonitor.CurrentValue.EnableFormatting) + if (!_optionsMonitor.CurrentValue.Formatting.IsEnabled()) { return null; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs index 7012b8173d7..df1062b30f7 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentOnTypeFormattingEndpoint.cs @@ -49,13 +49,13 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentOnTypeFormatting { _logger.LogInformation($"Starting OnTypeFormatting request for {request.TextDocument.Uri}."); - if (!_optionsMonitor.CurrentValue.EnableFormatting) + if (!_optionsMonitor.CurrentValue.Formatting.IsEnabled()) { _logger.LogInformation($"Formatting option disabled."); return null; } - if (!_optionsMonitor.CurrentValue.FormatOnType) + if (!_optionsMonitor.CurrentValue.Formatting.IsOnTypeEnabled()) { _logger.LogInformation($"Formatting on type disabled."); return null; @@ -89,7 +89,8 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentOnTypeFormatting return null; } - if (_razorFormattingService.TryGetOnTypeFormattingTriggerKind(codeDocument, hostDocumentIndex, request.Character, out var triggerCharacterKind)) + if (!_razorFormattingService.TryGetOnTypeFormattingTriggerKind(codeDocument, hostDocumentIndex, request.Character, out var triggerCharacterKind) || + triggerCharacterKind == RazorLanguageKind.Razor) { return null; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs index 7ba97f9f9fb..da8823a0f2a 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DocumentRangeFormattingEndpoint.cs @@ -37,7 +37,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentRangeFormattingP public async Task HandleRequestAsync(DocumentRangeFormattingParams request, RazorRequestContext requestContext, CancellationToken cancellationToken) { - if (!_optionsMonitor.CurrentValue.EnableFormatting) + if (!_optionsMonitor.CurrentValue.Formatting.IsEnabled()) { return null; } @@ -54,6 +54,16 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(DocumentRangeFormattingP return null; } + if (request.Options.OtherOptions is not null && + request.Options.OtherOptions.TryGetValue("fromPaste", out var fromPasteObj) && + fromPasteObj is bool fromPaste) + { + if (fromPaste && !_optionsMonitor.CurrentValue.Formatting.IsOnPasteEnabled()) + { + return null; + } + } + var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine); var htmlChanges = await _htmlFormatter.GetDocumentFormattingEditsAsync(documentContext.Snapshot, documentContext.Uri, request.Options, cancellationToken).ConfigureAwait(false); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs index c20911085c1..d2504d8e1ef 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/HtmlFormatter.cs @@ -47,7 +47,7 @@ public async Task> GetDocumentFormattingEditsAsync( return []; } - var sourceText = await documentSnapshot.GetTextAsync().ConfigureAwait(false); + var sourceText = await documentSnapshot.GetTextAsync(cancellationToken).ConfigureAwait(false); return result.Edits.SelectAsArray(sourceText.GetTextChange); } @@ -78,7 +78,7 @@ public async Task> GetOnTypeFormattingEditsAsync( return []; } - var sourceText = await documentSnapshot.GetTextAsync().ConfigureAwait(false); + var sourceText = await documentSnapshot.GetTextAsync(cancellationToken).ConfigureAwait(false); return result.Edits.SelectAsArray(sourceText.GetTextChange); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspFormattingCodeDocumentProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspFormattingCodeDocumentProvider.cs index e47da7f437d..e04d70fff32 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspFormattingCodeDocumentProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/LspFormattingCodeDocumentProvider.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor.Formatting; @@ -10,9 +11,9 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; internal sealed class LspFormattingCodeDocumentProvider : IFormattingCodeDocumentProvider { - public Task GetCodeDocumentAsync(IDocumentSnapshot snapshot) + public ValueTask GetCodeDocumentAsync(IDocumentSnapshot snapshot, CancellationToken cancellationToken) { // Formatting always uses design time - return snapshot.GetGeneratedOutputAsync(forceDesignTimeGeneratedOutput: true); + return snapshot.GetGeneratedOutputAsync(forceDesignTimeGeneratedOutput: true, cancellationToken); } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/GeneratedDocumentPublisher.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/GeneratedDocumentPublisher.cs index 9f10511a0e9..8f3ea213ae3 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/GeneratedDocumentPublisher.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/GeneratedDocumentPublisher.cs @@ -76,7 +76,7 @@ public void PublishCSharp(ProjectKey projectKey, string filePath, SourceText sou if (previouslyPublishedData.HostDocumentVersion > hostDocumentVersion) { // We've already published a newer version of this document. No-op. - _logger.LogWarning($"Skipping publish of C# for {filePath} because we've already published version {previouslyPublishedData.HostDocumentVersion}, and this request is for {hostDocumentVersion}."); + _logger.LogWarning($"Skipping publish of C# for {documentKey.ProjectKey}/{filePath} because we've already published version {previouslyPublishedData.HostDocumentVersion}, and this request is for {hostDocumentVersion} (and {projectKey})."); return; } @@ -161,6 +161,27 @@ private void ProjectManager_Changed(object? sender, ProjectChangeEventArgs args) switch (args.Kind) { + case ProjectChangeKind.DocumentRemoved: + { + if (!_options.IncludeProjectKeyInGeneratedFilePath) + { + break; + } + + // When a C# document is removed we remove it from the publishing, because it could come back with the same name + var key = new DocumentKey(args.ProjectKey, args.DocumentFilePath.AssumeNotNull()); + + lock (_publishedCSharpData) + { + if (_publishedCSharpData.Remove(key)) + { + _logger.LogDebug($"Removing previous C# publish data for {key.ProjectKey}/{key.DocumentFilePath}"); + } + } + + break; + } + case ProjectChangeKind.DocumentChanged: var documentFilePath = args.DocumentFilePath.AssumeNotNull(); @@ -180,27 +201,17 @@ private void ProjectManager_Changed(object? sender, ProjectChangeEventArgs args) lock (_publishedCSharpData) { - if (_publishedCSharpData.ContainsKey(documentKey)) + if (_publishedCSharpData.Remove(documentKey)) { - var removed = _publishedCSharpData.Remove(documentKey); - if (!removed) - { - _logger.LogError($"Published data should be protected by the project snapshot manager's thread and should never fail to remove."); - Debug.Fail("Published data should be protected by the project snapshot manager's thread and should never fail to remove."); - } + _logger.LogDebug($"Removing previous C# publish data for {documentKey.ProjectKey}/{documentKey.DocumentFilePath}"); } } lock (_publishedHtmlData) { - if (_publishedHtmlData.ContainsKey(documentFilePath)) + if (_publishedHtmlData.Remove(documentFilePath)) { - var removed = _publishedHtmlData.Remove(documentFilePath); - if (!removed) - { - _logger.LogError($"Published data should be protected by the project snapshot manager's thread and should never fail to remove."); - Debug.Fail("Published data should be protected by the project snapshot manager's thread and should never fail to remove."); - } + _logger.LogDebug($"Removing previous Html publish data for {documentKey.ProjectKey}/{documentKey.DocumentFilePath}"); } } } @@ -231,7 +242,10 @@ private void ProjectManager_Changed(object? sender, ProjectChangeEventArgs args) foreach (var key in keysToRemove) { - _publishedCSharpData.Remove(key); + if (_publishedCSharpData.Remove(key)) + { + _logger.LogDebug($"Removing previous C# publish data for {key.ProjectKey}/{key.DocumentFilePath}"); + } } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/GeneratedDocumentSynchronizer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/GeneratedDocumentSynchronizer.cs index d057edfc632..13bd64e2d54 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/GeneratedDocumentSynchronizer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/GeneratedDocumentSynchronizer.cs @@ -28,6 +28,13 @@ public void DocumentProcessed(RazorCodeDocument codeDocument, IDocumentSnapshot return; } + // If the document has been removed from the project, then don't do anything, or version numbers will be thrown off + if (!_projectManager.TryGetLoadedProject(document.Project.Key, out var project) || + !project.ContainsDocument(document.FilePath)) + { + return; + } + // If cohosting is on, then it is responsible for updating the Html buffer if (!_languageServerFeatureOptions.UseRazorCohostServer) { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/FormattingFlagExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/FormattingFlagExtensions.cs new file mode 100644 index 00000000000..0ac02c7ba6c --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/FormattingFlagExtensions.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.LanguageServer.Hosting; + +internal static class FormattingFlagExtensions +{ + public static bool IsEnabled(this FormattingFlags flags) + => flags.IsFlagSet(FormattingFlags.Enabled); + + public static bool IsOnTypeEnabled(this FormattingFlags flags) + => flags.IsEnabled() && flags.IsFlagSet(FormattingFlags.OnType); + + public static bool IsOnPasteEnabled(this FormattingFlags flags) + => flags.IsEnabled() && flags.IsFlagSet(FormattingFlags.OnPaste); +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/FormattingFlags.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/FormattingFlags.cs new file mode 100644 index 00000000000..b14073c22e3 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/FormattingFlags.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.Hosting; + +[Flags] +internal enum FormattingFlags +{ + Disabled = 0, + Enabled = 1, + OnPaste = 1 << 1, + OnType = 1 << 2, + All = Enabled | OnPaste | OnType +}; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs index 1931a4a1055..c8d1a284810 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/RazorLSPOptions.cs @@ -1,30 +1,29 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System; using Microsoft.CodeAnalysis.Razor.Settings; namespace Microsoft.AspNetCore.Razor.LanguageServer.Hosting; internal record RazorLSPOptions( - bool EnableFormatting, + FormattingFlags Formatting, bool AutoClosingTags, bool InsertSpaces, int TabSize, bool AutoShowCompletion, bool AutoListParams, - bool FormatOnType, bool AutoInsertAttributeQuotes, bool ColorBackground, bool CodeBlockBraceOnNextLine, bool CommitElementsWithSpace) { - public readonly static RazorLSPOptions Default = new(EnableFormatting: true, + public readonly static RazorLSPOptions Default = new(Formatting: FormattingFlags.All, AutoClosingTags: true, AutoListParams: true, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, - FormatOnType: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, @@ -35,15 +34,30 @@ internal record RazorLSPOptions( /// not defined in client settings. /// internal static RazorLSPOptions From(ClientSettings settings) - => new(Default.EnableFormatting, + => new(GetFormattingFlags(settings), settings.AdvancedSettings.AutoClosingTags, !settings.ClientSpaceSettings.IndentWithTabs, settings.ClientSpaceSettings.IndentSize, settings.ClientCompletionSettings.AutoShowCompletion, settings.ClientCompletionSettings.AutoListParams, - settings.AdvancedSettings.FormatOnType, settings.AdvancedSettings.AutoInsertAttributeQuotes, settings.AdvancedSettings.ColorBackground, settings.AdvancedSettings.CodeBlockBraceOnNextLine, settings.AdvancedSettings.CommitElementsWithSpace); + + private static FormattingFlags GetFormattingFlags(ClientSettings settings) + { + var flags = FormattingFlags.Enabled; + if (settings.AdvancedSettings.FormatOnPaste) + { + flags |= FormattingFlags.OnPaste; + } + + if (settings.AdvancedSettings.FormatOnType) + { + flags |= FormattingFlags.OnType; + } + + return flags; + } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs index 5160930cfe8..8e45716a3b4 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hover/HoverService.cs @@ -11,7 +11,7 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Syntax; -using Microsoft.AspNetCore.Razor.LanguageServer.Tooltip; +using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -26,13 +26,11 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Hover; internal sealed partial class HoverService( - LSPTagHelperTooltipFactory lspTagHelperTooltipFactory, - VSLSPTagHelperTooltipFactory vsLspTagHelperTooltipFactory, + IProjectSnapshotManager projectManager, IDocumentMappingService documentMappingService, IClientCapabilitiesService clientCapabilitiesService) : IHoverService { - private readonly LSPTagHelperTooltipFactory _lspTagHelperTooltipFactory = lspTagHelperTooltipFactory; - private readonly VSLSPTagHelperTooltipFactory _vsLspTagHelperTooltipFactory = vsLspTagHelperTooltipFactory; + private readonly IProjectSnapshotManager _projectManager = projectManager; private readonly IDocumentMappingService _documentMappingService = documentMappingService; private readonly IClientCapabilitiesService _clientCapabilitiesService = clientCapabilitiesService; @@ -261,7 +259,7 @@ internal sealed partial class HoverService( var attrDescriptionInfo = new AggregateBoundAttributeDescription(descriptionInfos); var isVSClient = clientCapabilities.SupportsVisualStudioExtensions; - if (isVSClient && _vsLspTagHelperTooltipFactory.TryCreateTooltip(attrDescriptionInfo, out ContainerElement? classifiedTextElement)) + if (isVSClient && ClassifiedTagHelperTooltipFactory.TryCreateTooltip(attrDescriptionInfo, out ContainerElement? classifiedTextElement)) { var vsHover = new VSInternalHover { @@ -276,7 +274,7 @@ internal sealed partial class HoverService( { var hoverContentFormat = GetHoverContentFormat(clientCapabilities); - if (!_lspTagHelperTooltipFactory.TryCreateTooltip(attrDescriptionInfo, hoverContentFormat, out var vsMarkupContent)) + if (!MarkupTagHelperTooltipFactory.TryCreateTooltip(attrDescriptionInfo, hoverContentFormat, out var vsMarkupContent)) { return null; } @@ -305,7 +303,10 @@ internal sealed partial class HoverService( var isVSClient = clientCapabilities.SupportsVisualStudioExtensions; if (isVSClient) { - var classifiedTextElement = await _vsLspTagHelperTooltipFactory.TryCreateTooltipContainerAsync(documentFilePath, elementDescriptionInfo, cancellationToken).ConfigureAwait(false); + var classifiedTextElement = await ClassifiedTagHelperTooltipFactory + .TryCreateTooltipContainerAsync(documentFilePath, elementDescriptionInfo, _projectManager.GetQueryOperations(), cancellationToken) + .ConfigureAwait(false); + if (classifiedTextElement is not null) { var vsHover = new VSInternalHover @@ -321,7 +322,10 @@ internal sealed partial class HoverService( var hoverContentFormat = GetHoverContentFormat(clientCapabilities); - var vsMarkupContent = await _lspTagHelperTooltipFactory.TryCreateTooltipAsync(documentFilePath, elementDescriptionInfo, hoverContentFormat, cancellationToken).ConfigureAwait(false); + var vsMarkupContent = await MarkupTagHelperTooltipFactory + .TryCreateTooltipAsync(documentFilePath, elementDescriptionInfo, _projectManager.GetQueryOperations(), hoverContentFormat, cancellationToken) + .ConfigureAwait(false); + if (vsMarkupContent is null) { return null; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Implementation/ImplementationEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Implementation/ImplementationEndpoint.cs index a2e89c2bbf7..1f1dff87281 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Implementation/ImplementationEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Implementation/ImplementationEndpoint.cs @@ -12,9 +12,9 @@ using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.VisualStudio.LanguageServer.Protocol; -using ImplementationResult = Microsoft.VisualStudio.LanguageServer.Protocol.SumType< +using ImplementationResult = System.Nullable; + Microsoft.VisualStudio.LanguageServer.Protocol.VSInternalReferenceItem[]>>; namespace Microsoft.AspNetCore.Razor.LanguageServer.Implementation; @@ -60,8 +60,10 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V protected async override Task HandleDelegatedResponseAsync(ImplementationResult delegatedResponse, TextDocumentPositionParams request, RazorRequestContext requestContext, DocumentPositionInfo positionInfo, CancellationToken cancellationToken) { + var result = delegatedResponse.GetValueOrDefault().Value; + // Not using .TryGetXXX because this does the null check for us too - if (delegatedResponse.Value is Location[] locations) + if (result is Location[] locations) { foreach (var loc in locations) { @@ -70,7 +72,7 @@ protected async override Task HandleDelegatedResponseAsync return locations; } - else if (delegatedResponse.Value is VSInternalReferenceItem[] referenceItems) + else if (result is VSInternalReferenceItem[] referenceItems) { foreach (var item in referenceItems) { @@ -80,6 +82,6 @@ protected async override Task HandleDelegatedResponseAsync return referenceItems; } - return default; + return null; } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs index 60b405b5faa..1e93b44046e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlayHints/InlayHintService.cs @@ -29,6 +29,16 @@ internal sealed class InlayHintService(IDocumentMappingService documentMappingSe var span = range.ToLinePositionSpan(); + cancellationToken.ThrowIfCancellationRequested(); + + // Sometimes the client sends us a request that doesn't match the file contents. Could be a bug with old requests + // not being cancelled, but no harm in being defensive + if (!codeDocument.Source.Text.TryGetAbsoluteIndex(span.Start, out var startIndex) || + !codeDocument.Source.Text.TryGetAbsoluteIndex(span.End, out var endIndex)) + { + return null; + } + // We are given a range by the client, but our mapping only succeeds if the start and end of the range can both be mapped // to C#. Since that doesn't logically match what we want from inlay hints, we instead get the minimum range of mappable // C# to get hints for. We'll filter that later, to remove the sections that can't be mapped back. diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs index 671a119fa53..03ff37b121a 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs @@ -29,7 +29,6 @@ internal sealed class InlineCompletionEndpoint( IDocumentMappingService documentMappingService, IClientConnection clientConnection, IFormattingCodeDocumentProvider formattingCodeDocumentProvider, - IAdhocWorkspaceFactory adhocWorkspaceFactory, RazorLSPOptionsMonitor optionsMonitor, ILoggerFactory loggerFactory) : IRazorRequestHandler, ICapabilitiesProvider @@ -39,10 +38,9 @@ internal sealed class InlineCompletionEndpoint( "if", "indexer", "interface", "invoke", "iterator", "iterindex", "lock", "mbox", "namespace", "#if", "#region", "prop", "propfull", "propg", "sim", "struct", "svm", "switch", "try", "tryf", "unchecked", "unsafe", "using", "while"); - private readonly IDocumentMappingService _documentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService)); - private readonly IClientConnection _clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection)); + private readonly IDocumentMappingService _documentMappingService = documentMappingService; + private readonly IClientConnection _clientConnection = clientConnection; private readonly IFormattingCodeDocumentProvider _formattingCodeDocumentProvider = formattingCodeDocumentProvider; - private readonly IAdhocWorkspaceFactory _adhocWorkspaceFactory = adhocWorkspaceFactory ?? throw new ArgumentNullException(nameof(adhocWorkspaceFactory)); private readonly RazorLSPOptionsMonitor _optionsMonitor = optionsMonitor; private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); @@ -63,10 +61,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalInlineCompleti public async Task HandleRequestAsync(VSInternalInlineCompletionRequest request, RazorRequestContext requestContext, CancellationToken cancellationToken) { - if (request is null) - { - throw new ArgumentNullException(nameof(request)); - } + ArgHelper.ThrowIfNull(request); _logger.LogInformation($"Starting request for {request.TextDocument.Uri} at {request.Position}."); @@ -118,7 +113,6 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalInlineCompleti using var items = new PooledArrayBuilder(list.Items.Length); foreach (var item in list.Items) { - var containsSnippet = item.TextFormat == InsertTextFormat.Snippet; var range = item.Range ?? projectedPosition.ToZeroWidthRange(); if (!_documentMappingService.TryMapToHostDocumentRange(codeDocument.GetCSharpDocument(), range, out var rangeInRazorDoc)) @@ -128,13 +122,11 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalInlineCompleti } var options = RazorFormattingOptions.From(request.Options, _optionsMonitor.CurrentValue.CodeBlockBraceOnNextLine); - using var formattingContext = FormattingContext.Create( - request.TextDocument.Uri, + var formattingContext = FormattingContext.Create( documentContext.Snapshot, codeDocument, options, - _formattingCodeDocumentProvider, - _adhocWorkspaceFactory); + _formattingCodeDocumentProvider); if (!TryGetSnippetWithAdjustedIndentation(formattingContext, item.Text, hostDocumentIndex, out var newSnippetText)) { continue; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/LspWorkspaceProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/LspWorkspaceProvider.cs deleted file mode 100644 index e6e0e83aeb8..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/LspWorkspaceProvider.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Razor.Workspaces; - -namespace Microsoft.AspNetCore.Razor.LanguageServer; - -internal sealed class LspWorkspaceProvider(IAdhocWorkspaceFactory workspaceFactory) : IWorkspaceProvider, IDisposable -{ - private readonly IAdhocWorkspaceFactory _workspaceFactory = workspaceFactory; - - private Workspace? _workspace; - private bool _disposed; - - void IDisposable.Dispose() - { - if (_disposed) - { - return; - } - - _workspace?.Dispose(); - _disposed = true; - } - - public Workspace GetWorkspace() - { - if (_disposed) - { - throw new ObjectDisposedException(nameof(LspWorkspaceProvider)); - } - - return _workspace ??= _workspaceFactory.Create(); - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/MapCode/MapCodeEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/MapCode/MapCodeEndpoint.cs index 4d61c202e32..029ddb93535 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/MapCode/MapCodeEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/MapCode/MapCodeEndpoint.cs @@ -105,7 +105,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V // We create a new Razor file based on each content in each mapping order to get the syntax tree that we'll later use to map. var newSnapshot = snapshot.WithText(SourceText.From(content)); - var codeToMap = await newSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); + var codeToMap = await newSnapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false); var mappingSuccess = await TryMapCodeAsync( codeToMap, mapping.FocusLocations, changes, mapCodeCorrelationId, documentContext, cancellationToken).ConfigureAwait(false); @@ -228,7 +228,7 @@ private async Task TryMapCodeAsync( razorNodesToMap.Add(nodeToMap); } - var sourceText = await documentContext.Snapshot.GetTextAsync().ConfigureAwait(false); + var sourceText = await documentContext.Snapshot.GetTextAsync(cancellationToken).ConfigureAwait(false); foreach (var nodeToMap in razorNodesToMap) { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Mapping/RazorLanguageQueryEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Mapping/RazorLanguageQueryEndpoint.cs index da40358e0af..efcff70265d 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Mapping/RazorLanguageQueryEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Mapping/RazorLanguageQueryEndpoint.cs @@ -42,8 +42,8 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(RazorLanguageQueryParams var documentSnapshot = documentContext.Snapshot; var documentVersion = documentContext.Snapshot.Version; - var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); - var sourceText = await documentSnapshot.GetTextAsync().ConfigureAwait(false); + var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false); + var sourceText = codeDocument.Source.Text; var hostDocumentIndex = sourceText.GetPosition(request.Position); var responsePosition = request.Position; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/OpenDocumentGenerator.Comparer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/OpenDocumentGenerator.Comparer.cs index 483f45fbb39..142654381b7 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/OpenDocumentGenerator.Comparer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/OpenDocumentGenerator.Comparer.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Razor.LanguageServer; @@ -28,12 +29,16 @@ public bool Equals(IDocumentSnapshot? x, IDocumentSnapshot? y) return false; } - return FilePathComparer.Instance.Equals(x.FilePath, y.FilePath); + return x.Project.Key.Equals(y.Project.Key) && + FilePathComparer.Instance.Equals(x.FilePath, y.FilePath); } public int GetHashCode(IDocumentSnapshot obj) { - return FilePathComparer.Instance.GetHashCode(obj); + var hash = HashCodeCombiner.Start(); + hash.Add(obj.Project.Key.Id, FilePathComparer.Instance); + hash.Add(obj.FileKind.AssumeNotNull(), FilePathComparer.Instance); + return hash.CombinedHash; } } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/OpenDocumentGenerator.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/OpenDocumentGenerator.cs index d282bc036b1..4923d948a15 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/OpenDocumentGenerator.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/OpenDocumentGenerator.cs @@ -75,7 +75,7 @@ private async ValueTask ProcessBatchAsync(ImmutableArray item return; } - var codeDocument = await document.GetGeneratedOutputAsync().ConfigureAwait(false); + var codeDocument = await document.GetGeneratedOutputAsync(token).ConfigureAwait(false); foreach (var listener in _listeners) { @@ -175,7 +175,7 @@ void EnqueueIfNecessary(IDocumentSnapshot document) return; } - _logger.LogDebug($"Enqueuing generation of {document.FilePath} in {document.Project.Key.Id}"); + _logger.LogDebug($"Enqueuing generation of {document.FilePath} in {document.Project.Key.Id} at version {document.Version}"); _workQueue.AddWork(document); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IProjectSnapshotManagerExtensions.SolutionQueryOperations.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IProjectSnapshotManagerExtensions.SolutionQueryOperations.cs new file mode 100644 index 00000000000..1c1abe06197 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IProjectSnapshotManagerExtensions.SolutionQueryOperations.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.AspNetCore.Razor.Utilities; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; + +internal static partial class IProjectSnapshotManagerExtensions +{ + private sealed class SolutionQueryOperations(IProjectSnapshotManager projectManager) : ISolutionQueryOperations + { + private readonly IProjectSnapshotManager _projectManager = projectManager; + + public IEnumerable GetProjects() + { + return _projectManager.GetProjects(); + } + + public ImmutableArray GetProjectsContainingDocument(string documentFilePath) + { + using var projects = new PooledArrayBuilder(); + + foreach (var project in _projectManager.GetProjects()) + { + // Always exclude the miscellaneous project. + if (project.Key == MiscFilesHostProject.Instance.Key) + { + continue; + } + + if (project.ContainsDocument(documentFilePath)) + { + projects.Add(project); + } + } + + return projects.DrainToImmutable(); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IProjectSnapshotManagerExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IProjectSnapshotManagerExtensions.cs index bf603c02b1c..67f55c3cfd3 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IProjectSnapshotManagerExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IProjectSnapshotManagerExtensions.cs @@ -1,13 +1,9 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.AspNetCore.Razor.Utilities; using Microsoft.CodeAnalysis.Razor; @@ -16,7 +12,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; -internal static class IProjectSnapshotManagerExtensions +internal static partial class IProjectSnapshotManagerExtensions { public static IProjectSnapshot GetMiscellaneousProject(this IProjectSnapshotManager projectManager) { @@ -61,7 +57,7 @@ public static bool TryResolveAllProjects( foreach (var project in potentialProjects) { - if (project.GetDocument(documentFilePath) is not null) + if (project.ContainsDocument(documentFilePath)) { builder.Add(project); } @@ -69,7 +65,7 @@ public static bool TryResolveAllProjects( var normalizedDocumentPath = FilePathNormalizer.Normalize(documentFilePath); var miscProject = projectManager.GetMiscellaneousProject(); - if (miscProject.GetDocument(normalizedDocumentPath) is not null) + if (miscProject.ContainsDocument(normalizedDocumentPath)) { builder.Add(miscProject); } @@ -91,10 +87,9 @@ public static bool TryResolveDocumentInAnyProject( foreach (var project in potentialProjects) { - if (project.GetDocument(normalizedDocumentPath) is { } projectDocument) + if (project.TryGetDocument(normalizedDocumentPath, out document)) { logger.LogTrace($"Found {documentFilePath} in {project.FilePath}"); - document = projectDocument; return true; } } @@ -102,10 +97,9 @@ public static bool TryResolveDocumentInAnyProject( logger.LogTrace($"Looking for {documentFilePath} in miscellaneous project."); var miscellaneousProject = projectManager.GetMiscellaneousProject(); - if (miscellaneousProject.GetDocument(normalizedDocumentPath) is { } miscDocument) + if (miscellaneousProject.TryGetDocument(normalizedDocumentPath, out document)) { logger.LogTrace($"Found {documentFilePath} in miscellaneous project."); - document = miscDocument; return true; } @@ -114,4 +108,7 @@ public static bool TryResolveDocumentInAnyProject( document = null; return false; } + + public static ISolutionQueryOperations GetQueryOperations(this IProjectSnapshotManager projectManager) + => new SolutionQueryOperations(projectManager); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IRazorProjectService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IRazorProjectService.cs index 942c95985bc..04091be0911 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IRazorProjectService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/IRazorProjectService.cs @@ -18,31 +18,4 @@ internal interface IRazorProjectService Task UpdateDocumentAsync(string filePath, SourceText sourceText, CancellationToken cancellationToken); Task CloseDocumentAsync(string filePath, CancellationToken cancellationToken); Task RemoveDocumentAsync(string filePath, CancellationToken cancellationToken); - - Task AddProjectAsync( - string filePath, - string intermediateOutputPath, - RazorConfiguration? configuration, - string? rootNamespace, - string? displayName, - CancellationToken cancellationToken); - - Task UpdateProjectAsync( - ProjectKey projectKey, - RazorConfiguration? configuration, - string? rootNamespace, - string displayName, - ProjectWorkspaceState projectWorkspaceState, - ImmutableArray documents, - CancellationToken cancellationToken); - - Task AddOrUpdateProjectAsync( - ProjectKey projectKey, - string filePath, - RazorConfiguration? configuration, - string? rootNamespace, - string displayName, - ProjectWorkspaceState projectWorkspaceState, - ImmutableArray documents, - CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/LspProjectSnapshotManager.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/LspProjectSnapshotManager.cs index ba2bbca8924..faad24dbb7f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/LspProjectSnapshotManager.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/LspProjectSnapshotManager.cs @@ -1,30 +1,19 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.ProjectEngineHost; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Workspaces; namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; internal class LspProjectSnapshotManager( IProjectEngineFactoryProvider projectEngineFactoryProvider, ILoggerFactory loggerFactory) - : ProjectSnapshotManager(projectEngineFactoryProvider, loggerFactory, initializer: AddMiscFilesProject), IProjectCollectionResolver + : ProjectSnapshotManager(projectEngineFactoryProvider, loggerFactory, initializer: AddMiscFilesProject) { private static void AddMiscFilesProject(Updater updater) { updater.ProjectAdded(MiscFilesHostProject.Instance); } - - public IEnumerable EnumerateProjects(IDocumentSnapshot snapshot) - { - return GetProjects(); - } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/MiscFilesHostProject.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/MiscFilesHostProject.cs index 52538e0730a..cb29d7e08ce 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/MiscFilesHostProject.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/MiscFilesHostProject.cs @@ -2,18 +2,16 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Utilities; +using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; -internal sealed class MiscFilesHostProject : HostProject +internal sealed record class MiscFilesHostProject : HostProject { public static MiscFilesHostProject Instance { get; } = Create(); @@ -26,12 +24,12 @@ public static bool IsMiscellaneousProject(IProjectSnapshot project) private MiscFilesHostProject( string directory, - string projectFilePath, + string filePath, string intermediateOutputPath, RazorConfiguration razorConfiguration, string? rootNamespace, string? displayName = null) - : base(projectFilePath, intermediateOutputPath, razorConfiguration, rootNamespace, displayName) + : base(filePath, intermediateOutputPath, razorConfiguration, rootNamespace, displayName) { DirectoryPath = directory; } @@ -39,8 +37,8 @@ private MiscFilesHostProject( private static MiscFilesHostProject Create() { var tempDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("n")); - var miscellaneousProjectPath = Path.Combine(tempDirectory, "__MISC_RAZOR_PROJECT__"); - var normalizedPath = FilePathNormalizer.Normalize(miscellaneousProjectPath); + var filePath = Path.Combine(tempDirectory, "__MISC_RAZOR_PROJECT__"); + var normalizedPath = FilePathNormalizer.Normalize(filePath); return new MiscFilesHostProject( tempDirectory, @@ -50,4 +48,25 @@ private static MiscFilesHostProject Create() rootNamespace: null, "Miscellaneous Files"); } + + public bool Equals(MiscFilesHostProject? other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + + return base.Equals(other) && + FilePathComparer.Instance.Equals(DirectoryPath, other.DirectoryPath); + } + + public override int GetHashCode() + { + var hash = HashCodeCombiner.Start(); + + hash.Add(base.GetHashCode()); + hash.Add(DirectoryPath, FilePathComparer.Instance); + + return hash.CombinedHash; + } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.TestAccessor.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.TestAccessor.cs index 402e6c2cc78..a0312a283c8 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.TestAccessor.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.TestAccessor.cs @@ -1,7 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System.Collections.Immutable; +using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.ProjectSystem; +using Microsoft.AspNetCore.Razor.Serialization; namespace Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; @@ -13,5 +18,24 @@ internal readonly struct TestAccessor(RazorProjectService instance) { public ValueTask WaitForInitializationAsync() => instance.WaitForInitializationAsync(); + + public async Task AddProjectAsync( + string filePath, + string intermediateOutputPath, + RazorConfiguration? configuration, + string? rootNamespace, + string? displayName, + CancellationToken cancellationToken) + { + var service = instance; + + await service.WaitForInitializationAsync().ConfigureAwait(false); + + return await instance._projectManager + .UpdateAsync( + updater => service.AddProjectCore(updater, filePath, intermediateOutputPath, configuration, rootNamespace, displayName), + cancellationToken) + .ConfigureAwait(false); + } } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs index 9f7d10a9cd4..25f498d7213 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/ProjectSystem/RazorProjectService.cs @@ -270,7 +270,7 @@ await _projectManager.UpdateAsync( { _logger.LogInformation($"Removing document '{textDocumentPath}' from project '{projectSnapshot.Key}'."); - updater.DocumentRemoved(projectSnapshot.Key, documentSnapshot.State.HostDocument); + updater.DocumentRemoved(projectSnapshot.Key, documentSnapshot.HostDocument); } }); }, @@ -313,23 +313,6 @@ private void ActOnDocumentInMultipleProjects(string filePath, Action AddProjectAsync( - string filePath, - string intermediateOutputPath, - RazorConfiguration? configuration, - string? rootNamespace, - string? displayName, - CancellationToken cancellationToken) - { - await WaitForInitializationAsync().ConfigureAwait(false); - - return await _projectManager - .UpdateAsync( - updater => AddProjectCore(updater, filePath, intermediateOutputPath, configuration, rootNamespace, displayName), - cancellationToken) - .ConfigureAwait(false); - } - private ProjectKey AddProjectCore(ProjectSnapshotManager.Updater updater, string filePath, string intermediateOutputPath, RazorConfiguration? configuration, string? rootNamespace, string? displayName) { var normalizedPath = FilePathNormalizer.Normalize(filePath); @@ -341,58 +324,9 @@ private ProjectKey AddProjectCore(ProjectSnapshotManager.Updater updater, string _logger.LogInformation($"Added project '{filePath}' with key {hostProject.Key} to project system."); - TryMigrateMiscellaneousDocumentsToProject(updater); - return hostProject.Key; } - public async Task UpdateProjectAsync( - ProjectKey projectKey, - RazorConfiguration? configuration, - string? rootNamespace, - string? displayName, - ProjectWorkspaceState projectWorkspaceState, - ImmutableArray documents, - CancellationToken cancellationToken) - { - await WaitForInitializationAsync().ConfigureAwait(false); - - await AddOrUpdateProjectCoreAsync( - projectKey, - filePath: null, - configuration, - rootNamespace, - displayName, - projectWorkspaceState, - documents, - cancellationToken) - .ConfigureAwait(false); - } - - public async Task AddOrUpdateProjectAsync( - ProjectKey projectKey, - string filePath, - RazorConfiguration? configuration, - string? rootNamespace, - string? displayName, - ProjectWorkspaceState projectWorkspaceState, - ImmutableArray documents, - CancellationToken cancellationToken) - { - await WaitForInitializationAsync().ConfigureAwait(false); - - await AddOrUpdateProjectCoreAsync( - projectKey, - filePath, - configuration, - rootNamespace, - displayName, - projectWorkspaceState, - documents, - cancellationToken) - .ConfigureAwait(false); - } - private Task AddOrUpdateProjectCoreAsync( ProjectKey projectKey, string? filePath, @@ -511,7 +445,7 @@ private void UpdateProjectDocuments( continue; } - var currentHostDocument = documentSnapshot.State.HostDocument; + var currentHostDocument = documentSnapshot.HostDocument; var newFilePath = EnsureFullPath(documentHandle.FilePath, projectDirectory); var newHostDocument = new HostDocument(newFilePath, documentHandle.TargetPath, documentHandle.FileKind); @@ -578,7 +512,7 @@ private void MoveDocument( return; } - var currentHostDocument = documentSnapshot.State.HostDocument; + var currentHostDocument = documentSnapshot.HostDocument; var textLoader = new DocumentSnapshotTextLoader(documentSnapshot); @@ -611,46 +545,4 @@ private static string EnsureFullPath(string filePath, string projectDirectory) return normalizedFilePath; } - - private void TryMigrateMiscellaneousDocumentsToProject(ProjectSnapshotManager.Updater updater) - { - var miscellaneousProject = _projectManager.GetMiscellaneousProject(); - - foreach (var documentFilePath in miscellaneousProject.DocumentFilePaths) - { - var projectSnapshot = _projectManager.FindPotentialProjects(documentFilePath).FirstOrDefault(); - if (projectSnapshot is null) - { - continue; - } - - if (miscellaneousProject.GetDocument(documentFilePath) is not DocumentSnapshot documentSnapshot) - { - continue; - } - - // Remove from miscellaneous project - updater.DocumentRemoved(miscellaneousProject.Key, documentSnapshot.State.HostDocument); - - // Add to new project - - var textLoader = new DocumentSnapshotTextLoader(documentSnapshot); - - // If we're moving from the misc files project to a real project, then target path will be the full path to the file - // and the next update to the project will update it to be a relative path. To save a bunch of busy work if that is - // the only change necessary, we can proactively do that work here. This also means that when we later find out about - // this document the "real" way, it will be equal to the one we already know about, and we won't lose content - var projectDirectory = FilePathNormalizer.GetNormalizedDirectoryName(projectSnapshot.FilePath); - var newTargetPath = documentSnapshot.TargetPath; - if (FilePathNormalizer.Normalize(newTargetPath).StartsWith(projectDirectory)) - { - newTargetPath = newTargetPath[projectDirectory.Length..]; - } - - var newHostDocument = new HostDocument(documentSnapshot.FilePath, newTargetPath, documentSnapshot.FileKind); - _logger.LogInformation($"Migrating '{documentFilePath}' from the '{miscellaneousProject.Key}' project to '{projectSnapshot.Key}' project."); - - updater.DocumentAdded(projectSnapshot.Key, newHostDocument, textLoader); - } - } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index be86fe01409..780d31c5e56 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -119,9 +119,6 @@ protected override ILspServices ConstructLspServices() // Add the logger as a service in case anything in CLaSP pulls it out to do logging services.AddSingleton(Logger); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); var featureOptions = _featureOptions ?? new DefaultLanguageServerFeatureOptions(); @@ -131,7 +128,6 @@ protected override ILspServices ConstructLspServices() services.AddLifeCycleServices(this, _clientConnection, _lspServerActivationTracker); - services.AddDiagnosticServices(); services.AddSemanticTokensServices(featureOptions); services.AddDocumentManagementServices(featureOptions); services.AddCompletionServices(); @@ -143,6 +139,9 @@ protected override ILspServices ConstructLspServices() if (!featureOptions.UseRazorCohostServer) { + // Diagnostics + services.AddDiagnosticServices(); + // Auto insert services.AddSingleton(); services.AddSingleton(); @@ -162,10 +161,8 @@ protected override ILspServices ConstructLspServices() // Other services.AddSingleton(); - // Get the DefaultSession for telemetry. This is set by VS with - // TelemetryService.SetDefaultSession and provides the correct - // appinsights keys etc services.AddSingleton(_telemetryReporter); + services.AddSingleton(); // Defaults: For when the caller hasn't provided them through the `configure` action. services.TryAddSingleton(); @@ -209,6 +206,8 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption services.AddHandlerWithCapabilities(); services.AddHandler(); + + services.AddHandlerWithCapabilities(); } services.AddHandler(); @@ -217,7 +216,6 @@ static void AddHandlers(IServiceCollection services, LanguageServerFeatureOption services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); - services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorRequestExecutionQueue.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorRequestExecutionQueue.cs index 5abaec0dd0f..157c7038534 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorRequestExecutionQueue.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorRequestExecutionQueue.cs @@ -19,12 +19,10 @@ public RazorRequestExecutionQueue(AbstractLanguageServer la _capabilitiesManager = languageServer.GetLspServices().GetRequiredService(); } - [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD003:Avoid awaiting foreign Tasks", Justification = "")] - public override Task WrapStartRequestTaskAsync(Task nonMutatingRequestTask, bool rethrowExceptions) + protected internal override void BeforeRequest(TRequest request) { + // Update the locale for this request to the desired LSP locale. CultureInfo.CurrentUICulture = GetCultureForRequest(); - - return nonMutatingRequestTask; } private CultureInfo GetCultureForRequest() diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Refactoring/RenameEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Refactoring/RenameEndpoint.cs index e7396106fce..2f3370d7187 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Refactoring/RenameEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Refactoring/RenameEndpoint.cs @@ -1,21 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; -using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; using Microsoft.AspNetCore.Razor.Threading; -using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -32,6 +23,7 @@ internal sealed class RenameEndpoint( LanguageServerFeatureOptions languageServerFeatureOptions, IDocumentMappingService documentMappingService, IEditMappingService editMappingService, + IProjectSnapshotManager projectManager, IClientConnection clientConnection, ILoggerFactory loggerFactory) : AbstractRazorDelegatingEndpoint( @@ -43,6 +35,7 @@ internal sealed class RenameEndpoint( private readonly IRenameService _renameService = renameService; private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; private readonly IEditMappingService _editMappingService = editMappingService; + private readonly IProjectSnapshotManager _projectManager = projectManager; public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, VSInternalClientCapabilities clientCapabilities) { @@ -64,7 +57,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V return SpecializedTasks.Null(); } - return _renameService.TryGetRazorRenameEditsAsync(documentContext, positionInfo, request.NewName, cancellationToken); + return _renameService.TryGetRazorRenameEditsAsync(documentContext, positionInfo, request.NewName, _projectManager.GetQueryOperations(), cancellationToken); } protected override bool IsSupported() diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/SR.resx b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/SR.resx index ad72987e68f..f652571c68b 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/SR.resx +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/SR.resx @@ -141,18 +141,9 @@ Generate Event Handler '{0}' - - Not available in - "Re-trigger completions..." - - Razor TagHelper Attribute Glyph - - - Razor TagHelper Element Glyph - Unknown ProjectChangeKind {0} @@ -162,4 +153,7 @@ statement + + Extract element to new component + \ No newline at end of file diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.cs.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.cs.xlf index 05e18dd9493..50a3db2584a 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.cs.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.cs.xlf @@ -27,6 +27,11 @@ Extrahovat blok do kódu na pozadí + + Extract element to new component + Extrahovat element do nové komponenty + + File was externally modified: {0} Došlo k externí úpravě souboru: {0} @@ -42,11 +47,6 @@ Generovat obslužnou rutinu události {0} - - Not available in - Není k dispozici v - - "Re-trigger completions..." Aktivovat znovu dokončení… @@ -57,16 +57,6 @@ příkaz - - Razor TagHelper Attribute Glyph - Piktogram atributu Razor TagHelper - - - - Razor TagHelper Element Glyph - Piktogram elementu Razor TagHelper - - Unknown ProjectChangeKind {0} Neznámý ProjectChangeKind {0} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.de.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.de.xlf index ec8d65eaadd..82383d256ab 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.de.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.de.xlf @@ -27,6 +27,11 @@ Block auf CodeBehind extrahieren + + Extract element to new component + Element in neue Komponente extrahieren + + File was externally modified: {0} Datei wurde extern modifiziert: {0}. @@ -42,11 +47,6 @@ Ereignishandler "{0}" generieren - - Not available in - Nicht verfügbar in - - "Re-trigger completions..." "Abschlüsse erneut auslösen..." @@ -57,16 +57,6 @@ Anweisung - - Razor TagHelper Attribute Glyph - Razor TagHelper-Attributsymbol - - - - Razor TagHelper Element Glyph - Razor TagHelper-Elementsymbol - - Unknown ProjectChangeKind {0} Unbekannte ProjectChangeKind {0} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.es.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.es.xlf index a5682b0ee0d..ef0adbcc20d 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.es.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.es.xlf @@ -27,6 +27,11 @@ Extraer el bloque al código subyacente + + Extract element to new component + Extraer elemento a componente nuevo + + File was externally modified: {0} El archivo se modificó externamente: {0} @@ -42,11 +47,6 @@ Generar controlador de eventos ''{0}'' - - Not available in - No disponible en - - "Re-trigger completions..." "Volver a desencadenar las finalizaciones..." @@ -57,16 +57,6 @@ instrucción - - Razor TagHelper Attribute Glyph - Glifo del atributo TagHelper de Razor - - - - Razor TagHelper Element Glyph - Glifo del elemento TagHelper de Razor - - Unknown ProjectChangeKind {0} ProjectChangeKind desconocido {0} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.fr.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.fr.xlf index 62b80c7f26d..faefba378b1 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.fr.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.fr.xlf @@ -27,6 +27,11 @@ Extraire le bloc vers le code-behind + + Extract element to new component + Extraire l’élément vers un nouveau composant + + File was externally modified: {0} Le fichier a été modifié en externe : {0} @@ -42,11 +47,6 @@ Générer le gestionnaire d’événements '{0}' - - Not available in - Non disponible dans - - "Re-trigger completions..." « Déclencher à nouveau les complétions... » @@ -57,16 +57,6 @@ déclaration - - Razor TagHelper Attribute Glyph - Glyphe d’attribut Razor TagHelper - - - - Razor TagHelper Element Glyph - Glyphe de l’élément Razor TagHelper - - Unknown ProjectChangeKind {0} ProjectChangeKind inconnu {0} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.it.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.it.xlf index d8f8e84cf39..03349a73eec 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.it.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.it.xlf @@ -27,6 +27,11 @@ Estrai il blocco in code-behind + + Extract element to new component + Estrarre elemento nel nuovo componente + + File was externally modified: {0} Il file è stato modificato esternamente: {0} @@ -42,11 +47,6 @@ Genera gestore dell'evento '{0}' - - Not available in - Non disponibili in - - "Re-trigger completions..." "Riattiva i completamenti..." @@ -57,16 +57,6 @@ istruzione - - Razor TagHelper Attribute Glyph - Glifo attributo TagHelper Razor - - - - Razor TagHelper Element Glyph - Glifo elemento TagHelper Razor - - Unknown ProjectChangeKind {0} ProjectChangeKind {0} sconosciuto diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ja.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ja.xlf index e8997eb83db..81b2eb97913 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ja.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ja.xlf @@ -27,6 +27,11 @@ ブロック抽出から分離コード + + Extract element to new component + 要素を新しいコンポーネントに抽出する + + File was externally modified: {0} ファイルが外部で変更されました: {0} @@ -42,11 +47,6 @@ イベント ハンドラー '{0}' の生成 - - Not available in - 次では使用できません: - - "Re-trigger completions..." "再トリガーの完了..." @@ -57,16 +57,6 @@ ステートメント - - Razor TagHelper Attribute Glyph - Razor TagHelper 属性のグリフ - - - - Razor TagHelper Element Glyph - Razor TagHelper 要素のグリフ - - Unknown ProjectChangeKind {0} 不明な ProjectChangeKind {0} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ko.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ko.xlf index 51c6c0c3d67..e7f265e494f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ko.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ko.xlf @@ -27,6 +27,11 @@ 코드 숨김에 블록 추출 + + Extract element to new component + 새 구성 요소에 요소 추출 + + File was externally modified: {0} {0}의 파일이 외부에서 수정되었습니다. @@ -42,11 +47,6 @@ 이벤트 처리기 '{0}' 생성 - - Not available in - 에서 사용할 수 없음 - - "Re-trigger completions..." "완료된 항목 다시 트리거" @@ -57,16 +57,6 @@ - - Razor TagHelper Attribute Glyph - Razor TagHelper 특성 문자 모양 - - - - Razor TagHelper Element Glyph - Razor TagHelper 요소 문자 모양 - - Unknown ProjectChangeKind {0} 알 수 없는 ProjectChangeKind {0} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.pl.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.pl.xlf index 08bc0df0bab..ce76233da57 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.pl.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.pl.xlf @@ -27,6 +27,11 @@ Wyodrębnij blok do kodu znajdującego się poza + + Extract element to new component + Wyodrębnij element do nowego składnika + + File was externally modified: {0} Plik został zmodyfikowany na zewnątrz: {0} @@ -42,11 +47,6 @@ Generuj obsługę zdarzeń „{0}” - - Not available in - Niedostępne w - - "Re-trigger completions..." "Ponownie wyzwalaj uzupełniania..." @@ -57,16 +57,6 @@ instrukcja - - Razor TagHelper Attribute Glyph - Symbol atrybutu pomocnika tagów składni Razor - - - - Razor TagHelper Element Glyph - Symbol elementu pomocnika tagów składni Razor - - Unknown ProjectChangeKind {0} Nieznany ProjectChangeKind {0} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.pt-BR.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.pt-BR.xlf index 39d5779ee15..46d39dc3345 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.pt-BR.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.pt-BR.xlf @@ -27,6 +27,11 @@ Extrair o bloco para codificar atrás + + Extract element to new component + Extrair elemento para o novo componente + + File was externally modified: {0} O arquivo foi modificado externamente: {0} @@ -42,11 +47,6 @@ Gerar manipulador de eventos '{0}' - - Not available in - Não disponível em - - "Re-trigger completions..." "Disparar conclusões novamente..." @@ -57,16 +57,6 @@ instrução - - Razor TagHelper Attribute Glyph - Atributo Glyph Razor TagHelper - - - - Razor TagHelper Element Glyph - Elemento Glyph Razor TagHelper - - Unknown ProjectChangeKind {0} ProjectChangeKind desconhecido {0} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ru.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ru.xlf index 15646d4c15f..25c67fe9d08 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ru.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.ru.xlf @@ -27,6 +27,11 @@ Извлечь блок в код программной части + + Extract element to new component + Извлечь элемент в новый компонент + + File was externally modified: {0} Файл был изменен извне: {0} @@ -42,11 +47,6 @@ Создать обработчик событий "{0}" - - Not available in - Недоступно в - - "Re-trigger completions..." "Повторный запуск завершений..." @@ -57,16 +57,6 @@ инструкция - - Razor TagHelper Attribute Glyph - Глиф атрибута TagHelper Razor - - - - Razor TagHelper Element Glyph - Глиф элемента TagHelper Razor - - Unknown ProjectChangeKind {0} Неизвестный ProjectChangeKind {0} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.tr.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.tr.xlf index 9527d1e496b..09ede15c645 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.tr.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.tr.xlf @@ -27,6 +27,11 @@ Bloğu arkadaki koda ayıkla + + Extract element to new component + Öğeyi yeni bileşene ayıklayın + + File was externally modified: {0} Dosya dışarıdan değiştirildi: {0} @@ -42,11 +47,6 @@ '{0}' Olay İşleyicisi Oluştur - - Not available in - Şurada kullanılamaz: - - "Re-trigger completions..." "Tamamlamaları yeniden tetikleyin..." @@ -57,16 +57,6 @@ deyim - - Razor TagHelper Attribute Glyph - Razor TagHelper Öznitelik Karakteri - - - - Razor TagHelper Element Glyph - Razor TagHelper Element Karakteri - - Unknown ProjectChangeKind {0} Bilinmeyen ProjectChangeKind {0} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.zh-Hans.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.zh-Hans.xlf index c20d488e323..dcf087f5dc4 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.zh-Hans.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.zh-Hans.xlf @@ -27,6 +27,11 @@ 将块提取到代码隐藏中 + + Extract element to new component + 将元素提取到新组件 + + File was externally modified: {0} 已从外部修改了文件: {0}。 @@ -42,11 +47,6 @@ 生成事件处理程序 "{0}" - - Not available in - 在以下位置不可用 - - "Re-trigger completions..." “重新触发完成…” @@ -57,16 +57,6 @@ 语句 - - Razor TagHelper Attribute Glyph - Razor TagHelper 特性字形 - - - - Razor TagHelper Element Glyph - Razor TagHelper 元素字形 - - Unknown ProjectChangeKind {0} 未知的 ProjectChangeKind {0} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.zh-Hant.xlf b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.zh-Hant.xlf index 5eb30910966..e79025401e8 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.zh-Hant.xlf +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/xlf/SR.zh-Hant.xlf @@ -27,6 +27,11 @@ 擷取區塊以在後方編碼 + + Extract element to new component + 將元素擷取至新元件 + + File was externally modified: {0} 已在外部修改檔案: {0} @@ -42,11 +47,6 @@ 產生事件處理常式 '{0}' - - Not available in - 無法使用於 - - "Re-trigger completions..." "重新觸發完成..." @@ -57,16 +57,6 @@ 陳述式 - - Razor TagHelper Attribute Glyph - Razor TagHelper 屬性字元 - - - - Razor TagHelper Element Glyph - Razor TagHelper 元素字符 - - Unknown ProjectChangeKind {0} 未知的 ProjectChangeKind {0} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Tooltip/LSPTagHelperTooltipFactory.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Tooltip/LSPTagHelperTooltipFactory.cs deleted file mode 100644 index e8dc72e35ef..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Tooltip/LSPTagHelperTooltipFactory.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Tooltip; -using Microsoft.VisualStudio.LanguageServer.Protocol; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.Tooltip; - -internal abstract class LSPTagHelperTooltipFactory(IProjectSnapshotManager projectManager) : TagHelperTooltipFactoryBase(projectManager) -{ - public abstract Task TryCreateTooltipAsync( - string documentFilePath, - AggregateBoundElementDescription elementDescriptionInfo, - MarkupKind markupKind, - CancellationToken cancellationToken); - - public abstract bool TryCreateTooltip( - AggregateBoundAttributeDescription attributeDescriptionInfo, - MarkupKind markupKind, - [NotNullWhen(true)] out MarkupContent? tooltipContent); -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Tooltip/VSLSPTagHelperTooltipFactory.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Tooltip/VSLSPTagHelperTooltipFactory.cs deleted file mode 100644 index f6ea104b830..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Tooltip/VSLSPTagHelperTooltipFactory.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Diagnostics.CodeAnalysis; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Tooltip; -using Microsoft.VisualStudio.Text.Adornments; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.Tooltip; - -internal abstract class VSLSPTagHelperTooltipFactory(IProjectSnapshotManager projectManager) : TagHelperTooltipFactoryBase(projectManager) -{ - public abstract Task TryCreateTooltipContainerAsync( - string documentFilePath, - AggregateBoundElementDescription elementDescriptionInfo, - CancellationToken cancellationToken); - - public abstract bool TryCreateTooltip( - AggregateBoundAttributeDescription attributeDescriptionInfo, - [NotNullWhen(true)] out ContainerElement? tooltipContent); - - public abstract Task TryCreateTooltipAsync( - string documentFilePath, - AggregateBoundElementDescription elementDescriptionInfo, - CancellationToken cancellationToken); - - public abstract bool TryCreateTooltip( - AggregateBoundAttributeDescription attributeDescriptionInfo, - [NotNullWhen(true)] out ClassifiedTextElement? tooltipContent); -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WorkspaceDiagnosticsRefresher.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WorkspaceDiagnosticsRefresher.cs new file mode 100644 index 00000000000..d7d5d4242c5 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WorkspaceDiagnosticsRefresher.cs @@ -0,0 +1,112 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.AspNetCore.Razor.Utilities; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Threading; + +namespace Microsoft.AspNetCore.Razor.LanguageServer; + +internal sealed class WorkspaceDiagnosticsRefresher : IRazorStartupService, IDisposable +{ + private readonly AsyncBatchingWorkQueue _queue; + private readonly IProjectSnapshotManager _projectSnapshotManager; + private readonly IClientCapabilitiesService _clientCapabilitiesService; + private readonly IClientConnection _clientConnection; + private bool? _supported; + private CancellationTokenSource _disposeTokenSource = new(); + + public WorkspaceDiagnosticsRefresher( + IProjectSnapshotManager projectSnapshotManager, + IClientCapabilitiesService clientCapabilitiesService, + IClientConnection clientConnection, + TimeSpan? delay = null) + { + _clientConnection = clientConnection; + _projectSnapshotManager = projectSnapshotManager; + _clientCapabilitiesService = clientCapabilitiesService; + _queue = new( + delay ?? TimeSpan.FromMilliseconds(200), + ProcessBatchAsync, + _disposeTokenSource.Token); + _projectSnapshotManager.Changed += ProjectSnapshotManager_Changed; + } + + public void Dispose() + { + if (_disposeTokenSource.IsCancellationRequested) + { + return; + } + + _projectSnapshotManager.Changed -= ProjectSnapshotManager_Changed; + _disposeTokenSource.Cancel(); + _disposeTokenSource.Dispose(); + } + + private ValueTask ProcessBatchAsync(CancellationToken token) + { + _clientConnection + .SendNotificationAsync(Methods.WorkspaceDiagnosticRefreshName, token) + .Forget(); + + return default; + } + + private void ProjectSnapshotManager_Changed(object? sender, ProjectChangeEventArgs e) + { + if (e.SolutionIsClosing) + { + return; + } + + _supported ??= GetSupported(); + + if (_supported != true) + { + return; + } + + if (e.Kind is not ProjectChangeKind.DocumentChanged) + { + _queue.AddWork(); + } + } + + private bool? GetSupported() + { + if (!_clientCapabilitiesService.CanGetClientCapabilities) + { + return null; + } + + return _clientCapabilitiesService.ClientCapabilities.Workspace?.Diagnostics?.RefreshSupport; + } + + internal TestAccessor GetTestAccessor() + => new(this); + + internal sealed class TestAccessor(WorkspaceDiagnosticsRefresher instance) + { + + public Task WaitForRefreshAsync() + { + if (instance._disposeTokenSource.IsCancellationRequested) + { + return Task.CompletedTask; + } + + return instance._queue.WaitUntilCurrentBatchCompletesAsync(); + } + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WorkspaceSemanticTokensRefreshNotifier.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WorkspaceSemanticTokensRefreshNotifier.cs index af8ae8a3d26..b0930093fa7 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WorkspaceSemanticTokensRefreshNotifier.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WorkspaceSemanticTokensRefreshNotifier.cs @@ -5,27 +5,24 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.AspNetCore.Razor.Utilities; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.VisualStudio.Threading; namespace Microsoft.AspNetCore.Razor.LanguageServer; -internal class WorkspaceSemanticTokensRefreshNotifier : IWorkspaceSemanticTokensRefreshNotifier, IDisposable +internal sealed class WorkspaceSemanticTokensRefreshNotifier : IWorkspaceSemanticTokensRefreshNotifier, IDisposable { - private static readonly TimeSpan s_delay = TimeSpan.FromMilliseconds(250); - private readonly IClientCapabilitiesService _clientCapabilitiesService; private readonly IClientConnection _clientConnection; private readonly CancellationTokenSource _disposeTokenSource; private readonly IDisposable _optionsChangeListener; - private readonly object _gate = new(); - private bool? _supportsRefresh; - private bool _waitingToRefresh; - private Task _refreshTask = Task.CompletedTask; + private readonly AsyncBatchingWorkQueue _queue; private bool _isColoringBackground; + private bool? _supportsRefresh; public WorkspaceSemanticTokensRefreshNotifier( IClientCapabilitiesService clientCapabilitiesService, @@ -37,10 +34,24 @@ public WorkspaceSemanticTokensRefreshNotifier( _disposeTokenSource = new(); + _queue = new( + TimeSpan.FromMilliseconds(250), + ProcessBatchAsync, + _disposeTokenSource.Token); + _isColoringBackground = optionsMonitor.CurrentValue.ColorBackground; _optionsChangeListener = optionsMonitor.OnChange(HandleOptionsChange); } + private ValueTask ProcessBatchAsync(CancellationToken token) + { + _clientConnection + .SendNotificationAsync(Methods.WorkspaceSemanticTokensRefreshName, token) + .Forget(); + + return default; + } + public void Dispose() { if (_disposeTokenSource.IsCancellationRequested) @@ -70,58 +81,35 @@ public void NotifyWorkspaceSemanticTokensRefresh() return; } - lock (_gate) + // We could have been called before the LSP server has even been initialized + if (!_clientCapabilitiesService.CanGetClientCapabilities) { - if (_waitingToRefresh) - { - // We're going to refresh shortly. - return; - } - - // We could have been called before the LSP server has even been initialized - if (!_clientCapabilitiesService.CanGetClientCapabilities) - { - return; - } - - _supportsRefresh ??= _clientCapabilitiesService.ClientCapabilities.Workspace?.SemanticTokens?.RefreshSupport ?? false; - - if (_supportsRefresh is false) - { - return; - } - - _refreshTask = RefreshAfterDelayAsync(); - _waitingToRefresh = true; + return; } - async Task RefreshAfterDelayAsync() - { - await Task.Delay(s_delay, _disposeTokenSource.Token).ConfigureAwait(false); - - _clientConnection - .SendNotificationAsync(Methods.WorkspaceSemanticTokensRefreshName, _disposeTokenSource.Token) - .Forget(); + _supportsRefresh ??= _clientCapabilitiesService.ClientCapabilities.Workspace?.SemanticTokens?.RefreshSupport ?? false; - _waitingToRefresh = false; + if (_supportsRefresh is false) + { + return; } + + _queue.AddWork(); } internal TestAccessor GetTestAccessor() => new(this); - internal class TestAccessor(WorkspaceSemanticTokensRefreshNotifier instance) + internal sealed class TestAccessor(WorkspaceSemanticTokensRefreshNotifier instance) { - public async Task WaitForNotificationAsync() + public Task WaitForNotificationAsync() { - Task refreshTask; - - lock (instance._gate) + if (instance._disposeTokenSource.IsCancellationRequested) { - refreshTask = instance._refreshTask; + return Task.CompletedTask; } - await refreshTask.ConfigureAwait(false); + return instance._queue.WaitUntilCurrentBatchCompletesAsync(); } } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WrapWithTag/WrapWithTagEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WrapWithTag/WrapWithTagEndpoint.cs index 0923e486b01..67359999a29 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WrapWithTag/WrapWithTagEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/WrapWithTag/WrapWithTagEndpoint.cs @@ -63,6 +63,19 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(WrapWithTagParams reques // Instead of C#, which certainly would be expected to go in an if statement, we'll see HTML, which obviously // is the better choice for this operation. var languageKind = codeDocument.GetLanguageKind(hostDocumentIndex, rightAssociative: true); + + // However, reverse scenario is possible as well, when we have + //
+ // |@if (true) {} + //

+ //
+ // in which case right-associative GetLanguageKind will return Razor and left-associative will return HTML + // We should hand that case as well, see https://github.com/dotnet/razor/issues/10819 + if (languageKind is RazorLanguageKind.Razor) + { + languageKind = codeDocument.GetLanguageKind(hostDocumentIndex, rightAssociative: false); + } + if (languageKind is not RazorLanguageKind.Html) { // In general, we don't support C# for obvious reasons, but we can support implicit expressions. ie diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/CompilationTagHelperResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/CompilationTagHelperResolver.cs deleted file mode 100644 index 05b2edf83cd..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/CompilationTagHelperResolver.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Buffers; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.AspNetCore.Razor.Telemetry; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Razor; - -namespace Microsoft.AspNetCore.Razor; - -internal sealed class CompilationTagHelperResolver(ITelemetryReporter telemetryReporter) -{ - private readonly ITelemetryReporter _telemetryReporter = telemetryReporter; - - public async ValueTask> GetTagHelpersAsync( - Project project, - RazorProjectEngine projectEngine, - CancellationToken cancellationToken) - { - var providers = projectEngine.Engine.Features - .OfType() - .OrderBy(static f => f.Order) - .ToImmutableArray(); - - if (providers is []) - { - return []; - } - - var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); - if (compilation is null || !CompilationTagHelperFeature.IsValidCompilation(compilation)) - { - return []; - } - - using var pooledHashSet = HashSetPool.GetPooledObject(out var results); - using var pooledWatch = StopwatchPool.GetPooledObject(out var watch); - using var pooledSpan = ArrayPool.Shared.GetPooledArraySpan(minimumLength: providers.Length, out var properties); - - var context = new TagHelperDescriptorProviderContext(compilation, results) - { - ExcludeHidden = true, - IncludeDocumentation = true - }; - - for (var i = 0; i < providers.Length; i++) - { - var provider = providers[i]; - - watch.Restart(); - provider.Execute(context); - watch.Stop(); - - properties[i] = new($"{provider.GetType().Name}.elapsedtimems", watch.ElapsedMilliseconds); - } - - _telemetryReporter.ReportEvent("taghelperresolver/gettaghelpers", Severity.Normal, properties); - - return [.. results]; - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Microsoft.AspNetCore.Razor.ProjectEngineHost.csproj b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Microsoft.AspNetCore.Razor.ProjectEngineHost.csproj index a1b367de03d..3e1f302dc61 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Microsoft.AspNetCore.Razor.ProjectEngineHost.csproj +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Microsoft.AspNetCore.Razor.ProjectEngineHost.csproj @@ -6,7 +6,7 @@ Microsoft.AspNetCore.Razor false false - true + true false true diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/ProjectExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/ProjectExtensions.cs new file mode 100644 index 00000000000..3b08ef93f77 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/ProjectExtensions.cs @@ -0,0 +1,87 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Buffers; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.AspNetCore.Razor.Telemetry; +using Microsoft.CodeAnalysis.Razor; + +namespace Microsoft.CodeAnalysis; + +internal static class ProjectExtensions +{ + private const string GetTagHelpersEventName = "taghelperresolver/gettaghelpers"; + private const string PropertySuffix = ".elapsedtimems"; + + /// + /// Gets the available tag helpers from the specified + /// using the given . + /// + /// + /// A telemetry event will be reported to . + /// + public static async ValueTask> GetTagHelpersAsync( + this Project project, + RazorProjectEngine projectEngine, + ITelemetryReporter telemetryReporter, + CancellationToken cancellationToken) + { + var providers = GetTagHelperDescriptorProviders(projectEngine); + + if (providers is []) + { + return []; + } + + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + if (compilation is null || !CompilationTagHelperFeature.IsValidCompilation(compilation)) + { + return []; + } + + using var pooledHashSet = HashSetPool.GetPooledObject(out var results); + using var pooledWatch = StopwatchPool.GetPooledObject(out var watch); + using var pooledSpan = ArrayPool.Shared.GetPooledArraySpan(minimumLength: providers.Length, out var properties); + + var context = new TagHelperDescriptorProviderContext(compilation, results) + { + ExcludeHidden = true, + IncludeDocumentation = true + }; + + var writeProperties = properties; + + foreach (var provider in providers) + { + watch.Restart(); + provider.Execute(context); + watch.Stop(); + + writeProperties[0] = new(provider.GetType().Name + PropertySuffix, watch.ElapsedMilliseconds); + writeProperties = writeProperties[1..]; + } + + telemetryReporter.ReportEvent(GetTagHelpersEventName, Severity.Normal, properties); + + return [.. results]; + } + + private static ImmutableArray GetTagHelperDescriptorProviders(RazorProjectEngine projectEngine) + { + using var result = new PooledArrayBuilder(); + + foreach (var feature in projectEngine.Engine.Features) + { + if (feature is ITagHelperDescriptorProvider provider) + { + result.Add(provider); + } + } + + return result.DrainToImmutableOrderedBy(static x => x.Order); + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Serialization/MessagePack/Formatters/RazorConfigurationFormatter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Serialization/MessagePack/Formatters/RazorConfigurationFormatter.cs index 94a229eedb7..6232c1fabdb 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Serialization/MessagePack/Formatters/RazorConfigurationFormatter.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Serialization/MessagePack/Formatters/RazorConfigurationFormatter.cs @@ -22,14 +22,10 @@ public override RazorConfiguration Deserialize(ref MessagePackReader reader, Ser var configurationName = CachedStringFormatter.Instance.Deserialize(ref reader, options) ?? string.Empty; var languageVersionText = CachedStringFormatter.Instance.Deserialize(ref reader, options) ?? string.Empty; + var suppressAddComponentParameter = reader.ReadBoolean(); + var useConsolidatedMvcViews = reader.ReadBoolean(); - count -= 2; - - if (reader.NextMessagePackType is MessagePackType.Boolean) - { - reader.ReadBoolean(); // forceRuntimeCodeGeneration - count -= 1; - } + count -= 4; using var builder = new PooledArrayBuilder(); @@ -45,14 +41,19 @@ public override RazorConfiguration Deserialize(ref MessagePackReader reader, Ser ? version : RazorLanguageVersion.Version_2_1; - return new(languageVersion, configurationName, extensions); + return new( + languageVersion, + configurationName, + extensions, + UseConsolidatedMvcViews: useConsolidatedMvcViews, + SuppressAddComponentParameter: suppressAddComponentParameter); } public override void Serialize(ref MessagePackWriter writer, RazorConfiguration value, SerializerCachingOptions options) { - // Write 3 values + 1 value per extension. + // Write 4 values + 1 value per extension. var extensions = value.Extensions; - var count = extensions.Length + 2; + var count = extensions.Length + 4; writer.WriteArrayHeader(count); @@ -67,7 +68,10 @@ public override void Serialize(ref MessagePackWriter writer, RazorConfiguration CachedStringFormatter.Instance.Serialize(ref writer, value.LanguageVersion.ToString(), options); } - count -= 2; + writer.Write(value.SuppressAddComponentParameter); + writer.Write(value.UseConsolidatedMvcViews); + + count -= 4; for (var i = 0; i < count; i++) { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Serialization/MessagePack/SerializationFormat.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Serialization/MessagePack/SerializationFormat.cs index bb550776606..0fc5c52667c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Serialization/MessagePack/SerializationFormat.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Serialization/MessagePack/SerializationFormat.cs @@ -9,5 +9,5 @@ internal static class SerializationFormat // or any of the types that compose it changes. This includes: RazorConfiguration, // ProjectWorkspaceState, TagHelperDescriptor, and DocumentSnapshotHandle. // NOTE: If this version is changed, a coordinated insertion is required between Roslyn and Razor for the C# extension. - public const int Version = 5; + public const int Version = 6; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Telemetry/ITelemetryReporter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Telemetry/ITelemetryReporter.cs index 7f0814a98b6..ea2475347f7 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Telemetry/ITelemetryReporter.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Telemetry/ITelemetryReporter.cs @@ -22,4 +22,14 @@ internal interface ITelemetryReporter void ReportEvent(string name, Severity severity, params ReadOnlySpan properties); void ReportFault(Exception exception, string? message, params object?[] @params); + + /// + /// Reports timing data for an lsp request + /// + /// The method name + /// The language for the request + /// How long the request was in the queue before it was handled by code + /// How long it took to handle the request + /// The result of handling the request + void ReportRequestTiming(string name, string? language, TimeSpan queuedDuration, TimeSpan requestDuration, TelemetryResult result); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Telemetry/NoOpTelemetryReporter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Telemetry/NoOpTelemetryReporter.cs index 28a937da055..36b37e8e8ae 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Telemetry/NoOpTelemetryReporter.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Telemetry/NoOpTelemetryReporter.cs @@ -54,4 +54,8 @@ public void ReportFault(Exception exception, string? message, params object?[] @ public TelemetryScope TrackLspRequest(string lspMethodName, string lspServerName, Guid correlationId) => TelemetryScope.Null; + + public void ReportRequestTiming(string name, string? language, TimeSpan queuedDuration, TimeSpan requestDuration, TelemetryResult result) + { + } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Telemetry/TelemetryResult.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Telemetry/TelemetryResult.cs new file mode 100644 index 00000000000..4e642467f80 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Telemetry/TelemetryResult.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Razor.Telemetry; + +internal enum TelemetryResult +{ + Succeeded, + Failed, + Cancelled +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace/RazorProjectInfoFactory.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Utilities/RazorProjectInfoFactory.cs similarity index 83% rename from src/Razor/src/Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace/RazorProjectInfoFactory.cs rename to src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Utilities/RazorProjectInfoFactory.cs index c99a25ec283..bb518d231a5 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace/RazorProjectInfoFactory.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Utilities/RazorProjectInfoFactory.cs @@ -1,23 +1,26 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.AspNetCore.Razor.ProjectEngineHost; using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.AspNetCore.Razor.Serialization; using Microsoft.AspNetCore.Razor.Telemetry; -using Microsoft.AspNetCore.Razor.Utilities; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Razor; -using Microsoft.Extensions.Logging; +using Microsoft.CodeAnalysis.Razor.Compiler.CSharp; -namespace Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace; +namespace Microsoft.AspNetCore.Razor.Utilities; internal static class RazorProjectInfoFactory { @@ -30,19 +33,17 @@ static RazorProjectInfoFactory() : StringComparison.OrdinalIgnoreCase; } - public static async Task ConvertAsync(Project project, ILogger? logger, CancellationToken cancellationToken) + public static async Task ConvertAsync(Project project, CancellationToken cancellationToken) { var projectPath = Path.GetDirectoryName(project.FilePath); if (projectPath is null) { - logger?.LogInformation("projectPath is null, skip conversion for {projectId}", project.Id); return null; } var intermediateOutputPath = Path.GetDirectoryName(project.CompilationOutputInfo.AssemblyPath); if (intermediateOutputPath is null) { - logger?.LogInformation("intermediatePath is null, skip conversion for {projectId}", project.Id); return null; } @@ -52,23 +53,19 @@ static RazorProjectInfoFactory() // Not a razor project if (documents.Length == 0) { - if (project.DocumentIds.Count == 0) - { - logger?.LogInformation("No razor documents for {projectId}", project.Id); - } - else - { - logger?.LogTrace("No documents in {projectId}", project.Id); - } - return null; } var csharpLanguageVersion = (project.ParseOptions as CSharpParseOptions)?.LanguageVersion ?? LanguageVersion.Default; - var options = project.AnalyzerOptions.AnalyzerConfigOptionsProvider; - var configuration = ComputeRazorConfigurationOptions(options, logger, out var defaultNamespace); + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + if (compilation is null) + { + return null; + } + var options = project.AnalyzerOptions.AnalyzerConfigOptionsProvider; + var configuration = ComputeRazorConfigurationOptions(options, compilation, out var defaultNamespace); var fileSystem = RazorProjectFileSystem.Create(projectPath); var defaultConfigure = (RazorProjectEngineBuilder builder) => @@ -89,8 +86,7 @@ static RazorProjectInfoFactory() fileSystem, configure: defaultConfigure); - var resolver = new CompilationTagHelperResolver(NoOpTelemetryReporter.Instance); - var tagHelpers = await resolver.GetTagHelpersAsync(project, engine, cancellationToken).ConfigureAwait(false); + var tagHelpers = await project.GetTagHelpersAsync(engine, NoOpTelemetryReporter.Instance, cancellationToken).ConfigureAwait(false); var projectWorkspaceState = ProjectWorkspaceState.Create(tagHelpers, csharpLanguageVersion); @@ -104,7 +100,7 @@ static RazorProjectInfoFactory() documents: documents); } - private static RazorConfiguration ComputeRazorConfigurationOptions(AnalyzerConfigOptionsProvider options, ILogger? logger, out string defaultNamespace) + private static RazorConfiguration ComputeRazorConfigurationOptions(AnalyzerConfigOptionsProvider options, Compilation compilation, out string defaultNamespace) { // See RazorSourceGenerator.RazorProviders.cs @@ -119,11 +115,17 @@ private static RazorConfiguration ComputeRazorConfigurationOptions(AnalyzerConfi if (!globalOptions.TryGetValue("build_property.RazorLangVersion", out var razorLanguageVersionString) || !RazorLanguageVersion.TryParse(razorLanguageVersionString, out var razorLanguageVersion)) { - logger?.LogTrace("Using default of latest language version"); razorLanguageVersion = RazorLanguageVersion.Latest; } - var razorConfiguration = new RazorConfiguration(razorLanguageVersion, configurationName, Extensions: [], UseConsolidatedMvcViews: true); + var suppressAddComponentParameter = !compilation.HasAddComponentParameter(); + + var razorConfiguration = new RazorConfiguration( + razorLanguageVersion, + configurationName, + Extensions: [], + UseConsolidatedMvcViews: true, + suppressAddComponentParameter); defaultNamespace = rootNamespace ?? "ASP"; // TODO: Source generator does this. Do we want it? @@ -182,7 +184,7 @@ private static string GetTargetPath(string documentFilePath, string normalizedPr private static bool TryGetFileKind(string filePath, [NotNullWhen(true)] out string? fileKind) { - var extension = Path.GetExtension(filePath.AsSpan()); + var extension = Path.GetExtension(filePath); if (extension.Equals(".cshtml", s_stringComparison)) { @@ -216,8 +218,8 @@ private static bool TryGetRazorFileName(string? filePath, [NotNullWhen(true)] ou var path = filePath.AsSpan(); // Generated files have a path like: virtualcsharp-razor:///e:/Scratch/RazorInConsole/Goo.cshtml__virtual.cs - if (path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && - (path.EndsWith(generatedRazorExtension, s_stringComparison) || path.EndsWith(generatedCshtmlExtension, s_stringComparison))) + if (path.StartsWith(prefix.AsSpan(), StringComparison.OrdinalIgnoreCase) && + (path.EndsWith(generatedRazorExtension.AsSpan(), s_stringComparison) || path.EndsWith(generatedCshtmlExtension.AsSpan(), s_stringComparison))) { // Go through the file path normalizer because it also does Uri decoding, and we're converting from a Uri to a path // but "new Uri(filePath).LocalPath" seems wasteful diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/AutoClosingTagOnAutoInsertProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/AutoClosingTagOnAutoInsertProvider.cs index e5e9febc604..c532fa188c6 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/AutoClosingTagOnAutoInsertProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/AutoInsert/AutoClosingTagOnAutoInsertProvider.cs @@ -207,27 +207,34 @@ private static bool CouldAutoCloseParentOrSelf(string currentTagName, RazorSynta endTag = parentElement.EndTag; } - var isNonTagStructure = potentialStartTagName is null; - if (isNonTagStructure) - { - // We don't want to look outside of our immediate parent for potential parents that we could auto-close because - // auto-closing one of those parents wouldn't actually auto-close them. For instance: - // - //
- // @if (true) - // { - //
|
- // } - // - // If we re-type the `>` in the inner-div we don't want to add another
because it would be out of scope - // for the parent
- return false; - } + // Note - potentialStartTagName can be null for cases when markup element is contained in markup + // or another non-tag structure.We skip non-tag structures and keep going up the tree. + // In cases where a tag is surrounded by a C# statement MarkupElementSyntax will be a child of a + // MarkupBlock, but might still be "stealing" the closing tag of an enclosing element with the + // same tag name. E.g. + // + //
+ // @if (true) + // { + //
| + // } + //
+ // + // In this case, inner
will be parsed as a complete tag with closing
, + // and the outer
will be missing closing tag. We need to keep going up the tree + // until we find either tree root or a tag with the same name missing closing tag. if (string.Equals(potentialStartTagName, currentTagName, StringComparison.Ordinal)) { - // Tag names equal, if the parent is missing an end-tag it could apply to that - // i.e.
|
+ // If we find a parent tag with the same name that's missing closing tag, + // it's likely the case above where inner "unbalanced" tag is "stealing" end tag from the + // parent "balanced" tag. So in reality the end tag for the inner tag is likely missing, + // thus we should insert it. E.g. in the example below + //
+ //
+ //
+ // the closing tag will be parsed as belonging to the inner
rather than outer, parent
+ // and the outer dive will be unbalanced/missing end tag. if (endTag is null) { return true; @@ -235,11 +242,15 @@ private static bool CouldAutoCloseParentOrSelf(string currentTagName, RazorSynta // Has an end-tag; however, it could be another level of parent which is OK lets keep going up } - else - { - // Different tag name, can't apply - return false; - } + + // Don't stop if encountering a different tag name. When there is an unclosed inner tag + // (normal case for auto-insert) syntax tree is pretty strange and wrapping tag with different name + // should not stop us from going up the tree. E.g. continue going up for this case + //
+ //
+ //
| + //
+ //
node = node.Parent; } while (node is not null); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CSSErrorCodes.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/CSSErrorCodes.cs similarity index 89% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CSSErrorCodes.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/CSSErrorCodes.cs index bafb8675ab1..658f918272f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CSSErrorCodes.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/CSSErrorCodes.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -namespace Microsoft.AspNetCore.Razor.LanguageServer; +namespace Microsoft.CodeAnalysis.Razor.Diagnostics; // Note: This type should be kept in sync with WTE's ErrorCodes.cs internal static class CSSErrorCodes diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/HTMLErrorCodes.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/HTMLErrorCodes.cs similarity index 97% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/HTMLErrorCodes.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/HTMLErrorCodes.cs index 015feacef97..dc0042a9dd7 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/HTMLErrorCodes.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/HTMLErrorCodes.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -namespace Microsoft.AspNetCore.Razor.LanguageServer; +namespace Microsoft.CodeAnalysis.Razor.Diagnostics; // Note: This type should be kept in sync with WTE's ErrorCodes.cs internal static class HtmlErrorCodes diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/RazorDiagnosticConverter.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/RazorDiagnosticConverter.cs similarity index 71% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/RazorDiagnosticConverter.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/RazorDiagnosticConverter.cs index 1ee9556ff92..bbbacbee1d5 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/RazorDiagnosticConverter.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/RazorDiagnosticConverter.cs @@ -2,15 +2,17 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; -using System.Collections.Generic; +using System.Collections.Immutable; using System.Globalization; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; -using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; +using LspDiagnostic = Microsoft.VisualStudio.LanguageServer.Protocol.Diagnostic; +using LspDiagnosticSeverity = Microsoft.VisualStudio.LanguageServer.Protocol.DiagnosticSeverity; +using LspRange = Microsoft.VisualStudio.LanguageServer.Protocol.Range; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics; +namespace Microsoft.CodeAnalysis.Razor.Diagnostics; internal static class RazorDiagnosticConverter { @@ -46,9 +48,9 @@ public static VSDiagnosticProjectInformation[] GetProjectInformation(IDocumentSn }]; } - internal static Diagnostic[] Convert(IReadOnlyList diagnostics, SourceText sourceText, IDocumentSnapshot documentSnapshot) + internal static LspDiagnostic[] Convert(ImmutableArray diagnostics, SourceText sourceText, IDocumentSnapshot documentSnapshot) { - var convertedDiagnostics = new Diagnostic[diagnostics.Count]; + var convertedDiagnostics = new LspDiagnostic[diagnostics.Length]; var i = 0; foreach (var diagnostic in diagnostics) @@ -60,18 +62,18 @@ internal static Diagnostic[] Convert(IReadOnlyList diagnostics, } // Internal for testing - internal static DiagnosticSeverity ConvertSeverity(RazorDiagnosticSeverity severity) + internal static LspDiagnosticSeverity ConvertSeverity(RazorDiagnosticSeverity severity) { return severity switch { - RazorDiagnosticSeverity.Error => DiagnosticSeverity.Error, - RazorDiagnosticSeverity.Warning => DiagnosticSeverity.Warning, - _ => DiagnosticSeverity.Information, + RazorDiagnosticSeverity.Error => LspDiagnosticSeverity.Error, + RazorDiagnosticSeverity.Warning => LspDiagnosticSeverity.Warning, + _ => LspDiagnosticSeverity.Information, }; } // Internal for testing - internal static Range? ConvertSpanToRange(SourceSpan sourceSpan, SourceText sourceText) + internal static LspRange? ConvertSpanToRange(SourceSpan sourceSpan, SourceText sourceText) { if (sourceSpan == SourceSpan.Undefined) { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/RazorTranslateDiagnosticsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/RazorTranslateDiagnosticsService.cs similarity index 85% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/RazorTranslateDiagnosticsService.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/RazorTranslateDiagnosticsService.cs index 11fc9b5f5aa..23e1adc835a 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Diagnostics/RazorTranslateDiagnosticsService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Diagnostics/RazorTranslateDiagnosticsService.cs @@ -18,12 +18,14 @@ using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; -using Diagnostic = Microsoft.VisualStudio.LanguageServer.Protocol.Diagnostic; -using DiagnosticSeverity = Microsoft.VisualStudio.LanguageServer.Protocol.DiagnosticSeverity; -using Range = Microsoft.VisualStudio.LanguageServer.Protocol.Range; -using SyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; +using LspDiagnostic = Microsoft.VisualStudio.LanguageServer.Protocol.Diagnostic; +using LspDiagnosticSeverity = Microsoft.VisualStudio.LanguageServer.Protocol.DiagnosticSeverity; +using LspRange = Microsoft.VisualStudio.LanguageServer.Protocol.Range; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Diagnostics; +namespace Microsoft.CodeAnalysis.Razor.Diagnostics; + +using RazorDiagnosticFactory = AspNetCore.Razor.Language.RazorDiagnosticFactory; +using SyntaxNode = AspNetCore.Razor.Language.Syntax.SyntaxNode; /// /// Contains several methods for mapping and filtering Razor and C# diagnostics. It allows for @@ -44,20 +46,27 @@ internal class RazorTranslateDiagnosticsService(IDocumentMappingService document ]).ToFrozenSet(); /// - /// Translates code diagnostics from one representation into another. + /// Translates code diagnostics from one representation into another. /// - /// The `RazorLanguageKind` of the `Diagnostic` objects included in `diagnostics`. - /// An array of `Diagnostic` objects to translate. - /// The `DocumentContext` for the code document associated with the diagnostics. - /// A `CancellationToken` to observe while waiting for the task to complete. + /// + /// The of the objects + /// included in . + /// + /// + /// An array of objects to translate. + /// + /// + /// The for the code document associated with the diagnostics. + /// + /// A token that can be checked to cancel work. /// An array of translated diagnostics - internal async Task TranslateAsync( + internal async Task TranslateAsync( RazorLanguageKind diagnosticKind, - Diagnostic[] diagnostics, - DocumentContext documentContext, + LspDiagnostic[] diagnostics, + IDocumentSnapshot documentSnapshot, CancellationToken cancellationToken) { - var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false); if (codeDocument.IsUnsupported() != false) { _logger.LogInformation($"Unsupported code document."); @@ -78,20 +87,20 @@ internal async Task TranslateAsync( var mappedDiagnostics = MapDiagnostics( diagnosticKind, filteredDiagnostics, - documentContext.Snapshot, + documentSnapshot, codeDocument); return mappedDiagnostics; } - private Diagnostic[] FilterCSharpDiagnostics(Diagnostic[] unmappedDiagnostics, RazorCodeDocument codeDocument) + private LspDiagnostic[] FilterCSharpDiagnostics(LspDiagnostic[] unmappedDiagnostics, RazorCodeDocument codeDocument) { return unmappedDiagnostics.Where(d => !ShouldFilterCSharpDiagnosticBasedOnErrorCode(d, codeDocument)).ToArray(); } - private static Diagnostic[] FilterHTMLDiagnostics( - Diagnostic[] unmappedDiagnostics, + private static LspDiagnostic[] FilterHTMLDiagnostics( + LspDiagnostic[] unmappedDiagnostics, RazorCodeDocument codeDocument) { var syntaxTree = codeDocument.GetSyntaxTree(); @@ -110,14 +119,14 @@ private static Diagnostic[] FilterHTMLDiagnostics( return filteredDiagnostics; } - private Diagnostic[] MapDiagnostics( + private LspDiagnostic[] MapDiagnostics( RazorLanguageKind languageKind, - Diagnostic[] diagnostics, + LspDiagnostic[] diagnostics, IDocumentSnapshot documentSnapshot, RazorCodeDocument codeDocument) { var projects = RazorDiagnosticConverter.GetProjectInformation(documentSnapshot); - using var mappedDiagnostics = new PooledArrayBuilder(); + using var mappedDiagnostics = new PooledArrayBuilder(); foreach (var diagnostic in diagnostics) { @@ -146,7 +155,7 @@ private Diagnostic[] MapDiagnostics( } private static bool InCSharpLiteral( - Diagnostic d, + LspDiagnostic d, SourceText sourceText, RazorSyntaxTree syntaxTree) { @@ -177,7 +186,7 @@ or SyntaxKind.CSharpStatementLiteral or SyntaxKind.CSharpEphemeralTextLiteral; } - private static bool AppliesToTagHelperTagName(Diagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree) + private static bool AppliesToTagHelperTagName(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree) { // Goal of this method is to filter diagnostics that touch TagHelper tag names. Reason being is TagHelpers can output anything. Meaning // If you have a TagHelper like: @@ -214,7 +223,7 @@ private static bool AppliesToTagHelperTagName(Diagnostic diagnostic, SourceText return true; } - private static bool ShouldFilterHtmlDiagnosticBasedOnErrorCode(Diagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree) + private static bool ShouldFilterHtmlDiagnosticBasedOnErrorCode(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree) { if (!diagnostic.Code.HasValue) { @@ -235,7 +244,7 @@ private static bool ShouldFilterHtmlDiagnosticBasedOnErrorCode(Diagnostic diagno _ => false, }; - static bool IsCSharpInStyleBlock(Diagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree) + static bool IsCSharpInStyleBlock(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree) { // C# in a style block causes diagnostics because the HTML background document replaces C# with "~" var owner = syntaxTree.FindInnermostNode(sourceText, diagnostic.Range.Start); @@ -253,7 +262,7 @@ static bool IsCSharpInStyleBlock(Diagnostic diagnostic, SourceText sourceText, R // Ideally this would be solved instead by not emitting the "!" at the HTML backing file, // but we don't currently have a system to accomplish that - static bool IsAnyFilteredTooFewElementsError(Diagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree) + static bool IsAnyFilteredTooFewElementsError(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree) { var owner = syntaxTree.FindInnermostNode(sourceText, diagnostic.Range.Start); if (owner is null) @@ -282,7 +291,7 @@ static bool IsAnyFilteredTooFewElementsError(Diagnostic diagnostic, SourceText s // Ideally this would be solved instead by not emitting the "!" at the HTML backing file, // but we don't currently have a system to accomplish that - static bool IsHtmlWithBangAndMatchingTags(Diagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree) + static bool IsHtmlWithBangAndMatchingTags(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree) { var owner = syntaxTree.FindInnermostNode(sourceText, diagnostic.Range.Start); if (owner is null) @@ -306,11 +315,11 @@ static bool IsHtmlWithBangAndMatchingTags(Diagnostic diagnostic, SourceText sour return haveBang && namesEquivalent; } - static bool IsAnyFilteredInvalidNestingError(Diagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree) + static bool IsAnyFilteredInvalidNestingError(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree) => IsInvalidNestingWarningWithinComponent(diagnostic, sourceText, syntaxTree) || IsInvalidNestingFromBody(diagnostic, sourceText, syntaxTree); - static bool IsInvalidNestingWarningWithinComponent(Diagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree) + static bool IsInvalidNestingWarningWithinComponent(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree) { var owner = syntaxTree.FindInnermostNode(sourceText, diagnostic.Range.Start); if (owner is null) @@ -325,7 +334,7 @@ static bool IsInvalidNestingWarningWithinComponent(Diagnostic diagnostic, Source // Ideally this would be solved instead by not emitting the "!" at the HTML backing file, // but we don't currently have a system to accomplish that - static bool IsInvalidNestingFromBody(Diagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree) + static bool IsInvalidNestingFromBody(LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree) { var owner = syntaxTree.FindInnermostNode(sourceText, diagnostic.Range.Start); if (owner is null) @@ -350,7 +359,7 @@ static bool IsInvalidNestingFromBody(Diagnostic diagnostic, SourceText sourceTex } private static bool InAttributeContainingCSharp( - Diagnostic diagnostic, + LspDiagnostic diagnostic, SourceText sourceText, RazorSyntaxTree syntaxTree, Dictionary processedAttributes) @@ -400,7 +409,7 @@ n is GenericBlockSyntax || } } - private bool ShouldFilterCSharpDiagnosticBasedOnErrorCode(Diagnostic diagnostic, RazorCodeDocument codeDocument) + private bool ShouldFilterCSharpDiagnosticBasedOnErrorCode(LspDiagnostic diagnostic, RazorCodeDocument codeDocument) { if (diagnostic.Code is not { } code || !code.TryGetSecond(out var str) || @@ -413,10 +422,10 @@ private bool ShouldFilterCSharpDiagnosticBasedOnErrorCode(Diagnostic diagnostic, { "CS1525" => ShouldIgnoreCS1525(diagnostic, codeDocument), _ => s_cSharpDiagnosticsToIgnore.Contains(str) && - diagnostic.Severity != DiagnosticSeverity.Error + diagnostic.Severity != LspDiagnosticSeverity.Error }; - bool ShouldIgnoreCS1525(Diagnostic diagnostic, RazorCodeDocument codeDocument) + bool ShouldIgnoreCS1525(LspDiagnostic diagnostic, RazorCodeDocument codeDocument) { if (CheckIfDocumentHasRazorDiagnostic(codeDocument, RazorDiagnosticFactory.TagHelper_EmptyBoundAttribute.Id) && TryGetOriginalDiagnosticRange(diagnostic, codeDocument, out var originalRange) && @@ -440,7 +449,7 @@ private static bool CheckIfDocumentHasRazorDiagnostic(RazorCodeDocument codeDocu return codeDocument.GetSyntaxTree().Diagnostics.Any(razorDiagnosticCode, static (d, code) => d.Id == code); } - private bool TryGetOriginalDiagnosticRange(Diagnostic diagnostic, RazorCodeDocument codeDocument, [NotNullWhen(true)] out Range? originalRange) + private bool TryGetOriginalDiagnosticRange(LspDiagnostic diagnostic, RazorCodeDocument codeDocument, [NotNullWhen(true)] out LspRange? originalRange) { if (IsRudeEditDiagnostic(diagnostic)) { @@ -460,7 +469,7 @@ private bool TryGetOriginalDiagnosticRange(Diagnostic diagnostic, RazorCodeDocum { // Couldn't remap the range correctly. // If this isn't an `Error` Severity Diagnostic we can discard it. - if (diagnostic.Severity != DiagnosticSeverity.Error) + if (diagnostic.Severity != LspDiagnosticSeverity.Error) { return false; } @@ -474,14 +483,14 @@ private bool TryGetOriginalDiagnosticRange(Diagnostic diagnostic, RazorCodeDocum return true; } - private static bool IsRudeEditDiagnostic(Diagnostic diagnostic) + private static bool IsRudeEditDiagnostic(LspDiagnostic diagnostic) { return diagnostic.Code.HasValue && diagnostic.Code.Value.TryGetSecond(out var str) && str.StartsWith("ENC"); } - private bool TryRemapRudeEditRange(Range diagnosticRange, RazorCodeDocument codeDocument, [NotNullWhen(true)] out Range? remappedRange) + private bool TryRemapRudeEditRange(LspRange diagnosticRange, RazorCodeDocument codeDocument, [NotNullWhen(true)] out LspRange? remappedRange) { // This is a rude edit diagnostic that has already been mapped to the Razor document. The mapping isn't absolutely correct though, // it's based on the runtime code generation of the Razor document therefore we need to re-map the already mapped diagnostic in a diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/IRazorGeneratedDocumentExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/IRazorGeneratedDocumentExtensions.cs index 214efe87b4d..e86c686c056 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/IRazorGeneratedDocumentExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/IRazorGeneratedDocumentExtensions.cs @@ -1,4 +1,7 @@ -using System; +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; using Microsoft.CodeAnalysis.Text; namespace Microsoft.AspNetCore.Razor.Language; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorSyntaxTreeExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorSyntaxTreeExtensions.cs index 251bb5f4c5b..03e54cccd0a 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorSyntaxTreeExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorSyntaxTreeExtensions.cs @@ -17,6 +17,9 @@ public static ImmutableArray GetSectionDirectives(this Raz public static ImmutableArray GetCodeBlockDirectives(this RazorSyntaxTree syntaxTree) => GetDirectives(syntaxTree, static d => d.DirectiveDescriptor?.Kind == DirectiveKind.CodeBlock); + public static ImmutableArray GetUsingDirectives(this RazorSyntaxTree syntaxTree) + => GetDirectives(syntaxTree, static d => d.IsUsingDirective(out var _)); + private static ImmutableArray GetDirectives(RazorSyntaxTree syntaxTree, Func predicate) { using var builder = new PooledArrayBuilder(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RoslynLspExtensions_SourceText.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RoslynLspExtensions_SourceText.cs index 5c5d391316f..2a7b95f56e3 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RoslynLspExtensions_SourceText.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RoslynLspExtensions_SourceText.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Razor; using Microsoft.CodeAnalysis.Text; namespace Roslyn.LanguageServer.Protocol; @@ -30,6 +29,5 @@ public static TextChange GetTextChange(this SourceText text, TextEdit edit) => new(text.GetTextSpan(edit.Range), edit.NewText); public static TextEdit GetTextEdit(this SourceText text, TextChange change) - => RoslynLspFactory.CreateTextEdit(text.GetRange(change.Span), change.NewText.AssumeNotNull()); - + => RoslynLspFactory.CreateTextEdit(text.GetRange(change.Span), change.NewText ?? ""); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/VsLspExtensions_SourceText.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/VsLspExtensions_SourceText.cs index 0df608360b8..1eff67c59aa 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/VsLspExtensions_SourceText.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/VsLspExtensions_SourceText.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Text; @@ -46,5 +45,5 @@ public static TextChange GetTextChange(this SourceText text, TextEdit edit) => new(text.GetTextSpan(edit.Range), edit.NewText); public static TextEdit GetTextEdit(this SourceText text, TextChange change) - => VsLspFactory.CreateTextEdit(text.GetRange(change.Span), change.NewText.AssumeNotNull()); + => VsLspFactory.CreateTextEdit(text.GetRange(change.Span), change.NewText ?? ""); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormatter.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormatter.cs index 67971c72e4b..1dd52b3e7b3 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormatter.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/CSharpFormatter.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -24,14 +25,14 @@ internal sealed class CSharpFormatter(IDocumentMappingService documentMappingSer private readonly IDocumentMappingService _documentMappingService = documentMappingService; - public async Task> FormatAsync(FormattingContext context, LinePositionSpan spanToFormat, CancellationToken cancellationToken) + public async Task> FormatAsync(HostWorkspaceServices hostWorkspaceServices, Document csharpDocument, FormattingContext context, LinePositionSpan spanToFormat, CancellationToken cancellationToken) { if (!_documentMappingService.TryMapToGeneratedDocumentRange(context.CodeDocument.GetCSharpDocument(), spanToFormat, out var projectedSpan)) { return []; } - var edits = await GetFormattingEditsAsync(context, projectedSpan, cancellationToken).ConfigureAwait(false); + var edits = await GetFormattingEditsAsync(hostWorkspaceServices, csharpDocument, projectedSpan, context.Options.ToIndentationOptions(), cancellationToken).ConfigureAwait(false); var mappedEdits = MapEditsToHostDocument(context.CodeDocument, edits); return mappedEdits; } @@ -39,13 +40,14 @@ public async Task> FormatAsync(FormattingContext cont public static async Task> GetCSharpIndentationAsync( FormattingContext context, HashSet projectedDocumentLocations, + HostWorkspaceServices hostWorkspaceServices, CancellationToken cancellationToken) { // Sorting ensures we count the marker offsets correctly. // We also want to ensure there are no duplicates to avoid duplicate markers. var filteredLocations = projectedDocumentLocations.OrderAsArray(); - var indentations = await GetCSharpIndentationCoreAsync(context, filteredLocations, cancellationToken).ConfigureAwait(false); + var indentations = await GetCSharpIndentationCoreAsync(context, filteredLocations, hostWorkspaceServices, cancellationToken).ConfigureAwait(false); return indentations; } @@ -56,18 +58,18 @@ private ImmutableArray MapEditsToHostDocument(RazorCodeDocument code return actualEdits.ToImmutableArray(); } - private static async Task> GetFormattingEditsAsync(FormattingContext context, LinePositionSpan projectedSpan, CancellationToken cancellationToken) + private static async Task> GetFormattingEditsAsync(HostWorkspaceServices hostWorkspaceServices, Document csharpDocument, LinePositionSpan projectedSpan, RazorIndentationOptions indentationOptions, CancellationToken cancellationToken) { - var csharpSourceText = context.CodeDocument.GetCSharpSourceText(); + var root = await csharpDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + var csharpSourceText = await csharpDocument.GetTextAsync(cancellationToken).ConfigureAwait(false); var spanToFormat = csharpSourceText.GetTextSpan(projectedSpan); - var root = await context.CSharpWorkspaceDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); Assumes.NotNull(root); - var changes = RazorCSharpFormattingInteractionService.GetFormattedTextChanges(context.CSharpWorkspace.Services, root, spanToFormat, context.Options.ToIndentationOptions(), cancellationToken); + var changes = RazorCSharpFormattingInteractionService.GetFormattedTextChanges(hostWorkspaceServices, root, spanToFormat, indentationOptions, cancellationToken); return changes.ToImmutableArray(); } - private static async Task> GetCSharpIndentationCoreAsync(FormattingContext context, ImmutableArray projectedDocumentLocations, CancellationToken cancellationToken) + private static async Task> GetCSharpIndentationCoreAsync(FormattingContext context, ImmutableArray projectedDocumentLocations, HostWorkspaceServices hostWorkspaceServices, CancellationToken cancellationToken) { // No point calling the C# formatting if we won't be interested in any of its work anyway if (projectedDocumentLocations.Length == 0) @@ -83,7 +85,7 @@ private static async Task> GetCSharpIndentationCoreAsync(Fo // At this point, we have added all the necessary markers and attached annotations. // Let's invoke the C# formatter and hope for the best. - var formattedRoot = RazorCSharpFormattingInteractionService.Format(context.CSharpWorkspace.Services, root, context.Options.ToIndentationOptions(), cancellationToken); + var formattedRoot = RazorCSharpFormattingInteractionService.Format(hostWorkspaceServices, root, context.Options.ToIndentationOptions(), cancellationToken); var formattedText = formattedRoot.GetText(); var desiredIndentationMap = new Dictionary(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs index e0a16ef90d4..52bb5579988 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingContext.cs @@ -7,32 +7,26 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Formatting; -internal sealed class FormattingContext : IDisposable +internal sealed class FormattingContext { - private readonly IAdhocWorkspaceFactory _workspaceFactory; private readonly IFormattingCodeDocumentProvider _codeDocumentProvider; - private Document? _csharpWorkspaceDocument; - private AdhocWorkspace? _csharpWorkspace; - private IReadOnlyList? _formattingSpans; private IReadOnlyDictionary? _indentations; private FormattingContext( IFormattingCodeDocumentProvider codeDocumentProvider, - IAdhocWorkspaceFactory workspaceFactory, - Uri uri, IDocumentSnapshot originalSnapshot, RazorCodeDocument codeDocument, RazorFormattingOptions options, @@ -41,8 +35,6 @@ private FormattingContext( char triggerCharacter) { _codeDocumentProvider = codeDocumentProvider; - _workspaceFactory = workspaceFactory; - Uri = uri; OriginalSnapshot = originalSnapshot; CodeDocument = codeDocument; Options = options; @@ -53,7 +45,6 @@ private FormattingContext( public static bool SkipValidateComponents { get; set; } - public Uri Uri { get; } public IDocumentSnapshot OriginalSnapshot { get; } public RazorCodeDocument CodeDocument { get; } public RazorFormattingOptions Options { get; } @@ -67,24 +58,6 @@ private FormattingContext( public string NewLineString => Environment.NewLine; - public Document CSharpWorkspaceDocument - { - get - { - if (_csharpWorkspaceDocument is null) - { - var workspace = CSharpWorkspace; - var project = workspace.AddProject("TestProject", LanguageNames.CSharp); - var csharpSourceText = CodeDocument.GetCSharpSourceText(); - _csharpWorkspaceDocument = workspace.AddDocument(project.Id, "TestDocument", csharpSourceText); - } - - return _csharpWorkspaceDocument; - } - } - - public AdhocWorkspace CSharpWorkspace => _csharpWorkspace ??= _workspaceFactory.Create(); - /// A Dictionary of int (line number) to IndentationContext. /// /// Don't use this to discover the indentation level you should have, use @@ -252,27 +225,18 @@ public bool TryGetFormattingSpan(int absoluteIndex, [NotNullWhen(true)] out Form return false; } - public void Dispose() - { - _csharpWorkspace?.Dispose(); - if (_csharpWorkspaceDocument != null) - { - _csharpWorkspaceDocument = null; - } - } - - public async Task WithTextAsync(SourceText changedText) + public async Task WithTextAsync(SourceText changedText, CancellationToken cancellationToken) { var changedSnapshot = OriginalSnapshot.WithText(changedText); - var codeDocument = await _codeDocumentProvider.GetCodeDocumentAsync(changedSnapshot).ConfigureAwait(false); + var codeDocument = await _codeDocumentProvider + .GetCodeDocumentAsync(changedSnapshot, cancellationToken) + .ConfigureAwait(false); DEBUG_ValidateComponents(CodeDocument, codeDocument); var newContext = new FormattingContext( _codeDocumentProvider, - _workspaceFactory, - Uri, OriginalSnapshot, codeDocument, Options, @@ -302,20 +266,16 @@ private static void DEBUG_ValidateComponents(RazorCodeDocument oldCodeDocument, } public static FormattingContext CreateForOnTypeFormatting( - Uri uri, IDocumentSnapshot originalSnapshot, RazorCodeDocument codeDocument, RazorFormattingOptions options, IFormattingCodeDocumentProvider codeDocumentProvider, - IAdhocWorkspaceFactory workspaceFactory, bool automaticallyAddUsings, int hostDocumentIndex, char triggerCharacter) { return new FormattingContext( codeDocumentProvider, - workspaceFactory, - uri, originalSnapshot, codeDocument, options, @@ -325,17 +285,13 @@ public static FormattingContext CreateForOnTypeFormatting( } public static FormattingContext Create( - Uri uri, IDocumentSnapshot originalSnapshot, RazorCodeDocument codeDocument, RazorFormattingOptions options, - IFormattingCodeDocumentProvider codeDocumentProvider, - IAdhocWorkspaceFactory workspaceFactory) + IFormattingCodeDocumentProvider codeDocumentProvider) { return new FormattingContext( codeDocumentProvider, - workspaceFactory, - uri, originalSnapshot, codeDocument, options, diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs index 7e57a27d3fc..99c81c15cc6 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/FormattingUtilities.cs @@ -1,7 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System; +using System.Collections.Immutable; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Text; +using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Formatting; @@ -136,4 +141,123 @@ public static string GetIndentationString(int indentation, bool insertSpaces, in var combined = string.Concat(tabPrefix, spaceSuffix); return combined; } + + /// + /// Unindents a span of text with a few caveats: + /// + /// 1. This assumes consistency in tabs/spaces for starting whitespace per line + /// 2. This doesn't format the text, just attempts to remove leading whitespace in a uniform way + /// 3. It will never remove non-whitespace + /// + /// This was made with extracting code into a new file in mind because it's not trivial to format that text and make + /// sure the indentation is right. Use with caution. + /// + public static void NaivelyUnindentSubstring(SourceText text, TextSpan extractionSpan, System.Text.StringBuilder builder) + { + var extractedText = text.GetSubTextString(extractionSpan); + var range = text.GetRange(extractionSpan); + if (range.Start.Line == range.End.Line) + { + builder.Append(extractedText); + return; + } + + var extractedTextSpan = extractedText.AsSpan(); + var indentation = GetNormalizedIndentation(text, extractionSpan); + + foreach (var lineRange in GetLineRanges(extractedText)) + { + var lineSpan = extractedTextSpan[lineRange]; + lineSpan = UnindentLine(lineSpan, indentation); + + foreach (var c in lineSpan) + { + builder.Append(c); + } + } + + // + // Local Methods + // + + static ReadOnlySpan UnindentLine(ReadOnlySpan line, int indentation) + { + var startCharacter = 0; + while (startCharacter < indentation && IsWhitespace(line[startCharacter])) + { + startCharacter++; + } + + return line[startCharacter..]; + } + + // Gets the smallest indentation of all the lines in a given span + static int GetNormalizedIndentation(SourceText sourceText, TextSpan span) + { + var indentation = int.MaxValue; + foreach (var line in sourceText.Lines) + { + if (!span.OverlapsWith(line.Span)) + { + continue; + } + + indentation = Math.Min(indentation, GetIndentation(line)); + } + + return indentation; + } + + static int GetIndentation(TextLine line) + { + if (line.Text is null) + { + return 0; + } + + var indentation = 0; + for (var position = line.Span.Start; position < line.Span.End; position++) + { + var c = line.Text[position]; + if (!IsWhitespace(c)) + { + break; + } + + indentation++; + } + + return indentation; + } + + static bool IsWhitespace(char c) + => c == ' ' || c == '\t'; + + static ImmutableArray GetLineRanges(string text) + { + using var builder = new PooledArrayBuilder(); + var start = 0; + var end = text.IndexOf('\n'); + while (true) + { + if (end == -1) + { + builder.Add(new(start, text.Length)); + break; + } + + // end + 1 to include the new line + builder.Add(new(start, end + 1)); + start = end + 1; + if (start == text.Length) + { + break; + } + + end = text.IndexOf('\n', start); + } + + return builder.DrainToImmutable(); + } + } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IFormattingCodeDocumentProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IFormattingCodeDocumentProvider.cs index 0d1c25da983..34ceec8cb23 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IFormattingCodeDocumentProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/IFormattingCodeDocumentProvider.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -9,5 +10,5 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; internal interface IFormattingCodeDocumentProvider { - Task GetCodeDocumentAsync(IDocumentSnapshot snapshot); + ValueTask GetCodeDocumentAsync(IDocumentSnapshot snapshot, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPass.cs index efea0297972..ebff05ba806 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPass.cs @@ -8,8 +8,10 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Razor.Formatting; @@ -19,13 +21,14 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; /// internal sealed class CSharpFormattingPass( IDocumentMappingService documentMappingService, + IHostServicesProvider hostServicesProvider, ILoggerFactory loggerFactory) - : CSharpFormattingPassBase(documentMappingService, isFormatOnType: false) + : CSharpFormattingPassBase(documentMappingService, hostServicesProvider, isFormatOnType: false) { - private readonly CSharpFormatter _csharpFormatter = new CSharpFormatter(documentMappingService); + private readonly CSharpFormatter _csharpFormatter = new(documentMappingService); private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); - public async override Task> ExecuteAsync(FormattingContext context, ImmutableArray changes, CancellationToken cancellationToken) + protected async override Task> ExecuteCoreAsync(FormattingContext context, RoslynWorkspaceHelper roslynWorkspaceHelper, ImmutableArray changes, CancellationToken cancellationToken) { // Apply previous edits if any. var originalText = context.SourceText; @@ -34,24 +37,25 @@ public async override Task> ExecuteAsync(FormattingCo if (changes.Length > 0) { changedText = changedText.WithChanges(changes); - changedContext = await context.WithTextAsync(changedText).ConfigureAwait(false); + changedContext = await context.WithTextAsync(changedText, cancellationToken).ConfigureAwait(false); } cancellationToken.ThrowIfCancellationRequested(); // Apply original C# edits - var csharpChanges = await FormatCSharpAsync(changedContext, cancellationToken).ConfigureAwait(false); + var csharpDocument = roslynWorkspaceHelper.CreateCSharpDocument(changedContext.CodeDocument); + var csharpChanges = await FormatCSharpAsync(changedContext, roslynWorkspaceHelper.HostWorkspaceServices, csharpDocument, cancellationToken).ConfigureAwait(false); if (csharpChanges.Length > 0) { changedText = changedText.WithChanges(csharpChanges); - changedContext = await changedContext.WithTextAsync(changedText).ConfigureAwait(false); + changedContext = await changedContext.WithTextAsync(changedText, cancellationToken).ConfigureAwait(false); _logger.LogTestOnly($"After FormatCSharpAsync:\r\n{changedText}"); } cancellationToken.ThrowIfCancellationRequested(); - var indentationChanges = await AdjustIndentationAsync(changedContext, startLine: 0, endLineInclusive: changedText.Lines.Count - 1, cancellationToken).ConfigureAwait(false); + var indentationChanges = await AdjustIndentationAsync(changedContext, startLine: 0, endLineInclusive: changedText.Lines.Count - 1, roslynWorkspaceHelper.HostWorkspaceServices, cancellationToken).ConfigureAwait(false); if (indentationChanges.Length > 0) { // Apply the edits that modify indentation. @@ -65,7 +69,7 @@ public async override Task> ExecuteAsync(FormattingCo return changedText.GetTextChangesArray(originalText); } - private async Task> FormatCSharpAsync(FormattingContext context, CancellationToken cancellationToken) + private async Task> FormatCSharpAsync(FormattingContext context, HostWorkspaceServices hostWorkspaceServices, Document csharpDocument, CancellationToken cancellationToken) { var sourceText = context.SourceText; @@ -81,7 +85,8 @@ private async Task> FormatCSharpAsync(FormattingConte // These should already be remapped. var spanToFormat = sourceText.GetLinePositionSpan(span); - var changes = await _csharpFormatter.FormatAsync(context, spanToFormat, cancellationToken).ConfigureAwait(false); + + var changes = await _csharpFormatter.FormatAsync(hostWorkspaceServices, csharpDocument, context, spanToFormat, cancellationToken).ConfigureAwait(false); csharpChanges.AddRange(changes.Where(e => spanToFormat.Contains(sourceText.GetLinePositionSpan(e.Span)))); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.RoslynWorkspaceHelper.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.RoslynWorkspaceHelper.cs new file mode 100644 index 00000000000..3ad4bfee98a --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.RoslynWorkspaceHelper.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Razor.Workspaces; + +namespace Microsoft.CodeAnalysis.Razor.Formatting; + +internal abstract partial class CSharpFormattingPassBase +{ + protected sealed class RoslynWorkspaceHelper(IHostServicesProvider hostServicesProvider) : IDisposable + { + private readonly Lazy _lazyWorkspace = new(() => CreateWorkspace(hostServicesProvider)); + + public HostWorkspaceServices HostWorkspaceServices => _lazyWorkspace.Value.Services; + + public Document CreateCSharpDocument(RazorCodeDocument codeDocument) + { + var project = _lazyWorkspace.Value.CurrentSolution.AddProject("TestProject", "TestProject", LanguageNames.CSharp); + var csharpSourceText = codeDocument.GetCSharpSourceText(); + return project.AddDocument("TestDocument", csharpSourceText); + } + + private static AdhocWorkspace CreateWorkspace(IHostServicesProvider hostServicesProvider) + { + var fallbackServices = hostServicesProvider.GetServices(); + var services = AdhocServices.Create( + workspaceServices: [], + languageServices: [], + fallbackServices); + + return new AdhocWorkspace(services); + } + + public void Dispose() + { + if (_lazyWorkspace.IsValueCreated) + { + _lazyWorkspace.Value.Dispose(); + } + } + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs index a88cfc0faba..b9a476872fe 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpFormattingPassBase.cs @@ -13,22 +13,31 @@ using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; namespace Microsoft.CodeAnalysis.Razor.Formatting; -internal abstract class CSharpFormattingPassBase(IDocumentMappingService documentMappingService, bool isFormatOnType) : IFormattingPass +internal abstract partial class CSharpFormattingPassBase(IDocumentMappingService documentMappingService, IHostServicesProvider hostServicesProvider, bool isFormatOnType) : IFormattingPass { private readonly bool _isFormatOnType = isFormatOnType; protected IDocumentMappingService DocumentMappingService { get; } = documentMappingService; - public abstract Task> ExecuteAsync(FormattingContext context, ImmutableArray changes, CancellationToken cancellationToken); + public async Task> ExecuteAsync(FormattingContext context, ImmutableArray changes, CancellationToken cancellationToken) + { + using var roslynWorkspaceHelper = new RoslynWorkspaceHelper(hostServicesProvider); + + return await ExecuteCoreAsync(context, roslynWorkspaceHelper, changes, cancellationToken).ConfigureAwait(false); + } + + protected abstract Task> ExecuteCoreAsync(FormattingContext context, RoslynWorkspaceHelper roslynWorkspaceHelper, ImmutableArray changes, CancellationToken cancellationToken); - protected async Task> AdjustIndentationAsync(FormattingContext context, int startLine, int endLineInclusive, CancellationToken cancellationToken) + protected async Task> AdjustIndentationAsync(FormattingContext context, int startLine, int endLineInclusive, HostWorkspaceServices hostWorkspaceServices, CancellationToken cancellationToken) { // In this method, the goal is to make final adjustments to the indentation of each line. // We will take into account the following, @@ -113,7 +122,7 @@ protected async Task> AdjustIndentationAsync(Formatti } // Now, invoke the C# formatter to obtain the CSharpDesiredIndentation for all significant locations. - var significantLocationIndentation = await CSharpFormatter.GetCSharpIndentationAsync(context, significantLocations, cancellationToken).ConfigureAwait(false); + var significantLocationIndentation = await CSharpFormatter.GetCSharpIndentationAsync(context, significantLocations, hostWorkspaceServices, cancellationToken).ConfigureAwait(false); // Build source mapping indentation scopes. var sourceMappingIndentations = new SortedDictionary(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs index cad882f26b9..58ba8c123b9 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/CSharpOnTypeFormattingPass.cs @@ -17,6 +17,7 @@ using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -27,12 +28,13 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; /// internal sealed class CSharpOnTypeFormattingPass( IDocumentMappingService documentMappingService, + IHostServicesProvider hostServicesProvider, ILoggerFactory loggerFactory) - : CSharpFormattingPassBase(documentMappingService, isFormatOnType: true) + : CSharpFormattingPassBase(documentMappingService, hostServicesProvider, isFormatOnType: true) { private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); - public async override Task> ExecuteAsync(FormattingContext context, ImmutableArray changes, CancellationToken cancellationToken) + protected async override Task> ExecuteCoreAsync(FormattingContext context, RoslynWorkspaceHelper roslynWorkspaceHelper, ImmutableArray changes, CancellationToken cancellationToken) { // Normalize and re-map the C# edits. var codeDocument = context.CodeDocument; @@ -42,7 +44,7 @@ public async override Task> ExecuteAsync(FormattingCo { if (!DocumentMappingService.TryMapToGeneratedDocumentPosition(codeDocument.GetCSharpDocument(), context.HostDocumentIndex, out _, out var projectedIndex)) { - _logger.LogWarning($"Failed to map to projected position for document {context.Uri}."); + _logger.LogWarning($"Failed to map to projected position for document {context.OriginalSnapshot.FilePath}."); return changes; } @@ -51,7 +53,7 @@ public async override Task> ExecuteAsync(FormattingCo formatOnReturn: true, formatOnTyping: true, formatOnSemicolon: true, formatOnCloseBrace: true); var formattingChanges = await RazorCSharpFormattingInteractionService.GetFormattingChangesAsync( - context.CSharpWorkspaceDocument, + roslynWorkspaceHelper.CreateCSharpDocument(context.CodeDocument), typedChar: context.TriggerCharacter, projectedIndex, context.Options.ToIndentationOptions(), @@ -107,7 +109,7 @@ public async override Task> ExecuteAsync(FormattingCo var formattedText = ApplyChangesAndTrackChange(originalText, filteredChanges, out _, out var spanAfterFormatting); _logger.LogTestOnly($"After C# changes:\r\n{formattedText}"); - var changedContext = await context.WithTextAsync(formattedText).ConfigureAwait(false); + var changedContext = await context.WithTextAsync(formattedText, cancellationToken).ConfigureAwait(false); var linePositionSpanAfterFormatting = formattedText.GetLinePositionSpan(spanAfterFormatting); cancellationToken.ThrowIfCancellationRequested(); @@ -117,9 +119,7 @@ public async override Task> ExecuteAsync(FormattingCo var cleanedText = formattedText.WithChanges(cleanupChanges); _logger.LogTestOnly($"After CleanupDocument:\r\n{cleanedText}"); - changedContext = await changedContext.WithTextAsync(cleanedText).ConfigureAwait(false); - - cancellationToken.ThrowIfCancellationRequested(); + changedContext = await changedContext.WithTextAsync(cleanedText, cancellationToken).ConfigureAwait(false); // At this point we should have applied all edits that adds/removes newlines. // Let's now ensure the indentation of each of those lines is correct. @@ -167,7 +167,7 @@ public async override Task> ExecuteAsync(FormattingCo Debug.Assert(cleanedText.Lines.Count > endLineInclusive, "Invalid range. This is unexpected."); - var indentationChanges = await AdjustIndentationAsync(changedContext, startLine, endLineInclusive, cancellationToken).ConfigureAwait(false); + var indentationChanges = await AdjustIndentationAsync(changedContext, startLine, endLineInclusive, roslynWorkspaceHelper.HostWorkspaceServices, cancellationToken).ConfigureAwait(false); if (indentationChanges.Length > 0) { // Apply the edits that modify indentation. diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs index 818cf7a70d2..63344d0c1f1 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/FormattingDiagnosticValidationPass.cs @@ -27,7 +27,7 @@ public async Task> ExecuteAsync(FormattingContext con var text = context.SourceText; var changedText = text.WithChanges(changes); - var changedContext = await context.WithTextAsync(changedText).ConfigureAwait(false); + var changedContext = await context.WithTextAsync(changedText, cancellationToken).ConfigureAwait(false); var changedDiagnostics = changedContext.CodeDocument.GetSyntaxTree().Diagnostics; // We want to ensure diagnostics didn't change, but since we're formatting things, its expected diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPassBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPassBase.cs index a2f62eecec6..14220eef2e3 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPassBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/HtmlFormattingPassBase.cs @@ -30,9 +30,11 @@ public virtual async Task> ExecuteAsync(FormattingCon if (changes.Length > 0) { - changedText = originalText.WithChanges(changes); + var filteredChanges = FilterIncomingChanges(changedContext, changes); + + changedText = originalText.WithChanges(filteredChanges); // Create a new formatting context for the changed razor document. - changedContext = await context.WithTextAsync(changedText).ConfigureAwait(false); + changedContext = await context.WithTextAsync(changedText, cancellationToken).ConfigureAwait(false); _logger.LogTestOnly($"After normalizedEdits:\r\n{changedText}"); } @@ -48,6 +50,27 @@ public virtual async Task> ExecuteAsync(FormattingCon return changedText.GetTextChangesArray(originalText); } + private static ImmutableArray FilterIncomingChanges(FormattingContext context, ImmutableArray changes) + { + var syntaxTree = context.CodeDocument.GetSyntaxTree(); + + using var changesToKeep = new PooledArrayBuilder(capacity: changes.Length); + + foreach (var change in changes) + { + // Don't keep changes that start inside of a razor comment block. + var comment = syntaxTree.Root.FindInnermostNode(change.Span.Start)?.FirstAncestorOrSelf(); + if (comment is not null) + { + continue; + } + + changesToKeep.Add(change); + } + + return changesToKeep.DrainToImmutable(); + } + private static ImmutableArray AdjustRazorIndentation(FormattingContext context) { // Assume HTML formatter has already run at this point and HTML is relatively indented correctly. diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs index ae6af5f5406..ab892f07de9 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/Passes/RazorFormattingPass.cs @@ -30,7 +30,7 @@ public async Task> ExecuteAsync(FormattingContext con if (changes.Length > 0) { changedText = changedText.WithChanges(changes); - changedContext = await context.WithTextAsync(changedText).ConfigureAwait(false); + changedContext = await context.WithTextAsync(changedText, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs index 8a15e2cb327..326ef4093a8 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Formatting/RazorFormattingService.cs @@ -30,7 +30,6 @@ internal class RazorFormattingService : IRazorFormattingService private static readonly FrozenSet s_htmlTriggerCharacterSet = FrozenSet.ToFrozenSet(["\n", "{", "}", ";"], StringComparer.Ordinal); private readonly IFormattingCodeDocumentProvider _codeDocumentProvider; - private readonly IAdhocWorkspaceFactory _workspaceFactory; private readonly ImmutableArray _documentFormattingPasses; private readonly ImmutableArray _validationPasses; @@ -40,14 +39,13 @@ internal class RazorFormattingService : IRazorFormattingService public RazorFormattingService( IFormattingCodeDocumentProvider codeDocumentProvider, IDocumentMappingService documentMappingService, - IAdhocWorkspaceFactory workspaceFactory, + IHostServicesProvider hostServicesProvider, ILoggerFactory loggerFactory) { _codeDocumentProvider = codeDocumentProvider; - _workspaceFactory = workspaceFactory; _htmlOnTypeFormattingPass = new HtmlOnTypeFormattingPass(loggerFactory); - _csharpOnTypeFormattingPass = new CSharpOnTypeFormattingPass(documentMappingService, loggerFactory); + _csharpOnTypeFormattingPass = new CSharpOnTypeFormattingPass(documentMappingService, hostServicesProvider, loggerFactory); _validationPasses = [ new FormattingDiagnosticValidationPass(loggerFactory), @@ -57,7 +55,7 @@ public RazorFormattingService( [ new HtmlFormattingPass(loggerFactory), new RazorFormattingPass(), - new CSharpFormattingPass(documentMappingService, loggerFactory), + new CSharpFormattingPass(documentMappingService, hostServicesProvider, loggerFactory), .. _validationPasses ]; } @@ -69,7 +67,9 @@ public async Task> GetDocumentFormattingChangesAsync( RazorFormattingOptions options, CancellationToken cancellationToken) { - var codeDocument = await _codeDocumentProvider.GetCodeDocumentAsync(documentContext.Snapshot).ConfigureAwait(false); + var codeDocument = await _codeDocumentProvider + .GetCodeDocumentAsync(documentContext.Snapshot, cancellationToken) + .ConfigureAwait(false); // Range formatting happens on every paste, and if there are Razor diagnostics in the file // that can make some very bad results. eg, given: @@ -97,13 +97,11 @@ public async Task> GetDocumentFormattingChangesAsync( var uri = documentContext.Uri; var documentSnapshot = documentContext.Snapshot; var hostDocumentVersion = documentContext.Snapshot.Version; - using var context = FormattingContext.Create( - uri, + var context = FormattingContext.Create( documentSnapshot, codeDocument, options, - _codeDocumentProvider, - _workspaceFactory); + _codeDocumentProvider); var originalText = context.SourceText; var result = htmlChanges; @@ -130,7 +128,7 @@ public Task> GetCSharpOnTypeFormattingChangesAsync(Do triggerCharacter, [_csharpOnTypeFormattingPass, .. _validationPasses], collapseChanges: false, - automaticallyAddUsings: false, + isCodeActionFormattingRequest: false, cancellationToken: cancellationToken); public Task> GetHtmlOnTypeFormattingChangesAsync(DocumentContext documentContext, ImmutableArray htmlChanges, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) @@ -142,7 +140,7 @@ public Task> GetHtmlOnTypeFormattingChangesAsync(Docu triggerCharacter, [_htmlOnTypeFormattingPass, .. _validationPasses], collapseChanges: false, - automaticallyAddUsings: false, + isCodeActionFormattingRequest: false, cancellationToken: cancellationToken); public async Task TryGetSingleCSharpEditAsync(DocumentContext documentContext, TextChange csharpEdit, RazorFormattingOptions options, CancellationToken cancellationToken) @@ -155,7 +153,7 @@ public Task> GetHtmlOnTypeFormattingChangesAsync(Docu triggerCharacter: '\0', [_csharpOnTypeFormattingPass, .. _validationPasses], collapseChanges: false, - automaticallyAddUsings: false, + isCodeActionFormattingRequest: false, cancellationToken: cancellationToken).ConfigureAwait(false); return razorChanges.SingleOrDefault(); } @@ -170,7 +168,7 @@ public Task> GetHtmlOnTypeFormattingChangesAsync(Docu triggerCharacter: '\0', [_csharpOnTypeFormattingPass], collapseChanges: true, - automaticallyAddUsings: true, + isCodeActionFormattingRequest: true, cancellationToken: cancellationToken).ConfigureAwait(false); return razorChanges.SingleOrDefault(); } @@ -187,7 +185,7 @@ public Task> GetHtmlOnTypeFormattingChangesAsync(Docu triggerCharacter: '\0', [_csharpOnTypeFormattingPass], collapseChanges: true, - automaticallyAddUsings: false, + isCodeActionFormattingRequest: false, cancellationToken: cancellationToken).ConfigureAwait(false); razorChanges = UnwrapCSharpSnippets(razorChanges); @@ -215,7 +213,7 @@ private async Task> ApplyFormattedChangesAsync( char triggerCharacter, ImmutableArray formattingPasses, bool collapseChanges, - bool automaticallyAddUsings, + bool isCodeActionFormattingRequest, CancellationToken cancellationToken) { // If we only received a single edit, let's always return a single edit back. @@ -223,16 +221,19 @@ private async Task> ApplyFormattedChangesAsync( collapseChanges |= generatedDocumentChanges.Length == 1; var documentSnapshot = documentContext.Snapshot; - var uri = documentContext.Uri; - var codeDocument = await _codeDocumentProvider.GetCodeDocumentAsync(documentSnapshot).ConfigureAwait(false); - using var context = FormattingContext.CreateForOnTypeFormatting( - uri, + + // Code actions were computed on the regular document, which with FUSE could be a runtime document. We have to make + // sure for code actions specifically we are formatting that same document, or TextChange spans may not line up + var codeDocument = isCodeActionFormattingRequest + ? await documentSnapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false) + : await _codeDocumentProvider.GetCodeDocumentAsync(documentSnapshot, cancellationToken).ConfigureAwait(false); + + var context = FormattingContext.CreateForOnTypeFormatting( documentSnapshot, codeDocument, options, _codeDocumentProvider, - _workspaceFactory, - automaticallyAddUsings: automaticallyAddUsings, + automaticallyAddUsings: isCodeActionFormattingRequest, hostDocumentIndex, triggerCharacter); var result = generatedDocumentChanges; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/AbstractRazorComponentDefinitionService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/AbstractRazorComponentDefinitionService.cs index 789d1fbdc05..486713db242 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/AbstractRazorComponentDefinitionService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/AbstractRazorComponentDefinitionService.cs @@ -25,7 +25,12 @@ internal abstract class AbstractRazorComponentDefinitionService( private readonly IDocumentMappingService _documentMappingService = documentMappingService; private readonly ILogger _logger = logger; - public async Task GetDefinitionAsync(IDocumentSnapshot documentSnapshot, DocumentPositionInfo positionInfo, bool ignoreAttributes, CancellationToken cancellationToken) + public async Task GetDefinitionAsync( + IDocumentSnapshot documentSnapshot, + DocumentPositionInfo positionInfo, + ISolutionQueryOperations solutionQueryOperations, + bool ignoreAttributes, + CancellationToken cancellationToken) { // If we're in C# then there is no point checking for a component tag, because there won't be one if (positionInfo.LanguageKind == RazorLanguageKind.CSharp) @@ -39,7 +44,7 @@ internal abstract class AbstractRazorComponentDefinitionService( return null; } - var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); + var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false); if (!RazorComponentDefinitionHelpers.TryGetBoundTagHelpers(codeDocument, positionInfo.HostDocumentIndex, ignoreAttributes, _logger, out var boundTagHelper, out var boundAttribute)) { @@ -47,7 +52,10 @@ internal abstract class AbstractRazorComponentDefinitionService( return null; } - var componentDocument = await _componentSearchEngine.TryLocateComponentAsync(documentSnapshot, boundTagHelper).ConfigureAwait(false); + var componentDocument = await _componentSearchEngine + .TryLocateComponentAsync(boundTagHelper, solutionQueryOperations, cancellationToken) + .ConfigureAwait(false); + if (componentDocument is null) { _logger.LogInformation($"Could not locate component document."); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/IRazorComponentDefinitionService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/IRazorComponentDefinitionService.cs index 86ebf62d35f..6a20fba5fc1 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/IRazorComponentDefinitionService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/IRazorComponentDefinitionService.cs @@ -14,5 +14,10 @@ namespace Microsoft.CodeAnalysis.Razor.GoToDefinition; /// internal interface IRazorComponentDefinitionService { - Task GetDefinitionAsync(IDocumentSnapshot documentSnapshot, DocumentPositionInfo positionInfo, bool ignoreAttributes, CancellationToken cancellationToken); + Task GetDefinitionAsync( + IDocumentSnapshot documentSnapshot, + DocumentPositionInfo positionInfo, + ISolutionQueryOperations solutionQueryOperations, + bool ignoreAttributes, + CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/RazorComponentDefinitionHelpers.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/RazorComponentDefinitionHelpers.cs index 8d5352dc20c..1b261699e0a 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/RazorComponentDefinitionHelpers.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/RazorComponentDefinitionHelpers.cs @@ -150,7 +150,7 @@ static bool TryGetTagName(RazorSyntaxNode node, [NotNullWhen(true)] out RazorSyn var csharpSyntaxTree = await documentSnapshot.GetCSharpSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); var root = await csharpSyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); + var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false); // Since we know how the compiler generates the C# source we can be a little specific here, and avoid // long tree walks. If the compiler ever changes how they generate their code, the tests for this will break diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/IAdhocWorkspaceFactory.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/IAdhocWorkspaceFactory.cs deleted file mode 100644 index d84d3f46073..00000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/IAdhocWorkspaceFactory.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using Microsoft.CodeAnalysis.Host; - -namespace Microsoft.CodeAnalysis.Razor.Workspaces; - -internal interface IAdhocWorkspaceFactory -{ - AdhocWorkspace Create(params IWorkspaceService[] workspaceServices); -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/IHostServicesProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/IHostServicesProvider.cs similarity index 81% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/IHostServicesProvider.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/IHostServicesProvider.cs index ed7b2c2a24b..87860a9fd33 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Hosting/IHostServicesProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/IHostServicesProvider.cs @@ -3,7 +3,7 @@ using Microsoft.CodeAnalysis.Host; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +namespace Microsoft.CodeAnalysis.Razor.Workspaces; internal interface IHostServicesProvider { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/IProjectCollectionResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/IProjectCollectionResolver.cs deleted file mode 100644 index 6c337d1838e..00000000000 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/IProjectCollectionResolver.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Collections.Generic; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; - -namespace Microsoft.CodeAnalysis.Razor.Workspaces; - -internal interface IProjectCollectionResolver -{ - IEnumerable EnumerateProjects(IDocumentSnapshot snapshot); -} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/IRazorComponentSearchEngine.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/IRazorComponentSearchEngine.cs index 1d519fc416e..45a998e88c1 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/IRazorComponentSearchEngine.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/IRazorComponentSearchEngine.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -9,5 +10,8 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces; internal interface IRazorComponentSearchEngine { - Task TryLocateComponentAsync(IDocumentSnapshot contextSnapshot, TagHelperDescriptor tagHelper); + Task TryLocateComponentAsync( + TagHelperDescriptor tagHelper, + ISolutionQueryOperations solutionQueryOperations, + CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ITagHelperResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ITagHelperResolver.cs index 34716da3f88..59135bb4d30 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ITagHelperResolver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ITagHelperResolver.cs @@ -11,8 +11,13 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces; internal interface ITagHelperResolver { + /// + /// Gets the available tag helpers from the specified + /// using the given to provide a + /// . + /// ValueTask> GetTagHelpersAsync( - Project workspaceProject, + Project project, IProjectSnapshot projectSnapshot, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj index e47366a1ae2..d74202c9306 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Microsoft.CodeAnalysis.Razor.Workspaces.csproj @@ -5,20 +5,14 @@ Razor is a markup syntax for adding server-side logic to web pages. This package contains the Razor design-time infrastructure. false false - true + true false - - $(NoWarn);IDE0073 - diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/NonCapturingTimer.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/NonCapturingTimer.cs new file mode 100644 index 00000000000..23bb7a90127 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/NonCapturingTimer.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +// https://github.com/dotnet/runtime/blob/11c86d8acba2f248b3afb5e8594f5f41ceebf098/src/libraries/Common/src/Extensions/NonCapturingTimer/NonCapturingTimer.cs + +using System; +using System.Threading; + +namespace Microsoft.Extensions.Internal; + +// A convenience API for interacting with System.Threading.Timer in a way +// that doesn't capture the ExecutionContext. We should be using this (or equivalent) +// everywhere we use timers to avoid rooting any values stored in asynclocals. +internal static class NonCapturingTimer +{ + public static Timer Create(TimerCallback callback, object state, TimeSpan dueTime, TimeSpan period) + { + if (callback is null) + { + throw new ArgumentNullException(nameof(callback)); + } + + // Don't capture the current ExecutionContext and its AsyncLocals onto the timer + var restoreFlow = false; + try + { + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + return new Timer(callback, state, dueTime, period); + } + finally + { + // Restore the current ExecutionContext + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } + } + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentContext.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentContext.cs index 207357dbbcc..e9b24b02cbb 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentContext.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentContext.cs @@ -51,9 +51,9 @@ public ValueTask GetCodeDocumentAsync(CancellationToken cance async ValueTask GetCodeDocumentCoreAsync(CancellationToken cancellationToken) { - var codeDocument = await Snapshot.GetGeneratedOutputAsync().ConfigureAwait(false); - - cancellationToken.ThrowIfCancellationRequested(); + var codeDocument = await Snapshot + .GetGeneratedOutputAsync(cancellationToken) + .ConfigureAwait(false); // Interlock to ensure that we only ever return one instance of RazorCodeDocument. // In race scenarios, when more than one RazorCodeDocument is produced, we want to @@ -70,9 +70,7 @@ public ValueTask GetSourceTextAsync(CancellationToken cancellationTo async ValueTask GetSourceTextCoreAsync(CancellationToken cancellationToken) { - var sourceText = await Snapshot.GetTextAsync().ConfigureAwait(false); - - cancellationToken.ThrowIfCancellationRequested(); + var sourceText = await Snapshot.GetTextAsync(cancellationToken).ConfigureAwait(false); // Interlock to ensure that we only ever return one instance of RazorCodeDocument. // In race scenarios, when more than one RazorCodeDocument is produced, we want to diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs index 7a145bf3ee1..a69c96dfbc0 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs @@ -1,54 +1,48 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; -internal class DocumentSnapshot : IDocumentSnapshot +internal sealed class DocumentSnapshot(ProjectSnapshot project, DocumentState state) : IDocumentSnapshot { - public string FileKind => State.HostDocument.FileKind; - public string FilePath => State.HostDocument.FilePath; - public string TargetPath => State.HostDocument.TargetPath; - public IProjectSnapshot Project => ProjectInternal; - public bool SupportsOutput => true; + private static readonly object s_csharpSyntaxTreeKey = new(); - public int Version => State.Version; + private readonly DocumentState _state = state; + private readonly ProjectSnapshot _project = project; - public ProjectSnapshot ProjectInternal { get; } - public DocumentState State { get; } + public HostDocument HostDocument => _state.HostDocument; - public DocumentSnapshot(ProjectSnapshot project, DocumentState state) - { - ProjectInternal = project ?? throw new ArgumentNullException(nameof(project)); - State = state ?? throw new ArgumentNullException(nameof(state)); - } + public string FileKind => _state.HostDocument.FileKind; + public string FilePath => _state.HostDocument.FilePath; + public string TargetPath => _state.HostDocument.TargetPath; + public IProjectSnapshot Project => _project; + public int Version => _state.Version; - public Task GetTextAsync() - => State.GetTextAsync(); + public ValueTask GetTextAsync(CancellationToken cancellationToken) + => _state.GetTextAsync(cancellationToken); - public Task GetTextVersionAsync() - => State.GetTextVersionAsync(); + public ValueTask GetTextVersionAsync(CancellationToken cancellationToken) + => _state.GetTextVersionAsync(cancellationToken); public bool TryGetText([NotNullWhen(true)] out SourceText? result) - => State.TryGetText(out result); + => _state.TryGetText(out result); public bool TryGetTextVersion(out VersionStamp result) - => State.TryGetTextVersion(out result); + => _state.TryGetTextVersion(out result); - public virtual bool TryGetGeneratedOutput([NotNullWhen(true)] out RazorCodeDocument? result) + public bool TryGetGeneratedOutput([NotNullWhen(true)] out RazorCodeDocument? result) { - if (State.IsGeneratedOutputResultAvailable) + if (_state.TryGetGeneratedOutputAndVersion(out var outputAndVersion)) { -#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits - result = State.GetGeneratedOutputAndVersionAsync(ProjectInternal, this).Result.output; -#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits + result = outputAndVersion.output; return true; } @@ -58,32 +52,61 @@ public virtual bool TryGetGeneratedOutput([NotNullWhen(true)] out RazorCodeDocum public IDocumentSnapshot WithText(SourceText text) { - return new DocumentSnapshot(ProjectInternal, State.WithText(text, VersionStamp.Create())); + return new DocumentSnapshot(_project, _state.WithText(text, VersionStamp.Create())); } - public async Task GetCSharpSyntaxTreeAsync(CancellationToken cancellationToken) + public ValueTask GetCSharpSyntaxTreeAsync(CancellationToken cancellationToken) { - var codeDocument = await GetGeneratedOutputAsync(forceDesignTimeGeneratedOutput: false).ConfigureAwait(false); - var csharpText = codeDocument.GetCSharpSourceText(); - return CSharpSyntaxTree.ParseText(csharpText, cancellationToken: cancellationToken); + return TryGetGeneratedOutput(out var codeDocument) + ? new(GetOrParseCSharpSyntaxTree(codeDocument, cancellationToken)) + : new(GetCSharpSyntaxTreeCoreAsync(cancellationToken)); + + async Task GetCSharpSyntaxTreeCoreAsync(CancellationToken cancellationToken) + { + var codeDocument = await GetGeneratedOutputAsync(forceDesignTimeGeneratedOutput: false, cancellationToken).ConfigureAwait(false); + return GetOrParseCSharpSyntaxTree(codeDocument, cancellationToken); + } } - public virtual async Task GetGeneratedOutputAsync(bool forceDesignTimeGeneratedOutput) + public async ValueTask GetGeneratedOutputAsync(bool forceDesignTimeGeneratedOutput, CancellationToken cancellationToken) { if (forceDesignTimeGeneratedOutput) { - return await GetDesignTimeGeneratedOutputAsync().ConfigureAwait(false); + return await GetDesignTimeGeneratedOutputAsync(cancellationToken).ConfigureAwait(false); } - var (output, _) = await State.GetGeneratedOutputAndVersionAsync(ProjectInternal, this).ConfigureAwait(false); + var (output, _) = await _state + .GetGeneratedOutputAndVersionAsync(_project, this, cancellationToken) + .ConfigureAwait(false); + return output; } - private async Task GetDesignTimeGeneratedOutputAsync() + private async Task GetDesignTimeGeneratedOutputAsync(CancellationToken cancellationToken) { - var tagHelpers = await Project.GetTagHelpersAsync(CancellationToken.None).ConfigureAwait(false); + var tagHelpers = await Project.GetTagHelpersAsync(cancellationToken).ConfigureAwait(false); var projectEngine = Project.GetProjectEngine(); - var imports = await DocumentState.GetImportsAsync(this, projectEngine).ConfigureAwait(false); - return await DocumentState.GenerateCodeDocumentAsync(this, projectEngine, imports, tagHelpers, forceRuntimeCodeGeneration: false).ConfigureAwait(false); + var imports = await DocumentState.GetImportsAsync(this, projectEngine, cancellationToken).ConfigureAwait(false); + return await DocumentState + .GenerateCodeDocumentAsync(this, projectEngine, imports, tagHelpers, forceRuntimeCodeGeneration: false, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Retrieves a cached Roslyn from the generated C# document. + /// If a tree has not yet been cached, a new one will be parsed and added to the cache. + /// + public static SyntaxTree GetOrParseCSharpSyntaxTree(RazorCodeDocument document, CancellationToken cancellationToken) + { + if (!document.Items.TryGetValue(s_csharpSyntaxTreeKey, out SyntaxTree? syntaxTree)) + { + var csharpText = document.GetCSharpSourceText(); + syntaxTree = CSharpSyntaxTree.ParseText(csharpText, cancellationToken: cancellationToken); + document.Items[s_csharpSyntaxTreeKey] = syntaxTree; + + return syntaxTree; + } + + return syntaxTree.AssumeNotNull(); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.ComputedStateTracker.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.ComputedStateTracker.cs index a22e2c4d746..3c8cda5bd82 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.ComputedStateTracker.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.ComputedStateTracker.cs @@ -11,11 +11,11 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; internal partial class DocumentState { - private class ComputedStateTracker + private class ComputedStateTracker(ComputedStateTracker? older = null) { - private readonly object _lock; + private readonly object _lock = new(); - private ComputedStateTracker? _older; + private ComputedStateTracker? _older = older; // We utilize a WeakReference here to avoid bloating committed memory. If pieces request document output inbetween GC collections // then we will provide the weak referenced task; otherwise we require any state requests to be re-computed. @@ -23,12 +23,6 @@ private class ComputedStateTracker private ComputedOutput? _computedOutput; - public ComputedStateTracker(DocumentState state, ComputedStateTracker? older = null) - { - _lock = state._lock; - _older = older; - } - public bool IsResultAvailable { get @@ -52,20 +46,39 @@ public bool IsResultAvailable } } - public async Task<(RazorCodeDocument, VersionStamp)> GetGeneratedOutputAndVersionAsync(ProjectSnapshot project, IDocumentSnapshot document) + public bool TryGetGeneratedOutputAndVersion(out (RazorCodeDocument Output, VersionStamp InputVersion) result) + { + if (_computedOutput?.TryGetCachedOutput(out var output, out var version) == true) + { + result = (output, version); + return true; + } + + result = default; + return false; + } + + public async Task<(RazorCodeDocument, VersionStamp)> GetGeneratedOutputAndVersionAsync( + ProjectSnapshot project, + IDocumentSnapshot document, + CancellationToken cancellationToken) { if (_computedOutput?.TryGetCachedOutput(out var cachedCodeDocument, out var cachedInputVersion) == true) { return (cachedCodeDocument, cachedInputVersion); } - var (codeDocument, inputVersion) = await GetMemoizedGeneratedOutputAndVersionAsync(project, document).ConfigureAwait(false); + var (codeDocument, inputVersion) = await GetMemoizedGeneratedOutputAndVersionAsync(project, document, cancellationToken) + .ConfigureAwait(false); _computedOutput = new ComputedOutput(codeDocument, inputVersion); return (codeDocument, inputVersion); } - private Task<(RazorCodeDocument, VersionStamp)> GetMemoizedGeneratedOutputAndVersionAsync(ProjectSnapshot project, IDocumentSnapshot document) + private Task<(RazorCodeDocument, VersionStamp)> GetMemoizedGeneratedOutputAndVersionAsync( + ProjectSnapshot project, + IDocumentSnapshot document, + CancellationToken cancellationToken) { if (project is null) { @@ -105,7 +118,7 @@ public bool IsResultAvailable } // Typically in VS scenarios this will run synchronously because all resources are readily available. - var outputTask = ComputeGeneratedOutputAndVersionAsync(project, document); + var outputTask = ComputeGeneratedOutputAndVersionAsync(project, document, cancellationToken); if (outputTask.IsCompleted) { // Compiling ran synchronously, lets just immediately propagate to the TCS @@ -156,7 +169,10 @@ static void PropagateToTaskCompletionSource( } } - private async Task<(RazorCodeDocument, VersionStamp)> ComputeGeneratedOutputAndVersionAsync(ProjectSnapshot project, IDocumentSnapshot document) + private async Task<(RazorCodeDocument, VersionStamp)> ComputeGeneratedOutputAndVersionAsync( + ProjectSnapshot project, + IDocumentSnapshot document, + CancellationToken cancellationToken) { // We only need to produce the generated code if any of our inputs is newer than the // previously cached output. @@ -167,11 +183,11 @@ static void PropagateToTaskCompletionSource( // - This document // // All of these things are cached, so no work is wasted if we do need to generate the code. - var configurationVersion = project.State.ConfigurationVersion; - var projectWorkspaceStateVersion = project.State.ProjectWorkspaceStateVersion; - var documentCollectionVersion = project.State.DocumentCollectionVersion; - var imports = await GetImportsAsync(document, project.GetProjectEngine()).ConfigureAwait(false); - var documentVersion = await document.GetTextVersionAsync().ConfigureAwait(false); + var configurationVersion = project.ConfigurationVersion; + var projectWorkspaceStateVersion = project.ProjectWorkspaceStateVersion; + var documentCollectionVersion = project.DocumentCollectionVersion; + var imports = await GetImportsAsync(document, project.GetProjectEngine(), cancellationToken).ConfigureAwait(false); + var documentVersion = await document.GetTextVersionAsync(cancellationToken).ConfigureAwait(false); // OK now that have the previous output and all of the versions, we can see if anything // has changed that would require regenerating the code. @@ -216,9 +232,9 @@ static void PropagateToTaskCompletionSource( } } - var tagHelpers = await project.GetTagHelpersAsync(CancellationToken.None).ConfigureAwait(false); + var tagHelpers = await project.GetTagHelpersAsync(cancellationToken).ConfigureAwait(false); var forceRuntimeCodeGeneration = project.Configuration.LanguageServerFlags?.ForceRuntimeCodeGeneration ?? false; - var codeDocument = await GenerateCodeDocumentAsync(document, project.GetProjectEngine(), imports, tagHelpers, forceRuntimeCodeGeneration).ConfigureAwait(false); + var codeDocument = await GenerateCodeDocumentAsync(document, project.GetProjectEngine(), imports, tagHelpers, forceRuntimeCodeGeneration, cancellationToken).ConfigureAwait(false); return (codeDocument, inputVersion); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs index fded1912608..129bc593aa4 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentState.cs @@ -1,9 +1,9 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; @@ -14,132 +14,123 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; internal partial class DocumentState { - private static readonly TextAndVersion s_emptyText = TextAndVersion.Create( + private static readonly LoadTextOptions s_loadTextOptions = new(SourceHashAlgorithm.Sha256); + + private static readonly TextAndVersion s_emptyTextAndVersion = TextAndVersion.Create( SourceText.From(string.Empty), VersionStamp.Default); - public static readonly Func> EmptyLoader = () => Task.FromResult(s_emptyText); + public static readonly TextLoader EmptyLoader = TextLoader.From(s_emptyTextAndVersion); + + public HostDocument HostDocument { get; } + public int Version { get; } - private readonly object _lock; + private TextAndVersion? _textAndVersion; + private readonly TextLoader _textLoader; private ComputedStateTracker? _computedState; - private readonly Func> _loader; - private Task? _loaderTask; - private SourceText? _sourceText; - private VersionStamp? _textVersion; - private readonly int _version; - - public static DocumentState Create( + private DocumentState( HostDocument hostDocument, int version, - Func>? loader) + TextAndVersion? textAndVersion, + TextLoader? textLoader) { - if (hostDocument is null) - { - throw new ArgumentNullException(nameof(hostDocument)); - } - - return new DocumentState(hostDocument, null, null, version, loader); + HostDocument = hostDocument; + Version = version; + _textAndVersion = textAndVersion; + _textLoader = textLoader ?? EmptyLoader; } - public static DocumentState Create( - HostDocument hostDocument, - Func>? loader) + // Internal for testing + internal DocumentState(HostDocument hostDocument, int version, SourceText text, VersionStamp textVersion) + : this(hostDocument, version, TextAndVersion.Create(text, textVersion), textLoader: null) { - if (hostDocument is null) - { - throw new ArgumentNullException(nameof(hostDocument)); - } - - return new DocumentState(hostDocument, null, null, version: 1, loader); } // Internal for testing - internal DocumentState( - HostDocument hostDocument, - SourceText? text, - VersionStamp? textVersion, - int version, - Func>? loader) + internal DocumentState(HostDocument hostDocument, int version, TextLoader loader) + : this(hostDocument, version, textAndVersion: null, loader) { - HostDocument = hostDocument; - _sourceText = text; - _textVersion = textVersion; - _version = version; - _loader = loader ?? EmptyLoader; - _lock = new object(); } - public HostDocument HostDocument { get; } - public int Version => _version; + public static DocumentState Create(HostDocument hostDocument, int version, TextLoader loader) + { + return new DocumentState(hostDocument, version, loader); + } + + public static DocumentState Create(HostDocument hostDocument, TextLoader loader) + { + return new DocumentState(hostDocument, version: 1, loader); + } - public bool IsGeneratedOutputResultAvailable => ComputedState.IsResultAvailable == true; + public bool IsGeneratedOutputResultAvailable => ComputedState.IsResultAvailable; private ComputedStateTracker ComputedState - { - get - { - if (_computedState is null) - { - lock (_lock) - { - _computedState ??= new ComputedStateTracker(this); - } - } + => _computedState ??= InterlockedOperations.Initialize(ref _computedState, new ComputedStateTracker()); - return _computedState; - } + public bool TryGetGeneratedOutputAndVersion(out (RazorCodeDocument output, VersionStamp inputVersion) result) + { + return ComputedState.TryGetGeneratedOutputAndVersion(out result); } - public Task<(RazorCodeDocument output, VersionStamp inputVersion)> GetGeneratedOutputAndVersionAsync(ProjectSnapshot project, DocumentSnapshot document) + public Task<(RazorCodeDocument output, VersionStamp inputVersion)> GetGeneratedOutputAndVersionAsync( + ProjectSnapshot project, + DocumentSnapshot document, + CancellationToken cancellationToken) { - return ComputedState.GetGeneratedOutputAndVersionAsync(project, document); + return ComputedState.GetGeneratedOutputAndVersionAsync(project, document, cancellationToken); } - public async Task GetTextAsync() + public ValueTask GetTextAndVersionAsync(CancellationToken cancellationToken) { - if (TryGetText(out var text)) - { - return text; - } + return _textAndVersion is TextAndVersion result + ? new(result) + : LoadTextAndVersionAsync(_textLoader, cancellationToken); - lock (_lock) + async ValueTask LoadTextAndVersionAsync(TextLoader loader, CancellationToken cancellationToken) { - _loaderTask = _loader(); - } + var textAndVersion = await loader + .LoadTextAndVersionAsync(s_loadTextOptions, cancellationToken) + .ConfigureAwait(false); - return (await _loaderTask.ConfigureAwait(false)).Text; + return InterlockedOperations.Initialize(ref _textAndVersion, textAndVersion); + } } - public async Task GetTextVersionAsync() + public ValueTask GetTextAsync(CancellationToken cancellationToken) { - if (TryGetTextVersion(out var version)) - { - return version; - } + return TryGetText(out var text) + ? new(text) + : GetTextCoreAsync(cancellationToken); - lock (_lock) + async ValueTask GetTextCoreAsync(CancellationToken cancellationToken) { - _loaderTask = _loader(); - } + var textAsVersion = await GetTextAndVersionAsync(cancellationToken).ConfigureAwait(false); - return (await _loaderTask.ConfigureAwait(false)).Version; + return textAsVersion.Text; + } } - public bool TryGetText([NotNullWhen(true)] out SourceText? result) + public ValueTask GetTextVersionAsync(CancellationToken cancellationToken) { - if (_sourceText is { } sourceText) + return TryGetTextVersion(out var version) + ? new(version) + : GetTextVersionCoreAsync(cancellationToken); + + async ValueTask GetTextVersionCoreAsync(CancellationToken cancellationToken) { - result = sourceText; - return true; + var textAsVersion = await GetTextAndVersionAsync(cancellationToken).ConfigureAwait(false); + + return textAsVersion.Version; } + } - if (_loaderTask is { } loaderTask && loaderTask.IsCompleted) + public bool TryGetText([NotNullWhen(true)] out SourceText? result) + { + if (_textAndVersion is { } textAndVersion) { -#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits - result = loaderTask.Result.Text; -#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits + result = textAndVersion.Text; return true; } @@ -149,17 +140,9 @@ public bool TryGetText([NotNullWhen(true)] out SourceText? result) public bool TryGetTextVersion(out VersionStamp result) { - if (_textVersion is { } version) + if (_textAndVersion is { } textAndVersion) { - result = version; - return true; - } - - if (_loaderTask is { } loaderTask && loaderTask.IsCompleted) - { -#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits - result = loaderTask.Result.Version; -#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits + result = textAndVersion.Version; return true; } @@ -169,13 +152,7 @@ public bool TryGetTextVersion(out VersionStamp result) public virtual DocumentState WithConfigurationChange() { - var state = new DocumentState(HostDocument, _sourceText, _textVersion, _version + 1, _loader) - { - // The source could not have possibly changed. - _sourceText = _sourceText, - _textVersion = _textVersion, - _loaderTask = _loaderTask, - }; + var state = new DocumentState(HostDocument, Version + 1, _textAndVersion, _textLoader); // Do not cache computed state @@ -184,58 +161,36 @@ public virtual DocumentState WithConfigurationChange() public virtual DocumentState WithImportsChange() { - var state = new DocumentState(HostDocument, _sourceText, _textVersion, _version + 1, _loader) - { - // The source could not have possibly changed. - _sourceText = _sourceText, - _textVersion = _textVersion, - _loaderTask = _loaderTask - }; + var state = new DocumentState(HostDocument, Version + 1, _textAndVersion, _textLoader); // Optimistically cache the computed state - state._computedState = new ComputedStateTracker(state, _computedState); + state._computedState = new ComputedStateTracker(_computedState); return state; } public virtual DocumentState WithProjectWorkspaceStateChange() { - var state = new DocumentState(HostDocument, _sourceText, _textVersion, _version + 1, _loader) - { - // The source could not have possibly changed. - _sourceText = _sourceText, - _textVersion = _textVersion, - _loaderTask = _loaderTask - }; + var state = new DocumentState(HostDocument, Version + 1, _textAndVersion, _textLoader); // Optimistically cache the computed state - state._computedState = new ComputedStateTracker(state, _computedState); + state._computedState = new ComputedStateTracker(_computedState); return state; } - public virtual DocumentState WithText(SourceText sourceText, VersionStamp textVersion) + public virtual DocumentState WithText(SourceText text, VersionStamp textVersion) { - if (sourceText is null) - { - throw new ArgumentNullException(nameof(sourceText)); - } - // Do not cache the computed state - return new DocumentState(HostDocument, sourceText, textVersion, _version + 1, null); + return new DocumentState(HostDocument, Version + 1, TextAndVersion.Create(text, textVersion), textLoader: null); } - public virtual DocumentState WithTextLoader(Func> loader) + public virtual DocumentState WithTextLoader(TextLoader textLoader) { - if (loader is null) - { - throw new ArgumentNullException(nameof(loader)); - } - // Do not cache the computed state - return new DocumentState(HostDocument, null, null, _version + 1, loader); + return new DocumentState(HostDocument, Version + 1, textAndVersion: null, textLoader); } // Internal, because we are temporarily sharing code with CohostDocumentSnapshot @@ -243,7 +198,7 @@ internal static ImmutableArray GetImportsCore(IProjectSnapsho { var projectItem = projectEngine.FileSystem.GetItem(filePath, fileKind); - using var _1 = ListPool.GetPooledObject(out var importItems); + using var importItems = new PooledArrayBuilder(); foreach (var feature in projectEngine.ProjectFeatures.OfType()) { @@ -255,10 +210,10 @@ internal static ImmutableArray GetImportsCore(IProjectSnapsho if (importItems.Count == 0) { - return ImmutableArray.Empty; + return []; } - using var _2 = ArrayBuilderPool.GetPooledObject(out var imports); + using var imports = new PooledArrayBuilder(capacity: importItems.Count); foreach (var item in importItems) { @@ -273,28 +228,34 @@ internal static ImmutableArray GetImportsCore(IProjectSnapsho var defaultImport = new ImportDocumentSnapshot(project, item); imports.Add(defaultImport); } - else if (project.GetDocument(item.PhysicalPath) is { } import) + else if (project.TryGetDocument(item.PhysicalPath, out var import)) { imports.Add(import); } } - return imports.ToImmutable(); + return imports.DrainToImmutable(); } - internal static async Task GenerateCodeDocumentAsync(IDocumentSnapshot document, RazorProjectEngine projectEngine, ImmutableArray imports, ImmutableArray tagHelpers, bool forceRuntimeCodeGeneration) + internal static async Task GenerateCodeDocumentAsync( + IDocumentSnapshot document, + RazorProjectEngine projectEngine, + ImmutableArray imports, + ImmutableArray tagHelpers, + bool forceRuntimeCodeGeneration, + CancellationToken cancellationToken) { // OK we have to generate the code. using var importSources = new PooledArrayBuilder(imports.Length); foreach (var item in imports) { var importProjectItem = item.FilePath is null ? null : projectEngine.FileSystem.GetItem(item.FilePath, item.FileKind); - var sourceDocument = await GetRazorSourceDocumentAsync(item.Document, importProjectItem).ConfigureAwait(false); + var sourceDocument = await GetRazorSourceDocumentAsync(item.Document, importProjectItem, cancellationToken).ConfigureAwait(false); importSources.Add(sourceDocument); } var projectItem = document.FilePath is null ? null : projectEngine.FileSystem.GetItem(document.FilePath, document.FileKind); - var documentSource = await GetRazorSourceDocumentAsync(document, projectItem).ConfigureAwait(false); + var documentSource = await GetRazorSourceDocumentAsync(document, projectItem, cancellationToken).ConfigureAwait(false); if (forceRuntimeCodeGeneration) { @@ -304,23 +265,26 @@ internal static async Task GenerateCodeDocumentAsync(IDocumen return projectEngine.ProcessDesignTime(documentSource, fileKind: document.FileKind, importSources.DrainToImmutable(), tagHelpers); } - internal static async Task> GetImportsAsync(IDocumentSnapshot document, RazorProjectEngine projectEngine) + internal static async Task> GetImportsAsync(IDocumentSnapshot document, RazorProjectEngine projectEngine, CancellationToken cancellationToken) { var imports = GetImportsCore(document.Project, projectEngine, document.FilePath.AssumeNotNull(), document.FileKind.AssumeNotNull()); using var result = new PooledArrayBuilder(imports.Length); foreach (var snapshot in imports) { - var versionStamp = await snapshot.GetTextVersionAsync().ConfigureAwait(false); + var versionStamp = await snapshot.GetTextVersionAsync(cancellationToken).ConfigureAwait(false); result.Add(new ImportItem(snapshot.FilePath, versionStamp, snapshot)); } return result.DrainToImmutable(); } - private static async Task GetRazorSourceDocumentAsync(IDocumentSnapshot document, RazorProjectItem? projectItem) + private static async Task GetRazorSourceDocumentAsync( + IDocumentSnapshot document, + RazorProjectItem? projectItem, + CancellationToken cancellationToken) { - var sourceText = await document.GetTextAsync().ConfigureAwait(false); + var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); return RazorSourceDocument.Create(sourceText, RazorSourceDocumentProperties.Create(document.FilePath, projectItem?.RelativePhysicalPath)); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/GeneratedDocumentTextLoader.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/GeneratedDocumentTextLoader.cs index bf2dfab0f26..f5853dfee17 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/GeneratedDocumentTextLoader.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/GeneratedDocumentTextLoader.cs @@ -1,9 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -#nullable disable - -using System; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -12,30 +9,19 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; -internal class GeneratedDocumentTextLoader : TextLoader +internal class GeneratedDocumentTextLoader(IDocumentSnapshot document, string filePath) : TextLoader { - private readonly IDocumentSnapshot _document; - private readonly string _filePath; - private readonly VersionStamp _version; - - public GeneratedDocumentTextLoader(IDocumentSnapshot document, string filePath) - { - if (document is null) - { - throw new ArgumentNullException(nameof(document)); - } - - _document = document; - _filePath = filePath; - _version = VersionStamp.Create(); - } + private readonly IDocumentSnapshot _document = document; + private readonly string _filePath = filePath; + private readonly VersionStamp _version = VersionStamp.Create(); public override async Task LoadTextAndVersionAsync(LoadTextOptions options, CancellationToken cancellationToken) { - var output = await _document.GetGeneratedOutputAsync().ConfigureAwait(false); + var output = await _document.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false); - // Providing an encoding here is important for debuggability. Without this edit-and-continue - // won't work for projects with Razor files. - return TextAndVersion.Create(SourceText.From(output.GetCSharpDocument().GeneratedCode, Encoding.UTF8), _version, _filePath); + // Providing an encoding here is important for debuggability. + // Without this, edit-and-continue won't work for projects with Razor files. + var csharpSourceText = SourceText.From(output.GetCSharpDocument().GeneratedCode, Encoding.UTF8); + return TextAndVersion.Create(csharpSourceText, _version, _filePath); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs index adaf9ee5239..61f0bbdccc0 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostDocument.cs @@ -1,38 +1,20 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; using Microsoft.AspNetCore.Razor.Language; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; -internal class HostDocument +internal sealed record class HostDocument { - public string FileKind { get; } - public string FilePath { get; } - public string TargetPath { get; } + public string FileKind { get; init; } + public string FilePath { get; init; } + public string TargetPath { get; init; } - public HostDocument(HostDocument other) + public HostDocument(string filePath, string targetPath, string? fileKind = null) { - if (other is null) - { - throw new ArgumentNullException(nameof(other)); - } - - FileKind = other.FileKind; - FilePath = other.FilePath; - TargetPath = other.TargetPath; - } - - public HostDocument(string filePath, string targetPath) - : this(filePath, targetPath, fileKind: null) - { - } - - public HostDocument(string filePath, string targetPath, string? fileKind) - { - FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); - TargetPath = targetPath ?? throw new ArgumentNullException(nameof(targetPath)); + FilePath = filePath; + TargetPath = targetPath; FileKind = fileKind ?? FileKinds.GetFileKindFromFilePath(filePath); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostProject.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostProject.cs index 824f8dc1241..c1fdd685eb2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostProject.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/HostProject.cs @@ -1,28 +1,15 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; using System.IO; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.ProjectSystem; +using Microsoft.Extensions.Internal; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; -internal class HostProject +internal record class HostProject { - public HostProject(string projectFilePath, string intermediateOutputPath, RazorConfiguration razorConfiguration, string? rootNamespace, string? displayName = null) - { - FilePath = projectFilePath ?? throw new ArgumentNullException(nameof(projectFilePath)); - IntermediateOutputPath = intermediateOutputPath ?? throw new ArgumentNullException(nameof(intermediateOutputPath)); - Configuration = razorConfiguration ?? throw new ArgumentNullException(nameof(razorConfiguration)); - RootNamespace = rootNamespace; - DisplayName = displayName ?? Path.GetFileNameWithoutExtension(projectFilePath); - - Key = new ProjectKey(intermediateOutputPath); - } - - public RazorConfiguration Configuration { get; } - public ProjectKey Key { get; } /// @@ -35,10 +22,57 @@ public HostProject(string projectFilePath, string intermediateOutputPath, RazorC /// public string IntermediateOutputPath { get; } - public string? RootNamespace { get; } + public RazorConfiguration Configuration { get; init; } + + public string? RootNamespace { get; init; } /// /// An extra user-friendly string to show in the VS navigation bar to help the user, of the form "{ProjectFileName} ({Flavor})" /// - public string DisplayName { get; } + public string DisplayName { get; init; } + + public HostProject( + string filePath, + string intermediateOutputPath, + RazorConfiguration configuration, + string? rootNamespace, + string? displayName = null) + { + FilePath = filePath; + IntermediateOutputPath = intermediateOutputPath; + Configuration = configuration; + RootNamespace = rootNamespace; + DisplayName = displayName ?? Path.GetFileNameWithoutExtension(filePath); + + Key = new(intermediateOutputPath); + } + + public virtual bool Equals(HostProject? other) + { + if (ReferenceEquals(this, other)) + { + return true; + } + + return other is not null && + EqualityContract == other.EqualityContract && + FilePathComparer.Instance.Equals(FilePath, other.FilePath) && + FilePathComparer.Instance.Equals(IntermediateOutputPath, other.IntermediateOutputPath) && + Configuration == other.Configuration && + RootNamespace == other.RootNamespace && + DisplayName == other.DisplayName; + } + + public override int GetHashCode() + { + var hash = HashCodeCombiner.Start(); + + hash.Add(FilePath, FilePathComparer.Instance); + hash.Add(IntermediateOutputPath, FilePathComparer.Instance); + hash.Add(Configuration); + hash.Add(RootNamespace); + hash.Add(DisplayName); + + return hash.CombinedHash; + } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IDocumentSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IDocumentSnapshot.cs index c3e7f78f698..82840ac3bd9 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IDocumentSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IDocumentSnapshot.cs @@ -15,19 +15,22 @@ internal interface IDocumentSnapshot string? FilePath { get; } string? TargetPath { get; } IProjectSnapshot Project { get; } - bool SupportsOutput { get; } int Version { get; } - Task GetTextAsync(); - Task GetTextVersionAsync(); - Task GetGeneratedOutputAsync(bool forceDesignTimeGeneratedOutput); + ValueTask GetTextAsync(CancellationToken cancellationToken); + ValueTask GetTextVersionAsync(CancellationToken cancellationToken); + ValueTask GetGeneratedOutputAsync( + bool forceDesignTimeGeneratedOutput, + CancellationToken cancellationToken); /// - /// Gets the Roslyn syntax tree for the generated C# for this Razor document + /// Gets the Roslyn syntax tree for the generated C# for this Razor document /// - /// Using this from the LSP server side of things is not ideal. Use sparingly :) - Task GetCSharpSyntaxTreeAsync(CancellationToken cancellationToken); + /// + /// ⚠️ Should be used sparingly in language server scenarios. + /// + ValueTask GetCSharpSyntaxTreeAsync(CancellationToken cancellationToken); bool TryGetText([NotNullWhen(true)] out SourceText? result); bool TryGetTextVersion(out VersionStamp result); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IDocumentSnapshotExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IDocumentSnapshotExtensions.cs index 62a90f11432..5f44d2b1d47 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IDocumentSnapshotExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IDocumentSnapshotExtensions.cs @@ -11,7 +11,9 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; internal static class IDocumentSnapshotExtensions { - public static async Task TryGetTagHelperDescriptorAsync(this IDocumentSnapshot documentSnapshot, CancellationToken cancellationToken) + public static async Task TryGetTagHelperDescriptorAsync( + this IDocumentSnapshot documentSnapshot, + CancellationToken cancellationToken) { // No point doing anything if its not a component if (documentSnapshot.FileKind != FileKinds.Component) @@ -19,7 +21,7 @@ internal static class IDocumentSnapshotExtensions return null; } - var razorCodeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); + var razorCodeDocument = await documentSnapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false); if (razorCodeDocument is null) { return null; @@ -53,9 +55,10 @@ public static bool IsPathCandidateForComponent(this IDocumentSnapshot documentSn return fileName.AsSpan().Equals(path.Span, FilePathComparison.Instance); } - public static Task GetGeneratedOutputAsync(this IDocumentSnapshot documentSnapshot) + public static ValueTask GetGeneratedOutputAsync( + this IDocumentSnapshot documentSnapshot, + CancellationToken cancellationToken) { - return documentSnapshot.GetGeneratedOutputAsync(forceDesignTimeGeneratedOutput: false); + return documentSnapshot.GetGeneratedOutputAsync(forceDesignTimeGeneratedOutput: false, cancellationToken); } - } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IProjectSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IProjectSnapshot.cs index 5201a448104..3494b7c108d 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IProjectSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IProjectSnapshot.cs @@ -33,10 +33,12 @@ internal interface IProjectSnapshot string DisplayName { get; } VersionStamp Version { get; } LanguageVersion CSharpLanguageVersion { get; } - ValueTask> GetTagHelpersAsync(CancellationToken cancellationToken); ProjectWorkspaceState ProjectWorkspaceState { get; } RazorProjectEngine GetProjectEngine(); + ValueTask> GetTagHelpersAsync(CancellationToken cancellationToken); + + bool ContainsDocument(string filePath); IDocumentSnapshot? GetDocument(string filePath); bool TryGetDocument(string filePath, [NotNullWhen(true)] out IDocumentSnapshot? document); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IProjectSnapshotExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IProjectSnapshotExtensions.cs index 4735509191e..693b7d6265a 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IProjectSnapshotExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IProjectSnapshotExtensions.cs @@ -20,7 +20,7 @@ public static RazorProjectInfo ToRazorProjectInfo(this IProjectSnapshot project) foreach (var documentFilePath in project.DocumentFilePaths) { - if (project.GetDocument(documentFilePath) is { } document) + if (project.TryGetDocument(documentFilePath, out var document)) { var documentHandle = document.ToHandle(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ISolutionQueryOperations.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ISolutionQueryOperations.cs new file mode 100644 index 00000000000..068a4547d84 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ISolutionQueryOperations.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; + +internal interface ISolutionQueryOperations +{ + /// + /// Returns all Razor project snapshots. + /// + IEnumerable GetProjects(); + + /// + /// Returns all Razor valid project snapshots that contain the given document file path. + /// + /// A file path to a Razor document. + /// + /// In multi-targeting scenarios, this will return a project for each target that the + /// contains the document. + /// + ImmutableArray GetProjectsContainingDocument(string documentFilePath); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ImportDocumentSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ImportDocumentSnapshot.cs index 213a2f2352c..ae94a5e8026 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ImportDocumentSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ImportDocumentSnapshot.cs @@ -3,73 +3,61 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; -internal class ImportDocumentSnapshot : IDocumentSnapshot +internal sealed class ImportDocumentSnapshot(IProjectSnapshot project, RazorProjectItem item) : IDocumentSnapshot { + public IProjectSnapshot Project { get; } = project; + + private readonly RazorProjectItem _importItem = item; + private SourceText? _text; + // The default import file does not have a kind or paths. public string? FileKind => null; public string? FilePath => null; public string? TargetPath => null; - public bool SupportsOutput => false; - public IProjectSnapshot Project => _project; - - private readonly IProjectSnapshot _project; - private readonly RazorProjectItem _importItem; - private SourceText? _sourceText; - private readonly VersionStamp _version; - - public ImportDocumentSnapshot(IProjectSnapshot project, RazorProjectItem item) - { - _project = project; - _importItem = item; - _version = VersionStamp.Default; - } - public int Version => 1; - public async Task GetTextAsync() + public ValueTask GetTextAsync(CancellationToken cancellationToken) { - using (var stream = _importItem.Read()) - using (var reader = new StreamReader(stream)) + return TryGetText(out var text) + ? new(text) + : ReadTextAsync(); + + ValueTask ReadTextAsync() { - var content = await reader.ReadToEndAsync().ConfigureAwait(false); - _sourceText = SourceText.From(content); - } + using var stream = _importItem.Read(); + var sourceText = SourceText.From(stream); - return _sourceText; + var result = _text ??= InterlockedOperations.Initialize(ref _text, sourceText); + return new(result); + } } - public Task GetGeneratedOutputAsync(bool _) + public ValueTask GetGeneratedOutputAsync( + bool forceDesignTimeGeneratedOutput, + CancellationToken cancellationToken) => throw new NotSupportedException(); - public Task GetTextVersionAsync() - { - return Task.FromResult(_version); - } + public ValueTask GetTextVersionAsync(CancellationToken cancellationToken) + => new(VersionStamp.Default); public bool TryGetText([NotNullWhen(true)] out SourceText? result) { - if (_sourceText is { } sourceText) - { - result = sourceText; - return true; - } - - result = null; - return false; + result = _text; + return result is not null; } public bool TryGetTextVersion(out VersionStamp result) { - result = _version; + result = VersionStamp.Default; return true; } @@ -79,6 +67,6 @@ public bool TryGetGeneratedOutput([NotNullWhen(true)] out RazorCodeDocument? res public IDocumentSnapshot WithText(SourceText text) => throw new NotSupportedException(); - public Task GetCSharpSyntaxTreeAsync(CancellationToken cancellationToken) + public ValueTask GetCSharpSyntaxTreeAsync(CancellationToken cancellationToken) => throw new NotSupportedException(); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs index 1278e741d68..8f844402e2e 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshot.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; @@ -16,67 +15,84 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; -internal class ProjectSnapshot : IProjectSnapshot +internal sealed class ProjectSnapshot(ProjectState state) : IProjectSnapshot { - private readonly object _lock; + private readonly ProjectState _state = state; - private readonly Dictionary _documents; + private readonly object _gate = new(); + private readonly Dictionary _filePathToDocumentMap = new(FilePathNormalizingComparer.Instance); - public ProjectSnapshot(ProjectState state) - { - State = state ?? throw new ArgumentNullException(nameof(state)); - - _lock = new object(); - _documents = new Dictionary(FilePathNormalizingComparer.Instance); - } - - public ProjectKey Key => State.HostProject.Key; - - public ProjectState State { get; } - - public RazorConfiguration Configuration => HostProject.Configuration; - - public IEnumerable DocumentFilePaths => State.Documents.Keys; - - public int DocumentCount => State.Documents.Count; + public HostProject HostProject => _state.HostProject; - public string FilePath => State.HostProject.FilePath; + public ProjectKey Key => _state.HostProject.Key; + public RazorConfiguration Configuration => _state.HostProject.Configuration; + public IEnumerable DocumentFilePaths => _state.Documents.Keys; + public string FilePath => _state.HostProject.FilePath; + public string IntermediateOutputPath => _state.HostProject.IntermediateOutputPath; + public string? RootNamespace => _state.HostProject.RootNamespace; + public string DisplayName => _state.HostProject.DisplayName; + public VersionStamp Version => _state.Version; + public LanguageVersion CSharpLanguageVersion => _state.CSharpLanguageVersion; + public ProjectWorkspaceState ProjectWorkspaceState => _state.ProjectWorkspaceState; - public string IntermediateOutputPath => State.HostProject.IntermediateOutputPath; + public int DocumentCount => _state.Documents.Count; - public string? RootNamespace => State.HostProject.RootNamespace; + public VersionStamp ConfigurationVersion => _state.ConfigurationVersion; + public VersionStamp ProjectWorkspaceStateVersion => _state.ProjectWorkspaceStateVersion; + public VersionStamp DocumentCollectionVersion => _state.DocumentCollectionVersion; - public string DisplayName => State.HostProject.DisplayName; + public RazorProjectEngine GetProjectEngine() + => _state.ProjectEngine; - public LanguageVersion CSharpLanguageVersion => State.CSharpLanguageVersion; + public ValueTask> GetTagHelpersAsync(CancellationToken cancellationToken) + => new(_state.TagHelpers); - public HostProject HostProject => State.HostProject; - - public virtual VersionStamp Version => State.Version; - - public ValueTask> GetTagHelpersAsync(CancellationToken cancellationToken) => new(State.TagHelpers); + public bool ContainsDocument(string filePath) + { + lock (_gate) + { + // PERF: It's intentional that we call _filePathToDocumentMap.ContainsKey(...) + // before State.Documents.ContainsKey(...), even though the latter check is + // enough to return the correct answer. This is because _filePathToDocumentMap is + // a Dictionary<,>, which has O(1) lookup, and State.Documents is an + // ImmutableDictionary<,>, which has O(log n) lookup. So, checking _filePathToDocumentMap + // first is faster if the DocumentSnapshot has already been created. + + return _filePathToDocumentMap.ContainsKey(filePath) || + _state.Documents.ContainsKey(filePath); + } + } - public ProjectWorkspaceState ProjectWorkspaceState => State.ProjectWorkspaceState; + public IDocumentSnapshot? GetDocument(string filePath) + => TryGetDocument(filePath, out var document) + ? document + : null; - public virtual IDocumentSnapshot? GetDocument(string filePath) + public bool TryGetDocument(string filePath, [NotNullWhen(true)] out IDocumentSnapshot? document) { - lock (_lock) + lock (_gate) { - if (!_documents.TryGetValue(filePath, out var result) && - State.Documents.TryGetValue(filePath, out var state)) + // Have we already seen this document? If so, return it! + if (_filePathToDocumentMap.TryGetValue(filePath, out var snapshot)) { - result = new DocumentSnapshot(this, state); - _documents.Add(filePath, result); + document = snapshot; + return true; } - return result; - } - } + // Do we have DocumentSate for this document? If not, we're done! + if (!_state.Documents.TryGetValue(filePath, out var state)) + { + document = null; + return false; + } - public bool TryGetDocument(string filePath, [NotNullWhen(true)] out IDocumentSnapshot? document) - { - document = GetDocument(filePath); - return document is not null; + // If we have DocumentState, go ahead and create a new DocumentSnapshot. + snapshot = new DocumentSnapshot(this, state); + _filePathToDocumentMap.Add(filePath, snapshot); + + document = snapshot; + return true; + } } /// @@ -86,36 +102,26 @@ public bool TryGetDocument(string filePath, [NotNullWhen(true)] out IDocumentSna /// public ImmutableArray GetRelatedDocuments(IDocumentSnapshot document) { - if (document is null) - { - throw new ArgumentNullException(nameof(document)); - } - var targetPath = document.TargetPath.AssumeNotNull(); - if (!State.ImportsToRelatedDocuments.TryGetValue(targetPath, out var relatedDocuments)) + if (!_state.ImportsToRelatedDocuments.TryGetValue(targetPath, out var relatedDocuments)) { - return ImmutableArray.Empty; + return []; } - lock (_lock) + lock (_gate) { - using var _ = ArrayBuilderPool.GetPooledObject(out var builder); + using var builder = new PooledArrayBuilder(capacity: relatedDocuments.Length); foreach (var relatedDocumentFilePath in relatedDocuments) { - if (GetDocument(relatedDocumentFilePath) is { } relatedDocument) + if (TryGetDocument(relatedDocumentFilePath, out var relatedDocument)) { builder.Add(relatedDocument); } } - return builder.ToImmutableArray(); + return builder.DrainToImmutable(); } } - - public virtual RazorProjectEngine GetProjectEngine() - { - return State.ProjectEngine; - } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs index 40f7d967401..fc596af32dd 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectSnapshotManager.cs @@ -26,8 +26,6 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; // The implementation will create a ProjectSnapshot for each HostProject. internal partial class ProjectSnapshotManager : IProjectSnapshotManager, IDisposable { - private static readonly LoadTextOptions s_loadTextOptions = new(SourceHashAlgorithm.Sha256); - private readonly IProjectEngineFactoryProvider _projectEngineFactoryProvider; private readonly Dispatcher _dispatcher; private readonly bool _initialized; @@ -511,17 +509,23 @@ private static Entry ComputeNewEntry(Entry originalEntry, IUpdateProjectAction a switch (action) { case AddDocumentAction(var newDocument, var textLoader): - return new Entry(originalEntry.State.WithAddedHostDocument(newDocument, CreateTextAndVersionFunc(textLoader))); + return new Entry(originalEntry.State.WithAddedHostDocument(newDocument, textLoader)); case RemoveDocumentAction(var originalDocument): return new Entry(originalEntry.State.WithRemovedHostDocument(originalDocument)); case CloseDocumentAction(var textLoader): { - documentState.AssumeNotNull(); + // If the document being closed has already been removed from the project then we no-op + if (documentState is null) + { + return originalEntry; + } + var state = originalEntry.State.WithChangedHostDocument( documentState.HostDocument, - () => textLoader.LoadTextAndVersionAsync(s_loadTextOptions, cancellationToken: default)); + textLoader); + return new Entry(state); } @@ -538,14 +542,9 @@ private static Entry ComputeNewEntry(Entry originalEntry, IUpdateProjectAction a } else { - var newState = originalEntry.State.WithChangedHostDocument(documentState.HostDocument, async () => - { - olderText = await documentState.GetTextAsync().ConfigureAwait(false); - olderVersion = await documentState.GetTextVersionAsync().ConfigureAwait(false); - - var version = sourceText.ContentEquals(olderText) ? olderVersion : olderVersion.GetNewerVersion(); - return TextAndVersion.Create(sourceText, version, documentState.HostDocument.FilePath); - }); + var newState = originalEntry.State.WithChangedHostDocument( + documentState.HostDocument, + new UpdatedTextLoader(documentState, sourceText)); return new Entry(newState); } @@ -555,14 +554,18 @@ private static Entry ComputeNewEntry(Entry originalEntry, IUpdateProjectAction a { var newState = originalEntry.State.WithChangedHostDocument( documentState.AssumeNotNull().HostDocument, - CreateTextAndVersionFunc(textLoader)); + textLoader); return new Entry(newState); } case DocumentTextChangedAction(var sourceText): { - documentState.AssumeNotNull(); + // If the document being changed has already been removed from the project then we no-op + if (documentState is null) + { + return originalEntry; + } if (documentState.TryGetText(out var olderText) && documentState.TryGetTextVersion(out var olderVersion)) @@ -574,14 +577,9 @@ private static Entry ComputeNewEntry(Entry originalEntry, IUpdateProjectAction a } else { - var state = originalEntry.State.WithChangedHostDocument(documentState.HostDocument, async () => - { - olderText = await documentState.GetTextAsync().ConfigureAwait(false); - olderVersion = await documentState.GetTextVersionAsync().ConfigureAwait(false); - - var version = sourceText.ContentEquals(olderText) ? olderVersion : olderVersion.GetNewerVersion(); - return TextAndVersion.Create(sourceText, version, documentState.HostDocument.FilePath); - }); + var state = originalEntry.State.WithChangedHostDocument( + documentState.HostDocument, + new UpdatedTextLoader(documentState, sourceText)); return new Entry(state); } @@ -596,12 +594,19 @@ private static Entry ComputeNewEntry(Entry originalEntry, IUpdateProjectAction a default: throw new InvalidOperationException($"Unexpected action type {action.GetType()}"); } + } - static Func> CreateTextAndVersionFunc(TextLoader textLoader) + private sealed class UpdatedTextLoader(DocumentState oldState, SourceText newSourceText) : TextLoader + { + public override async Task LoadTextAndVersionAsync(LoadTextOptions options, CancellationToken cancellationToken) { - return textLoader is null - ? DocumentState.EmptyLoader - : (() => textLoader.LoadTextAndVersionAsync(s_loadTextOptions, CancellationToken.None)); + var oldTextAndVersion = await oldState.GetTextAndVersionAsync(cancellationToken).ConfigureAwait(false); + + var version = newSourceText.ContentEquals(oldTextAndVersion.Text) + ? oldTextAndVersion.Version + : oldTextAndVersion.Version.GetNewerVersion(); + + return TextAndVersion.Create(newSourceText, version); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs index dde23d8e562..9e753414420 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ProjectState.cs @@ -6,7 +6,6 @@ using System.Collections.Immutable; using System.IO; using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.ProjectEngineHost; @@ -224,7 +223,7 @@ RazorProjectEngine CreateProjectEngine() public VersionStamp ConfigurationVersion { get; } - public ProjectState WithAddedHostDocument(HostDocument hostDocument, Func> loader) + public ProjectState WithAddedHostDocument(HostDocument hostDocument, TextLoader loader) { if (hostDocument is null) { @@ -321,7 +320,7 @@ public ProjectState WithChangedHostDocument(HostDocument hostDocument, SourceTex return state; } - public ProjectState WithChangedHostDocument(HostDocument hostDocument, Func> loader) + public ProjectState WithChangedHostDocument(HostDocument hostDocument, TextLoader loader) { if (hostDocument is null) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/LanguageServerConstants.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/LanguageServerConstants.cs index be2fb1feec3..cdd0ce37190 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/LanguageServerConstants.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/LanguageServerConstants.cs @@ -37,6 +37,8 @@ public static class CodeActions public const string ExtractToCodeBehindAction = "ExtractToCodeBehind"; + public const string ExtractToNewComponentAction = "ExtractToNewComponent"; + public const string CreateComponentFromTag = "CreateComponentFromTag"; public const string AddUsing = "AddUsing"; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorComponentSearchEngine.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorComponentSearchEngine.cs index 23bd2720ea8..070970f6b65 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorComponentSearchEngine.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorComponentSearchEngine.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor.Logging; @@ -9,32 +10,40 @@ namespace Microsoft.CodeAnalysis.Razor.Workspaces; -internal class RazorComponentSearchEngine( - IProjectCollectionResolver projectCollectionResolver, - ILoggerFactory loggerFactory) - : IRazorComponentSearchEngine +internal class RazorComponentSearchEngine(ILoggerFactory loggerFactory) : IRazorComponentSearchEngine { - private readonly IProjectCollectionResolver _projectResolver = projectCollectionResolver; private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); - /// Search for a component in a project based on its tag name and fully qualified name. + /// + /// Search for a component in a project based on its tag name and fully qualified name. + /// + /// + /// A to find the corresponding Razor component for. + /// + /// + /// An to enumerate project snapshots. + /// + /// + /// A token that is checked to cancel work. + /// + /// + /// The corresponding if found, otherwise. + /// /// - /// This method makes several assumptions about the nature of components. First, it assumes that a component - /// a given name `Name` will be located in a file `Name.razor`. Second, it assumes that the namespace the - /// component is present in has the same name as the assembly its corresponding tag helper is loaded from. - /// Implicitly, this method inherits any assumptions made by TrySplitNamespaceAndType. + /// This method makes several assumptions about the nature of components. First, + /// it assumes that a component a given name "Name" will be located in a file + /// "Name.razor". Second, it assumes that the namespace the component is present in + /// has the same name as the assembly its corresponding tag helper is loaded from. + /// Implicitly, this method inherits any assumptions made by TrySplitNamespaceAndType. /// - /// A document snapshot that provides context to enumerate project snapshots - /// A TagHelperDescriptor to find the corresponding Razor component for. - /// The corresponding DocumentSnapshot if found, null otherwise. - /// Thrown if is null. - public async Task TryLocateComponentAsync(IDocumentSnapshot contextSnapshot, TagHelperDescriptor tagHelper) + /// + /// Thrown if is . + /// + public async Task TryLocateComponentAsync( + TagHelperDescriptor tagHelper, + ISolutionQueryOperations solutionQueryOperations, + CancellationToken cancellationToken) { - if (tagHelper is null) - { - throw new ArgumentNullException(nameof(tagHelper)); - } - var typeName = tagHelper.GetTypeNameIdentifier(); var namespaceName = tagHelper.GetTypeNamespace(); if (typeName == null || namespaceName == null) @@ -45,25 +54,23 @@ internal class RazorComponentSearchEngine( var lookupSymbolName = RemoveGenericContent(typeName.AsMemory()); - var projects = _projectResolver.EnumerateProjects(contextSnapshot); - - foreach (var project in projects) + foreach (var project in solutionQueryOperations.GetProjects()) { foreach (var path in project.DocumentFilePaths) { // Get document and code document - if (project.GetDocument(path) is not { } documentSnapshot) + if (!project.TryGetDocument(path, out var document)) { continue; } // Rule out if not Razor component with correct name - if (!documentSnapshot.IsPathCandidateForComponent(lookupSymbolName)) + if (!document.IsPathCandidateForComponent(lookupSymbolName)) { continue; } - var razorCodeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); + var razorCodeDocument = await document.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false); if (razorCodeDocument is null) { continue; @@ -75,7 +82,7 @@ internal class RazorComponentSearchEngine( continue; } - return documentSnapshot; + return document; } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorSyntaxFacts.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorSyntaxFacts.cs index 36a77a23129..31534762185 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorSyntaxFacts.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/RazorSyntaxFacts.cs @@ -131,4 +131,7 @@ public static bool IsAnyStartTag(RazorSyntaxNode n) public static bool IsAnyEndTag(RazorSyntaxNode n) => n.Kind is SyntaxKind.MarkupEndTag or SyntaxKind.MarkupTagHelperEndTag; + + public static bool IsInCodeBlock(RazorSyntaxNode n) + => n.FirstAncestorOrSelf(n => n is RazorDirectiveSyntax { DirectiveDescriptor.Directive: "code" }) is not null; } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteDiagnosticsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteDiagnosticsService.cs new file mode 100644 index 00000000000..e8f841af660 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteDiagnosticsService.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using RoslynLspDiagnostic = Microsoft.VisualStudio.LanguageServer.Protocol.Diagnostic; + +namespace Microsoft.CodeAnalysis.Razor.Remote; + +internal interface IRemoteDiagnosticsService : IRemoteJsonService +{ + ValueTask> GetDiagnosticsAsync( + JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, + JsonSerializableDocumentId documentId, + RoslynLspDiagnostic[] csharpDiagnostics, + RoslynLspDiagnostic[] htmlDiagnostics, + CancellationToken cancellationToken); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs index 4ede4292572..2e4257cdb3c 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs @@ -35,6 +35,7 @@ internal static class RazorServices (typeof(IRemoteDocumentSymbolService), null), (typeof(IRemoteRenameService), null), (typeof(IRemoteGoToImplementationService), null), + (typeof(IRemoteDiagnosticsService), null), ]; private const string ComponentName = "Razor"; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteAutoInsertOptions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteAutoInsertOptions.cs index 9281506849c..2ef7a3224d5 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteAutoInsertOptions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RemoteAutoInsertOptions.cs @@ -1,7 +1,9 @@ -using System.Runtime.Serialization; +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Runtime.Serialization; using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.Settings; -using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.Razor.Remote; @@ -15,21 +17,17 @@ internal readonly record struct RemoteAutoInsertOptions public bool FormatOnType { get; init; } = true; [DataMember(Order = 2)] - public RazorFormattingOptions FormattingOptions { get; init; } = new RazorFormattingOptions() - { - InsertSpaces = true, - TabSize = 4 - }; + public RazorFormattingOptions FormattingOptions { get; init; } = new(); public RemoteAutoInsertOptions() { } - public static RemoteAutoInsertOptions From(ClientSettings clientSettings, FormattingOptions formattingOptions) + public static RemoteAutoInsertOptions From(ClientSettings clientSettings, RazorFormattingOptions formattingOptions) => new() { EnableAutoClosingTags = clientSettings.AdvancedSettings.AutoClosingTags, FormatOnType = clientSettings.AdvancedSettings.FormatOnType, - FormattingOptions = RazorFormattingOptions.From(formattingOptions, codeBlockBraceOnNextLine: false) + FormattingOptions = formattingOptions }; } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/IRenameService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/IRenameService.cs index 6567bf92ba2..0c2f6b6044c 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/IRenameService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/IRenameService.cs @@ -11,5 +11,10 @@ namespace Microsoft.CodeAnalysis.Razor.Rename; internal interface IRenameService { - Task TryGetRazorRenameEditsAsync(DocumentContext documentContext, DocumentPositionInfo positionInfo, string newName, CancellationToken cancellationToken); + Task TryGetRazorRenameEditsAsync( + DocumentContext documentContext, + DocumentPositionInfo positionInfo, + string newName, + ISolutionQueryOperations solutionQueryOperations, + CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/RenameService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/RenameService.cs index 472202fc51a..15283267027 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/RenameService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Rename/RenameService.cs @@ -26,14 +26,17 @@ namespace Microsoft.CodeAnalysis.Razor.Rename; internal class RenameService( IRazorComponentSearchEngine componentSearchEngine, - IProjectCollectionResolver projectCollectionResolver, LanguageServerFeatureOptions languageServerFeatureOptions) : IRenameService { private readonly IRazorComponentSearchEngine _componentSearchEngine = componentSearchEngine; - private readonly IProjectCollectionResolver _projectCollectionResolver = projectCollectionResolver; private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; - public async Task TryGetRazorRenameEditsAsync(DocumentContext documentContext, DocumentPositionInfo positionInfo, string newName, CancellationToken cancellationToken) + public async Task TryGetRazorRenameEditsAsync( + DocumentContext documentContext, + DocumentPositionInfo positionInfo, + string newName, + ISolutionQueryOperations solutionQueryOperations, + CancellationToken cancellationToken) { // We only support renaming of .razor components, not .cshtml tag helpers if (!FileKinds.IsComponent(documentContext.FileKind)) @@ -55,7 +58,9 @@ internal class RenameService( return null; } - var originComponentDocumentSnapshot = await _componentSearchEngine.TryLocateComponentAsync(documentContext.Snapshot, originTagHelpers.First()).ConfigureAwait(false); + var originComponentDocumentSnapshot = await _componentSearchEngine + .TryLocateComponentAsync(originTagHelpers.First(), solutionQueryOperations, cancellationToken) + .ConfigureAwait(false); if (originComponentDocumentSnapshot is null) { return null; @@ -73,11 +78,11 @@ internal class RenameService( documentChanges.Add(fileRename); AddEditsForCodeDocument(documentChanges, originTagHelpers, newName, documentContext.Uri, codeDocument); - var documentSnapshots = GetAllDocumentSnapshots(documentContext); + var documentSnapshots = GetAllDocumentSnapshots(documentContext.FilePath, solutionQueryOperations); foreach (var documentSnapshot in documentSnapshots) { - await AddEditsForCodeDocumentAsync(documentChanges, originTagHelpers, newName, documentSnapshot).ConfigureAwait(false); + await AddEditsForCodeDocumentAsync(documentChanges, originTagHelpers, newName, documentSnapshot, cancellationToken).ConfigureAwait(false); } foreach (var documentChange in documentChanges) @@ -95,17 +100,17 @@ internal class RenameService( }; } - private ImmutableArray GetAllDocumentSnapshots(DocumentContext skipDocumentContext) + private static ImmutableArray GetAllDocumentSnapshots(string filePath, ISolutionQueryOperations solutionQueryOperations) { using var documentSnapshots = new PooledArrayBuilder(); using var _ = StringHashSetPool.GetPooledObject(out var documentPaths); - foreach (var project in _projectCollectionResolver.EnumerateProjects(skipDocumentContext.Snapshot)) + foreach (var project in solutionQueryOperations.GetProjects()) { foreach (var documentPath in project.DocumentFilePaths) { // We've already added refactoring edits for our document snapshot - if (string.Equals(documentPath, skipDocumentContext.FilePath, FilePathComparison.Instance)) + if (FilePathComparer.Instance.Equals(documentPath, filePath)) { continue; } @@ -117,7 +122,7 @@ private ImmutableArray GetAllDocumentSnapshots(DocumentContex } // Add to the list and add the path to the set - if (project.GetDocument(documentPath) is not { } snapshot) + if (!project.TryGetDocument(documentPath, out var snapshot)) { throw new InvalidOperationException($"{documentPath} in project {project.FilePath} but not retrievable"); } @@ -162,14 +167,15 @@ private async Task AddEditsForCodeDocumentAsync( List> documentChanges, ImmutableArray originTagHelpers, string newName, - IDocumentSnapshot documentSnapshot) + IDocumentSnapshot documentSnapshot, + CancellationToken cancellationToken) { if (!FileKinds.IsComponent(documentSnapshot.FileKind)) { return; } - var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); + var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(cancellationToken).ConfigureAwait(false); if (codeDocument.IsUnsupported()) { return; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/SR.resx b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/SR.resx index ac9ac407183..e0a0efc1cdd 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/SR.resx +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/SR.resx @@ -163,4 +163,13 @@ Razor language services not configured properly, missing language service '{0}'. + + Not available in + + + Razor TagHelper Attribute Glyph + + + Razor TagHelper Element Glyph + \ No newline at end of file diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.cs.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.cs.xlf index 150cb3ff499..31f86168480 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.cs.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.cs.xlf @@ -59,6 +59,11 @@ Služby jazyka Razor nejsou správně nakonfigurované, chybí služba jazyka {0}. + + Not available in + Není k dispozici v + + Character '{0}' outside of the {1} range of '{2}' was queried. The document may not be up to date. Proběhl dotaz na znak {0} mimo rozsah {1} v {2}. Dokument nemusí být aktuální. @@ -69,6 +74,16 @@ Proběhl dotaz na řádek {0} mimo rozsah {1} {2}. Dokument nemusí být aktuální. + + Razor TagHelper Attribute Glyph + Piktogram atributu Razor TagHelper + + + + Razor TagHelper Element Glyph + Piktogram elementu Razor TagHelper + + Attempted to visit a RazorMetaCode other than '{' or '}'. Došlo k pokusu navštívit RazorMetaCode jiný než '{' or '}'. diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.de.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.de.xlf index 61aee78762f..1726a2f73d0 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.de.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.de.xlf @@ -59,6 +59,11 @@ Razor-Sprachdienste sind nicht ordnungsgemäß konfiguriert, der Sprachdienst "{0}" fehlt. + + Not available in + Nicht verfügbar in + + Character '{0}' outside of the {1} range of '{2}' was queried. The document may not be up to date. Das Zeichen "{0}" außerhalb des {1} Bereichs von "{2}" wurde abgefragt. Das Dokument ist möglicherweise nicht auf dem neuesten Stand. @@ -69,6 +74,16 @@ Die Zeile "{0}" außerhalb des {1} Bereichs von "{2}" wurde abgefragt. Das Dokument ist möglicherweise nicht auf dem neuesten Stand. + + Razor TagHelper Attribute Glyph + Razor TagHelper-Attributsymbol + + + + Razor TagHelper Element Glyph + Razor TagHelper-Elementsymbol + + Attempted to visit a RazorMetaCode other than '{' or '}'. Es wurde versucht, einen anderen RazorMetaCode als '{' or '}' zu besuchen. diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.es.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.es.xlf index dbfc1397a26..af8cec0ae9d 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.es.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.es.xlf @@ -59,6 +59,11 @@ Los servicios de lenguaje Razor no están configurados correctamente; falta el servicio de idioma "{0}". + + Not available in + No disponible en + + Character '{0}' outside of the {1} range of '{2}' was queried. The document may not be up to date. Se consultó el carácter '{0}' fuera del intervalo {1} de '{2}'. Es posible que el documento no esté actualizado. @@ -69,6 +74,16 @@ La línea '{0}' se consultó fuera del {1} rango de '{2}'. Es posible que el documento no esté actualizado. + + Razor TagHelper Attribute Glyph + Glifo del atributo TagHelper de Razor + + + + Razor TagHelper Element Glyph + Glifo del elemento TagHelper de Razor + + Attempted to visit a RazorMetaCode other than '{' or '}'. Se intentó visitar un RazorMetaCode distinto de '{' or '}'. diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.fr.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.fr.xlf index 919895dbf7c..83af245e366 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.fr.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.fr.xlf @@ -59,6 +59,11 @@ Les services de langage Razor ne sont pas configurés correctement, le service de langage «{0}» manquant. + + Not available in + Non disponible dans + + Character '{0}' outside of the {1} range of '{2}' was queried. The document may not be up to date. Le caractère « {0} » en dehors de la plage {1} de « {2} » a été interrogé. Le document n’est peut-être pas à jour. @@ -69,6 +74,16 @@ La ligne «{0}» en dehors de la plage{1} de «{2}» a été interrogée. Le document n’est peut-être pas à jour. + + Razor TagHelper Attribute Glyph + Glyphe d’attribut Razor TagHelper + + + + Razor TagHelper Element Glyph + Glyphe de l’élément Razor TagHelper + + Attempted to visit a RazorMetaCode other than '{' or '}'. Tentative de visite d’un RazorMetaCode autre que '{' or '}'. diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.it.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.it.xlf index e03729ccb1d..5317948e62d 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.it.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.it.xlf @@ -59,6 +59,11 @@ I servizi di linguaggio Razor non sono configurati correttamente. Manca il servizio di linguaggio '{0}'. + + Not available in + Non disponibili in + + Character '{0}' outside of the {1} range of '{2}' was queried. The document may not be up to date. È stata eseguita una query sul carattere '{0}' non compresa nell'intervallo {1} di '{2}'. Il documento potrebbe non essere aggiornato. @@ -69,6 +74,16 @@ È stata eseguita una query sulla riga '{0}' non compresa nell'intervallo {1} di '{2}'. Il documento potrebbe non essere aggiornato. + + Razor TagHelper Attribute Glyph + Glifo attributo TagHelper Razor + + + + Razor TagHelper Element Glyph + Glifo elemento TagHelper Razor + + Attempted to visit a RazorMetaCode other than '{' or '}'. È stato effettuato un tentativo di visitare un elemento RazorMetaCode diverso da '{' or '}'. diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ja.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ja.xlf index 472ca328ed2..55bbf78dcd9 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ja.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ja.xlf @@ -59,6 +59,11 @@ Razor 言語サービスが正しく構成されていません。言語サービス '{0}' がありません。 + + Not available in + 次では使用できません: + + Character '{0}' outside of the {1} range of '{2}' was queried. The document may not be up to date. '{2}' の {1} 範囲外の文字 '{0}' がクエリされました。ドキュメントが最新ではない可能性があります。 @@ -69,6 +74,16 @@ '{2}' の {1} 範囲外の行 '{0}' がクエリされました。ドキュメントが最新ではない可能性があります。 + + Razor TagHelper Attribute Glyph + Razor TagHelper 属性のグリフ + + + + Razor TagHelper Element Glyph + Razor TagHelper 要素のグリフ + + Attempted to visit a RazorMetaCode other than '{' or '}'. '{' or '}' 以外の RazorMetaCode にアクセスしようとしました。 diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ko.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ko.xlf index 4542bd59c0d..2931b15ec1e 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ko.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ko.xlf @@ -59,6 +59,11 @@ Razor 언어 서비스가 제대로 구성되지 않았습니다. 언어 서비스 '{0}'이(가) 없습니다. + + Not available in + 에서 사용할 수 없음 + + Character '{0}' outside of the {1} range of '{2}' was queried. The document may not be up to date. '{2}'의 {1} 범위를 벗어나는 문자 '{0}'이(가) 쿼리되었습니다. 문서가 최신이 아닐 수 있습니다. @@ -69,6 +74,16 @@ '{2}'의 {1} 범위를 벗어나는 줄 '{0}'을(를) 쿼리했습니다. 문서가 최신이 아닐 수 있습니다. + + Razor TagHelper Attribute Glyph + Razor TagHelper 특성 문자 모양 + + + + Razor TagHelper Element Glyph + Razor TagHelper 요소 문자 모양 + + Attempted to visit a RazorMetaCode other than '{' or '}'. ''{' or '}' 이외의 RazorMetaCode를 방문하려고 했습니다. diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.pl.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.pl.xlf index f7b1a69b107..60f05b66521 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.pl.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.pl.xlf @@ -59,6 +59,11 @@ Usługi języka dla składni Razor nie zostały prawidłowo skonfigurowane — brak usługi językowej "{0}". + + Not available in + Niedostępne w + + Character '{0}' outside of the {1} range of '{2}' was queried. The document may not be up to date. Zbadano znak „{0}” poza zakresem {1} „{2}”. Dokument może być nieaktualny. @@ -69,6 +74,16 @@ Wykonano zapytanie wiersza "{0}" poza zakresem {1} "{2}". Dokument może być nieaktualny. + + Razor TagHelper Attribute Glyph + Symbol atrybutu pomocnika tagów składni Razor + + + + Razor TagHelper Element Glyph + Symbol elementu pomocnika tagów składni Razor + + Attempted to visit a RazorMetaCode other than '{' or '}'. Podjęto próbę odwiedzenia elementu RazorMetaCode innego niż '{' or '}'. diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.pt-BR.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.pt-BR.xlf index 746a4efee2b..e834585d7ed 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.pt-BR.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.pt-BR.xlf @@ -59,6 +59,11 @@ Serviços de Linguagem Razor não configurados corretamente, serviço de linguagem '{0}' ausente. + + Not available in + Não disponível em + + Character '{0}' outside of the {1} range of '{2}' was queried. The document may not be up to date. O caractere '{0}' fora do {1} intervalo de '{2}' foi consultado. É possível que o documento não esteja atualizado. @@ -69,6 +74,16 @@ A linha '{0}' fora do intervalo {1} de '{2}' foi consultada. O documento pode não estar atualizado. + + Razor TagHelper Attribute Glyph + Atributo Glyph Razor TagHelper + + + + Razor TagHelper Element Glyph + Elemento Glyph Razor TagHelper + + Attempted to visit a RazorMetaCode other than '{' or '}'. Tentativa de visitar um RazorMetaCode diferente de '{' or '}'. diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ru.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ru.xlf index 9e17ac79fc4..b4577c8ec99 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ru.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.ru.xlf @@ -59,6 +59,11 @@ Языковые службы Razor настроены неправильно, отсутствует языковая служба "{0}". + + Not available in + Недоступно в + + Character '{0}' outside of the {1} range of '{2}' was queried. The document may not be up to date. Запрошен символ "{0}" за пределами диапазона {1} "{2}". Возможно, документ не обновлен. @@ -69,6 +74,16 @@ Запрошена строка "{0}" за пределами диапазона {1} "{2}". Возможно, документ не обновлен. + + Razor TagHelper Attribute Glyph + Глиф атрибута TagHelper Razor + + + + Razor TagHelper Element Glyph + Глиф элемента TagHelper Razor + + Attempted to visit a RazorMetaCode other than '{' or '}'. Предпринята попытка посетить RazorMetaCode, отличный от "{' or '}". diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.tr.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.tr.xlf index 1b1846db9e8..0faa1367043 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.tr.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.tr.xlf @@ -59,6 +59,11 @@ Razor dil hizmetleri düzgün yapılandırılmadı, '{0}' dil hizmeti eksik. + + Not available in + Şurada kullanılamaz: + + Character '{0}' outside of the {1} range of '{2}' was queried. The document may not be up to date. '{2}' {1} aralığının dışındaki '{0}' karakteri sorgulandı. Belge güncel olmayabilir. @@ -69,6 +74,16 @@ {1} / '{2}' aralığının dışındaki '{0}' satırı sorgulandı. Belge güncel olmayabilir. + + Razor TagHelper Attribute Glyph + Razor TagHelper Öznitelik Karakteri + + + + Razor TagHelper Element Glyph + Razor TagHelper Element Karakteri + + Attempted to visit a RazorMetaCode other than '{' or '}'. '{' or '}' dışında bir RazorMetaCode ziyaret edilmeye çalışıldı. diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.zh-Hans.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.zh-Hans.xlf index 63a41117bc2..51764818206 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.zh-Hans.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.zh-Hans.xlf @@ -59,6 +59,11 @@ Razor 语言服务未正确配置,缺少语言服务 "{0}"。 + + Not available in + 在以下位置不可用 + + Character '{0}' outside of the {1} range of '{2}' was queried. The document may not be up to date. 查询了“{2}”的 {1} 范围外的字符“{0}”。文档可能不是最新的。 @@ -69,6 +74,16 @@ 查询了 "{0}" 的 {1} 范围外的行 "{2}"。文档可能不是最新的。 + + Razor TagHelper Attribute Glyph + Razor TagHelper 特性字形 + + + + Razor TagHelper Element Glyph + Razor TagHelper 元素字形 + + Attempted to visit a RazorMetaCode other than '{' or '}'. 已尝试访问除 "{' or '}" 之外的 RazorMetaCode。 diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.zh-Hant.xlf b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.zh-Hant.xlf index 5719f9dccf7..74d00b2d74b 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.zh-Hant.xlf +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.zh-Hant.xlf @@ -59,6 +59,11 @@ 未正確設定 Razor 語言服務,遺漏語言服務 '{0}'。 + + Not available in + 無法使用於 + + Character '{0}' outside of the {1} range of '{2}' was queried. The document may not be up to date. 已查詢 '{2}' 的 {1} 範圍以外的字元 '{0}'。文件可能不是最新。 @@ -69,6 +74,16 @@ 已查詢 '{2}' 之 {1} 範圍以外的行 '{0}'。文件可能不是最新狀態。 + + Razor TagHelper Attribute Glyph + Razor TagHelper 屬性字元 + + + + Razor TagHelper Element Glyph + Razor TagHelper 元素字符 + + Attempted to visit a RazorMetaCode other than '{' or '}'. 嘗試瀏覽 '{' or '}' 除外的 RazorMetaCode。 diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Settings/ClientSettings.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Settings/ClientSettings.cs index 568b7f38da7..f0e9036a619 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Settings/ClientSettings.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Settings/ClientSettings.cs @@ -32,7 +32,7 @@ internal sealed record ClientSpaceSettings(bool IndentWithTabs, int IndentSize) public int IndentSize { get; } = IndentSize >= 0 ? IndentSize : throw new ArgumentOutOfRangeException(nameof(IndentSize)); } -internal sealed record ClientAdvancedSettings(bool FormatOnType, bool AutoClosingTags, bool AutoInsertAttributeQuotes, bool ColorBackground, bool CodeBlockBraceOnNextLine, bool CommitElementsWithSpace, SnippetSetting SnippetSetting, LogLevel LogLevel) +internal sealed record ClientAdvancedSettings(bool FormatOnType, bool AutoClosingTags, bool AutoInsertAttributeQuotes, bool ColorBackground, bool CodeBlockBraceOnNextLine, bool CommitElementsWithSpace, SnippetSetting SnippetSetting, LogLevel LogLevel, bool FormatOnPaste) { - public static readonly ClientAdvancedSettings Default = new(FormatOnType: true, AutoClosingTags: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: true, SnippetSetting.All, LogLevel.Warning); + public static readonly ClientAdvancedSettings Default = new(FormatOnType: true, AutoClosingTags: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: true, SnippetSetting.All, LogLevel.Warning, FormatOnPaste: true); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/TextDiffer.IntArray.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/TextDiffer.IntArray.cs index ba51627d53c..120685d419f 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/TextDiffer.IntArray.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/TextDifferencing/TextDiffer.IntArray.cs @@ -1,4 +1,5 @@ -// Licensed under the MIT license. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. using System; using System.Buffers; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Tooltip/DefaultVSLSPTagHelperTooltipFactory.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Tooltip/ClassifiedTagHelperTooltipFactory.cs similarity index 76% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Tooltip/DefaultVSLSPTagHelperTooltipFactory.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Tooltip/ClassifiedTagHelperTooltipFactory.cs index 176f774ffa1..73f78994d69 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Tooltip/DefaultVSLSPTagHelperTooltipFactory.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Tooltip/ClassifiedTagHelperTooltipFactory.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Collections.Frozen; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; @@ -9,17 +10,18 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Tooltip; using Microsoft.VisualStudio.Core.Imaging; using Microsoft.VisualStudio.Text.Adornments; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Tooltip; +namespace Microsoft.CodeAnalysis.Razor.Tooltip; -internal class DefaultVSLSPTagHelperTooltipFactory(IProjectSnapshotManager projectManager) : VSLSPTagHelperTooltipFactory(projectManager) +internal static class ClassifiedTagHelperTooltipFactory { + public const string TypeClassificationName = "Type"; + private static readonly Guid s_imageCatalogGuid = new("{ae27a6b0-e345-4288-96df-5eaf394ee369}"); // Internal for testing @@ -32,11 +34,12 @@ internal class DefaultVSLSPTagHelperTooltipFactory(IProjectSnapshotManager proje new ImageId(s_imageCatalogGuid, 2429), // KnownImageIds.Type = 2429 SR.TagHelper_Attribute_Glyph); - private static readonly IReadOnlyList s_cSharpPrimitiveTypes = - new string[] { "bool", "byte", "sbyte", "char", "decimal", "double", "float", "int", "uint", - "nint", "nuint", "long", "ulong", "short", "ushort", "object", "string", "dynamic" }; + private static readonly FrozenSet s_csharpPrimitiveTypes = + FrozenSet.ToFrozenSet([ + "bool", "byte", "sbyte", "char", "decimal", "double", "float", "int", "uint", + "nint", "nuint", "long", "ulong", "short", "ushort", "object", "string", "dynamic"]); - private static readonly IReadOnlyDictionary s_typeNameToAlias = new Dictionary(StringComparer.Ordinal) + private static readonly FrozenDictionary s_typeNameToAlias = new Dictionary(StringComparer.Ordinal) { { "Int32", "int" }, { "Int64", "long" }, @@ -47,21 +50,27 @@ internal class DefaultVSLSPTagHelperTooltipFactory(IProjectSnapshotManager proje { "Boolean", "bool" }, { "String", "string" }, { "Char", "char" } - }; - - private static readonly ClassifiedTextRun s_space = new(VSPredefinedClassificationTypeNames.WhiteSpace, " "); - private static readonly ClassifiedTextRun s_dot = new(VSPredefinedClassificationTypeNames.Punctuation, "."); - private static readonly ClassifiedTextRun s_newLine = new(VSPredefinedClassificationTypeNames.WhiteSpace, Environment.NewLine); - private static readonly ClassifiedTextRun s_nullableType = new(VSPredefinedClassificationTypeNames.Punctuation, "?"); - - public override async Task TryCreateTooltipContainerAsync(string documentFilePath, AggregateBoundElementDescription elementDescriptionInfo, CancellationToken cancellationToken) + }.ToFrozenDictionary(); + + private static readonly ClassifiedTextRun s_space = new(ClassificationTypeNames.WhiteSpace, " "); + private static readonly ClassifiedTextRun s_dot = new(ClassificationTypeNames.Punctuation, "."); + private static readonly ClassifiedTextRun s_newLine = new(ClassificationTypeNames.WhiteSpace, Environment.NewLine); + private static readonly ClassifiedTextRun s_nullableType = new(ClassificationTypeNames.Punctuation, "?"); + + public static async Task TryCreateTooltipContainerAsync( + string documentFilePath, + AggregateBoundElementDescription elementDescriptionInfo, + ISolutionQueryOperations solutionQueryOperations, + CancellationToken cancellationToken) { if (elementDescriptionInfo is null) { throw new ArgumentNullException(nameof(elementDescriptionInfo)); } - var descriptionClassifications = await TryClassifyElementAsync(documentFilePath, elementDescriptionInfo, cancellationToken).ConfigureAwait(false); + var descriptionClassifications = await TryClassifyElementAsync( + documentFilePath, elementDescriptionInfo, solutionQueryOperations, cancellationToken).ConfigureAwait(false); + if (descriptionClassifications.IsDefaultOrEmpty) { return null; @@ -70,7 +79,7 @@ internal class DefaultVSLSPTagHelperTooltipFactory(IProjectSnapshotManager proje return CombineClassifiedTextRuns(descriptionClassifications, ClassGlyph); } - public override bool TryCreateTooltip(AggregateBoundAttributeDescription attributeDescriptionInfo, [NotNullWhen(true)] out ContainerElement? tooltipContent) + public static bool TryCreateTooltip(AggregateBoundAttributeDescription attributeDescriptionInfo, [NotNullWhen(true)] out ContainerElement? tooltipContent) { if (attributeDescriptionInfo is null) { @@ -89,14 +98,20 @@ public override bool TryCreateTooltip(AggregateBoundAttributeDescription attribu // TO-DO: This method can be removed once LSP's VSCompletionItem supports returning ContainerElements for // its Description property, tracked by https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1319274. - public override async Task TryCreateTooltipAsync(string documentFilePath, AggregateBoundElementDescription elementDescriptionInfo, CancellationToken cancellationToken) + public static async Task TryCreateTooltipAsync( + string documentFilePath, + AggregateBoundElementDescription elementDescriptionInfo, + ISolutionQueryOperations solutionQueryOperations, + CancellationToken cancellationToken) { if (elementDescriptionInfo is null) { throw new ArgumentNullException(nameof(elementDescriptionInfo)); } - var descriptionClassifications = await TryClassifyElementAsync(documentFilePath, elementDescriptionInfo, cancellationToken).ConfigureAwait(false); + var descriptionClassifications = await TryClassifyElementAsync( + documentFilePath, elementDescriptionInfo, solutionQueryOperations, cancellationToken).ConfigureAwait(false); + if (descriptionClassifications.IsDefaultOrEmpty) { return null; @@ -107,7 +122,7 @@ public override bool TryCreateTooltip(AggregateBoundAttributeDescription attribu // TO-DO: This method can be removed once LSP's VSCompletionItem supports returning ContainerElements for // its Description property, tracked by https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1319274. - public override bool TryCreateTooltip(AggregateBoundAttributeDescription attributeDescriptionInfo, [NotNullWhen(true)] out ClassifiedTextElement? tooltipContent) + public static bool TryCreateTooltip(AggregateBoundAttributeDescription attributeDescriptionInfo, [NotNullWhen(true)] out ClassifiedTextElement? tooltipContent) { if (attributeDescriptionInfo is null) { @@ -124,7 +139,11 @@ public override bool TryCreateTooltip(AggregateBoundAttributeDescription attribu return true; } - private async Task> TryClassifyElementAsync(string documentFilePath, AggregateBoundElementDescription elementInfo, CancellationToken cancellationToken) + private static async Task> TryClassifyElementAsync( + string documentFilePath, + AggregateBoundElementDescription elementInfo, + ISolutionQueryOperations solutionQueryOperations, + CancellationToken cancellationToken) { var associatedTagHelperInfos = elementInfo.DescriptionInfos; if (associatedTagHelperInfos.Length == 0) @@ -150,7 +169,8 @@ private async Task> TryClassifyElement TryClassifySummary(documentationRuns, descriptionInfo.Documentation); // 3. Project availability - await AddProjectAvailabilityInfoAsync(documentFilePath, descriptionInfo.TagHelperTypeName, documentationRuns, cancellationToken).ConfigureAwait(false); + await AddProjectAvailabilityInfoAsync( + documentFilePath, descriptionInfo.TagHelperTypeName, solutionQueryOperations, documentationRuns, cancellationToken).ConfigureAwait(false); // 4. Combine type + summary information descriptions.Add(new DescriptionClassification(typeRuns, documentationRuns)); @@ -159,13 +179,20 @@ private async Task> TryClassifyElement return descriptions.DrainToImmutable(); } - private async Task AddProjectAvailabilityInfoAsync(string documentFilePath, string tagHelperTypeName, List documentationRuns, CancellationToken cancellationToken) + private static async Task AddProjectAvailabilityInfoAsync( + string documentFilePath, + string tagHelperTypeName, + ISolutionQueryOperations solutionQueryOperations, + List documentationRuns, + CancellationToken cancellationToken) { - var availability = await GetProjectAvailabilityAsync(documentFilePath, tagHelperTypeName, cancellationToken).ConfigureAwait(false); + var availability = await solutionQueryOperations + .GetProjectAvailabilityTextAsync(documentFilePath, tagHelperTypeName, cancellationToken) + .ConfigureAwait(false); if (availability is not null) { - documentationRuns.Add(new ClassifiedTextRun(VSPredefinedClassificationTypeNames.Text, availability)); + documentationRuns.Add(new ClassifiedTextRun(ClassificationTypeNames.Text, availability)); } } @@ -195,12 +222,12 @@ private static bool TryClassifyAttribute(AggregateBoundAttributeDescription attr returnTypeName = descriptionInfo.ReturnTypeName; } - var reducedReturnTypeName = ReduceTypeName(returnTypeName); + var reducedReturnTypeName = DocCommentHelpers.ReduceTypeName(returnTypeName); ClassifyReducedTypeName(typeRuns, reducedReturnTypeName); typeRuns.Add(s_space); ClassifyTypeName(typeRuns, descriptionInfo.TypeName); typeRuns.Add(s_dot); - typeRuns.Add(new ClassifiedTextRun(VSPredefinedClassificationTypeNames.Identifier, descriptionInfo.PropertyName)); + typeRuns.Add(new ClassifiedTextRun(ClassificationTypeNames.Identifier, descriptionInfo.PropertyName)); // 2. Classify summary var documentationRuns = new List(); @@ -216,7 +243,7 @@ private static bool TryClassifyAttribute(AggregateBoundAttributeDescription attr private static void ClassifyTypeName(List runs, string tagHelperTypeName) { - var reducedTypeName = ReduceTypeName(tagHelperTypeName); + var reducedTypeName = DocCommentHelpers.ReduceTypeName(tagHelperTypeName); if (reducedTypeName == tagHelperTypeName) { ClassifyReducedTypeName(runs, reducedTypeName); @@ -246,7 +273,7 @@ private static void ClassifyTypeName(List runs, string tagHel } else { - runs.Add(new ClassifiedTextRun(VSPredefinedClassificationTypeNames.Text, typeNamePart)); + runs.Add(new ClassifiedTextRun(ClassificationTypeNames.Text, typeNamePart)); } } } @@ -272,7 +299,7 @@ private static void ClassifyReducedTypeName(List runs, string // also need to reduce the inner type name(s), e.g. 'List' if (ch is '<' or '>' or '[' or ']' && currentRunTextStr.Contains('.')) { - var reducedName = ReduceTypeName(currentRunTextStr); + var reducedName = DocCommentHelpers.ReduceTypeName(currentRunTextStr); ClassifyShortName(runs, reducedName); } else @@ -283,7 +310,7 @@ private static void ClassifyReducedTypeName(List runs, string currentTextRun.Clear(); } - runs.Add(new ClassifiedTextRun(VSPredefinedClassificationTypeNames.Punctuation, ch.ToString())); + runs.Add(new ClassifiedTextRun(ClassificationTypeNames.Punctuation, ch.ToString())); } else { @@ -309,17 +336,17 @@ private static void ClassifyShortName(List runs, string typeN // Case 1: Type can be aliased as a C# built-in type (e.g. Boolean -> bool, Int32 -> int, etc.). if (s_typeNameToAlias.TryGetValue(typeName, out var aliasedTypeName)) { - runs.Add(new ClassifiedTextRun(VSPredefinedClassificationTypeNames.Keyword, aliasedTypeName)); + runs.Add(new ClassifiedTextRun(ClassificationTypeNames.Keyword, aliasedTypeName)); } // Case 2: Type is a C# built-in type (e.g. bool, int, etc.). - else if (s_cSharpPrimitiveTypes.Contains(typeName)) + else if (s_csharpPrimitiveTypes.Contains(typeName)) { - runs.Add(new ClassifiedTextRun(VSPredefinedClassificationTypeNames.Keyword, typeName)); + runs.Add(new ClassifiedTextRun(ClassificationTypeNames.Keyword, typeName)); } // Case 3: All other types. else { - runs.Add(new ClassifiedTextRun(VSPredefinedClassificationTypeNames.Type, typeName)); + runs.Add(new ClassifiedTextRun(TypeClassificationName, typeName)); } if (nullableType) @@ -330,7 +357,7 @@ private static void ClassifyShortName(List runs, string typeN private static bool TryClassifySummary(List runs, string? documentation) { - if (!TryExtractSummary(documentation, out var summaryContent)) + if (!DocCommentHelpers.TryExtractSummary(documentation, out var summaryContent)) { return false; } @@ -354,12 +381,12 @@ internal static void CleanAndClassifySummaryContent(List runs summaryContent = summaryContent.Replace("", Environment.NewLine); summaryContent = summaryContent.Replace("", Environment.NewLine); - var codeMatches = ExtractCodeMatches(summaryContent); - var crefMatches = ExtractCrefMatches(summaryContent); + var codeMatches = DocCommentHelpers.ExtractCodeMatches(summaryContent); + var crefMatches = DocCommentHelpers.ExtractCrefMatches(summaryContent); if (codeMatches.Count == 0 && crefMatches.Count == 0) { - runs.Add(new ClassifiedTextRun(VSPredefinedClassificationTypeNames.Text, summaryContent)); + runs.Add(new ClassifiedTextRun(ClassificationTypeNames.Text, summaryContent)); return; } @@ -384,10 +411,10 @@ internal static void CleanAndClassifySummaryContent(List runs ClassifyExistingTextRun(runs, currentTextRun); // We've processed the existing string, now we can process the code block. - var value = currentCodeMatch.Groups[TagContentGroupName].Value; + var value = currentCodeMatch.Groups[DocCommentHelpers.TagContentGroupName].Value; if (value.Length != 0) { - runs.Add(new ClassifiedTextRun(VSPredefinedClassificationTypeNames.Text, value.ToString(), ClassifiedTextRunStyle.UseClassificationFont)); + runs.Add(new ClassifiedTextRun(ClassificationTypeNames.Text, value.ToString(), ClassifiedTextRunStyle.UseClassificationFont)); } i += currentCodeMatch.Length - 1; @@ -398,8 +425,8 @@ internal static void CleanAndClassifySummaryContent(List runs ClassifyExistingTextRun(runs, currentTextRun); // We've processed the existing string, now we can process the actual cref. - var value = currentCrefMatch.Groups[TagContentGroupName].Value; - var reducedValue = ReduceCrefValue(value); + var value = currentCrefMatch.Groups[DocCommentHelpers.TagContentGroupName].Value; + var reducedValue = DocCommentHelpers.ReduceCrefValue(value); reducedValue = reducedValue.Replace("{", "<").Replace("}", ">").Replace("`1", "<>"); ClassifyTypeName(runs, reducedValue); @@ -418,7 +445,7 @@ static void ClassifyExistingTextRun(List runs, StringBuilder { if (currentTextRun.Length != 0) { - runs.Add(new ClassifiedTextRun(VSPredefinedClassificationTypeNames.Text, currentTextRun.ToString())); + runs.Add(new ClassifiedTextRun(ClassificationTypeNames.Text, currentTextRun.ToString())); currentTextRun.Clear(); } } @@ -474,22 +501,5 @@ private static ClassifiedTextElement GenerateClassifiedTextElement(ImmutableArra return new ClassifiedTextElement(runs); } - // Internal for testing - // Adapted from VS' PredefinedClassificationTypeNames - internal static class VSPredefinedClassificationTypeNames - { - public const string Identifier = "identifier"; - - public const string Keyword = "keyword"; - - public const string Punctuation = "punctuation"; - - public const string Text = "text"; - - public const string Type = "type"; - - public const string WhiteSpace = "whitespace"; - } - private record DescriptionClassification(IReadOnlyList Type, IReadOnlyList Documentation); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Tooltip/TagHelperTooltipFactoryBase.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Tooltip/DocCommentHelpers.cs similarity index 67% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Tooltip/TagHelperTooltipFactoryBase.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Tooltip/DocCommentHelpers.cs index 8fb38996888..198c133b179 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Tooltip/TagHelperTooltipFactoryBase.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Tooltip/DocCommentHelpers.cs @@ -4,78 +4,20 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; -using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Tooltip; +namespace Microsoft.CodeAnalysis.Razor.Tooltip; -internal abstract class TagHelperTooltipFactoryBase(IProjectSnapshotManager projectManager) +internal static class DocCommentHelpers { - protected static readonly string TagContentGroupName = "content"; + public const string TagContentGroupName = "content"; + private static readonly Regex s_codeRegex = new Regex($"""<(?:c|code)>(?<{TagContentGroupName}>.*?)<\/(?:c|code)>""", RegexOptions.Compiled, TimeSpan.FromSeconds(1)); private static readonly Regex s_crefRegex = new Regex($"""<(?:see|seealso)[\s]+cref="(?<{TagContentGroupName}>[^">]+)"[^>]*>""", RegexOptions.Compiled, TimeSpan.FromSeconds(1)); - private static readonly IReadOnlyList s_newLineChars = new char[] { '\n', '\r' }; - - private readonly IProjectSnapshotManager _projectManager = projectManager; - - internal async Task GetProjectAvailabilityAsync(string documentFilePath, string tagHelperTypeName, CancellationToken cancellationToken) - { - if (!_projectManager.TryResolveAllProjects(documentFilePath, out var projectSnapshots)) - { - return null; - } - - using var _ = StringBuilderPool.GetPooledObject(out var builder); - - foreach (var project in projectSnapshots) - { - if (MiscFilesHostProject.IsMiscellaneousProject(project)) - { - continue; - } - - var found = false; - var tagHelpers = await project.GetTagHelpersAsync(cancellationToken).ConfigureAwait(false); - foreach (var tagHelper in tagHelpers) - { - if (tagHelper.GetTypeName() == tagHelperTypeName) - { - found = true; - break; - } - } - - if (!found) - { - if (builder.Length == 0) - { - builder.AppendLine(); - builder.Append($"⚠️ {SR.Not_Available_In}:"); - } - - builder.AppendLine(); - builder.Append(" "); - builder.Append(project.DisplayName); - } - } - - if (builder.Length == 0) - { - return null; - } + private static readonly char[] s_newLineChars = ['\n', '\r']; - return builder.ToString(); - } - - // Internal for testing - internal static string ReduceCrefValue(string value) + public static string ReduceCrefValue(string value) { // cref values come in the following formats: // Type = "T:Microsoft.AspNetCore.SomeTagHelpers.SomeTypeName" @@ -105,75 +47,11 @@ internal static string ReduceCrefValue(string value) return value; } - // Internal for testing - internal static string ReduceTypeName(string content) => ReduceFullName(content, reduceWhenDotCount: 1); - - // Internal for testing - internal static string ReduceMemberName(string content) => ReduceFullName(content, reduceWhenDotCount: 2); + public static string ReduceTypeName(string content) + => ReduceFullName(content, reduceWhenDotCount: 1); - // Internal for testing - internal static bool TryExtractSummary(string? documentation, [NotNullWhen(returnValue: true)] out string? summary) - { - const string SummaryStartTag = ""; - const string SummaryEndTag = ""; - - if (documentation is null || documentation == string.Empty) - { - summary = null; - return false; - } - - documentation = documentation.Trim(s_newLineChars.ToArray()); - - var summaryTagStart = documentation.IndexOf(SummaryStartTag, StringComparison.OrdinalIgnoreCase); - var summaryTagEndStart = documentation.IndexOf(SummaryEndTag, StringComparison.OrdinalIgnoreCase); - if (summaryTagStart == -1 || summaryTagEndStart == -1) - { - // A really wrong but cheap way to check if this is XML - if (!documentation.StartsWith("<", StringComparison.Ordinal) && !documentation.EndsWith(">", StringComparison.Ordinal)) - { - // This doesn't look like a doc comment, we'll return it as-is. - summary = documentation; - return true; - } - - summary = null; - return false; - } - - var summaryContentStart = summaryTagStart + SummaryStartTag.Length; - var summaryContentLength = summaryTagEndStart - summaryContentStart; - - summary = documentation.Substring(summaryContentStart, summaryContentLength); - return true; - } - - internal static List ExtractCodeMatches(string summaryContent) - { - var successfulMatches = ExtractSuccessfulMatches(s_codeRegex, summaryContent); - return successfulMatches; - } - - internal static List ExtractCrefMatches(string summaryContent) - { - var successfulMatches = ExtractSuccessfulMatches(s_crefRegex, summaryContent); - return successfulMatches; - } - - private static List ExtractSuccessfulMatches(Regex regex, string summaryContent) - { - var matches = regex.Matches(summaryContent); - var successfulMatches = new List(); - foreach (Match match in matches) - { - if (match.Success) - { - successfulMatches.Add(match); - } - } - - return successfulMatches; - } + public static string ReduceMemberName(string content) + => ReduceFullName(content, reduceWhenDotCount: 2); private static string ReduceFullName(string content, int reduceWhenDotCount) { @@ -208,7 +86,8 @@ private static string ReduceFullName(string content, int reduceWhenDotCount) { i--; } - } while (scope != 0 && i >= 0); + } + while (scope != 0 && i >= 0); if (i < 0) { @@ -231,7 +110,8 @@ private static string ReduceFullName(string content, int reduceWhenDotCount) { i--; } - } while (scope != 0 && i >= 0); + } + while (scope != 0 && i >= 0); if (i < 0) { @@ -254,7 +134,8 @@ private static string ReduceFullName(string content, int reduceWhenDotCount) { i--; } - } while (scope != 0 && i >= 0); + } + while (scope != 0 && i >= 0); if (i < 0) { @@ -277,4 +158,67 @@ private static string ReduceFullName(string content, int reduceWhenDotCount) // Could not reduce name return content; } + + public static bool TryExtractSummary(string? documentation, [NotNullWhen(true)] out string? summary) + { + const string SummaryStartTag = ""; + const string SummaryEndTag = ""; + + if (documentation is null || documentation == string.Empty) + { + summary = null; + return false; + } + + documentation = documentation.Trim(s_newLineChars); + + var summaryTagStart = documentation.IndexOf(SummaryStartTag, StringComparison.OrdinalIgnoreCase); + var summaryTagEndStart = documentation.IndexOf(SummaryEndTag, StringComparison.OrdinalIgnoreCase); + if (summaryTagStart == -1 || summaryTagEndStart == -1) + { + // A really wrong but cheap way to check if this is XML + if (!documentation.StartsWith("<", StringComparison.Ordinal) && !documentation.EndsWith(">", StringComparison.Ordinal)) + { + // This doesn't look like a doc comment, we'll return it as-is. + summary = documentation; + return true; + } + + summary = null; + return false; + } + + var summaryContentStart = summaryTagStart + SummaryStartTag.Length; + var summaryContentLength = summaryTagEndStart - summaryContentStart; + + summary = documentation.Substring(summaryContentStart, summaryContentLength); + return true; + } + + public static List ExtractCodeMatches(string summaryContent) + { + var successfulMatches = ExtractSuccessfulMatches(s_codeRegex, summaryContent); + return successfulMatches; + } + + public static List ExtractCrefMatches(string summaryContent) + { + var successfulMatches = ExtractSuccessfulMatches(s_crefRegex, summaryContent); + return successfulMatches; + } + + private static List ExtractSuccessfulMatches(Regex regex, string summaryContent) + { + var matches = regex.Matches(summaryContent); + var successfulMatches = new List(); + foreach (Match match in matches) + { + if (match.Success) + { + successfulMatches.Add(match); + } + } + + return successfulMatches; + } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Tooltip/Extensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Tooltip/Extensions.cs new file mode 100644 index 00000000000..2a8038838d4 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Tooltip/Extensions.cs @@ -0,0 +1,103 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Razor.Tooltip; + +internal static class Extensions +{ + internal static async Task GetProjectAvailabilityTextAsync( + this ISolutionQueryOperations solutionQueryOperations, + string documentFilePath, + string tagHelperTypeName, + CancellationToken cancellationToken) + { + var projects = await solutionQueryOperations + .GetProjectAvailabilityAsync(documentFilePath, tagHelperTypeName, cancellationToken) + .ConfigureAwait(false); + + if (projects.IsEmpty) + { + return null; + } + + using var _ = StringBuilderPool.GetPooledObject(out var builder); + + foreach (var (project, isAvailable) in projects) + { + if (isAvailable) + { + continue; + } + + if (builder.Length == 0) + { + builder.AppendLine(); + builder.Append($"⚠️ {SR.Not_Available_In}:"); + } + + builder.AppendLine(); + builder.Append(" "); + builder.Append(project.DisplayName); + } + + if (builder.Length == 0) + { + return null; + } + + return builder.ToString(); + } + + /// + /// Returns the Razor projects that contain the document specified by file path and a + /// that indicates whether or not the given tag helper is available within a project. + /// + internal static async Task> GetProjectAvailabilityAsync( + this ISolutionQueryOperations solutionQueryOperations, + string documentFilePath, + string tagHelperTypeName, + CancellationToken cancellationToken) + { + var projects = solutionQueryOperations.GetProjectsContainingDocument(documentFilePath); + if (projects.IsEmpty) + { + return []; + } + + using var result = new PooledArrayBuilder<(IProjectSnapshot, bool IsAvailable)>(capacity: projects.Length); + + foreach (var project in projects) + { + var containsTagHelper = await project.ContainsTagHelperAsync(tagHelperTypeName, cancellationToken).ConfigureAwait(false); + + result.Add((project, IsAvailable: containsTagHelper)); + } + + return result.DrainToImmutable(); + } + + internal static async Task ContainsTagHelperAsync( + this IProjectSnapshot projectSnapshot, + string tagHelperTypeName, + CancellationToken cancellationToken) + { + var tagHelpers = await projectSnapshot.GetTagHelpersAsync(cancellationToken).ConfigureAwait(false); + + foreach (var tagHelper in tagHelpers) + { + if (tagHelper.GetTypeName() == tagHelperTypeName) + { + return true; + } + } + + return false; + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Tooltip/DefaultLSPTagHelperTooltipFactory.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Tooltip/MarkupTagHelperTooltipFactory.cs similarity index 85% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Tooltip/DefaultLSPTagHelperTooltipFactory.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Tooltip/MarkupTagHelperTooltipFactory.cs index e51fbbdcb2e..7c6635b6433 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Tooltip/DefaultLSPTagHelperTooltipFactory.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Tooltip/MarkupTagHelperTooltipFactory.cs @@ -7,19 +7,18 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Tooltip; using Microsoft.VisualStudio.LanguageServer.Protocol; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Tooltip; +namespace Microsoft.CodeAnalysis.Razor.Tooltip; -internal class DefaultLSPTagHelperTooltipFactory(IProjectSnapshotManager projectManager) : LSPTagHelperTooltipFactory(projectManager) +internal static class MarkupTagHelperTooltipFactory { - public override async Task TryCreateTooltipAsync( + public static async Task TryCreateTooltipAsync( string documentFilePath, AggregateBoundElementDescription elementDescriptionInfo, + ISolutionQueryOperations solutionQueryOperations, MarkupKind markupKind, CancellationToken cancellationToken) { @@ -52,7 +51,7 @@ internal class DefaultLSPTagHelperTooltipFactory(IProjectSnapshotManager project } var tagHelperType = descriptionInfo.TagHelperTypeName; - var reducedTypeName = ReduceTypeName(tagHelperType); + var reducedTypeName = DocCommentHelpers.ReduceTypeName(tagHelperType); // If the reducedTypeName != tagHelperType, then the type is prefixed by a namespace if (reducedTypeName != tagHelperType) @@ -66,7 +65,7 @@ internal class DefaultLSPTagHelperTooltipFactory(IProjectSnapshotManager project StartOrEndBold(descriptionBuilder, markupKind); var documentation = descriptionInfo.Documentation; - if (TryExtractSummary(documentation, out var summaryContent)) + if (DocCommentHelpers.TryExtractSummary(documentation, out var summaryContent)) { descriptionBuilder.AppendLine(); descriptionBuilder.AppendLine(); @@ -74,7 +73,10 @@ internal class DefaultLSPTagHelperTooltipFactory(IProjectSnapshotManager project descriptionBuilder.Append(finalSummaryContent); } - var availability = await GetProjectAvailabilityAsync(documentFilePath, tagHelperType, cancellationToken).ConfigureAwait(false); + var availability = await solutionQueryOperations + .GetProjectAvailabilityTextAsync(documentFilePath, tagHelperType, cancellationToken) + .ConfigureAwait(false); + if (availability is not null) { descriptionBuilder.AppendLine(); @@ -82,14 +84,14 @@ internal class DefaultLSPTagHelperTooltipFactory(IProjectSnapshotManager project } } - return new MarkupContent + return new MarkupContent { Kind = markupKind, Value = descriptionBuilder.ToString(), }; } - public override bool TryCreateTooltip( + public static bool TryCreateTooltip( AggregateBoundAttributeDescription attributeDescriptionInfo, MarkupKind markupKind, [NotNullWhen(true)] out MarkupContent? tooltipContent) @@ -129,12 +131,12 @@ public override bool TryCreateTooltip( returnTypeName = descriptionInfo.ReturnTypeName; } - var reducedReturnTypeName = ReduceTypeName(returnTypeName); + var reducedReturnTypeName = DocCommentHelpers.ReduceTypeName(returnTypeName); descriptionBuilder.Append(reducedReturnTypeName); StartOrEndBold(descriptionBuilder, markupKind); descriptionBuilder.Append(' '); var tagHelperTypeName = descriptionInfo.TypeName; - var reducedTagHelperTypeName = ReduceTypeName(tagHelperTypeName); + var reducedTagHelperTypeName = DocCommentHelpers.ReduceTypeName(tagHelperTypeName); descriptionBuilder.Append(reducedTagHelperTypeName); descriptionBuilder.Append('.'); StartOrEndBold(descriptionBuilder, markupKind); @@ -142,7 +144,7 @@ public override bool TryCreateTooltip( StartOrEndBold(descriptionBuilder, markupKind); var documentation = descriptionInfo.Documentation; - if (!TryExtractSummary(documentation, out var summaryContent)) + if (!DocCommentHelpers.TryExtractSummary(documentation, out var summaryContent)) { continue; } @@ -170,7 +172,7 @@ internal static string CleanSummaryContent(string summaryContent) // if there's a in the summary element when it's shown in the completion description window // it'll be serialized as html (wont show). summaryContent = summaryContent.Trim(); - var crefMatches = ExtractCrefMatches(summaryContent); + var crefMatches = DocCommentHelpers.ExtractCrefMatches(summaryContent); using var _ = StringBuilderPool.GetPooledObject(out var summaryBuilder); @@ -181,8 +183,8 @@ internal static string CleanSummaryContent(string summaryContent) var cref = crefMatches[i]; if (cref.Success) { - var value = cref.Groups[TagContentGroupName].Value; - var reducedValue = ReduceCrefValue(value); + var value = cref.Groups[DocCommentHelpers.TagContentGroupName].Value; + var reducedValue = DocCommentHelpers.ReduceCrefValue(value); reducedValue = reducedValue.Replace("{", "<").Replace("}", ">"); summaryBuilder.Remove(cref.Index, cref.Length); summaryBuilder.Insert(cref.Index, $"`{reducedValue}`"); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteAutoInsertService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteAutoInsertService.cs index 119d984e730..27dcc789526 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteAutoInsertService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/AutoInsert/RemoteAutoInsertService.cs @@ -137,7 +137,9 @@ private async ValueTask TryResolveInsertionInCSharpAsync( return Response.NoFurtherHandling; } - var generatedDocument = await remoteDocumentContext.Snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false); + var generatedDocument = await remoteDocumentContext.Snapshot + .GetGeneratedDocumentAsync(cancellationToken) + .ConfigureAwait(false); var autoInsertResponseItem = await OnAutoInsert.GetOnAutoInsertResponseAsync( generatedDocument, diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Diagnostics/RemoteDiagnosticsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Diagnostics/RemoteDiagnosticsService.cs new file mode 100644 index 00000000000..26fa41db058 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Diagnostics/RemoteDiagnosticsService.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor.Diagnostics; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; +using LspDiagnostic = Microsoft.VisualStudio.LanguageServer.Protocol.Diagnostic; + +namespace Microsoft.CodeAnalysis.Remote.Razor; + +internal sealed class RemoteDiagnosticsService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteDiagnosticsService +{ + internal sealed class Factory : FactoryBase + { + protected override IRemoteDiagnosticsService CreateService(in ServiceArgs args) + => new RemoteDiagnosticsService(in args); + } + + private readonly RazorTranslateDiagnosticsService _translateDiagnosticsService = args.ExportProvider.GetExportedValue(); + + public ValueTask> GetDiagnosticsAsync( + JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, + JsonSerializableDocumentId documentId, + LspDiagnostic[] csharpDiagnostics, + LspDiagnostic[] htmlDiagnostics, + CancellationToken cancellationToken) + => RunServiceAsync( + solutionInfo, + documentId, + context => GetDiagnosticsAsync(context, csharpDiagnostics, htmlDiagnostics, cancellationToken), + cancellationToken); + + private async ValueTask> GetDiagnosticsAsync( + RemoteDocumentContext context, + LspDiagnostic[] csharpDiagnostics, + LspDiagnostic[] htmlDiagnostics, + CancellationToken cancellationToken) + { + // We've got C# and Html, lets get Razor diagnostics + var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + // Yes, CSharpDocument.Documents are the Razor diagnostics. Don't ask. + var razorDiagnostics = codeDocument.GetCSharpDocument().Diagnostics; + + return [ + .. RazorDiagnosticConverter.Convert(razorDiagnostics, codeDocument.Source.Text, context.Snapshot), + .. await _translateDiagnosticsService.TranslateAsync(RazorLanguageKind.CSharp, csharpDiagnostics, context.Snapshot, cancellationToken), + .. await _translateDiagnosticsService.TranslateAsync(RazorLanguageKind.Html, htmlDiagnostics, context.Snapshot, cancellationToken) + ]; + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Diagnostics/RemoteRazorTranslateDiagnosticsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Diagnostics/RemoteRazorTranslateDiagnosticsService.cs new file mode 100644 index 00000000000..78ae0833889 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Diagnostics/RemoteRazorTranslateDiagnosticsService.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Composition; +using Microsoft.CodeAnalysis.Razor.Diagnostics; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.Logging; + +namespace Microsoft.CodeAnalysis.Remote.Razor.Diagnostics; + +[Export(typeof(RazorTranslateDiagnosticsService)), Shared] +[method: ImportingConstructor] +internal sealed class RemoteRazorTranslateDiagnosticsService( + IDocumentMappingService documentMappingService, + ILoggerFactory loggerFactory) : RazorTranslateDiagnosticsService(documentMappingService, loggerFactory) +{ +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentHighlight/RemoteDocumentHighlightService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentHighlight/RemoteDocumentHighlightService.cs index 2d58f0a80bb..0fdb8119224 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentHighlight/RemoteDocumentHighlightService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentHighlight/RemoteDocumentHighlightService.cs @@ -27,8 +27,6 @@ protected override IRemoteDocumentHighlightService CreateService(in ServiceArgs => new RemoteDocumentHighlightService(in args); } - private readonly IDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue(); - public ValueTask GetHighlightsAsync( RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, @@ -64,9 +62,11 @@ private async ValueTask GetHighlightsAsync( } var csharpDocument = codeDocument.GetCSharpDocument(); - if (_documentMappingService.TryMapToGeneratedDocumentPosition(csharpDocument, index, out var mappedPosition, out _)) + if (DocumentMappingService.TryMapToGeneratedDocumentPosition(csharpDocument, index, out var mappedPosition, out _)) { - var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false); + var generatedDocument = await context.Snapshot + .GetGeneratedDocumentAsync(cancellationToken) + .ConfigureAwait(false); var highlights = await DocumentHighlights.GetHighlightsAsync(generatedDocument, mappedPosition, cancellationToken).ConfigureAwait(false); @@ -76,7 +76,7 @@ private async ValueTask GetHighlightsAsync( foreach (var highlight in highlights) { - if (_documentMappingService.TryMapToHostDocumentRange(csharpDocument, highlight.Range.ToLinePositionSpan(), out var mappedRange)) + if (DocumentMappingService.TryMapToHostDocumentRange(csharpDocument, highlight.Range.ToLinePositionSpan(), out var mappedRange)) { highlight.Range = Roslyn.LanguageServer.Protocol.RoslynLspExtensions.ToRange(mappedRange); results.Add(RemoteDocumentHighlight.FromRoslynDocumentHighlight(highlight)); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteDocumentMappingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteDocumentMappingService.cs index 0d3a170efac..a70d05dafb2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteDocumentMappingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteDocumentMappingService.cs @@ -20,11 +20,11 @@ namespace Microsoft.CodeAnalysis.Remote.Razor.DocumentMapping; [method: ImportingConstructor] internal sealed class RemoteDocumentMappingService( IFilePathService filePathService, - DocumentSnapshotFactory documentSnapshotFactory, + RemoteSnapshotManager snapshotManager, ILoggerFactory loggerFactory) : AbstractDocumentMappingService(filePathService, loggerFactory.GetOrCreateLogger()) { - private readonly DocumentSnapshotFactory _documentSnapshotFactory = documentSnapshotFactory; + private readonly RemoteSnapshotManager _snapshotManager = snapshotManager; public async Task<(Uri MappedDocumentUri, LinePositionSpan MappedRange)> MapToHostDocumentUriAndRangeAsync( RemoteDocumentSnapshot originSnapshot, @@ -52,10 +52,11 @@ internal sealed class RemoteDocumentMappingService( return (generatedDocumentUri, generatedDocumentRange); } - var razorDocumentSnapshot = _documentSnapshotFactory.GetOrCreate(razorDocument); + var razorDocumentSnapshot = _snapshotManager.GetSnapshot(razorDocument); - var razorCodeDocument = await razorDocumentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); + var razorCodeDocument = await razorDocumentSnapshot + .GetGeneratedOutputAsync(cancellationToken) + .ConfigureAwait(false); if (razorCodeDocument is null) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteEditMappingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteEditMappingService.cs index 7110af27edd..9897a0e4585 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteEditMappingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentMapping/RemoteEditMappingService.cs @@ -17,9 +17,9 @@ namespace Microsoft.CodeAnalysis.Remote.Razor.DocumentMapping; internal sealed class RemoteEditMappingService( IDocumentMappingService documentMappingService, IFilePathService filePathService, - DocumentSnapshotFactory documentSnapshotFactory) : AbstractEditMappingService(documentMappingService, filePathService) + RemoteSnapshotManager snapshotManager) : AbstractEditMappingService(documentMappingService, filePathService) { - private readonly DocumentSnapshotFactory _documentSnapshotFactory = documentSnapshotFactory; + private readonly RemoteSnapshotManager _snapshotManager = snapshotManager; protected override bool TryGetDocumentContext(IDocumentSnapshot contextDocumentSnapshot, Uri razorDocumentUri, VSProjectContext? projectContext, [NotNullWhen(true)] out DocumentContext? documentContext) { @@ -35,7 +35,7 @@ protected override bool TryGetDocumentContext(IDocumentSnapshot contextDocumentS return false; } - var razorDocumentSnapshot = _documentSnapshotFactory.GetOrCreate(razorDocument); + var razorDocumentSnapshot = _snapshotManager.GetSnapshot(razorDocument); documentContext = new RemoteDocumentContext(razorDocumentUri, razorDocumentSnapshot); return true; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentSymbols/RemoteDocumentSymbolService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentSymbols/RemoteDocumentSymbolService.cs index 8cda0e8854c..d04786bfa5c 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentSymbols/RemoteDocumentSymbolService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentSymbols/RemoteDocumentSymbolService.cs @@ -33,7 +33,10 @@ protected override IRemoteDocumentSymbolService CreateService(in ServiceArgs arg private async ValueTask?> GetDocumentSymbolsAsync(RemoteDocumentContext context, bool useHierarchicalSymbols, CancellationToken cancellationToken) { - var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false); + var generatedDocument = await context.Snapshot + .GetGeneratedDocumentAsync(cancellationToken) + .ConfigureAwait(false); + var csharpSymbols = await ExternalHandlers.DocumentSymbols.GetDocumentSymbolsAsync(generatedDocument, useHierarchicalSymbols, cancellationToken).ConfigureAwait(false); var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/FoldingRanges/RemoteFoldingRangeService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/FoldingRanges/RemoteFoldingRangeService.cs index 87e4673b004..0e587b74773 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/FoldingRanges/RemoteFoldingRangeService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/FoldingRanges/RemoteFoldingRangeService.cs @@ -41,7 +41,9 @@ private async ValueTask> GetFoldingRangesAsyn ImmutableArray htmlRanges, CancellationToken cancellationToken) { - var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false); + var generatedDocument = await context.Snapshot + .GetGeneratedDocumentAsync(cancellationToken) + .ConfigureAwait(false); var csharpRanges = await ExternalHandlers.FoldingRanges.GetFoldingRangesAsync(generatedDocument, cancellationToken).ConfigureAwait(false); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingCodeDocumentProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingCodeDocumentProvider.cs index 64c90c21cb5..914e05b6139 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingCodeDocumentProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingCodeDocumentProvider.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System.Composition; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.CodeAnalysis.Razor.Formatting; @@ -12,9 +13,9 @@ namespace Microsoft.CodeAnalysis.Remote.Razor.Formatting; [Export(typeof(IFormattingCodeDocumentProvider)), Shared] internal sealed class RemoteFormattingCodeDocumentProvider : IFormattingCodeDocumentProvider { - public Task GetCodeDocumentAsync(IDocumentSnapshot snapshot) + public ValueTask GetCodeDocumentAsync(IDocumentSnapshot snapshot, CancellationToken cancellationToken) { // Formatting always uses design time - return snapshot.GetGeneratedOutputAsync(forceDesignTimeGeneratedOutput: true); + return snapshot.GetGeneratedOutputAsync(forceDesignTimeGeneratedOutput: true, cancellationToken); } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingService.cs index 29d47f91bc3..b9921462a6a 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteFormattingService.cs @@ -97,10 +97,10 @@ public ValueTask GetOnTypeFormattingTriggerKindAsync( => RunServiceAsync( solutionInfo, documentId, - context => IsValidOnTypeFormattingTriggerAsync(context, linePosition, triggerCharacter, cancellationToken), + context => GetOnTypeFormattingTriggerKindAsync(context, linePosition, triggerCharacter, cancellationToken), cancellationToken); - private async ValueTask IsValidOnTypeFormattingTriggerAsync(RemoteDocumentContext context, LinePosition linePosition, string triggerCharacter, CancellationToken cancellationToken) + private async ValueTask GetOnTypeFormattingTriggerKindAsync(RemoteDocumentContext context, LinePosition linePosition, string triggerCharacter, CancellationToken cancellationToken) { var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); var sourceText = codeDocument.Source.Text; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingService.cs index 6974dabd019..7de42441ecc 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Formatting/RemoteRazorFormattingService.cs @@ -11,7 +11,7 @@ namespace Microsoft.CodeAnalysis.Remote.Razor.Formatting; [Export(typeof(IRazorFormattingService)), Shared] [method: ImportingConstructor] -internal sealed class RemoteRazorFormattingService(IFormattingCodeDocumentProvider codeDocumentProvider, IDocumentMappingService documentMappingService, IAdhocWorkspaceFactory adhocWorkspaceFactory, ILoggerFactory loggerFactory) - : RazorFormattingService(codeDocumentProvider, documentMappingService, adhocWorkspaceFactory, loggerFactory) +internal sealed class RemoteRazorFormattingService(IFormattingCodeDocumentProvider codeDocumentProvider, IDocumentMappingService documentMappingService, IHostServicesProvider hostServicesProvider, ILoggerFactory loggerFactory) + : RazorFormattingService(codeDocumentProvider, documentMappingService, hostServicesProvider, loggerFactory) { } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToDefinition/RemoteGoToDefinitionService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToDefinition/RemoteGoToDefinitionService.cs index be9183f857c..e5a496e6ac2 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToDefinition/RemoteGoToDefinitionService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToDefinition/RemoteGoToDefinitionService.cs @@ -62,7 +62,10 @@ protected override IRemoteGoToDefinitionService CreateService(in ServiceArgs arg if (positionInfo.LanguageKind is RazorLanguageKind.Html or RazorLanguageKind.Razor) { // First, see if this is a Razor component. We ignore attributes here, because they're better served by the C# handler. - var componentLocation = await _componentDefinitionService.GetDefinitionAsync(context.Snapshot, positionInfo, ignoreAttributes: true, cancellationToken).ConfigureAwait(false); + var componentLocation = await _componentDefinitionService + .GetDefinitionAsync(context.Snapshot, positionInfo, context.GetSolutionQueryOperations(), ignoreAttributes: true, cancellationToken) + .ConfigureAwait(false); + if (componentLocation is not null) { // Convert from VS LSP Location to Roslyn. This can be removed when Razor moves fully onto Roslyn's LSP types. @@ -80,7 +83,9 @@ protected override IRemoteGoToDefinitionService CreateService(in ServiceArgs arg } // Finally, call into C#. - var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false); + var generatedDocument = await context.Snapshot + .GetGeneratedDocumentAsync(cancellationToken) + .ConfigureAwait(false); var locations = await ExternalHandlers.GoToDefinition .GetDefinitionsAsync( diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToImplementation/RemoteGoToImplementationService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToImplementation/RemoteGoToImplementationService.cs index 920df3589f2..aabf32dfb63 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToImplementation/RemoteGoToImplementationService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToImplementation/RemoteGoToImplementationService.cs @@ -73,7 +73,9 @@ protected override IRemoteGoToImplementationService CreateService(in ServiceArgs } // Finally, call into C#. - var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false); + var generatedDocument = await context.Snapshot + .GetGeneratedDocumentAsync(cancellationToken) + .ConfigureAwait(false); var locations = await ExternalHandlers.GoToImplementation .FindImplementationsAsync( diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/InlayHints/RemoteInlayHintService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/InlayHints/RemoteInlayHintService.cs index 1dc9c7fcef4..b8235e8ff98 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/InlayHints/RemoteInlayHintService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/InlayHints/RemoteInlayHintService.cs @@ -41,6 +41,16 @@ protected override IRemoteInlayHintService CreateService(in ServiceArgs args) var span = inlayHintParams.Range.ToLinePositionSpan(); + cancellationToken.ThrowIfCancellationRequested(); + + // Sometimes the client sends us a request that doesn't match the file contents. Could be a bug with old requests + // not being cancelled, but no harm in being defensive + if (!codeDocument.Source.Text.TryGetAbsoluteIndex(span.Start, out var startIndex) || + !codeDocument.Source.Text.TryGetAbsoluteIndex(span.End, out var endIndex)) + { + return null; + } + // We are given a range by the client, but our mapping only succeeds if the start and end of the range can both be mapped // to C#. Since that doesn't logically match what we want from inlay hints, we instead get the minimum range of mappable // C# to get hints for. We'll filter that later, to remove the sections that can't be mapped back. @@ -51,7 +61,9 @@ protected override IRemoteInlayHintService CreateService(in ServiceArgs args) return null; } - var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false); + var generatedDocument = await context.Snapshot + .GetGeneratedDocumentAsync(cancellationToken) + .ConfigureAwait(false); var textDocument = inlayHintParams.TextDocument.WithUri(generatedDocument.CreateUri()); var range = projectedLinePositionSpan.ToRange(); @@ -105,7 +117,9 @@ public ValueTask ResolveHintAsync(JsonSerializableRazorPinnedSolution private async ValueTask ResolveInlayHintAsync(RemoteDocumentContext context, InlayHint inlayHint, CancellationToken cancellationToken) { - var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false); + var generatedDocument = await context.Snapshot + .GetGeneratedDocumentAsync(cancellationToken) + .ConfigureAwait(false); return await InlayHints.ResolveInlayHintAsync(generatedDocument, inlayHint, cancellationToken).ConfigureAwait(false); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj index 3357fd1eb79..981af5285e5 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Microsoft.CodeAnalysis.Remote.Razor.csproj @@ -5,7 +5,7 @@ Razor is a markup syntax for adding server-side logic to web pages. This package contains the Razor design-time infrastructure. false false - true + true false diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.csproj b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.csproj index d616ecdca89..bbee7f242dc 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.csproj +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Microsoft.VisualStudio.RazorExtension.csproj @@ -18,7 +18,7 @@ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - true + true false true @@ -44,7 +44,7 @@ - + diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Options/AdvancedOptionPage.cs b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Options/AdvancedOptionPage.cs index f8a290761a7..264135fa679 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Options/AdvancedOptionPage.cs +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/Options/AdvancedOptionPage.cs @@ -25,6 +25,7 @@ internal class AdvancedOptionPage : DialogPage private bool? _commitElementsWithSpace; private SnippetSetting? _snippets; private LogLevel? _logLevel; + private bool? _formatOnPaste; public AdvancedOptionPage() { @@ -91,6 +92,15 @@ public bool CodeBlockBraceOnNextLine set => _codeBlockBraceOnNextLine = value; } + [LocCategory(nameof(VSPackage.Formatting))] + [LocDescription(nameof(VSPackage.Setting_FormattingOnPasteDescription))] + [LocDisplayName(nameof(VSPackage.Setting_FormattingOnPasteDisplayName))] + public bool FormatOnPaste + { + get => _formatOnPaste ?? true; + set => _formatOnPaste = value; + } + [LocCategory(nameof(VSPackage.Completion))] [LocDescription(nameof(VSPackage.Setting_SnippetsDescription))] [LocDisplayName(nameof(VSPackage.Setting_SnippetsDisplayName))] diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/SyntaxVisualizer/SourceMappingTagger.cs b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/SyntaxVisualizer/SourceMappingTagger.cs index d2eb24543c4..3a65a49cf7d 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/SyntaxVisualizer/SourceMappingTagger.cs +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/SyntaxVisualizer/SourceMappingTagger.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Threading; using Microsoft.AspNetCore.Razor.Language; using Microsoft.VisualStudio.Razor.Documents; using Microsoft.VisualStudio.Shell; @@ -39,21 +39,22 @@ public IEnumerable> GetTags(NormalizedSnapshotSpanCol { if (!Enabled || spans.Count == 0) { - return Enumerable.Empty>(); + return []; } var snapshot = spans[0].Snapshot; if (!_textDocumentFactoryService.TryGetTextDocument(_buffer, out var textDocument)) { - return Enumerable.Empty>(); + return []; } - var codeDocument = ThreadHelper.JoinableTaskFactory.Run(() => _sourceMappingProjectChangeTrigger.Value.GetRazorCodeDocumentAsync(textDocument.FilePath)); + var codeDocument = ThreadHelper.JoinableTaskFactory.Run( + () => _sourceMappingProjectChangeTrigger.Value.GetRazorCodeDocumentAsync(textDocument.FilePath, CancellationToken.None)); if (codeDocument is null) { - return Enumerable.Empty>(); + return []; } return GetTagsWorker(codeDocument, snapshot); diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/SyntaxVisualizer/SyntaxVisualizerControl.xaml.cs b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/SyntaxVisualizer/SyntaxVisualizerControl.xaml.cs index 9261c3db0a2..cfe269b9fc7 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/SyntaxVisualizer/SyntaxVisualizerControl.xaml.cs +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/SyntaxVisualizer/SyntaxVisualizerControl.xaml.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Threading; using System.Windows; using System.Windows.Controls; using Microsoft.AspNetCore.Razor.Language; @@ -539,7 +540,8 @@ private void treeView_KeyUp(object sender, System.Windows.Input.KeyEventArgs e) var filePath = hostDocumentUri.GetAbsoluteOrUNCPath().Replace('/', '\\'); - var codeDocument = _joinableTaskFactory.Run(() => _codeDocumentProvider.GetRazorCodeDocumentAsync(filePath)); + var codeDocument = _joinableTaskFactory.Run( + () => _codeDocumentProvider.GetRazorCodeDocumentAsync(filePath, CancellationToken.None)); if (codeDocument is null) { return null; diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/UnifiedSettings/razor.registration.json b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/UnifiedSettings/razor.registration.json index 46dc8be958d..0d67235b36a 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/UnifiedSettings/razor.registration.json +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/UnifiedSettings/razor.registration.json @@ -1,10 +1,10 @@ { "properties": { /* - * Migration works by using the previous path that we stored in the VsUserSettingsRegister. This comes - * from the OptionsStorage strings related to the option with the form of "{collection}\{name}". - * Names much match exactly for the setting to work. - */ + * Migration works by using the previous path that we stored in the VsUserSettingsRegister. This comes + * from the OptionsStorage strings related to the option with the form of "{collection}\{name}". + * Names much match exactly for the setting to work. + */ "textEditor.razor.advanced.formatOnType": { "type": "boolean", "default": true, @@ -19,6 +19,20 @@ } } }, + "textEditor.razor.advanced.formatOnPaste": { + "type": "boolean", + "default": true, + "title": "@Setting_FormattingOnPasteDisplayName;{13b72f58-279e-49e0-a56d-296be02f0805}", + "description": "@Setting_FormattingOnPasteDescription;{13b72f58-279e-49e0-a56d-296be02f0805}", + "migration": { + "pass": { + "input": { + "store": "VsUserSettingsRegistry", + "path": "Razor\\FormatOnPaste" + } + } + } + }, "textEditor.razor.advanced.autoClosingTags": { "type": "boolean", "default": true, @@ -92,11 +106,7 @@ "textEditor.razor.advanced.snippets": { "type": "string", "default": "all", - "enum": [ - "all", - "custom", - "none" - ], + "enum": ["all", "custom", "none"], "enumItemLabels": [ "@Setting_SnippetsAll;{13b72f58-279e-49e0-a56d-296be02f0805}", "@Setting_SnippetsCustom;{13b72f58-279e-49e0-a56d-296be02f0805}", @@ -199,4 +209,4 @@ "legacyOptionPageId": "8EBB7F64-5BF7-49E6-9023-7CD7B9912203" } } -} \ No newline at end of file +} diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/VSPackage.resx b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/VSPackage.resx index 7a9e0223550..9942e572b2f 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/VSPackage.resx +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/VSPackage.resx @@ -180,6 +180,12 @@ Commit elements with space + + Format on paste + + + If true, formatting will be enabled when pasting content + If true, formatting will be enabled while typing diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.cs.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.cs.xlf index f761260c51b..b349bd852b9 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.cs.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.cs.xlf @@ -92,6 +92,16 @@ Potvrzovat elementy mezerníkem + + If true, formatting will be enabled when pasting content + Pokud je true, při vkládání obsahu se povolí formátování. + + + + Format on paste + Formátovat při vložení + + If true, formatting will be enabled while typing Pokud je hodnota true, při psaní se povolí formátování. diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.de.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.de.xlf index c3df4f2b10f..9b8101e961b 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.de.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.de.xlf @@ -92,6 +92,16 @@ Elemente durch Drücken der LEERTASTE committen + + If true, formatting will be enabled when pasting content + „True“ gibt an, dass die Formatierung beim Einfügen von Inhalten aktiviert wird. + + + + Format on paste + Beim Einfügen formatieren + + If true, formatting will be enabled while typing Bei "true" wird die Formatierung während der Eingabe aktiviert. diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.es.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.es.xlf index 61ad53589dd..6feb41cb5b9 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.es.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.es.xlf @@ -92,6 +92,16 @@ Confirmar elementos con espacio + + If true, formatting will be enabled when pasting content + Si es true, se habilitará la aplicación de formato al pegar contenido + + + + Format on paste + Dar formato al pegar + + If true, formatting will be enabled while typing Si es true, el formato se habilitará al escribir diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.fr.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.fr.xlf index 5c7c6139fef..69271cdb768 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.fr.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.fr.xlf @@ -92,6 +92,16 @@ Valider des éléments avec de l’espace + + If true, formatting will be enabled when pasting content + Si la valeur est true, la mise en forme sera activée lors du collage du contenu + + + + Format on paste + Mettre en forme lors du collage + + If true, formatting will be enabled while typing Si la valeur est true, la mise en forme est activée lors de la saisie diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.it.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.it.xlf index ff83731d724..83e5a587e77 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.it.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.it.xlf @@ -92,6 +92,16 @@ Eseguire il commit degli elementi con lo spazio + + If true, formatting will be enabled when pasting content + Se true, la formattazione verrà abilitata quando viene incollato il contenuto + + + + Format on paste + Formatta quando si incolla + + If true, formatting will be enabled while typing Se true, la formattazione verrà abilitata durante la digitazione diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ja.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ja.xlf index 264240d6ed0..16c78d177d0 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ja.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ja.xlf @@ -92,6 +92,16 @@ スペースを含む要素のコミット + + If true, formatting will be enabled when pasting content + true の場合、コンテンツを貼り付けたときに書式設定が有効になります + + + + Format on paste + 貼り付け時に書式設定する + + If true, formatting will be enabled while typing true の場合、入力中に書式設定が有効になります diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ko.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ko.xlf index c844ba635ac..9bafbc2ee71 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ko.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ko.xlf @@ -92,6 +92,16 @@ 공백을 사용하여 요소 커밋 + + If true, formatting will be enabled when pasting content + true이면 콘텐츠를 붙여넣을 때 서식을 사용할 수 있습니다. + + + + Format on paste + 붙여넣기 시 서식 지정 + + If true, formatting will be enabled while typing true이면 입력하는 동안 서식을 사용할 수 있습니다. diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.pl.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.pl.xlf index dc3dfbd58a3..a632e3f14ac 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.pl.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.pl.xlf @@ -92,6 +92,16 @@ Zatwierdź elementy za pomocą klawisza Spacja + + If true, formatting will be enabled when pasting content + Jeśli wartość jest równa true, formatowanie będzie włączane podczas wklejania zawartości + + + + Format on paste + Formatuj przy wklejeniu + + If true, formatting will be enabled while typing Jeśli ma wartość „prawda”, formatowanie będzie włączone podczas pisania diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.pt-BR.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.pt-BR.xlf index c2ad5566dfc..c13c6321f22 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.pt-BR.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.pt-BR.xlf @@ -92,6 +92,16 @@ Confirmar elementos com espaço + + If true, formatting will be enabled when pasting content + Se for true, a formatação será habilitada ao colar conteúdo + + + + Format on paste + Formatar ao colar + + If true, formatting will be enabled while typing Se verdadeiro, a formatação será habilitada durante a digitação diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ru.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ru.xlf index 534feeab666..53ff562ecb8 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ru.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.ru.xlf @@ -92,6 +92,16 @@ Фиксация элементов клавишей ПРОБЕЛ + + If true, formatting will be enabled when pasting content + Если ИСТИНА, форматирование будет включено при вставке контента. + + + + Format on paste + Форматировать при вставке + + If true, formatting will be enabled while typing Если задано значение true, при вводе текста будет включено форматирование diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.tr.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.tr.xlf index 2d871d26639..a7c1192b28c 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.tr.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.tr.xlf @@ -92,6 +92,16 @@ Öğeleri boşluk çubuğuyla işle + + If true, formatting will be enabled when pasting content + Doğru ise içerik yapıştırılırken biçimlendirme etkinleştirilir + + + + Format on paste + Yapıştırma sırasında biçimlendir + + If true, formatting will be enabled while typing True ise, yazma sırasında biçimlendirme etkinleştirilir diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.zh-Hans.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.zh-Hans.xlf index 1a428d7836c..d9bd12153f3 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.zh-Hans.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.zh-Hans.xlf @@ -92,6 +92,16 @@ 使用空格键提交元素 + + If true, formatting will be enabled when pasting content + 如果为 true,则粘贴内容时将启用格式设置 + + + + Format on paste + 粘贴时设置格式 + + If true, formatting will be enabled while typing 如果为 true,则在键入时将启用格式设置 diff --git a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.zh-Hant.xlf b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.zh-Hant.xlf index 1779abb2bac..10cd111d098 100644 --- a/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.zh-Hant.xlf +++ b/src/Razor/src/Microsoft.VisualStudio.RazorExtension/xlf/VSPackage.zh-Hant.xlf @@ -92,6 +92,16 @@ 透過空格鍵認可元素 + + If true, formatting will be enabled when pasting content + 如為 true,貼上內容時將啟用格式化 + + + + Format on paste + 於貼上時格式化 + + If true, formatting will be enabled while typing 如果為 True,則會在輸入時啟用格式化 diff --git a/src/Razor/test/Directory.Build.props b/src/Razor/test/Directory.Build.props index a0207c093ee..0b4ec55df46 100644 --- a/src/Razor/test/Directory.Build.props +++ b/src/Razor/test/Directory.Build.props @@ -2,7 +2,7 @@ - true + true $(DefaultNetCoreTargetFrameworks) $(DeveloperBuildTestTfms) $(StandardTestTfms) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.Test/RazorProjectInfoSerializerTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.Test/RazorProjectInfoSerializerTest.cs index 465d490de1f..0b5ea2a7a73 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.Test/RazorProjectInfoSerializerTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.ExternalAccess.RoslynWorkspace.Test/RazorProjectInfoSerializerTest.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.AspNetCore.Razor.Utilities; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Text; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/AutoClosingTagOnAutoInsertProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/AutoClosingTagOnAutoInsertProviderTest.cs index 3eba6dff3fd..6120d307985 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/AutoClosingTagOnAutoInsertProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/AutoInsert/AutoClosingTagOnAutoInsertProviderTest.cs @@ -169,7 +169,7 @@ public void OnTypeCloseAngle_TagAlreadyHasEndTag() [Fact] [WorkItem("https://github.com/dotnet/aspnetcore/issues/36125")] - public void OnTypeCloseAngle_TagDoesNotAutoCloseOutOfScope() + public void OnTypeCloseAngle_TagDoesAutoCloseOutOfScope() { RunAutoInsertTest( input: """ @@ -183,11 +183,61 @@ public void OnTypeCloseAngle_TagDoesNotAutoCloseOutOfScope()
@if (true) { -
+
$0
} """); } + [Fact] + [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2251322")] + public void OnTypeCloseAngle_TagDoesAutoCloseInsideCSharpStatement() + { + RunAutoInsertTest( + input: """ +
+ @if (true) + { +
$$ + } +
+ """, + expected: """ +
+ @if (true) + { +
$0
+ } +
+ """); + } + + [Fact] + [WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2251322")] + public void OnTypeCloseAngle_TagDoesAutoCloseInsideDifferentTag() + { + RunAutoInsertTest( + input: """ +
+
+ @if (true) + { +
$$ + } +
+
+ """, + expected: """ +
+
+ @if (true) + { +
$0
+ } +
+
+ """); + } + [Fact] [WorkItem("https://github.com/dotnet/aspnetcore/issues/36125")] public void OnTypeCloseAngle_VoidTagHasEndTag_ShouldStillClose() diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionProviderTest.cs index f11d350837b..1bb0caefa35 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/DefaultCSharpCodeActionProviderTest.cs @@ -9,13 +9,13 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; using Microsoft.CodeAnalysis.Testing; -using Microsoft.CodeAnalysis.Text; using Microsoft.NET.Sdk.Razor.SourceGenerators; using Microsoft.VisualStudio.LanguageServer.Protocol; using Moq; @@ -338,15 +338,25 @@ private static RazorCodeActionContext CreateRazorCodeActionContext( var csharpDocumentWithDiagnostic = new RazorCSharpDocument(codeDocument, csharpDocument.GeneratedCode, csharpDocument.Options, [diagnostic]); codeDocument.SetCSharpDocument(csharpDocumentWithDiagnostic); - var documentSnapshot = Mock.Of(document => - document.GetGeneratedOutputAsync(It.IsAny()) == Task.FromResult(codeDocument) && - document.GetTextAsync() == Task.FromResult(codeDocument.Source.Text) && - document.Project.GetTagHelpersAsync(It.IsAny()) == new ValueTask>(tagHelpers), MockBehavior.Strict); - - var sourceText = SourceText.From(text); - - var context = new RazorCodeActionContext(request, documentSnapshot, codeDocument, location, sourceText, supportsFileCreation, supportsCodeActionResolve); - - return context; + var documentSnapshotMock = new StrictMock(); + documentSnapshotMock + .Setup(x => x.GetGeneratedOutputAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(codeDocument); + documentSnapshotMock + .Setup(x => x.GetTextAsync(It.IsAny())) + .ReturnsAsync(codeDocument.Source.Text); + documentSnapshotMock + .Setup(x => x.Project.GetTagHelpersAsync(It.IsAny())) + .ReturnsAsync(tagHelpers); + + return new RazorCodeActionContext( + request, + documentSnapshotMock.Object, + codeDocument, + location, + location, + codeDocument.Source.Text, + supportsFileCreation, + supportsCodeActionResolve); } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/TypeAccessibilityCodeActionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/TypeAccessibilityCodeActionProviderTest.cs index 4a48e62efb9..abdcea15a5f 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/TypeAccessibilityCodeActionProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CSharp/TypeAccessibilityCodeActionProviderTest.cs @@ -16,7 +16,6 @@ using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; -using Microsoft.CodeAnalysis.Text; using Microsoft.NET.Sdk.Razor.SourceGenerators; using Microsoft.VisualStudio.LanguageServer.Protocol; using Moq; @@ -466,15 +465,25 @@ private static RazorCodeActionContext CreateRazorCodeActionContext( var csharpDocumentWithDiagnostic = new RazorCSharpDocument(codeDocument, csharpDocument.GeneratedCode, csharpDocument.Options, [diagnostic]); codeDocument.SetCSharpDocument(csharpDocumentWithDiagnostic); - var documentSnapshot = Mock.Of(document => - document.GetGeneratedOutputAsync(It.IsAny()) == Task.FromResult(codeDocument) && - document.GetTextAsync() == Task.FromResult(codeDocument.Source.Text) && - document.Project.GetTagHelpersAsync(It.IsAny()) == new ValueTask>(tagHelpers), MockBehavior.Strict); - - var sourceText = SourceText.From(text); - - var context = new RazorCodeActionContext(request, documentSnapshot, codeDocument, location, sourceText, supportsFileCreation, supportsCodeActionResolve); - - return context; + var documentSnapshotMock = new StrictMock(); + documentSnapshotMock + .Setup(x => x.GetGeneratedOutputAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(codeDocument); + documentSnapshotMock + .Setup(x => x.GetTextAsync(It.IsAny())) + .ReturnsAsync(codeDocument.Source.Text); + documentSnapshotMock + .Setup(x => x.Project.GetTagHelpersAsync(It.IsAny())) + .ReturnsAsync(tagHelpers); + + return new RazorCodeActionContext( + request, + documentSnapshotMock.Object, + codeDocument, + location, + location, + codeDocument.Source.Text, + supportsFileCreation, + supportsCodeActionResolve); } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs index edf7e2a760d..83d1e9a8878 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndToEndTest.NetFx.cs @@ -59,7 +59,6 @@ private GenerateMethodCodeActionResolver[] CreateRazorCodeActionResolvers( new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory), razorFormattingService) ]; - #region CSharp CodeAction Tests [Fact] @@ -792,13 +791,12 @@ public async Task Handle_GenerateMethod_VaryIndentSize(bool insertSpaces, int ta """; var razorLSPOptions = new RazorLSPOptions( - EnableFormatting: true, + FormattingFlags.All, AutoClosingTags: true, insertSpaces, tabSize, AutoShowCompletion: true, AutoListParams: true, - FormatOnType: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, @@ -1149,7 +1147,7 @@ private async Task ValidateCodeActionAsync( AssertEx.EqualOrDiff(expected, actual); } - private static VSInternalCodeAction? GetCodeActionToRun(string codeAction, int childActionIndex, SumType[] result) + protected static VSInternalCodeAction? GetCodeActionToRun(string codeAction, int childActionIndex, SumType[] result) { var codeActionToRun = (VSInternalCodeAction?)result.SingleOrDefault(e => ((RazorVSInternalCodeAction)e.Value!).Name == codeAction || ((RazorVSInternalCodeAction)e.Value!).Title == codeAction).Value; if (codeActionToRun?.Children?.Length > 0) @@ -1160,7 +1158,7 @@ private async Task ValidateCodeActionAsync( return codeActionToRun; } - private async Task[]> GetCodeActionsAsync( + internal async Task[]> GetCodeActionsAsync( Uri uri, TextSpan textSpan, SourceText sourceText, @@ -1207,7 +1205,7 @@ private async Task[]> GetCodeActionsAsync( return result; } - private async Task GetEditsAsync( + internal async Task GetEditsAsync( VSInternalCodeAction codeActionToRun, RazorRequestContext requestContext, IClientConnection clientConnection, @@ -1234,7 +1232,7 @@ private async Task GetEditsAsync( return documentEdits; } - private class GenerateMethodResolverDocumentContextFactory : TestDocumentContextFactory + internal class GenerateMethodResolverDocumentContextFactory : TestDocumentContextFactory { private readonly List _tagHelperDescriptors; @@ -1263,8 +1261,7 @@ public override bool TryCreate( } var projectWorkspaceState = ProjectWorkspaceState.Create(_tagHelperDescriptors.ToImmutableArray()); - var testDocumentSnapshot = TestDocumentSnapshot.Create(FilePath, CodeDocument.Source.Text.ToString(), CodeAnalysis.VersionStamp.Default, projectWorkspaceState); - testDocumentSnapshot.With(CodeDocument); + var testDocumentSnapshot = TestDocumentSnapshot.Create(FilePath, CodeDocument, projectWorkspaceState); context = CreateDocumentContext(new Uri(FilePath), testDocumentSnapshot); return true; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndpointTest.cs index ea7425ad5c4..931b083bceb 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/CodeActionEndpointTest.cs @@ -473,7 +473,7 @@ public async Task GenerateRazorCodeActionContextAsync_WithSelectionRange() }; // Act - var razorCodeActionContext = await codeActionEndpoint.GenerateRazorCodeActionContextAsync(request, documentContext.Snapshot); + var razorCodeActionContext = await codeActionEndpoint.GenerateRazorCodeActionContextAsync(request, documentContext.Snapshot, DisposalToken); // Assert Assert.NotNull(razorCodeActionContext); @@ -501,7 +501,7 @@ public async Task GenerateRazorCodeActionContextAsync_WithoutSelectionRange() }; // Act - var razorCodeActionContext = await codeActionEndpoint.GenerateRazorCodeActionContextAsync(request, documentContext.Snapshot); + var razorCodeActionContext = await codeActionEndpoint.GenerateRazorCodeActionContextAsync(request, documentContext.Snapshot, DisposalToken); // Assert Assert.NotNull(razorCodeActionContext); @@ -526,7 +526,7 @@ public async Task GetCSharpCodeActionsFromLanguageServerAsync_InvalidRangeMappin Context = new VSInternalCodeActionContext() }; - var context = await codeActionEndpoint.GenerateRazorCodeActionContextAsync(request, documentContext.Snapshot); + var context = await codeActionEndpoint.GenerateRazorCodeActionContextAsync(request, documentContext.Snapshot, DisposalToken); Assert.NotNull(context); // Act @@ -562,7 +562,7 @@ public async Task GetCSharpCodeActionsFromLanguageServerAsync_ReturnsCodeActions } }; - var context = await codeActionEndpoint.GenerateRazorCodeActionContextAsync(request, documentContext.Snapshot); + var context = await codeActionEndpoint.GenerateRazorCodeActionContextAsync(request, documentContext.Snapshot, DisposalToken); Assert.NotNull(context); // Act diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionProviderTest.cs index e948ebdb666..b7e26d27575 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Html/DefaultHtmlCodeActionProviderTest.cs @@ -15,7 +15,6 @@ using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; using Microsoft.CodeAnalysis.Testing; -using Microsoft.CodeAnalysis.Text; using Microsoft.NET.Sdk.Razor.SourceGenerators; using Microsoft.VisualStudio.LanguageServer.Protocol; using Moq; @@ -157,15 +156,26 @@ private static RazorCodeActionContext CreateRazorCodeActionContext( }); var codeDocument = projectEngine.ProcessDesignTime(sourceDocument, FileKinds.Component, importSources: default, tagHelpers); - var documentSnapshot = Mock.Of(document => - document.GetGeneratedOutputAsync(It.IsAny()) == Task.FromResult(codeDocument) && - document.GetTextAsync() == Task.FromResult(codeDocument.Source.Text) && - document.Project.GetTagHelpersAsync(It.IsAny()) == new ValueTask>(tagHelpers), MockBehavior.Strict); + var documentSnapshotMock = new StrictMock(); + documentSnapshotMock + .Setup(x => x.GetGeneratedOutputAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(codeDocument); + documentSnapshotMock + .Setup(x => x.GetTextAsync(It.IsAny())) + .ReturnsAsync(codeDocument.Source.Text); + documentSnapshotMock + .Setup(x => x.Project.GetTagHelpersAsync(It.IsAny())) + .ReturnsAsync(tagHelpers); + + return new RazorCodeActionContext( + request, + documentSnapshotMock.Object, + codeDocument, + location, + location, + codeDocument.Source.Text, + supportsFileCreation, + supportsCodeActionResolve); - var sourceText = SourceText.From(text); - - var context = new RazorCodeActionContext(request, documentSnapshot, codeDocument, location, sourceText, supportsFileCreation, supportsCodeActionResolve); - - return context; } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ComponentAccessibilityCodeActionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ComponentAccessibilityCodeActionProviderTest.cs index 0fc53c12226..5ea3c3f6bf5 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ComponentAccessibilityCodeActionProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ComponentAccessibilityCodeActionProviderTest.cs @@ -7,12 +7,12 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; using Microsoft.CodeAnalysis.Testing; -using Microsoft.CodeAnalysis.Text; using Microsoft.NET.Sdk.Razor.SourceGenerators; using Microsoft.VisualStudio.LanguageServer.Protocol; using Moq; @@ -469,15 +469,25 @@ private static RazorCodeActionContext CreateRazorCodeActionContext(VSCodeActionP var csharpDocumentWithDiagnostic = new RazorCSharpDocument(codeDocument, csharpDocument.GeneratedCode, csharpDocument.Options, [diagnostic]); codeDocument.SetCSharpDocument(csharpDocumentWithDiagnostic); - var documentSnapshot = Mock.Of(document => - document.GetGeneratedOutputAsync(It.IsAny()) == Task.FromResult(codeDocument) && - document.GetTextAsync() == Task.FromResult(codeDocument.Source.Text) && - document.Project.GetTagHelpersAsync(It.IsAny()) == new ValueTask>(tagHelpers), MockBehavior.Strict); - - var sourceText = SourceText.From(text); - - var context = new RazorCodeActionContext(request, documentSnapshot, codeDocument, location, sourceText, supportsFileCreation, SupportsCodeActionResolve: true); - - return context; + var documentSnapshotMock = new StrictMock(); + documentSnapshotMock + .Setup(x => x.GetGeneratedOutputAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(codeDocument); + documentSnapshotMock + .Setup(x => x.GetTextAsync(It.IsAny())) + .ReturnsAsync(codeDocument.Source.Text); + documentSnapshotMock + .Setup(x => x.Project.GetTagHelpersAsync(It.IsAny())) + .ReturnsAsync(tagHelpers); + + return new RazorCodeActionContext( + request, + documentSnapshotMock.Object, + codeDocument, + location, + location, + codeDocument.Source.Text, + supportsFileCreation, + SupportsCodeActionResolve: true); } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionProviderTest.cs index 698d462b310..7918a6aea42 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionProviderTest.cs @@ -5,11 +5,13 @@ using System.Collections.Immutable; using System.Linq; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; +using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; @@ -395,14 +397,22 @@ private static RazorCodeActionContext CreateRazorCodeActionContext(VSCodeActionP })); codeDocument.SetSyntaxTree(syntaxTree); - var documentSnapshot = Mock.Of(document => - document.GetGeneratedOutputAsync(It.IsAny()) == Task.FromResult(codeDocument) && - document.GetTextAsync() == Task.FromResult(codeDocument.Source.Text), MockBehavior.Strict); - - var sourceText = SourceText.From(text); - - var context = new RazorCodeActionContext(request, documentSnapshot, codeDocument, location, sourceText, supportsFileCreation, SupportsCodeActionResolve: true); - - return context; + var documentSnapshotMock = new StrictMock(); + documentSnapshotMock + .Setup(x => x.GetGeneratedOutputAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(codeDocument); + documentSnapshotMock + .Setup(x => x.GetTextAsync(It.IsAny())) + .ReturnsAsync(codeDocument.Source.Text); + + return new RazorCodeActionContext( + request, + documentSnapshotMock.Object, + codeDocument, + location, + location, + codeDocument.Source.Text, + supportsFileCreation, + SupportsCodeActionResolve: true); } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToComponentCodeActionProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToComponentCodeActionProviderTest.cs new file mode 100644 index 00000000000..a54aa4c24f7 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToComponentCodeActionProviderTest.cs @@ -0,0 +1,498 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.AspNetCore.Razor.Language.Extensions; +using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions; +using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; +using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Moq; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.CodeActions.Razor; + +public class ExtractToComponentCodeActionProviderTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) +{ + [Fact] + public async Task Handle_InvalidFileKind() + { + // Arrange + var documentPath = "c:/Test.razor"; + var contents = """ + @page "/" + + Home + +
+
+

Div a title

+

Div [||]a par

+
+
+

Div b title

+

Div b par

+
+
+ +

Hello, world!

+ + Welcome to your new app. + """; + TestFileMarkupParser.GetSpan(contents, out contents, out var selectionSpan); + + var request = new VSCodeActionParams() + { + TextDocument = new VSTextDocumentIdentifier { Uri = new Uri(documentPath) }, + Range = VsLspFactory.DefaultRange, + Context = new VSInternalCodeActionContext() + }; + + var context = CreateRazorCodeActionContext(request, selectionSpan, documentPath, contents); + context.CodeDocument.SetFileKind(FileKinds.Legacy); + + var provider = new ExtractToComponentCodeActionProvider(); + + // Act + var commandOrCodeActionContainer = await provider.ProvideAsync(context, default); + + // Assert + Assert.Empty(commandOrCodeActionContainer); + } + + [Fact] + public Task Handle_SinglePointSelection_ReturnsNotEmpty() + => TestAsync(""" + @page "/" + + Home + +
+ {|result:<{|selection:|}div> +

Div a title

+

Div a par

+
|} +
+

Div b title

+

Div b par

+
+
+ +

Hello, world!

+ + Welcome to your new app. + """); + + [Fact] + public Task Handle_InProperMarkup_ReturnsEmpty() + => TestAsync(""" + @page "/" + + Home + +
+
+

Div a title

+

Div {|selection:|}a par

+
+
+

Div b title

+

Div b par

+
+
+ +

Hello, world!

+ + Welcome to your new app. + """); + + [Fact] + public Task Handle_IsInCodeBlock_ReturnsEmpty() + => TestAsync(""" + @page "/" + + @code + { + {|selection:public int I { get; set; } + public void M() + { + }|} + } + """); + + [Fact] + public Task Handle_MultiPointSelection_ReturnsNotEmpty() + => TestAsync(""" + @page "/" + + Home + +
+ {|result:{|selection:
+ |}

Div a title

+

Div a par

+
|} +
+

Div b title

+

Div b par

+
+
+ +

Hello, world!

+ + Welcome to your new app. + """); + + [Fact] + public Task Handle_MultiPointSelection_WithEndAfterElement() + => TestAsync(""" + @page "/" + @namespace MarketApp.Pages.Product.Home + + namespace MarketApp.Pages.Product.Home + + Home + +
+ {|result:{|selection:
+

Div a title

+

Div a par

+
+
+

Div b title

+

Div b par

+
|}|} +
+ +

Hello, world!

+ + Welcome to your new app. + """); + + [Fact] + public Task Handle_MultiPointSelection_WithEndInsideSiblingElement() + => TestAsync(""" + @page "/" + @namespace MarketApp.Pages.Product.Home + + namespace MarketApp.Pages.Product.Home + + Home + +
+ {|result:{|selection:
+

Div a title

+

Div a par

+
+
+

Div b title

|} +

Div b par

+
|} +
+ +

Hello, world!

+ + Welcome to your new app. + """); + + [Fact] + public Task Handle_MultiPointSelection_WithEndInsideElement() + => TestAsync(""" + @page "/" + @namespace MarketApp.Pages.Product.Home + + namespace MarketApp.Pages.Product.Home + + Home + +
+ {|result:{|selection:
+

Div a title

+

Div a par

|} +
|} +
+

Div b title

+

Div b par

+
+
+ +

Hello, world!

+ + Welcome to your new app. + """); + + [Fact] + public Task Handle_MultiPointSelection_WithNestedEnd() + => TestAsync(""" + @page "/" + @namespace MarketApp.Pages.Product.Home + + namespace MarketApp.Pages.Product.Home + + Home + +
+ {|result:{|selection:
+

Div a title

+

Div a par

+
+
+
+
+

Div b title|}

+

Div b par

+
+
+
|} +
+ +

Hello, world!

+ + Welcome to your new app. + """); + + [Fact] + public Task Handle_MultiPointSelection_WithNestedStart() + => TestAsync(""" + @page "/" + @namespace MarketApp.Pages.Product.Home + + namespace MarketApp.Pages.Product.Home + + Home + +
+ {|result:
+
+
+

{|selection:Div a title

+

Div a par

+
+
+
+
+
+
+

Div b title

+

Div b par

+
+
+
|}|} +
+ +

Hello, world!

+ + Welcome to your new app. + """); + + [Fact] + public Task Handle_MultiPointSelection_WithNestedStartAndEnd() + => TestAsync(""" + @page "/" + @namespace MarketApp.Pages.Product.Home + + namespace MarketApp.Pages.Product.Home + + Home + +
+ {|result:
+
+
+

{|selection:Div a title

+

Div a par

+
+
+
+
+
+
+

Div b title

+

Div b par|}

+
+
+
|} +
+ +

Hello, world!

+ + Welcome to your new app. + """); + + [Fact] + public Task Handle_MultiPointSelection_StartSelfClosing() + => TestAsync(""" + @page "/" + + Home + +
+ {|result:{|selection: +
+

Div a title

+

Div a par

+
|}|} +
+

Div b title

+

Div b par

+
+
+ +

Hello, world!

+ + Welcome to your new app. + """); + + [Fact] + public Task Handle_MultipointSelection_CodeBlock() + => TestAsync(""" + {|result:{|selection:

Hello

+ + @code { + |} + }|} + """); + + [Fact] + public Task Handle_MultipointSelection_IfBlock() + => TestAsync(""" + {|result:{|selection:

Hello

+ + @if (true) { + |} + }|} + """); + + [Fact] + public Task Handle_MultipointSelection_EmbeddedIfBlock() + => TestAsync(""" + {|result:{|selection:

Hello

+ +
+
+ @if (true) { + |} + } +
+
|} + """); + + [Fact] + public Task Handle_MultipointSelection_CSharpBlock() + => TestAsync( + """ + {|result:blah
+ + @{ + RenderFragment fragment = @|} ; + }|} + """); + + private static RazorCodeActionContext CreateRazorCodeActionContext( + VSCodeActionParams request, + TextSpan selectionSpan, + string filePath, + string text, + string? relativePath = null, + bool supportsFileCreation = true) + { + relativePath ??= filePath; + + var sourceDocument = RazorSourceDocument.Create(text, RazorSourceDocumentProperties.Create(filePath, relativePath)); + var options = RazorParserOptions.Create(o => + { + o.Directives.Add(ComponentCodeDirective.Directive); + o.Directives.Add(FunctionsDirective.Directive); + }); + var syntaxTree = RazorSyntaxTree.Parse(sourceDocument, options); + + var codeDocument = TestRazorCodeDocument.Create(sourceDocument, imports: default); + codeDocument.SetFileKind(FileKinds.Component); + codeDocument.SetCodeGenerationOptions(RazorCodeGenerationOptions.Create(o => + { + o.RootNamespace = "ExtractToComponentTest"; + })); + codeDocument.SetSyntaxTree(syntaxTree); + + var documentSnapshot = new StrictMock(); + documentSnapshot + .Setup(document => document.GetTextAsync(It.IsAny())) + .ReturnsAsync(codeDocument.Source.Text); + + var sourceText = SourceText.From(text); + + var context = new RazorCodeActionContext( + request, + documentSnapshot.Object, + codeDocument, + new SourceLocation(selectionSpan.Start, -1, -1), + new SourceLocation(selectionSpan.End, -1, -1), + sourceText, + supportsFileCreation, + SupportsCodeActionResolve: true); + + return context; + } + + /// + /// Tests the contents where the expected start/end are marked by '[|' and '$$' + /// + private async Task TestAsync(string contents) + { + // Arrange + var documentPath = "c:/Test.razor"; + TestFileMarkupParser.GetSpans(contents, out contents, out ImmutableDictionary> spans); + + var selectionSpan = spans["selection"].Single(); + var resultSpan = spans.ContainsKey("result") + ? spans["result"].Single() + : default; + + var request = new VSCodeActionParams() + { + TextDocument = new VSTextDocumentIdentifier { Uri = new Uri(documentPath) }, + Range = VsLspFactory.DefaultRange, + Context = new VSInternalCodeActionContext() + }; + + var context = CreateRazorCodeActionContext(request, selectionSpan, documentPath, contents); + + var lineSpan = context.SourceText.GetLinePositionSpan(selectionSpan); + request.Range = VsLspFactory.CreateRange(lineSpan); + + var provider = new ExtractToComponentCodeActionProvider(); + + // Act + var commandOrCodeActionContainer = await provider.ProvideAsync(context, default); + + // Assert + if (resultSpan.IsEmpty) + { + Assert.Empty(commandOrCodeActionContainer); + return; + } + + Assert.NotEmpty(commandOrCodeActionContainer); + var codeAction = Assert.Single(commandOrCodeActionContainer); + var razorCodeActionResolutionParams = ((JsonElement)codeAction.Data!).Deserialize(); + Assert.NotNull(razorCodeActionResolutionParams); + var actionParams = ((JsonElement)razorCodeActionResolutionParams.Data).Deserialize(); + Assert.NotNull(actionParams); + Assert.Equal(resultSpan.Start, actionParams.Start); + Assert.Equal(resultSpan.End, actionParams.End); + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToComponentCodeActionResolverTest.NetFx.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToComponentCodeActionResolverTest.NetFx.cs new file mode 100644 index 00000000000..ff1db5fa20a --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToComponentCodeActionResolverTest.NetFx.cs @@ -0,0 +1,392 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor; +using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; +using Microsoft.AspNetCore.Razor.LanguageServer.Formatting; +using Microsoft.AspNetCore.Razor.ProjectSystem; +using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; +using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; +using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Roslyn.Test.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions; + +public class ExtractToComponentCodeActionResolverTest(ITestOutputHelper testOutput) : CodeActionEndToEndTest(testOutput) +{ + private const string ExtractToComponentTitle = "Extract element to new component"; + + [Fact] + public async Task Handle_SingleElement() + { + var input = """ + <[||]div id="a"> +

Div a title

+ +

Div a par

+
+
+ +
+ """; + + var expectedRazorComponent = """ +
+

Div a title

+ +

Div a par

+
+ """; + + var expectedOriginalDocument = """ + +
+ +
+ """; + + await TestAsync( + input, + expectedOriginalDocument, + expectedRazorComponent); + } + + [Fact] + public async Task Handle_SiblingElement() + { + var input = """ + <[|div id="a"> +

Div a title

+ +

Div a par

+
+
+ + + """; + + var expectedRazorComponent = """ +
+

Div a title

+ +

Div a par

+
+
+ +
+ """; + + var expectedOriginalDocument = """ + + """; + + await TestAsync( + input, + expectedOriginalDocument, + expectedRazorComponent); + } + + [Fact] + public async Task Handle_AddsUsings() + { + var input = """ + @using MyApp.Data + @using MyApp.Models + + <[|div id="parent"> +
+
+
+

Deeply nested par +

+
+
+
+ """; + + var expectedRazorComponent = """ + @using MyApp.Data + @using MyApp.Models + +
+
+
+
+

Deeply nested par

+
+
+
+
+ """; + + var expectedOriginalDocument = """ + @using MyApp.Data + @using MyApp.Models + + + """; + + await TestAsync( + input, + expectedOriginalDocument, + expectedRazorComponent); + } + + [Fact] + public async Task Handle_NestedNodes() + { + var input = """ + @page "/" + @namespace MarketApp.Pages.Product.Home + + namespace MarketApp.Pages.Product.Home + + Home + +
+
+
+
+

[|Div a title

+

Div a par

+
+
+
+
+
+
+

Div b title

+

Div b par|]

+
+
+
+
+ +

Hello, world!

+ + Welcome to your new app. + """; + + var expectedRazorComponent = """ +
+
+
+

Div a title

+

Div a par

+
+
+
+
+
+
+

Div b title

+

Div b par

+
+
+
+ """; + + var expectedOriginalDocument = """ + @page "/" + @namespace MarketApp.Pages.Product.Home + + namespace MarketApp.Pages.Product.Home + + Home + +
+ +
+ +

Hello, world!

+ + Welcome to your new app. + """; + + await TestAsync( + input, + expectedOriginalDocument, + expectedRazorComponent); + } + + [Fact] + public async Task Handle_StartNodeContainsEndNode() + { + var input = """ + <[|div id="parent"> +
+
+
+

Deeply nested par +

+
+
+
+ """; + + var expectedRazorComponent = """ +
+
+
+
+

Deeply nested par

+
+
+
+
+ """; + + var expectedOriginalDocument = """ + + """; + + await TestAsync( + input, + expectedOriginalDocument, + expectedRazorComponent); + } + + private async Task TestAsync( + string input, + string expectedOriginalDocument, + string? expectedNewComponent) + { + TestFileMarkupParser.GetSpan(input, out input, out var textSpan); + + var razorFilePath = "C:/path/to/test.razor"; + var componentFilePath = "C:/path/to/Component.razor"; + var codeDocument = CreateCodeDocument(input, filePath: razorFilePath); + var sourceText = codeDocument.Source.Text; + var uri = new Uri(razorFilePath); + var languageServer = await CreateLanguageServerAsync(codeDocument, razorFilePath); + var documentContext = CreateDocumentContext(uri, codeDocument); + var requestContext = new RazorRequestContext(documentContext, null!, "lsp/method", uri: null); + + var result = await GetCodeActionsAsync( + uri, + textSpan, + sourceText, + requestContext, + languageServer, + razorProviders: [new ExtractToComponentCodeActionProvider()], + null); + + Assert.NotEmpty(result); + var codeActionToRun = GetCodeActionToRun(ExtractToComponentTitle, 0, result); + + if (expectedNewComponent is null) + { + Assert.Null(codeActionToRun); + return; + } + + Assert.NotNull(codeActionToRun); + + var resolver = new ExtractToComponentCodeActionResolver( + new GenerateMethodResolverDocumentContextFactory(razorFilePath, codeDocument), + TestLanguageServerFeatureOptions.Instance); + + var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory, codeDocument, null); + var changes = await GetEditsAsync( + codeActionToRun, + requestContext, + languageServer, + [resolver] + ); + + var edits = changes.Where(change => change.TextDocument.Uri.AbsolutePath == componentFilePath).Single(); + var actual = edits.Edits.Select(edit => edit.NewText).Single(); + + AssertEx.EqualOrDiff(expectedNewComponent, actual); + + var originalDocumentEdits = changes + .Where(change => change.TextDocument.Uri.AbsolutePath == razorFilePath) + .SelectMany(change => change.Edits.Select(sourceText.GetTextChange)); + var documentText = sourceText.WithChanges(originalDocumentEdits).ToString(); + AssertEx.EqualOrDiff(expectedOriginalDocument, documentText); + } + + private class ExtractToComponentResolverDocumentContextFactory : TestDocumentContextFactory + { + private readonly List _tagHelperDescriptors; + + public ExtractToComponentResolverDocumentContextFactory + (string filePath, + RazorCodeDocument codeDocument, + TagHelperDescriptor[]? tagHelpers = null) + : base(filePath, codeDocument) + { + _tagHelperDescriptors = CreateTagHelperDescriptors(); + if (tagHelpers is not null) + { + _tagHelperDescriptors.AddRange(tagHelpers); + } + } + + public override bool TryCreate( + Uri documentUri, + VSProjectContext? projectContext, + [NotNullWhen(true)] out DocumentContext? context) + { + if (FilePath is null || CodeDocument is null) + { + context = null; + return false; + } + + var projectWorkspaceState = ProjectWorkspaceState.Create(_tagHelperDescriptors.ToImmutableArray()); + var testDocumentSnapshot = TestDocumentSnapshot.Create(FilePath, CodeDocument, projectWorkspaceState); + + context = CreateDocumentContext(new Uri(FilePath), testDocumentSnapshot); + return true; + } + + private static List CreateTagHelperDescriptors() + { + return BuildTagHelpers().ToList(); + + static IEnumerable BuildTagHelpers() + { + var builder = TagHelperDescriptorBuilder.Create("oncontextmenu", "Microsoft.AspNetCore.Components"); + builder.SetMetadata( + new KeyValuePair(ComponentMetadata.EventHandler.EventArgsType, "Microsoft.AspNetCore.Components.Web.MouseEventArgs"), + new KeyValuePair(ComponentMetadata.SpecialKindKey, ComponentMetadata.EventHandler.TagHelperKind)); + yield return builder.Build(); + + builder = TagHelperDescriptorBuilder.Create("onclick", "Microsoft.AspNetCore.Components"); + builder.SetMetadata( + new KeyValuePair(ComponentMetadata.EventHandler.EventArgsType, "Microsoft.AspNetCore.Components.Web.MouseEventArgs"), + new KeyValuePair(ComponentMetadata.SpecialKindKey, ComponentMetadata.EventHandler.TagHelperKind)); + + yield return builder.Build(); + + builder = TagHelperDescriptorBuilder.Create("oncopy", "Microsoft.AspNetCore.Components"); + builder.SetMetadata( + new KeyValuePair(ComponentMetadata.EventHandler.EventArgsType, "Microsoft.AspNetCore.Components.Web.ClipboardEventArgs"), + new KeyValuePair(ComponentMetadata.SpecialKindKey, ComponentMetadata.EventHandler.TagHelperKind)); + + yield return builder.Build(); + + builder = TagHelperDescriptorBuilder.Create("ref", "Microsoft.AspNetCore.Components"); + builder.SetMetadata( + new KeyValuePair(ComponentMetadata.SpecialKindKey, ComponentMetadata.Ref.TagHelperKind), + new KeyValuePair(ComponentMetadata.Common.DirectiveAttribute, bool.TrueString)); + + yield return builder.Build(); + } + } + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeDocumentReferenceHolderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeDocumentReferenceHolderTest.cs index 227ec56b1f4..7662dfbf0f4 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeDocumentReferenceHolderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeDocumentReferenceHolderTest.cs @@ -19,9 +19,9 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer; public class CodeDocumentReferenceHolderTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) { private static readonly HostProject s_hostProject = new( - projectFilePath: "C:/path/to/project.csproj", + filePath: "C:/path/to/project.csproj", intermediateOutputPath: "C:/path/to/obj", - razorConfiguration: RazorConfiguration.Default, + configuration: RazorConfiguration.Default, rootNamespace: "TestNamespace"); private static readonly HostDocument s_hostDocument = new("C:/path/to/file.razor", "file.razor"); @@ -44,7 +44,7 @@ public async Task DocumentProcessed_ReferencesGeneratedCodeDocument() { // Arrange var documentSnapshot = await CreateDocumentSnapshotAsync(); - var codeDocumentReference = await ProcessDocumentAndRetrieveOutputAsync(documentSnapshot); + var codeDocumentReference = await ProcessDocumentAndRetrieveOutputAsync(documentSnapshot, DisposalToken); // Act PerformFullGC(); @@ -70,8 +70,8 @@ public async Task UnrelatedDocumentChanged_ReferencesGeneratedCodeDocument() Assert.NotNull(unrelatedDocumentSnapshot); - var mainCodeDocumentReference = await ProcessDocumentAndRetrieveOutputAsync(documentSnapshot); - var unrelatedCodeDocumentReference = await ProcessDocumentAndRetrieveOutputAsync(unrelatedDocumentSnapshot); + var mainCodeDocumentReference = await ProcessDocumentAndRetrieveOutputAsync(documentSnapshot, DisposalToken); + var unrelatedCodeDocumentReference = await ProcessDocumentAndRetrieveOutputAsync(unrelatedDocumentSnapshot, DisposalToken); // Act await _projectManager.UpdateAsync(updater => @@ -91,7 +91,7 @@ public async Task DocumentChanged_DereferencesGeneratedCodeDocument() { // Arrange var documentSnapshot = await CreateDocumentSnapshotAsync(); - var codeDocumentReference = await ProcessDocumentAndRetrieveOutputAsync(documentSnapshot); + var codeDocumentReference = await ProcessDocumentAndRetrieveOutputAsync(documentSnapshot, DisposalToken); // Act @@ -111,7 +111,7 @@ public async Task DocumentRemoved_DereferencesGeneratedCodeDocument() { // Arrange var documentSnapshot = await CreateDocumentSnapshotAsync(); - var codeDocumentReference = await ProcessDocumentAndRetrieveOutputAsync(documentSnapshot); + var codeDocumentReference = await ProcessDocumentAndRetrieveOutputAsync(documentSnapshot, DisposalToken); // Act await _projectManager.UpdateAsync(updater => @@ -130,12 +130,12 @@ public async Task ProjectChanged_DereferencesGeneratedCodeDocument() { // Arrange var documentSnapshot = await CreateDocumentSnapshotAsync(); - var codeDocumentReference = await ProcessDocumentAndRetrieveOutputAsync(documentSnapshot); + var codeDocumentReference = await ProcessDocumentAndRetrieveOutputAsync(documentSnapshot, DisposalToken); // Act await _projectManager.UpdateAsync(updater => { - updater.ProjectConfigurationChanged(new HostProject(s_hostProject.FilePath, s_hostProject.IntermediateOutputPath, RazorConfiguration.Default, rootNamespace: "NewRootNamespace")); + updater.ProjectConfigurationChanged(s_hostProject with { Configuration = RazorConfiguration.Default, RootNamespace = "NewRootNamespace" }); }); PerformFullGC(); @@ -149,7 +149,7 @@ public async Task ProjectRemoved_DereferencesGeneratedCodeDocument() { // Arrange var documentSnapshot = await CreateDocumentSnapshotAsync(); - var codeDocumentReference = await ProcessDocumentAndRetrieveOutputAsync(documentSnapshot); + var codeDocumentReference = await ProcessDocumentAndRetrieveOutputAsync(documentSnapshot, DisposalToken); // Act await _projectManager.UpdateAsync(updater => @@ -176,9 +176,9 @@ private Task CreateDocumentSnapshotAsync() } [MethodImpl(MethodImplOptions.NoInlining)] - private async Task> ProcessDocumentAndRetrieveOutputAsync(IDocumentSnapshot documentSnapshot) + private async Task> ProcessDocumentAndRetrieveOutputAsync(IDocumentSnapshot documentSnapshot, CancellationToken cancellationToken) { - var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(); + var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(cancellationToken); _referenceHolder.DocumentProcessed(codeDocument, documentSnapshot); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/CompletionListProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/CompletionListProviderTest.cs index e793aa35659..c44f32afc7a 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/CompletionListProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/CompletionListProviderTest.cs @@ -38,7 +38,7 @@ public CompletionListProviderTest(ITestOutputHelper testOutput) _razorCompletionProvider = new TestRazorCompletionListProvider(_completionList1, new[] { SharedTriggerCharacter, }, LoggerFactory); _delegatedCompletionProvider = new TestDelegatedCompletionListProvider(_completionList2, new[] { SharedTriggerCharacter, CompletionList2OnlyTriggerCharacter }); _completionContext = new VSInternalCompletionContext(); - _documentContext = TestDocumentContext.From("C:/path/to/file.cshtml"); + _documentContext = TestDocumentContext.Create("C:/path/to/file.cshtml"); _clientCapabilities = new VSInternalClientCapabilities(); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/DefaultVSLSPTagHelperTooltipFactoryTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/DefaultVSLSPTagHelperTooltipFactoryTest.cs deleted file mode 100644 index 30e3a398773..00000000000 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/DefaultVSLSPTagHelperTooltipFactoryTest.cs +++ /dev/null @@ -1,689 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -#nullable disable - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.CodeAnalysis.Razor.Tooltip; -using Microsoft.VisualStudio.Text.Adornments; -using Xunit; -using Xunit.Abstractions; -using static Microsoft.AspNetCore.Razor.LanguageServer.Tooltip.DefaultVSLSPTagHelperTooltipFactory; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.Tooltip; - -public class DefaultVSLSPTagHelperTooltipFactoryTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) -{ - [Fact] - public void CleanAndClassifySummaryContent_ClassifiedTextElement_ReplacesSeeCrefs() - { - // Arrange - var runs = new List(); - var summary = "Accepts s"; - - // Act - CleanAndClassifySummaryContent(runs, summary); - - // Assert - - // Expected output: - // Accepts Lists - Assert.Collection(runs, - run => AssertExpectedClassification(run, "Accepts ", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, "List", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, "<", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "string", VSPredefinedClassificationTypeNames.Keyword), - run => AssertExpectedClassification(run, ">", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "s", VSPredefinedClassificationTypeNames.Text)); - } - - [Fact] - public void CleanSummaryContent_ClassifiedTextElement_ReplacesSeeAlsoCrefs() - { - // Arrange - var runs = new List(); - var summary = "Accepts s"; - - // Act - CleanAndClassifySummaryContent(runs, summary); - - // Assert - - // Expected output: - // Accepts Lists - Assert.Collection(runs, - run => AssertExpectedClassification(run, "Accepts ", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, "List", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, "<", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "string", VSPredefinedClassificationTypeNames.Keyword), - run => AssertExpectedClassification(run, ">", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "s", VSPredefinedClassificationTypeNames.Text)); - } - - [Fact] - public void CleanSummaryContent_ClassifiedTextElement_TrimsSurroundingWhitespace() - { - // Arrange - var runs = new List(); - var summary = @" - Hello - - World - -"; - - // Act - CleanAndClassifySummaryContent(runs, summary); - - // Assert - - // Expected output: - // Hello - // - // World - Assert.Collection(runs, run => AssertExpectedClassification( - run, """ - Hello - - World - """, VSPredefinedClassificationTypeNames.Text)); - } - - [Fact] - public void CleanSummaryContent_ClassifiedTextElement_ClassifiesCodeBlocks() - { - // Arrange - var runs = new List(); - var summary = @"code: This is code and This is some other code."; - - // Act - CleanAndClassifySummaryContent(runs, summary); - - // Assert - - // Expected output: - // code: This is code and This is some other code. - Assert.Collection(runs, - run => AssertExpectedClassification(run, "code: ", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, "This is code", VSPredefinedClassificationTypeNames.Text, ClassifiedTextRunStyle.UseClassificationFont), - run => AssertExpectedClassification(run, " and ", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, "This is some other code", VSPredefinedClassificationTypeNames.Text, ClassifiedTextRunStyle.UseClassificationFont), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Text)); - } - - [Fact] - public void CleanSummaryContent_ClassifiedTextElement_ClassifiesCBlocks() - { - // Arrange - var runs = new List(); - var summary = @"code: This is code and This is some other code."; - - // Act - CleanAndClassifySummaryContent(runs, summary); - - // Assert - - // Expected output: - // code: This is code and This is some other code. - Assert.Collection(runs, - run => AssertExpectedClassification(run, "code: ", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, "This is code", VSPredefinedClassificationTypeNames.Text, ClassifiedTextRunStyle.UseClassificationFont), - run => AssertExpectedClassification(run, " and ", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, "This is some other code", VSPredefinedClassificationTypeNames.Text, ClassifiedTextRunStyle.UseClassificationFont), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Text)); - } - - [Fact] - public void CleanSummaryContent_ClassifiedTextElement_ParasCreateNewLines() - { - // Arrange - var runs = new List(); - var summary = @"Summary description: -Paragraph text. -End summary description."; - - // Act - CleanAndClassifySummaryContent(runs, summary); - - // Assert - - // Expected output: - // code: This is code and This is some other code. - Assert.Collection(runs, run => AssertExpectedClassification( - run, """ - Summary description: - - Paragraph text. - - End summary description. - """, - VSPredefinedClassificationTypeNames.Text)); - } - - [Fact] - public async Task TryCreateTooltip_ClassifiedTextElement_NoAssociatedTagHelperDescriptions_ReturnsFalse() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultVSLSPTagHelperTooltipFactory(projectManager); - var elementDescription = AggregateBoundElementDescription.Empty; - - // Act - var classifiedTextElement = await descriptionFactory.TryCreateTooltipAsync("file.razor", elementDescription, CancellationToken.None); - - // Assert - Assert.Null(classifiedTextElement); - } - - [Fact] - public async Task TryCreateTooltip_ClassifiedTextElement_Element_SingleAssociatedTagHelper_ReturnsTrue_NestedTypes() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultVSLSPTagHelperTooltipFactory(projectManager); - var associatedTagHelperInfos = new[] - { - new BoundElementDescriptionInfo( - "Microsoft.AspNetCore.SomeTagHelper", - "Uses s"), - }; - var elementDescription = new AggregateBoundElementDescription(associatedTagHelperInfos.ToImmutableArray()); - - // Act - var classifiedTextElement = await descriptionFactory.TryCreateTooltipAsync("file.razor", elementDescription, CancellationToken.None); - - // Assert - Assert.NotNull(classifiedTextElement); - - // Expected output: - // Microsoft.AspNetCore.SomeTagHelper - // Uses List>s - Assert.Collection(classifiedTextElement.Runs, - run => AssertExpectedClassification(run, "Microsoft", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "AspNetCore", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "SomeTagHelper", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, Environment.NewLine, VSPredefinedClassificationTypeNames.WhiteSpace), - run => AssertExpectedClassification(run, "Uses ", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, "List", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, "<", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "List", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, "<", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "string", VSPredefinedClassificationTypeNames.Keyword), - run => AssertExpectedClassification(run, ">", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, ">", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "s", VSPredefinedClassificationTypeNames.Text)); - } - - [Fact] - public async Task TryCreateTooltip_ClassifiedTextElement_Element_NamespaceContainsTypeName_ReturnsTrue() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultVSLSPTagHelperTooltipFactory(projectManager); - var associatedTagHelperInfos = new[] - { - new BoundElementDescriptionInfo( - "Microsoft.AspNetCore.SomeTagHelper.SomeTagHelper", - "Uses s"), - }; - var elementDescription = new AggregateBoundElementDescription(associatedTagHelperInfos.ToImmutableArray()); - - // Act - var classifiedTextElement = await descriptionFactory.TryCreateTooltipAsync("file.razor", elementDescription, CancellationToken.None); - - // Assert - Assert.NotNull(classifiedTextElement); - - // Expected output: - // Microsoft.AspNetCore.SomeTagHelper.SomeTagHelper - // Uses Cs - Assert.Collection(classifiedTextElement.Runs, - run => AssertExpectedClassification(run, "Microsoft", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "AspNetCore", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "SomeTagHelper", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "SomeTagHelper", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, Environment.NewLine, VSPredefinedClassificationTypeNames.WhiteSpace), - run => AssertExpectedClassification(run, "Uses ", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, "C", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, "<", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "B", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, ">", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "s", VSPredefinedClassificationTypeNames.Text)); - } - - [Fact] - public async Task TryCreateTooltip_ClassifiedTextElement_Element_MultipleAssociatedTagHelpers_ReturnsTrue() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultVSLSPTagHelperTooltipFactory(projectManager); - var associatedTagHelperInfos = new[] - { - new BoundElementDescriptionInfo("Microsoft.AspNetCore.SomeTagHelper", "\nUses s\n"), - new BoundElementDescriptionInfo("Microsoft.AspNetCore.OtherTagHelper", "\nAlso uses s\n\r\n\r\r"), - }; - var elementDescription = new AggregateBoundElementDescription(associatedTagHelperInfos.ToImmutableArray()); - - // Act - var classifiedTextElement = await descriptionFactory.TryCreateTooltipAsync("file.razor", elementDescription, CancellationToken.None); - - // Assert - Assert.NotNull(classifiedTextElement); - - // Expected output: - // Microsoft.AspNetCore.SomeTagHelper - // Uses Lists - // - // Microsoft.AspNetCore.OtherTagHelper - // Also uses Lists - Assert.Collection(classifiedTextElement.Runs, - run => AssertExpectedClassification(run, "Microsoft", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "AspNetCore", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "SomeTagHelper", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, Environment.NewLine, VSPredefinedClassificationTypeNames.WhiteSpace), - run => AssertExpectedClassification(run, "Uses ", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, "List", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, "<", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "string", VSPredefinedClassificationTypeNames.Keyword), - run => AssertExpectedClassification(run, ">", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "s", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, Environment.NewLine, VSPredefinedClassificationTypeNames.WhiteSpace), - run => AssertExpectedClassification(run, Environment.NewLine, VSPredefinedClassificationTypeNames.WhiteSpace), - run => AssertExpectedClassification(run, "Microsoft", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "AspNetCore", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "OtherTagHelper", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, Environment.NewLine, VSPredefinedClassificationTypeNames.WhiteSpace), - run => AssertExpectedClassification(run, "Also uses ", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, "List", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, "<", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "string", VSPredefinedClassificationTypeNames.Keyword), - run => AssertExpectedClassification(run, ">", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "s", VSPredefinedClassificationTypeNames.Text)); - } - - [Fact] - public void TryCreateTooltip_ClassifiedTextElement_NoAssociatedAttributeDescriptions_ReturnsFalse() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultVSLSPTagHelperTooltipFactory(projectManager); - var elementDescription = AggregateBoundAttributeDescription.Empty; - - // Act - var result = descriptionFactory.TryCreateTooltip(elementDescription, out ClassifiedTextElement classifiedTextElement); - - // Assert - Assert.False(result); - Assert.Null(classifiedTextElement); - } - - [Fact] - public void TryCreateTooltip_ClassifiedTextElement_Attribute_SingleAssociatedAttribute_ReturnsTrue_NestedTypes() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultVSLSPTagHelperTooltipFactory(projectManager); - var associatedAttributeDescriptions = new[] - { - new BoundAttributeDescriptionInfo( - ReturnTypeName: "System.String", - TypeName: "Microsoft.AspNetCore.SomeTagHelpers.SomeTypeName", - PropertyName: "SomeProperty", - Documentation: "Uses s") - }; - var attributeDescription = new AggregateBoundAttributeDescription(associatedAttributeDescriptions.ToImmutableArray()); - - // Act - var result = descriptionFactory.TryCreateTooltip(attributeDescription, out ClassifiedTextElement classifiedTextElement); - - // Assert - Assert.True(result); - - // Expected output: - // string Microsoft.AspNetCore.SomeTagHelpers.SomeTypeName.SomeProperty - // Uses List>s - Assert.Collection(classifiedTextElement.Runs, - run => AssertExpectedClassification(run, "string", VSPredefinedClassificationTypeNames.Keyword), - run => AssertExpectedClassification(run, " ", VSPredefinedClassificationTypeNames.WhiteSpace), - run => AssertExpectedClassification(run, "Microsoft", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "AspNetCore", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "SomeTagHelpers", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "SomeTypeName", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "SomeProperty", VSPredefinedClassificationTypeNames.Identifier), - run => AssertExpectedClassification(run, Environment.NewLine, VSPredefinedClassificationTypeNames.WhiteSpace), - run => AssertExpectedClassification(run, "Uses ", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, "List", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, "<", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "List", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, "<", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "string", VSPredefinedClassificationTypeNames.Keyword), - run => AssertExpectedClassification(run, ">", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, ">", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "s", VSPredefinedClassificationTypeNames.Text)); - } - - [Fact] - public void TryCreateTooltip_ClassifiedTextElement_Attribute_MultipleAssociatedAttributes_ReturnsTrue() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultVSLSPTagHelperTooltipFactory(projectManager); - var associatedAttributeDescriptions = new[] - { - new BoundAttributeDescriptionInfo( - ReturnTypeName: "System.String", - TypeName: "Microsoft.AspNetCore.SomeTagHelpers.SomeTypeName", - PropertyName: "SomeProperty", - Documentation: "Uses s"), - new BoundAttributeDescriptionInfo( - PropertyName: "AnotherProperty", - TypeName: "Microsoft.AspNetCore.SomeTagHelpers.AnotherTypeName", - ReturnTypeName: "System.Boolean?", - Documentation: "\nUses s\n"), - }; - var attributeDescription = new AggregateBoundAttributeDescription(associatedAttributeDescriptions.ToImmutableArray()); - - // Act - var result = descriptionFactory.TryCreateTooltip(attributeDescription, out ClassifiedTextElement classifiedTextElement); - - // Assert - Assert.True(result); - - // Expected output: - // string Microsoft.AspNetCore.SomeTagHelpers.SomeTypeName.SomeProperty - // Uses Lists - // - // bool? Microsoft.AspNetCore.SomeTagHelpers.AnotherTypeName.AnotherProperty - // Uses Lists - Assert.Collection(classifiedTextElement.Runs, - run => AssertExpectedClassification(run, "string", VSPredefinedClassificationTypeNames.Keyword), - run => AssertExpectedClassification(run, " ", VSPredefinedClassificationTypeNames.WhiteSpace), - run => AssertExpectedClassification(run, "Microsoft", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "AspNetCore", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "SomeTagHelpers", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "SomeTypeName", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "SomeProperty", VSPredefinedClassificationTypeNames.Identifier), - run => AssertExpectedClassification(run, Environment.NewLine, VSPredefinedClassificationTypeNames.WhiteSpace), - run => AssertExpectedClassification(run, "Uses ", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, "List", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, "<", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "string", VSPredefinedClassificationTypeNames.Keyword), - run => AssertExpectedClassification(run, ">", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "s", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, Environment.NewLine, VSPredefinedClassificationTypeNames.WhiteSpace), - run => AssertExpectedClassification(run, Environment.NewLine, VSPredefinedClassificationTypeNames.WhiteSpace), - run => AssertExpectedClassification(run, "bool", VSPredefinedClassificationTypeNames.Keyword), - run => AssertExpectedClassification(run, "?", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, " ", VSPredefinedClassificationTypeNames.WhiteSpace), - run => AssertExpectedClassification(run, "Microsoft", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "AspNetCore", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "SomeTagHelpers", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "AnotherTypeName", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "AnotherProperty", VSPredefinedClassificationTypeNames.Identifier), - run => AssertExpectedClassification(run, Environment.NewLine, VSPredefinedClassificationTypeNames.WhiteSpace), - run => AssertExpectedClassification(run, "Uses ", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, "List", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, "<", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "string", VSPredefinedClassificationTypeNames.Keyword), - run => AssertExpectedClassification(run, ">", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "s", VSPredefinedClassificationTypeNames.Text)); - } - - [Fact] - public async Task TryCreateTooltip_ContainerElement_NoAssociatedTagHelperDescriptions_ReturnsFalse() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultVSLSPTagHelperTooltipFactory(projectManager); - var elementDescription = AggregateBoundElementDescription.Empty; - - // Act - var containerElement = await descriptionFactory.TryCreateTooltipContainerAsync("file.razor", elementDescription, CancellationToken.None); - - // Assert - Assert.Null(containerElement); - } - - [Fact] - public async Task TryCreateTooltip_ContainerElement_Attribute_MultipleAssociatedTagHelpers_ReturnsTrue() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultVSLSPTagHelperTooltipFactory(projectManager); - var associatedTagHelperInfos = new[] - { - new BoundElementDescriptionInfo("Microsoft.AspNetCore.SomeTagHelper", "\nUses s\n"), - new BoundElementDescriptionInfo("Microsoft.AspNetCore.OtherTagHelper", "\nAlso uses s\n\r\n\r\r"), - }; - var elementDescription = new AggregateBoundElementDescription(associatedTagHelperInfos.ToImmutableArray()); - - // Act - var container = await descriptionFactory.TryCreateTooltipContainerAsync("file.razor", elementDescription, CancellationToken.None); - - // Assert - Assert.NotNull(container); - var containerElements = container.Elements.ToList(); - - // Expected output: - // [Class Glyph] Microsoft.AspNetCore.SomeTagHelper - // Uses Lists - // - // [Class Glyph] Microsoft.AspNetCore.OtherTagHelper - // Also uses Lists - Assert.Equal(ContainerElementStyle.Stacked, container.Style); - Assert.Equal(5, containerElements.Count); - - // [Class Glyph] Microsoft.AspNetCore.SomeTagHelper - var innerContainer = ((ContainerElement)containerElements[0]).Elements.ToList(); - var classifiedTextElement = (ClassifiedTextElement)innerContainer[1]; - Assert.Equal(2, innerContainer.Count); - Assert.Equal(ClassGlyph, innerContainer[0]); - Assert.Collection(classifiedTextElement.Runs, - run => AssertExpectedClassification(run, "Microsoft", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "AspNetCore", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "SomeTagHelper", VSPredefinedClassificationTypeNames.Type)); - - // Uses Lists - innerContainer = ((ContainerElement)containerElements[1]).Elements.ToList(); - classifiedTextElement = (ClassifiedTextElement)innerContainer[0]; - Assert.Single(innerContainer); - Assert.Collection(classifiedTextElement.Runs, - run => AssertExpectedClassification(run, "Uses ", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, "List", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, "<", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "string", VSPredefinedClassificationTypeNames.Keyword), - run => AssertExpectedClassification(run, ">", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "s", VSPredefinedClassificationTypeNames.Text)); - - // new line - innerContainer = ((ContainerElement)containerElements[2]).Elements.ToList(); - classifiedTextElement = (ClassifiedTextElement)innerContainer[0]; - Assert.Single(innerContainer); - Assert.Empty(classifiedTextElement.Runs); - - // [Class Glyph] Microsoft.AspNetCore.OtherTagHelper - innerContainer = ((ContainerElement)containerElements[3]).Elements.ToList(); - classifiedTextElement = (ClassifiedTextElement)innerContainer[1]; - Assert.Equal(2, innerContainer.Count); - Assert.Equal(ClassGlyph, innerContainer[0]); - Assert.Collection(classifiedTextElement.Runs, - run => AssertExpectedClassification(run, "Microsoft", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "AspNetCore", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "OtherTagHelper", VSPredefinedClassificationTypeNames.Type)); - - // Also uses Lists - innerContainer = ((ContainerElement)containerElements[4]).Elements.ToList(); - classifiedTextElement = (ClassifiedTextElement)innerContainer[0]; - Assert.Single(innerContainer); - Assert.Collection(classifiedTextElement.Runs, - run => AssertExpectedClassification(run, "Also uses ", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, "List", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, "<", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "string", VSPredefinedClassificationTypeNames.Keyword), - run => AssertExpectedClassification(run, ">", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "s", VSPredefinedClassificationTypeNames.Text)); - } - - [Fact] - public void TryCreateTooltip_ContainerElement_NoAssociatedAttributeDescriptions_ReturnsFalse() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultVSLSPTagHelperTooltipFactory(projectManager); - var elementDescription = AggregateBoundAttributeDescription.Empty; - - // Act - var result = descriptionFactory.TryCreateTooltip(elementDescription, out ContainerElement containerElement); - - // Assert - Assert.False(result); - Assert.Null(containerElement); - } - - [Fact] - public void TryCreateTooltip_ContainerElement_Attribute_MultipleAssociatedAttributes_ReturnsTrue() - { - // Arrange - var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultVSLSPTagHelperTooltipFactory(projectManager); - var associatedAttributeDescriptions = new[] - { - new BoundAttributeDescriptionInfo( - ReturnTypeName: "System.String", - TypeName: "Microsoft.AspNetCore.SomeTagHelpers.SomeTypeName", - PropertyName: "SomeProperty", - Documentation: "Uses s"), - new BoundAttributeDescriptionInfo( - PropertyName: "AnotherProperty", - TypeName: "Microsoft.AspNetCore.SomeTagHelpers.AnotherTypeName", - ReturnTypeName: "System.Boolean?", - Documentation: "\nUses s\n"), - }; - var attributeDescription = new AggregateBoundAttributeDescription(associatedAttributeDescriptions.ToImmutableArray()); - - // Act - var result = descriptionFactory.TryCreateTooltip(attributeDescription, out ContainerElement container); - - // Assert - Assert.True(result); - var containerElements = container.Elements.ToList(); - - // Expected output: - // [Property Glyph] string Microsoft.AspNetCore.SomeTagHelpers.SomeTypeName.SomeProperty - // Uses Lists - // - // [Property Glyph] bool? Microsoft.AspNetCore.SomeTagHelpers.AnotherTypeName.AnotherProperty - // Uses Lists - Assert.Equal(ContainerElementStyle.Stacked, container.Style); - Assert.Equal(5, containerElements.Count); - - // [TagHelper Glyph] string Microsoft.AspNetCore.SomeTagHelpers.SomeTypeName.SomeProperty - var innerContainer = ((ContainerElement)containerElements[0]).Elements.ToList(); - var classifiedTextElement = (ClassifiedTextElement)innerContainer[1]; - Assert.Equal(2, innerContainer.Count); - Assert.Equal(PropertyGlyph, innerContainer[0]); - Assert.Collection(classifiedTextElement.Runs, - run => AssertExpectedClassification(run, "string", VSPredefinedClassificationTypeNames.Keyword), - run => AssertExpectedClassification(run, " ", VSPredefinedClassificationTypeNames.WhiteSpace), - run => AssertExpectedClassification(run, "Microsoft", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "AspNetCore", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "SomeTagHelpers", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "SomeTypeName", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "SomeProperty", VSPredefinedClassificationTypeNames.Identifier)); - - // Uses Lists - innerContainer = ((ContainerElement)containerElements[1]).Elements.ToList(); - classifiedTextElement = (ClassifiedTextElement)innerContainer[0]; - Assert.Single(innerContainer); - Assert.Collection(classifiedTextElement.Runs, - run => AssertExpectedClassification(run, "Uses ", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, "List", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, "<", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "string", VSPredefinedClassificationTypeNames.Keyword), - run => AssertExpectedClassification(run, ">", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "s", VSPredefinedClassificationTypeNames.Text)); - - // new line - innerContainer = ((ContainerElement)containerElements[2]).Elements.ToList(); - classifiedTextElement = (ClassifiedTextElement)innerContainer[0]; - Assert.Single(innerContainer); - Assert.Empty(classifiedTextElement.Runs); - - // [TagHelper Glyph] bool? Microsoft.AspNetCore.SomeTagHelpers.AnotherTypeName.AnotherProperty - innerContainer = ((ContainerElement)containerElements[3]).Elements.ToList(); - classifiedTextElement = (ClassifiedTextElement)innerContainer[1]; - Assert.Equal(2, innerContainer.Count); - Assert.Equal(PropertyGlyph, innerContainer[0]); - Assert.Collection(classifiedTextElement.Runs, - run => AssertExpectedClassification(run, "bool", VSPredefinedClassificationTypeNames.Keyword), - run => AssertExpectedClassification(run, "?", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, " ", VSPredefinedClassificationTypeNames.WhiteSpace), - run => AssertExpectedClassification(run, "Microsoft", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "AspNetCore", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "SomeTagHelpers", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "AnotherTypeName", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "AnotherProperty", VSPredefinedClassificationTypeNames.Identifier)); - - // Uses Lists - innerContainer = ((ContainerElement)containerElements[4]).Elements.ToList(); - classifiedTextElement = (ClassifiedTextElement)innerContainer[0]; - Assert.Single(innerContainer); - Assert.Collection(classifiedTextElement.Runs, - run => AssertExpectedClassification(run, "Uses ", VSPredefinedClassificationTypeNames.Text), - run => AssertExpectedClassification(run, "List", VSPredefinedClassificationTypeNames.Type), - run => AssertExpectedClassification(run, "<", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "string", VSPredefinedClassificationTypeNames.Keyword), - run => AssertExpectedClassification(run, ">", VSPredefinedClassificationTypeNames.Punctuation), - run => AssertExpectedClassification(run, "s", VSPredefinedClassificationTypeNames.Text)); - } - - internal static void AssertExpectedClassification( - ClassifiedTextRun run, - string expectedText, - string expectedClassificationType, - ClassifiedTextRunStyle expectedClassificationStyle = ClassifiedTextRunStyle.Plain) - { - Assert.Equal(expectedText, run.Text); - Assert.Equal(expectedClassificationType, run.ClassificationTypeName); - Assert.Equal(expectedClassificationStyle, run.Style); - } -} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionItemResolverTest.NetFx.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionItemResolverTest.NetFx.cs index 06fcc14d264..4bad7b08bcd 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionItemResolverTest.NetFx.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionItemResolverTest.NetFx.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Formatting; using Microsoft.AspNetCore.Razor.LanguageServer.Test; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.AspNetCore.Razor.Test.Common.Mef; using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; @@ -26,7 +25,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; -[UseExportProvider] public class DelegatedCompletionItemResolverTest : LanguageServerTestBase { private readonly VSInternalClientCapabilities _clientCapabilities; @@ -52,7 +50,7 @@ public DelegatedCompletionItemResolverTest(ITestOutputHelper testOutput) } }; - var documentContext = TestDocumentContext.From("C:/path/to/file.cshtml"); + var documentContext = TestDocumentContext.Create("C:/path/to/file.cshtml"); _csharpCompletionParams = new DelegatedCompletionParams( documentContext.GetTextDocumentIdentifierAndVersion(), VsLspFactory.CreatePosition(10, 6), @@ -275,7 +273,7 @@ private async Task CreateCSharpServerAsync(RazorCodeDocumen CSharpTestLspServer csharpServer) { var completionContext = new VSInternalCompletionContext() { TriggerKind = CompletionTriggerKind.Invoked }; - var documentContext = TestDocumentContext.From("C:/path/to/file.razor", codeDocument); + var documentContext = TestDocumentContext.Create("C:/path/to/file.razor", codeDocument); var provider = TestDelegatedCompletionListProvider.Create(csharpServer, LoggerFactory, DisposalToken); var completionList = await provider.GetCompletionListAsync( diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionListProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionListProviderTest.cs index cc82c40eb4d..a36c6a557fb 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionListProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DelegatedCompletionListProviderTest.cs @@ -9,19 +9,20 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.AspNetCore.Razor.Test.Common.Mef; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; +using Moq; using Xunit; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; -[UseExportProvider] public class DelegatedCompletionListProviderTest : LanguageServerTestBase { private readonly TestDelegatedCompletionListProvider _provider; @@ -40,7 +41,7 @@ public async Task ResponseRewritersGetExecutedInOrder() // Arrange var completionContext = new VSInternalCompletionContext(); var codeDocument = CreateCodeDocument("<"); - var documentContext = TestDocumentContext.From("C:/path/to/file.cshtml", codeDocument, hostDocumentVersion: 0); + var documentContext = TestDocumentContext.Create("C:/path/to/file.cshtml", codeDocument, hostDocumentVersion: 0); var rewriter1 = new TestResponseRewriter(order: 100); var rewriter2 = new TestResponseRewriter(order: 20); var provider = TestDelegatedCompletionListProvider.Create(LoggerFactory, rewriter1, rewriter2); @@ -61,7 +62,7 @@ public async Task HtmlDelegation_Invoked() // Arrange var completionContext = new VSInternalCompletionContext() { TriggerKind = CompletionTriggerKind.Invoked }; var codeDocument = CreateCodeDocument("<"); - var documentContext = TestDocumentContext.From("C:/path/to/file.cshtml", codeDocument, hostDocumentVersion: 1337); + var documentContext = TestDocumentContext.Create("C:/path/to/file.cshtml", codeDocument, hostDocumentVersion: 1337); // Act await _provider.GetCompletionListAsync( @@ -88,7 +89,7 @@ public async Task HtmlDelegation_TriggerCharacter() TriggerCharacter = "<", }; var codeDocument = CreateCodeDocument("<"); - var documentContext = TestDocumentContext.From("C:/path/to/file.cshtml", codeDocument, hostDocumentVersion: 1337); + var documentContext = TestDocumentContext.Create("C:/path/to/file.cshtml", codeDocument, hostDocumentVersion: 1337); // Act await _provider.GetCompletionListAsync( @@ -116,7 +117,7 @@ public async Task HtmlDelegation_UnsupportedTriggerCharacter_TranslatesToInvoked TriggerCharacter = "|", }; var codeDocument = CreateCodeDocument("|"); - var documentContext = TestDocumentContext.From("C:/path/to/file.cshtml", codeDocument, hostDocumentVersion: 1337); + var documentContext = TestDocumentContext.Create("C:/path/to/file.cshtml", codeDocument, hostDocumentVersion: 1337); // Act await _provider.GetCompletionListAsync( @@ -144,7 +145,7 @@ public async Task Delegation_NullResult_ToIncompleteResult() TriggerCharacter = "<", }; var codeDocument = CreateCodeDocument("<"); - var documentContext = TestDocumentContext.From("C:/path/to/file.cshtml", codeDocument, hostDocumentVersion: 1337); + var documentContext = TestDocumentContext.Create("C:/path/to/file.cshtml", codeDocument, hostDocumentVersion: 1337); var provider = TestDelegatedCompletionListProvider.CreateWithNullResponse(LoggerFactory); // Act @@ -192,7 +193,7 @@ public async Task RazorDelegation_Noop() // Arrange var completionContext = new VSInternalCompletionContext() { TriggerKind = CompletionTriggerKind.Invoked }; var codeDocument = CreateCodeDocument("@functions "); - var documentContext = TestDocumentContext.From("C:/path/to/file.razor", codeDocument, hostDocumentVersion: 1337); + var documentContext = TestDocumentContext.Create("C:/path/to/file.razor", codeDocument, hostDocumentVersion: 1337); // Act var completionList = await _provider.GetCompletionListAsync( @@ -215,7 +216,7 @@ public async Task ProvisionalCompletion_TranslatesToCSharpWithProvisionalTextEdi TriggerCharacter = ".", }; var codeDocument = CreateCodeDocument("@DateTime."); - var documentContext = TestDocumentContext.From("C:/path/to/file.cshtml", codeDocument, hostDocumentVersion: 1337); + var documentContext = TestDocumentContext.Create("C:/path/to/file.cshtml", codeDocument, hostDocumentVersion: 1337); // Act await _provider.GetCompletionListAsync( @@ -245,7 +246,7 @@ public async Task DotTriggerInMiddleOfCSharpImplicitExpressionNotTreatedAsProvis TriggerCharacter = ".", }; var codeDocument = CreateCodeDocument("@DateTime.Now"); - var documentContext = TestDocumentContext.From("C:/path/to/file.cshtml", codeDocument, hostDocumentVersion: 1337); + var documentContext = TestDocumentContext.Create("C:/path/to/file.cshtml", codeDocument, hostDocumentVersion: 1337); // Act await _provider.GetCompletionListAsync( @@ -284,17 +285,18 @@ public async Task ShouldIncludeSnippets(string input, bool shouldIncludeSnippets TestFileMarkupParser.GetPosition(input, out var code, out var cursorPosition); var codeDocument = CreateCodeDocument(code); - var documentContext = TestDocumentContext.From("C:/path/to/file.cshtml", codeDocument, hostDocumentVersion: 1337); + var documentContext = TestDocumentContext.Create("C:/path/to/file.cshtml", codeDocument, hostDocumentVersion: 1337); - var documentMappingService = new TestDocumentMappingService() - { - LanguageKind = RazorLanguageKind.Html, - GeneratedPosition = new LinePosition(0, cursorPosition) - }; + var generatedPosition = new LinePosition(0, cursorPosition); + + var documentMappingServiceMock = new StrictMock(); + documentMappingServiceMock + .Setup(x => x.TryMapToGeneratedDocumentPosition(It.IsAny(), It.IsAny(), out generatedPosition, out It.Ref.IsAny)) + .Returns(true); var completionProvider = new DelegatedCompletionListProvider( - Array.Empty(), - documentMappingService, + responseRewriters: [], + documentMappingServiceMock.Object, clientConnection, new CompletionListCache()); @@ -370,7 +372,7 @@ private async Task GetCompletionListAsync(string conte TriggerCharacter = triggerCharacter, InvokeKind = invocationKind, }; - var documentContext = TestDocumentContext.From("C:/path/to/file.razor", codeDocument, hostDocumentVersion: 1337); + var documentContext = TestDocumentContext.Create("C:/path/to/file.razor", codeDocument, hostDocumentVersion: 1337); var provider = TestDelegatedCompletionListProvider.Create(csharpServer, LoggerFactory, DisposalToken); var completionList = await provider.GetCompletionListAsync( diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/ResponseRewriterTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/ResponseRewriterTestBase.cs index b338d8d03a1..db8ab5ed69a 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/ResponseRewriterTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/ResponseRewriterTestBase.cs @@ -27,7 +27,7 @@ private protected async Task GetRewrittenCompletionLis { var completionContext = new VSInternalCompletionContext(); var codeDocument = CreateCodeDocument(documentContent); - var documentContext = TestDocumentContext.From("C:/path/to/file.cshtml", codeDocument); + var documentContext = TestDocumentContext.Create("C:/path/to/file.cshtml", codeDocument); var provider = TestDelegatedCompletionListProvider.Create(initialCompletionList, LoggerFactory, rewriter ?? Rewriter); var clientCapabilities = new VSInternalClientCapabilities(); var completionList = await provider.GetCompletionListAsync(absoluteIndex, completionContext, documentContext, clientCapabilities, correlationId: Guid.Empty, cancellationToken: DisposalToken); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionItemResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionItemResolverTest.cs index 7d1ff4b8bed..91e538e628f 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionItemResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionItemResolverTest.cs @@ -7,9 +7,9 @@ using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.Tooltip; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.CodeAnalysis.Razor.Completion; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Tooltip; using Microsoft.VisualStudio.LanguageServer.Protocol; using Xunit; @@ -19,8 +19,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; public class RazorCompletionItemResolverTest : LanguageServerTestBase { - private readonly LSPTagHelperTooltipFactory _lspTagHelperTooltipFactory; - private readonly VSLSPTagHelperTooltipFactory _vsLspTagHelperTooltipFactory; + private readonly IProjectSnapshotManager _projectManager; private readonly VSInternalCompletionSetting _completionCapability; private readonly VSInternalClientCapabilities _defaultClientCapability; private readonly VSInternalClientCapabilities _vsClientCapability; @@ -30,10 +29,8 @@ public class RazorCompletionItemResolverTest : LanguageServerTestBase public RazorCompletionItemResolverTest(ITestOutputHelper testOutput) : base(testOutput) { - var projectManager = CreateProjectSnapshotManager(); + _projectManager = CreateProjectSnapshotManager(); - _lspTagHelperTooltipFactory = new DefaultLSPTagHelperTooltipFactory(projectManager); - _vsLspTagHelperTooltipFactory = new DefaultVSLSPTagHelperTooltipFactory(projectManager); _completionCapability = new VSInternalCompletionSetting() { CompletionItem = new CompletionItemSetting() @@ -69,7 +66,7 @@ public RazorCompletionItemResolverTest(ITestOutputHelper testOutput) public async Task ResolveAsync_DirectiveCompletion_ReturnsCompletionItemWithDocumentation() { // Arrange - var resolver = new RazorCompletionItemResolver(_lspTagHelperTooltipFactory, _vsLspTagHelperTooltipFactory); + var resolver = new RazorCompletionItemResolver(_projectManager); var razorCompletionItem = new RazorCompletionItem("TestItem", "TestItem", RazorCompletionItemKind.Directive); razorCompletionItem.SetDirectiveCompletionDescription(new DirectiveCompletionDescription("Test directive")); var completionList = CreateLSPCompletionList(razorCompletionItem); @@ -87,7 +84,7 @@ public async Task ResolveAsync_DirectiveCompletion_ReturnsCompletionItemWithDocu public async Task ResolveAsync_MarkupTransitionCompletion_ReturnsCompletionItemWithDocumentation() { // Arrange - var resolver = new RazorCompletionItemResolver(_lspTagHelperTooltipFactory, _vsLspTagHelperTooltipFactory); + var resolver = new RazorCompletionItemResolver(_projectManager); var razorCompletionItem = new RazorCompletionItem("@...", "@", RazorCompletionItemKind.MarkupTransition); razorCompletionItem.SetMarkupTransitionCompletionDescription(new MarkupTransitionCompletionDescription("Test description")); var completionList = CreateLSPCompletionList(razorCompletionItem); @@ -105,7 +102,7 @@ public async Task ResolveAsync_MarkupTransitionCompletion_ReturnsCompletionItemW public async Task ResolveAsync_DirectiveAttributeCompletion_ReturnsCompletionItemWithDocumentation() { // Arrange - var resolver = new RazorCompletionItemResolver(_lspTagHelperTooltipFactory, _vsLspTagHelperTooltipFactory); + var resolver = new RazorCompletionItemResolver(_projectManager); var razorCompletionItem = new RazorCompletionItem("TestItem", "TestItem", RazorCompletionItemKind.DirectiveAttribute); razorCompletionItem.SetAttributeCompletionDescription(_attributeDescription); var completionList = CreateLSPCompletionList(razorCompletionItem); @@ -123,7 +120,7 @@ public async Task ResolveAsync_DirectiveAttributeCompletion_ReturnsCompletionIte public async Task ResolveAsync_DirectiveAttributeParameterCompletion_ReturnsCompletionItemWithDocumentation() { // Arrange - var resolver = new RazorCompletionItemResolver(_lspTagHelperTooltipFactory, _vsLspTagHelperTooltipFactory); + var resolver = new RazorCompletionItemResolver(_projectManager); var razorCompletionItem = new RazorCompletionItem("TestItem", "TestItem", RazorCompletionItemKind.DirectiveAttributeParameter); razorCompletionItem.SetAttributeCompletionDescription(_attributeDescription); var completionList = CreateLSPCompletionList(razorCompletionItem); @@ -141,7 +138,7 @@ public async Task ResolveAsync_DirectiveAttributeParameterCompletion_ReturnsComp public async Task ResolveAsync_TagHelperElementCompletion_ReturnsCompletionItemWithDocumentation() { // Arrange - var resolver = new RazorCompletionItemResolver(_lspTagHelperTooltipFactory, _vsLspTagHelperTooltipFactory); + var resolver = new RazorCompletionItemResolver(_projectManager); var razorCompletionItem = new RazorCompletionItem("TestItem", "TestItem", RazorCompletionItemKind.TagHelperElement); razorCompletionItem.SetTagHelperElementDescriptionInfo(_elementDescription); var completionList = CreateLSPCompletionList(razorCompletionItem); @@ -159,7 +156,7 @@ public async Task ResolveAsync_TagHelperElementCompletion_ReturnsCompletionItemW public async Task ResolveAsync_TagHelperAttribute_ReturnsCompletionItemWithDocumentation() { // Arrange - var resolver = new RazorCompletionItemResolver(_lspTagHelperTooltipFactory, _vsLspTagHelperTooltipFactory); + var resolver = new RazorCompletionItemResolver(_projectManager); var razorCompletionItem = new RazorCompletionItem("TestItem", "TestItem", RazorCompletionItemKind.TagHelperAttribute); razorCompletionItem.SetAttributeCompletionDescription(_attributeDescription); var completionList = CreateLSPCompletionList(razorCompletionItem); @@ -177,7 +174,7 @@ public async Task ResolveAsync_TagHelperAttribute_ReturnsCompletionItemWithDocum public async Task ResolveAsync_VS_DirectiveAttributeCompletion_ReturnsCompletionItemWithDescription() { // Arrange - var resolver = new RazorCompletionItemResolver(_lspTagHelperTooltipFactory, _vsLspTagHelperTooltipFactory); + var resolver = new RazorCompletionItemResolver(_projectManager); var razorCompletionItem = new RazorCompletionItem("TestItem", "TestItem", RazorCompletionItemKind.DirectiveAttribute); razorCompletionItem.SetAttributeCompletionDescription(_attributeDescription); var completionList = CreateLSPCompletionList(razorCompletionItem); @@ -195,7 +192,7 @@ public async Task ResolveAsync_VS_DirectiveAttributeCompletion_ReturnsCompletion public async Task ResolveAsync_VS_DirectiveAttributeParameterCompletion_ReturnsCompletionItemWithDescription() { // Arrange - var resolver = new RazorCompletionItemResolver(_lspTagHelperTooltipFactory, _vsLspTagHelperTooltipFactory); + var resolver = new RazorCompletionItemResolver(_projectManager); var razorCompletionItem = new RazorCompletionItem("TestItem", "TestItem", RazorCompletionItemKind.DirectiveAttributeParameter); razorCompletionItem.SetAttributeCompletionDescription(_attributeDescription); var completionList = CreateLSPCompletionList(razorCompletionItem); @@ -213,7 +210,7 @@ public async Task ResolveAsync_VS_DirectiveAttributeParameterCompletion_ReturnsC public async Task ResolveAsync_VS_TagHelperElementCompletion_ReturnsCompletionItemWithDescription() { // Arrange - var resolver = new RazorCompletionItemResolver(_lspTagHelperTooltipFactory, _vsLspTagHelperTooltipFactory); + var resolver = new RazorCompletionItemResolver(_projectManager); var razorCompletionItem = new RazorCompletionItem("TestItem", "TestItem", RazorCompletionItemKind.TagHelperElement); razorCompletionItem.SetTagHelperElementDescriptionInfo(_elementDescription); var completionList = CreateLSPCompletionList(razorCompletionItem); @@ -231,7 +228,7 @@ public async Task ResolveAsync_VS_TagHelperElementCompletion_ReturnsCompletionIt public async Task ResolveAsync_VS_TagHelperAttribute_ReturnsCompletionItemWithDescription() { // Arrange - var resolver = new RazorCompletionItemResolver(_lspTagHelperTooltipFactory, _vsLspTagHelperTooltipFactory); + var resolver = new RazorCompletionItemResolver(_projectManager); var razorCompletionItem = new RazorCompletionItem("TestItem", "TestItem", RazorCompletionItemKind.TagHelperAttribute); razorCompletionItem.SetAttributeCompletionDescription(_attributeDescription); var completionList = CreateLSPCompletionList(razorCompletionItem); @@ -249,9 +246,9 @@ public async Task ResolveAsync_VS_TagHelperAttribute_ReturnsCompletionItemWithDe public async Task ResolveAsync_NonTagHelperCompletion_Noops() { // Arrange - var resolver = new RazorCompletionItemResolver(_lspTagHelperTooltipFactory, _vsLspTagHelperTooltipFactory); + var resolver = new RazorCompletionItemResolver(_projectManager); var completionItem = new VSInternalCompletionItem(); - var completionList = new VSInternalCompletionList() { Items = new[] { completionItem } }; + var completionList = new VSInternalCompletionList() { Items = [completionItem] }; // Act var resolvedCompletionItem = await resolver.ResolveAsync( @@ -261,9 +258,9 @@ public async Task ResolveAsync_NonTagHelperCompletion_Noops() Assert.Null(resolvedCompletionItem); } - private VSInternalCompletionList CreateLSPCompletionList(params RazorCompletionItem[] razorCompletionItems) - => RazorCompletionListProvider.CreateLSPCompletionList(razorCompletionItems.ToImmutableArray(), _defaultClientCapability); + private VSInternalCompletionList CreateLSPCompletionList(params ImmutableArray razorCompletionItems) + => RazorCompletionListProvider.CreateLSPCompletionList(razorCompletionItems, _defaultClientCapability); - private RazorCompletionResolveContext CreateCompletionResolveContext(RazorCompletionItem razorCompletionItem) - => new RazorCompletionResolveContext("file.razor", ImmutableArray.Empty.Add(razorCompletionItem)); + private static RazorCompletionResolveContext CreateCompletionResolveContext(RazorCompletionItem razorCompletionItem) + => new("file.razor", [razorCompletionItem]); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionListProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionListProviderTest.cs index 1f80264a23c..e941770fc8f 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionListProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionListProviderTest.cs @@ -364,7 +364,7 @@ public async Task GetCompletionListAsync_ProvidesDirectiveCompletionItems(string var documentPath = "C:/path/to/document.cshtml"; TestFileMarkupParser.GetPosition(documentText, out documentText, out var cursorPosition); var codeDocument = CreateCodeDocument(documentText); - var documentContext = TestDocumentContext.From(documentPath, codeDocument); + var documentContext = TestDocumentContext.Create(documentPath, codeDocument); var provider = new RazorCompletionListProvider(_completionFactsService, _completionListCache, LoggerFactory); // Act @@ -385,7 +385,7 @@ public async Task GetCompletionListAsync_ProvidesDirectiveCompletions_Incomplete // Arrange var documentPath = "C:/path/to/document.cshtml"; var codeDocument = CreateCodeDocument("@"); - var documentContext = TestDocumentContext.From(documentPath, codeDocument); + var documentContext = TestDocumentContext.Create(documentPath, codeDocument); var completionContext = new VSInternalCompletionContext() { TriggerKind = CompletionTriggerKind.TriggerForIncompleteCompletions, @@ -418,7 +418,7 @@ public async Task GetCompletionListAsync_ProvidesInjectOnIncomplete_KeywordIn() var tagHelperContext = TagHelperDocumentContext.Create(prefix: string.Empty, [tagHelper]); var codeDocument = CreateCodeDocument("@in"); codeDocument.SetTagHelperContext(tagHelperContext); - var documentContext = TestDocumentContext.From(documentPath, codeDocument); + var documentContext = TestDocumentContext.Create(documentPath, codeDocument); var provider = new RazorCompletionListProvider(_completionFactsService, _completionListCache, LoggerFactory); var completionContext = new VSInternalCompletionContext() { @@ -447,7 +447,7 @@ public async Task GetCompletionListAsync_DoesNotProvideInjectOnInvoked() var tagHelperContext = TagHelperDocumentContext.Create(prefix: string.Empty, [tagHelper]); var codeDocument = CreateCodeDocument("@inje"); codeDocument.SetTagHelperContext(tagHelperContext); - var documentContext = TestDocumentContext.From(documentPath, codeDocument); + var documentContext = TestDocumentContext.Create(documentPath, codeDocument); var provider = new RazorCompletionListProvider(_completionFactsService, _completionListCache, LoggerFactory); var completionContext = new VSInternalCompletionContext() { @@ -475,7 +475,7 @@ public async Task GetCompletionListAsync_ProvidesInjectOnIncomplete() var tagHelperContext = TagHelperDocumentContext.Create(prefix: string.Empty, [tagHelper]); var codeDocument = CreateCodeDocument("@inje"); codeDocument.SetTagHelperContext(tagHelperContext); - var documentContext = TestDocumentContext.From(documentPath, codeDocument); + var documentContext = TestDocumentContext.Create(documentPath, codeDocument); var provider = new RazorCompletionListProvider(_completionFactsService, _completionListCache, LoggerFactory); var completionContext = new VSInternalCompletionContext() { @@ -505,7 +505,7 @@ public async Task GetCompletionListAsync_ProvidesTagHelperElementCompletionItems var tagHelperContext = TagHelperDocumentContext.Create(prefix: string.Empty, [tagHelper]); var codeDocument = CreateCodeDocument("<"); codeDocument.SetTagHelperContext(tagHelperContext); - var documentContext = TestDocumentContext.From(documentPath, codeDocument); + var documentContext = TestDocumentContext.Create(documentPath, codeDocument); var provider = new RazorCompletionListProvider(_completionFactsService, _completionListCache, LoggerFactory); // Act @@ -535,7 +535,7 @@ public async Task GetCompletionListAsync_ProvidesTagHelperAttributeItems() var tagHelperContext = TagHelperDocumentContext.Create(prefix: string.Empty, [tagHelper]); var codeDocument = CreateCodeDocument(" { builder.Features.Add(new ConfigureRazorParserOptions(useRoslynTokenizer: true, CSharpParseOptions.Default)); + RazorExtensions.Register(builder); }); var fileKind = filePath.EndsWith(".razor", StringComparison.Ordinal) ? FileKinds.Component : FileKinds.Legacy; var codeDocument = projectEngine.ProcessDesignTime(sourceDocument, fileKind, importSources: default, tagHelpers); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperTooltipFactoryBaseTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperTooltipFactoryBaseTest.cs deleted file mode 100644 index e5ccc0e268d..00000000000 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperTooltipFactoryBaseTest.cs +++ /dev/null @@ -1,537 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -#nullable disable - -using System.Collections.Immutable; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.Components; -using Microsoft.AspNetCore.Razor.LanguageServer.Tooltip; -using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Roslyn.Test.Utilities; -using Xunit; -using Xunit.Abstractions; -using static Microsoft.AspNetCore.Razor.Language.CommonMetadata; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.Completion; - -public class TagHelperTooltipFactoryBaseTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) -{ - [Fact] - public void ReduceTypeName_Plain() - { - // Arrange - var content = "Microsoft.AspNetCore.SomeTagHelpers.SomeTypeName"; - - // Act - var reduced = TagHelperTooltipFactoryBase.ReduceTypeName(content); - - // Assert - Assert.Equal("SomeTypeName", reduced); - } - - [Fact] - public void ReduceTypeName_Generics() - { - // Arrange - var content = "System.Collections.Generic.List"; - - // Act - var reduced = TagHelperTooltipFactoryBase.ReduceTypeName(content); - - // Assert - Assert.Equal("List", reduced); - } - - [Fact] - public void ReduceTypeName_CrefGenerics() - { - // Arrange - var content = "System.Collections.Generic.List{System.String}"; - - // Act - var reduced = TagHelperTooltipFactoryBase.ReduceTypeName(content); - - // Assert - Assert.Equal("List{System.String}", reduced); - } - - [Fact] - public void ReduceTypeName_NestedGenerics() - { - // Arrange - var content = "Microsoft.AspNetCore.SometTagHelpers.SomeType>"; - - // Act - var reduced = TagHelperTooltipFactoryBase.ReduceTypeName(content); - - // Assert - Assert.Equal("SomeType>", reduced); - } - - [Theory] - [InlineData("Microsoft.AspNetCore.SometTagHelpers.SomeType.Foo.Bar>")] - [InlineData("Microsoft.AspNetCore.SometTagHelpers.SomeType.Foo.Bar{Baz.Phi}}")] - public void ReduceTypeName_UnbalancedDocs_NotRecoverable_ReturnsOriginalContent(string content) - { - // Arrange - - // Act - var reduced = TagHelperTooltipFactoryBase.ReduceTypeName(content); - - // Assert - Assert.Equal(content, reduced); - } - - [Fact] - public void ReduceMemberName_Plain() - { - // Arrange - var content = "Microsoft.AspNetCore.SometTagHelpers.SomeType.SomeProperty"; - - // Act - var reduced = TagHelperTooltipFactoryBase.ReduceMemberName(content); - - // Assert - Assert.Equal("SomeType.SomeProperty", reduced); - } - - [Fact] - public void ReduceMemberName_Generics() - { - // Arrange - var content = "Microsoft.AspNetCore.SometTagHelpers.SomeType.SomeProperty"; - - // Act - var reduced = TagHelperTooltipFactoryBase.ReduceMemberName(content); - - // Assert - Assert.Equal("SomeType.SomeProperty", reduced); - } - - [Fact] - public void ReduceMemberName_CrefGenerics() - { - // Arrange - var content = "Microsoft.AspNetCore.SometTagHelpers.SomeType{Foo.Bar}.SomeProperty{Foo.Bar}"; - - // Act - var reduced = TagHelperTooltipFactoryBase.ReduceMemberName(content); - - // Assert - Assert.Equal("SomeType{Foo.Bar}.SomeProperty{Foo.Bar}", reduced); - } - - [Fact] - public void ReduceMemberName_NestedGenericsMethodsTypes() - { - // Arrange - var content = "Microsoft.AspNetCore.SometTagHelpers.SomeType>.SomeMethod(Foo.Bar,Baz.Fi)"; - - // Act - var reduced = TagHelperTooltipFactoryBase.ReduceMemberName(content); - - // Assert - Assert.Equal("SomeType>.SomeMethod(Foo.Bar,Baz.Fi)", reduced); - } - - [Theory] - [InlineData("Microsoft.AspNetCore.SometTagHelpers.SomeType.Foo.Bar>")] - [InlineData("Microsoft.AspNetCore.SometTagHelpers.SomeType.Foo.Bar{Baz.Phi}}")] - [InlineData("Microsoft.AspNetCore.SometTagHelpers.SomeType.Foo.Bar(Baz.Phi))")] - [InlineData("Microsoft.AspNetCore.SometTagHelpers.SomeType.Foo{.>")] - public void ReduceMemberName_UnbalancedDocs_NotRecoverable_ReturnsOriginalContent(string content) - { - // Arrange - - // Act - var reduced = TagHelperTooltipFactoryBase.ReduceMemberName(content); - - // Assert - Assert.Equal(content, reduced); - } - - [Fact] - public void ReduceCrefValue_InvalidShortValue_ReturnsEmptyString() - { - // Arrange - var content = "T:"; - - // Act - var value = TagHelperTooltipFactoryBase.ReduceCrefValue(content); - - // Assert - Assert.Equal(string.Empty, value); - } - - [Fact] - public void ReduceCrefValue_InvalidUnknownIdentifierValue_ReturnsEmptyString() - { - // Arrange - var content = "X:"; - - // Act - var value = TagHelperTooltipFactoryBase.ReduceCrefValue(content); - - // Assert - Assert.Equal(string.Empty, value); - } - - [Fact] - public void ReduceCrefValue_Type() - { - // Arrange - var content = "T:Microsoft.AspNetCore.SometTagHelpers.SomeType"; - - // Act - var value = TagHelperTooltipFactoryBase.ReduceCrefValue(content); - - // Assert - Assert.Equal("SomeType", value); - } - - [Fact] - public void ReduceCrefValue_Property() - { - // Arrange - var content = "P:Microsoft.AspNetCore.SometTagHelpers.SomeType.SomeProperty"; - - // Act - var value = TagHelperTooltipFactoryBase.ReduceCrefValue(content); - - // Assert - Assert.Equal("SomeType.SomeProperty", value); - } - - [Fact] - public void ReduceCrefValue_Member() - { - // Arrange - var content = "P:Microsoft.AspNetCore.SometTagHelpers.SomeType.SomeMember"; - - // Act - var value = TagHelperTooltipFactoryBase.ReduceCrefValue(content); - - // Assert - Assert.Equal("SomeType.SomeMember", value); - } - - [Fact] - public void TryExtractSummary_Null_ReturnsFalse() - { - // Arrange & Act - var result = TagHelperTooltipFactoryBase.TryExtractSummary(documentation: null, out var summary); - - // Assert - Assert.False(result); - Assert.Null(summary); - } - - [Fact] - public void TryExtractSummary_ExtractsSummary_ReturnsTrue() - { - // Arrange - var expectedSummary = " Hello World "; - var documentation = $@" -Prefixed invalid content - - -{expectedSummary} - -Suffixed invalid content"; - - // Act - var result = TagHelperTooltipFactoryBase.TryExtractSummary(documentation, out var summary); - - // Assert - Assert.True(result); - Assert.Equal(expectedSummary, summary); - } - - [Fact] - public void TryExtractSummary_NoStartSummary_ReturnsFalse() - { - // Arrange - var documentation = @" -Prefixed invalid content - - - - -Suffixed invalid content"; - - // Act - var result = TagHelperTooltipFactoryBase.TryExtractSummary(documentation, out var summary); - - // Assert - Assert.True(result); - Assert.Equal(@"Prefixed invalid content - - - - -Suffixed invalid content", summary); - } - - [Fact] - public void TryExtractSummary_NoEndSummary_ReturnsTrue() - { - // Arrange - var documentation = @" -Prefixed invalid content - - - - -Suffixed invalid content"; - - // Act - var result = TagHelperTooltipFactoryBase.TryExtractSummary(documentation, out var summary); - - // Assert - Assert.True(result); - Assert.Equal(@"Prefixed invalid content - - - - -Suffixed invalid content", summary); - } - - [Fact] - public void TryExtractSummary_XMLButNoSummary_ReturnsFalse() - { - // Arrange - var documentation = @" -param1 -Result -"; - - // Act - var result = TagHelperTooltipFactoryBase.TryExtractSummary(documentation, out var summary); - - // Assert - Assert.False(result); - Assert.Null(summary); - } - - [Fact] - public void TryExtractSummary_NoXml_ReturnsTrue() - { - // Arrange - var documentation = @" -There is no xml, but I got you this < and the >. -"; - - // Act - var result = TagHelperTooltipFactoryBase.TryExtractSummary(documentation, out var summary); - - // Assert - Assert.True(result); - Assert.Equal("There is no xml, but I got you this < and the >.", summary); - } - - [Fact] - public async Task GetAvailableProjects_NoProjects_ReturnsNull() - { - var projectManager = CreateProjectSnapshotManager(); - var service = new TestTagHelperToolTipFactory(projectManager); - - var availability = await service.GetProjectAvailabilityAsync("file.razor", "MyTagHelper", CancellationToken.None); - - Assert.Null(availability); - } - - [Fact] - public async Task GetAvailableProjects_OneProject_ReturnsNull() - { - var builder = TagHelperDescriptorBuilder.Create(ComponentMetadata.Component.TagHelperKind, "TestTagHelper", "TestAssembly"); - builder.TagMatchingRule(rule => rule.TagName = "Test"); - var tagHelperTypeName = "TestNamespace.TestTagHelper"; - builder.Metadata(TypeName(tagHelperTypeName)); - var tagHelpers = ImmutableArray.Create(builder.Build()); - var projectWorkspaceState = ProjectWorkspaceState.Create(tagHelpers); - - var hostProject = new HostProject( - "C:/path/to/project.csproj", - "C:/path/to/obj/1", - RazorConfiguration.Default, - rootNamespace: null, - displayName: "project"); - - var hostDocument = new HostDocument( - "C:/path/to/file.razor", - "file.razor", - FileKinds.Component); - - var projectManager = CreateProjectSnapshotManager(); - - await projectManager.UpdateAsync(updater => - { - updater.ProjectAdded(hostProject); - updater.ProjectWorkspaceStateChanged(hostProject.Key, projectWorkspaceState); - updater.DocumentAdded(hostProject.Key, hostDocument, TestMocks.CreateTextLoader(hostDocument.FilePath, text: "")); - }); - - var service = new TestTagHelperToolTipFactory(projectManager); - - var availability = await service.GetProjectAvailabilityAsync(hostDocument.FilePath, tagHelperTypeName, CancellationToken.None); - - Assert.Null(availability); - } - - [Fact] - public async Task GetAvailableProjects_AvailableInAllProjects_ReturnsNull() - { - var builder = TagHelperDescriptorBuilder.Create(ComponentMetadata.Component.TagHelperKind, "TestTagHelper", "TestAssembly"); - builder.TagMatchingRule(rule => rule.TagName = "Test"); - var tagHelperTypeName = "TestNamespace.TestTagHelper"; - builder.Metadata(TypeName(tagHelperTypeName)); - var tagHelpers = ImmutableArray.Create(builder.Build()); - var projectWorkspaceState = ProjectWorkspaceState.Create(tagHelpers); - - var hostProject1 = new HostProject( - "C:/path/to/project.csproj", - "C:/path/to/obj/1", - RazorConfiguration.Default, - rootNamespace: null, - displayName: "project1"); - - var hostProject2 = new HostProject( - "C:/path/to/project.csproj", - "C:/path/to/obj/2", - RazorConfiguration.Default, - rootNamespace: null, - displayName: "project2"); - - var hostDocument = new HostDocument( - "C:/path/to/file.razor", - "file.razor", - FileKinds.Component); - - var projectManager = CreateProjectSnapshotManager(); - - await projectManager.UpdateAsync(updater => - { - updater.ProjectAdded(hostProject1); - updater.ProjectWorkspaceStateChanged(hostProject1.Key, projectWorkspaceState); - updater.DocumentAdded(hostProject1.Key, hostDocument, TestMocks.CreateTextLoader(hostDocument.FilePath, text: "")); - - updater.ProjectAdded(hostProject2); - updater.ProjectWorkspaceStateChanged(hostProject2.Key, projectWorkspaceState); - updater.DocumentAdded(hostProject2.Key, hostDocument, TestMocks.CreateTextLoader(hostDocument.FilePath, text: "")); - }); - - var service = new TestTagHelperToolTipFactory(projectManager); - - var availability = await service.GetProjectAvailabilityAsync(hostDocument.FilePath, tagHelperTypeName, CancellationToken.None); - - Assert.Null(availability); - } - - [Fact] - public async Task GetAvailableProjects_NotAvailableInAllProjects_ReturnsText() - { - var builder = TagHelperDescriptorBuilder.Create(ComponentMetadata.Component.TagHelperKind, "TestTagHelper", "TestAssembly"); - builder.TagMatchingRule(rule => rule.TagName = "Test"); - var tagHelperTypeName = "TestNamespace.TestTagHelper"; - builder.Metadata(TypeName(tagHelperTypeName)); - var tagHelpers = ImmutableArray.Create(builder.Build()); - var projectWorkspaceState = ProjectWorkspaceState.Create(tagHelpers); - - var hostProject1 = new HostProject( - "C:/path/to/project.csproj", - "C:/path/to/obj/1", - RazorConfiguration.Default, - rootNamespace: null, - displayName: "project1"); - - var hostProject2 = new HostProject( - "C:/path/to/project.csproj", - "C:/path/to/obj/2", - RazorConfiguration.Default, - rootNamespace: null, - displayName: "project2"); - - var hostDocument = new HostDocument( - "C:/path/to/file.razor", - "file.razor", - FileKinds.Component); - - var projectManager = CreateProjectSnapshotManager(); - - await projectManager.UpdateAsync(updater => - { - updater.ProjectAdded(hostProject1); - updater.ProjectWorkspaceStateChanged(hostProject1.Key, projectWorkspaceState); - updater.DocumentAdded(hostProject1.Key, hostDocument, TestMocks.CreateTextLoader(hostDocument.FilePath, text: "")); - - updater.ProjectAdded(hostProject2); - updater.DocumentAdded(hostProject2.Key, hostDocument, TestMocks.CreateTextLoader(hostDocument.FilePath, text: "")); - }); - - var service = new TestTagHelperToolTipFactory(projectManager); - - var availability = await service.GetProjectAvailabilityAsync(hostDocument.FilePath, tagHelperTypeName, CancellationToken.None); - - AssertEx.EqualOrDiff(""" - - ⚠️ Not available in: - project2 - """, availability); - } - - [Fact] - public async Task GetAvailableProjects_NotAvailableInAnyProject_ReturnsText() - { - var hostProject1 = new HostProject( - "C:/path/to/project.csproj", - "C:/path/to/obj/1", - RazorConfiguration.Default, - rootNamespace: null, - displayName: "project1"); - - var hostProject2 = new HostProject( - "C:/path/to/project.csproj", - "C:/path/to/obj/2", - RazorConfiguration.Default, - rootNamespace: null, - displayName: "project2"); - - var hostDocument = new HostDocument( - "C:/path/to/file.razor", - "file.razor", - FileKinds.Component); - - var projectManager = CreateProjectSnapshotManager(); - - await projectManager.UpdateAsync(updater => - { - updater.ProjectAdded(hostProject1); - updater.DocumentAdded(hostProject1.Key, hostDocument, TestMocks.CreateTextLoader(hostDocument.FilePath, text: "")); - - updater.ProjectAdded(hostProject2); - updater.DocumentAdded(hostProject2.Key, hostDocument, TestMocks.CreateTextLoader(hostDocument.FilePath, text: "")); - }); - - var service = new TestTagHelperToolTipFactory(projectManager); - - var availability = await service.GetProjectAvailabilityAsync(hostDocument.FilePath, "MyTagHelper", CancellationToken.None); - - AssertEx.EqualOrDiff(""" - - ⚠️ Not available in: - project1 - project2 - """, availability); - } -} - -file class TestTagHelperToolTipFactory(IProjectSnapshotManager projectManager) : TagHelperTooltipFactoryBase(projectManager) -{ -} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TestDocumentMappingService.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TestDocumentMappingService.cs deleted file mode 100644 index 2dd5836ce16..00000000000 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TestDocumentMappingService.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis.Razor.DocumentMapping; -using Microsoft.CodeAnalysis.Razor.Protocol; -using Microsoft.CodeAnalysis.Text; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; - -internal class TestDocumentMappingService : IDocumentMappingService -{ - public RazorLanguageKind LanguageKind { get; set; } - public LinePosition? GeneratedPosition { get; set; } - public int GeneratedIndex { get; set; } - - public IEnumerable GetHostDocumentEdits(IRazorGeneratedDocument generatedDocument, ImmutableArray generatedDocumentEdits) - => []; - - public RazorLanguageKind GetLanguageKind(RazorCodeDocument codeDocument, int hostDocumentIndex, bool rightAssociative) - => LanguageKind; - - public Task<(Uri MappedDocumentUri, LinePositionSpan MappedRange)> MapToHostDocumentUriAndRangeAsync(Uri generatedDocumentUri, LinePositionSpan generatedDocumentRange, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public bool TryMapToGeneratedDocumentOrNextCSharpPosition(IRazorGeneratedDocument generatedDocument, int hostDocumentIndex, out LinePosition generatedPosition, out int generatedIndex) - { - throw new NotImplementedException(); - } - - public bool TryMapToGeneratedDocumentPosition(IRazorGeneratedDocument generatedDocument, int hostDocumentIndex, out LinePosition generatedPosition, out int generatedIndex) - { - if (GeneratedPosition is null) - { - generatedPosition = default; - generatedIndex = default; - return false; - } - - generatedPosition = GeneratedPosition.Value; - generatedIndex = GeneratedIndex; - return true; - } - - public bool TryMapToGeneratedDocumentRange(IRazorGeneratedDocument generatedDocument, LinePositionSpan hostDocumentRange, out LinePositionSpan generatedDocumentRange) - { - throw new NotImplementedException(); - } - - public bool TryMapToHostDocumentPosition(IRazorGeneratedDocument generatedDocument, int generatedDocumentIndex, out LinePosition hostDocumentPosition, out int hostDocumentIndex) - { - throw new NotImplementedException(); - } - - public bool TryMapToHostDocumentRange(IRazorGeneratedDocument generatedDocument, LinePositionSpan generatedDocumentRange, MappingBehavior mappingBehavior, out LinePositionSpan hostDocumentRange) - { - throw new NotImplementedException(); - } -} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultRazorConfigurationServiceTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultRazorConfigurationServiceTest.cs index 29048394810..9be85770208 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultRazorConfigurationServiceTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DefaultRazorConfigurationServiceTest.cs @@ -22,7 +22,7 @@ public async Task GetLatestOptionsAsync_ReturnsExpectedOptions() { // Arrange var expectedOptions = new RazorLSPOptions( - EnableFormatting: false, AutoClosingTags: false, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, FormatOnType: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: true, CommitElementsWithSpace: false); + FormattingFlags.Disabled, AutoClosingTags: false, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: true, CommitElementsWithSpace: false); var razorJsonString = """ @@ -94,7 +94,7 @@ public void BuildOptions_VSCodeOptionsOnly_ReturnsExpected() { // Arrange - purposely choosing options opposite of default var expectedOptions = new RazorLSPOptions( - EnableFormatting: false, AutoClosingTags: false, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, FormatOnType: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: true, CommitElementsWithSpace: false); + FormattingFlags.Disabled, AutoClosingTags: false, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: true, CommitElementsWithSpace: false); var razorJsonString = """ { "format": { @@ -131,7 +131,7 @@ public void BuildOptions_VSOptionsOnly_ReturnsExpected() { // Arrange - purposely choosing options opposite of default var expectedOptions = new RazorLSPOptions( - EnableFormatting: true, AutoClosingTags: false, InsertSpaces: false, TabSize: 8, AutoShowCompletion: true, AutoListParams: true, FormatOnType: false, AutoInsertAttributeQuotes: false, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: false); + FormattingFlags.Enabled, AutoClosingTags: false, InsertSpaces: false, TabSize: 8, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: false, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: false); var razorJsonString = """ { } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Definition/DefinitionEndpointDelegationTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Definition/DefinitionEndpointDelegationTest.cs index 2f56371bdcc..a766d66314e 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Definition/DefinitionEndpointDelegationTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Definition/DefinitionEndpointDelegationTest.cs @@ -231,20 +231,20 @@ private async Task VerifyCSharpGoToDefinitionAsync(string input, string? filePat await projectManager.UpdateAsync(updater => { updater.ProjectAdded(new( - projectFilePath: "C:/path/to/project.csproj", + filePath: "C:/path/to/project.csproj", intermediateOutputPath: "C:/path/to/obj", - razorConfiguration: RazorConfiguration.Default, + configuration: RazorConfiguration.Default, rootNamespace: "project")); }); - var componentSearchEngine = new RazorComponentSearchEngine(projectManager, LoggerFactory); + var componentSearchEngine = new RazorComponentSearchEngine(LoggerFactory); var componentDefinitionService = new RazorComponentDefinitionService(componentSearchEngine, DocumentMappingService, LoggerFactory); var razorUri = new Uri(razorFilePath); Assert.True(DocumentContextFactory.TryCreate(razorUri, out var documentContext)); var requestContext = CreateRazorRequestContext(documentContext); - var endpoint = new DefinitionEndpoint(componentDefinitionService, DocumentMappingService, LanguageServerFeatureOptions, languageServer, LoggerFactory); + var endpoint = new DefinitionEndpoint(componentDefinitionService, DocumentMappingService, projectManager, LanguageServerFeatureOptions, languageServer, LoggerFactory); var request = new TextDocumentPositionParams { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Definition/RazorComponentDefinitionHelpersTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Definition/RazorComponentDefinitionHelpersTest.cs index 8f18f575019..d2034729071 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Definition/RazorComponentDefinitionHelpersTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Definition/RazorComponentDefinitionHelpersTest.cs @@ -390,7 +390,7 @@ private async Task VerifyTryGetPropertyRangeAsync(string content, string propert var codeDocument = CreateCodeDocument(content); var expectedRange = codeDocument.Source.Text.GetRange(selection); - var snapshot = TestDocumentSnapshot.Create("test.razor", content).With(codeDocument); + var snapshot = TestDocumentSnapshot.Create("test.razor", codeDocument); var documentMappingService = new LspDocumentMappingService(FilePathService, new TestDocumentContextFactory(), LoggerFactory); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/CSharpDiagnosticsEndToEndTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/CSharpDiagnosticsEndToEndTest.cs index 8122d21ac0e..6a650134e75 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/CSharpDiagnosticsEndToEndTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/CSharpDiagnosticsEndToEndTest.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; +using Microsoft.CodeAnalysis.Razor.Diagnostics; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/DocumentPullDiagnosticsEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/DocumentPullDiagnosticsEndpointTest.cs index ecbadccdd23..c930ee00dd7 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/DocumentPullDiagnosticsEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/DocumentPullDiagnosticsEndpointTest.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; +using Microsoft.CodeAnalysis.Razor.Diagnostics; using Microsoft.VisualStudio.LanguageServer.Protocol; using Moq; using Xunit; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/RazorDiagnosticConverterTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/RazorDiagnosticConverterTest.cs index 4e670005541..b62496963d5 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/RazorDiagnosticConverterTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/RazorDiagnosticConverterTest.cs @@ -6,6 +6,7 @@ using System.Globalization; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; +using Microsoft.CodeAnalysis.Razor.Diagnostics; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; using Xunit; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/RazorDiagnosticsPublisherTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/RazorDiagnosticsPublisherTest.cs index 869786c9280..01a8e65ea6f 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/RazorDiagnosticsPublisherTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Diagnostics/RazorDiagnosticsPublisherTest.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor.Diagnostics; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -92,9 +93,8 @@ public async Task DocumentProcessed_NewWorkQueued_RestartsTimer() { // Arrange Assert.NotNull(_openedDocument.FilePath); - var processedOpenDocument = TestDocumentSnapshot.Create(_openedDocument.FilePath); var codeDocument = CreateCodeDocument(s_singleRazorDiagnostic); - processedOpenDocument.With(codeDocument); + var processedOpenDocument = TestDocumentSnapshot.Create(_openedDocument.FilePath, codeDocument); var clientConnectionMock = new StrictMock(); clientConnectionMock @@ -162,9 +162,9 @@ public async Task PublishDiagnosticsAsync_NewDocumentDiagnosticsGetPublished(boo }; Assert.NotNull(_openedDocument.FilePath); - var processedOpenDocument = TestDocumentSnapshot.Create(_openedDocument.FilePath); var codeDocument = CreateCodeDocument(shouldContainRazorDiagnostic ? s_singleRazorDiagnostic : []); - processedOpenDocument.With(codeDocument); + var processedOpenDocument = TestDocumentSnapshot.Create(_openedDocument.FilePath, codeDocument); + Assert.NotNull(processedOpenDocument.FilePath); var clientConnectionMock = new StrictMock(); var requestResult = new FullDocumentDiagnosticReport(); @@ -238,9 +238,10 @@ public async Task PublishDiagnosticsAsync_NewRazorDiagnosticsGetPublished() { // Arrange Assert.NotNull(_openedDocument.FilePath); - var processedOpenDocument = TestDocumentSnapshot.Create(_openedDocument.FilePath); var codeDocument = CreateCodeDocument(s_singleRazorDiagnostic); - processedOpenDocument.With(codeDocument); + var processedOpenDocument = TestDocumentSnapshot.Create(_openedDocument.FilePath, codeDocument); + Assert.NotNull(processedOpenDocument.FilePath); + var clientConnectionMock = new StrictMock(); clientConnectionMock .Setup(server => server.SendRequestAsync?>( @@ -295,9 +296,10 @@ public async Task PublishDiagnosticsAsync_NewCSharpDiagnosticsGetPublished() { // Arrange Assert.NotNull(_openedDocument.FilePath); - var processedOpenDocument = TestDocumentSnapshot.Create(_openedDocument.FilePath); var codeDocument = CreateCodeDocument([]); - processedOpenDocument.With(codeDocument); + var processedOpenDocument = TestDocumentSnapshot.Create(_openedDocument.FilePath, codeDocument); + Assert.NotNull(processedOpenDocument.FilePath); + var arranging = true; var clientConnectionMock = new StrictMock(); clientConnectionMock @@ -365,10 +367,11 @@ public async Task PublishDiagnosticsAsync_NoopsIfRazorDiagnosticsAreSameAsPrevio Assert.Equal(_openedDocumentUri, @params.TextDocument.Uri); }) .Returns(Task.FromResult(new SumType?(new FullDocumentDiagnosticReport()))); + Assert.NotNull(_openedDocument.FilePath); - var processedOpenDocument = TestDocumentSnapshot.Create(_openedDocument.FilePath); var codeDocument = CreateCodeDocument(s_singleRazorDiagnostic); - processedOpenDocument.With(codeDocument); + var processedOpenDocument = TestDocumentSnapshot.Create(_openedDocument.FilePath, codeDocument); + Assert.NotNull(processedOpenDocument.FilePath); var documentContextFactory = new TestDocumentContextFactory(_openedDocument.FilePath, codeDocument); var filePathService = new LSPFilePathService(TestLanguageServerFeatureOptions.Instance); @@ -388,9 +391,10 @@ public async Task PublishDiagnosticsAsync_NoopsIfCSharpDiagnosticsAreSameAsPrevi { // Arrange Assert.NotNull(_openedDocument.FilePath); - var processedOpenDocument = TestDocumentSnapshot.Create(_openedDocument.FilePath); var codeDocument = CreateCodeDocument([]); - processedOpenDocument.With(codeDocument); + var processedOpenDocument = TestDocumentSnapshot.Create(_openedDocument.FilePath, codeDocument); + Assert.NotNull(processedOpenDocument.FilePath); + var clientConnectionMock = new StrictMock(); var arranging = true; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DocumentHighlighting/DocumentHighlightEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DocumentHighlighting/DocumentHighlightEndpointTest.cs index 655f2796130..6510f2dd7ee 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DocumentHighlighting/DocumentHighlightEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DocumentHighlighting/DocumentHighlightEndpointTest.cs @@ -11,7 +11,6 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.AspNetCore.Razor.Test.Common.Mef; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Testing; @@ -23,7 +22,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.DocumentHighlighting; -[UseExportProvider] public class DocumentHighlightEndpointTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) { [Fact] diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DocumentPresentation/TextDocumentUriPresentationEndpointTests.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DocumentPresentation/TextDocumentUriPresentationEndpointTests.cs index 123764dc623..0e3e8509efd 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DocumentPresentation/TextDocumentUriPresentationEndpointTests.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DocumentPresentation/TextDocumentUriPresentationEndpointTests.cs @@ -29,29 +29,31 @@ public async Task Handle_SimpleComponent_ReturnsResult() // Arrange var projectManager = CreateProjectSnapshotManager(); - var project = await projectManager.UpdateAsync(updater => + var hostProject = TestHostProject.Create("c:/path/project.csproj"); + var hostDocument1 = TestHostDocument.Create(hostProject, "c:/path/index.razor"); + var hostDocument2 = TestHostDocument.Create(hostProject, "c:/path/MyTagHelper.razor"); + + await projectManager.UpdateAsync(updater => { - return updater.CreateAndAddProject("c:/path/project.csproj"); + updater.ProjectAdded(hostProject); + updater.DocumentAdded(hostProject.Key, hostDocument1, hostDocument1.CreateEmptyTextLoader()); + updater.DocumentAdded(hostProject.Key, hostDocument2, hostDocument2.CreateEmptyTextLoader()); }); - await projectManager.CreateAndAddDocumentAsync(project, "c:/path/index.razor"); - await projectManager.CreateAndAddDocumentAsync(project, "c:/path/MyTagHelper.razor"); - var droppedUri = new Uri("file:///c:/path/MyTagHelper.razor"); var builder = TagHelperDescriptorBuilder.Create("MyTagHelper", "MyAssembly"); builder.SetMetadata(TypeNameIdentifier("MyTagHelper"), TypeNamespace("TestRootNamespace")); await projectManager.UpdateAsync(updater => { - updater.ProjectWorkspaceStateChanged(project.Key, ProjectWorkspaceState.Create([builder.Build()])); + updater.ProjectWorkspaceStateChanged(hostProject.Key, ProjectWorkspaceState.Create([builder.Build()])); }); - var razorFilePath = "c:/path/index.razor"; - var uri = new Uri(razorFilePath); + var uri = new Uri(hostDocument1.FilePath); await projectManager.UpdateAsync(updater => { - updater.DocumentOpened(project.Key, razorFilePath, SourceText.From("
")); + updater.DocumentOpened(hostProject.Key, hostDocument1.FilePath, SourceText.From("
")); }); var documentContextFactory = new DocumentContextFactory(projectManager, LoggerFactory); @@ -87,29 +89,31 @@ public async Task Handle_SimpleComponentWithChildFile_ReturnsResult() // Arrange var projectManager = CreateProjectSnapshotManager(); - var project = await projectManager.UpdateAsync(updater => + var hostProject = TestHostProject.Create("c:/path/project.csproj"); + var hostDocument1 = TestHostDocument.Create(hostProject, "c:/path/index.razor"); + var hostDocument2 = TestHostDocument.Create(hostProject, "c:/path/MyTagHelper.razor"); + + await projectManager.UpdateAsync(updater => { - return updater.CreateAndAddProject("c:/path/project.csproj"); + updater.ProjectAdded(hostProject); + updater.DocumentAdded(hostProject.Key, hostDocument1, hostDocument1.CreateEmptyTextLoader()); + updater.DocumentAdded(hostProject.Key, hostDocument2, hostDocument2.CreateEmptyTextLoader()); }); - await projectManager.CreateAndAddDocumentAsync(project, "c:/path/index.razor"); - await projectManager.CreateAndAddDocumentAsync(project, "c:/path/MyTagHelper.razor"); - var droppedUri = new Uri("file:///c:/path/MyTagHelper.razor"); var builder = TagHelperDescriptorBuilder.Create("MyTagHelper", "MyAssembly"); builder.SetMetadata(TypeNameIdentifier("MyTagHelper"), TypeNamespace("TestRootNamespace")); await projectManager.UpdateAsync(updater => { - updater.ProjectWorkspaceStateChanged(project.Key, ProjectWorkspaceState.Create([builder.Build()])); + updater.ProjectWorkspaceStateChanged(hostProject.Key, ProjectWorkspaceState.Create([builder.Build()])); }); - var razorFilePath = "c:/path/index.razor"; - var uri = new Uri(razorFilePath); + var uri = new Uri(hostDocument1.FilePath); await projectManager.UpdateAsync(updater => { - updater.DocumentOpened(project.Key, razorFilePath, SourceText.From("
")); + updater.DocumentOpened(hostProject.Key, hostDocument1.FilePath, SourceText.From("
")); }); var documentContextFactory = new DocumentContextFactory(projectManager, LoggerFactory); @@ -150,14 +154,17 @@ public async Task Handle_ComponentWithRequiredAttribute_ReturnsResult() // Arrange var projectManager = CreateProjectSnapshotManager(); - var project = await projectManager.UpdateAsync(updater => + var hostProject = TestHostProject.Create("c:/path/project.csproj"); + var hostDocument1 = TestHostDocument.Create(hostProject, "c:/path/index.razor"); + var hostDocument2 = TestHostDocument.Create(hostProject, "c:/path/fetchdata.razor"); + + await projectManager.UpdateAsync(updater => { - return updater.CreateAndAddProject("c:/path/project.csproj"); + updater.ProjectAdded(hostProject); + updater.DocumentAdded(hostProject.Key, hostDocument1, hostDocument1.CreateEmptyTextLoader()); + updater.DocumentAdded(hostProject.Key, hostDocument2, hostDocument2.CreateEmptyTextLoader()); }); - await projectManager.CreateAndAddDocumentAsync(project, "c:/path/index.razor"); - await projectManager.CreateAndAddDocumentAsync(project, "c:/path/fetchdata.razor"); - var droppedUri = new Uri("file:///c:/path/fetchdata.razor"); var builder = TagHelperDescriptorBuilder.Create("FetchData", "MyAssembly"); builder.SetMetadata(TypeNameIdentifier("FetchData"), TypeNamespace("TestRootNamespace")); @@ -170,15 +177,14 @@ public async Task Handle_ComponentWithRequiredAttribute_ReturnsResult() await projectManager.UpdateAsync(updater => { - updater.ProjectWorkspaceStateChanged(project.Key, ProjectWorkspaceState.Create([builder.Build()])); + updater.ProjectWorkspaceStateChanged(hostProject.Key, ProjectWorkspaceState.Create([builder.Build()])); }); - var razorFilePath = "c:/path/index.razor"; - var uri = new Uri(razorFilePath); + var uri = new Uri(hostDocument1.FilePath); await projectManager.UpdateAsync(updater => { - updater.DocumentOpened(project.Key, razorFilePath, SourceText.From("
")); + updater.DocumentOpened(hostProject.Key, hostDocument1.FilePath, SourceText.From("
")); }); var documentContextFactory = new DocumentContextFactory(projectManager, LoggerFactory); @@ -308,14 +314,17 @@ public async Task Handle_ComponentWithNestedFiles_ReturnsResult() // Arrange var projectManager = CreateProjectSnapshotManager(); - var project = await projectManager.UpdateAsync(updater => + var hostProject = TestHostProject.Create("c:/path/project.csproj"); + var hostDocument1 = TestHostDocument.Create(hostProject, "c:/path/index.razor"); + var hostDocument2 = TestHostDocument.Create(hostProject, "c:/path/fetchdata.razor"); + + await projectManager.UpdateAsync(updater => { - return updater.CreateAndAddProject("c:/path/project.csproj"); + updater.ProjectAdded(hostProject); + updater.DocumentAdded(hostProject.Key, hostDocument1, hostDocument1.CreateEmptyTextLoader()); + updater.DocumentAdded(hostProject.Key, hostDocument2, hostDocument2.CreateEmptyTextLoader()); }); - await projectManager.CreateAndAddDocumentAsync(project, "c:/path/index.razor"); - await projectManager.CreateAndAddDocumentAsync(project, "c:/path/fetchdata.razor"); - var droppedUri1 = new Uri("file:///c:/path/fetchdata.razor.cs"); var droppedUri2 = new Uri("file:///c:/path/fetchdata.razor"); var builder = TagHelperDescriptorBuilder.Create("FetchData", "MyAssembly"); @@ -323,20 +332,19 @@ public async Task Handle_ComponentWithNestedFiles_ReturnsResult() await projectManager.UpdateAsync(updater => { - updater.ProjectWorkspaceStateChanged(project.Key, ProjectWorkspaceState.Create([builder.Build()])); + updater.ProjectWorkspaceStateChanged(hostProject.Key, ProjectWorkspaceState.Create([builder.Build()])); }); - var razorFilePath = "c:/path/index.razor"; - var uri = new Uri(razorFilePath); + var uri = new Uri(hostDocument1.FilePath); await projectManager.UpdateAsync(updater => { - updater.DocumentOpened(project.Key, razorFilePath, SourceText.From("
")); + updater.DocumentOpened(hostProject.Key, hostDocument1.FilePath, SourceText.From("
")); }); var documentSnapshot = projectManager - .GetLoadedProject(project.Key) - .GetDocument(razorFilePath); + .GetLoadedProject(hostProject.Key) + .GetDocument(hostDocument1.FilePath); Assert.NotNull(documentSnapshot); var documentContextFactory = new DocumentContextFactory(projectManager, LoggerFactory); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DocumentSnapshotTextLoaderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DocumentSnapshotTextLoaderTest.cs index 0df799b3119..a24180b07cb 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DocumentSnapshotTextLoaderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DocumentSnapshotTextLoaderTest.cs @@ -1,8 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -#nullable disable - +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -13,24 +12,21 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer; -public class DocumentSnapshotTextLoaderTest : ToolingTestBase +public class DocumentSnapshotTextLoaderTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput) { - public DocumentSnapshotTextLoaderTest(ITestOutputHelper testOutput) - : base(testOutput) - { - } - [Fact] public async Task LoadTextAndVersionAsync_CreatesTextAndVersionFromDocumentsText() { // Arrange var expectedSourceText = SourceText.From("Hello World"); - var result = Task.FromResult(expectedSourceText); - var snapshot = Mock.Of(doc => doc.GetTextAsync() == result, MockBehavior.Strict); - var textLoader = new DocumentSnapshotTextLoader(snapshot); + var snapshotMock = new StrictMock(); + snapshotMock + .Setup(x => x.GetTextAsync(It.IsAny())) + .ReturnsAsync(expectedSourceText); + var textLoader = new DocumentSnapshotTextLoader(snapshotMock.Object); // Act - var actual = await textLoader.LoadTextAndVersionAsync(default, default); + var actual = await textLoader.LoadTextAndVersionAsync(options: default, DisposalToken); // Assert Assert.Same(expectedSourceText, actual.Text); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/FindNodeTests.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/FindNodeTests.cs index 4b55819c919..5adc6703356 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/FindNodeTests.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/FindNodeTests.cs @@ -75,8 +75,9 @@ protected override async Task OnInitializedAsync() [InlineData(5, 20, SyntaxKind.MarkupTextLiteral, true)] [InlineData(20, 21, SyntaxKind.CSharpTransition, false)] [InlineData(20, 21, SyntaxKind.CSharpTransition, true)] - [InlineData(21, 43, SyntaxKind.CSharpStatementLiteral, false)] - [InlineData(21, 43, SyntaxKind.CSharpStatementLiteral, true)] + [InlineData(21, 41, SyntaxKind.CSharpStatementLiteral, false)] + [InlineData(21, 41, SyntaxKind.CSharpStatementLiteral, true)] + [InlineData(41, 43, SyntaxKind.RazorMetaCode, true)] [InlineData(43, 44, SyntaxKind.CSharpTransition, false)] [InlineData(43, 44, SyntaxKind.CSharpTransition, true)] [InlineData(44, 50, SyntaxKind.CSharpExpressionLiteral, false)] diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/CSharpStatementBlockOnTypeFormattingTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/CSharpStatementBlockOnTypeFormattingTest.cs index 9e769c37366..465befd46c2 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/CSharpStatementBlockOnTypeFormattingTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/CSharpStatementBlockOnTypeFormattingTest.cs @@ -4,12 +4,15 @@ #nullable disable using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.Formatting; using Xunit; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; -public class CSharpStatementBlockOnTypeFormattingTest(ITestOutputHelper testOutput) : FormattingTestBase(testOutput) +[Collection(HtmlFormattingCollection.Name)] +public class CSharpStatementBlockOnTypeFormattingTest(HtmlFormattingFixture fixture, ITestOutputHelper testOutput) + : FormattingTestBase(fixture.Service, testOutput) { [Fact] public async Task CloseCurly_IfBlock_SingleLineAsync() @@ -120,6 +123,23 @@ await RunOnTypeFormattingTestAsync( triggerCharacter: ';'); } + [Fact] + public async Task Semicolon_PropertyGet() + { + await RunOnTypeFormattingTestAsync( + input: """ + @code { + private string Name {get;$$} + } + """, + expected: """ + @code { + private string Name { get; } + } + """, + triggerCharacter: ';'); + } + [Fact] public async Task Semicolon_AddsLineAtEndOfDocument() { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/CodeActionFormattingTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/CodeActionFormattingTest.cs index c7067eae067..b5f33ec5aa6 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/CodeActionFormattingTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/CodeActionFormattingTest.cs @@ -4,12 +4,15 @@ #nullable disable using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.Formatting; using Xunit; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; -public class CodeActionFormattingTest(ITestOutputHelper testOutput) : FormattingTestBase(testOutput) +[Collection(HtmlFormattingCollection.Name)] +public class CodeActionFormattingTest(HtmlFormattingFixture fixture, ITestOutputHelper testOutput) + : FormattingTestBase(fixture.Service, testOutput) { [Fact] public async Task AddDebuggerDisplay() diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/CodeDirectiveFormattingTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/CodeDirectiveFormattingTest.cs index 24ca9cf007a..fbccac31ee2 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/CodeDirectiveFormattingTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/CodeDirectiveFormattingTest.cs @@ -5,12 +5,15 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.Razor.Formatting; using Xunit; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; -public class CodeDirectiveFormattingTest(ITestOutputHelper testOutput) : FormattingTestBase(testOutput) +[Collection(HtmlFormattingCollection.Name)] +public class CodeDirectiveFormattingTest(HtmlFormattingFixture fixture, ITestOutputHelper testOutput) + : FormattingTestBase(fixture.Service, testOutput) { internal override bool UseTwoPhaseCompilation => true; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/CodeDirectiveOnTypeFormattingTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/CodeDirectiveOnTypeFormattingTest.cs index e879f2b59cd..37992612a07 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/CodeDirectiveOnTypeFormattingTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/CodeDirectiveOnTypeFormattingTest.cs @@ -5,12 +5,15 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.Razor.Formatting; using Xunit; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; -public class CodeDirectiveOnTypeFormattingTest(ITestOutputHelper testOutput) : FormattingTestBase(testOutput) +[Collection(HtmlFormattingCollection.Name)] +public class CodeDirectiveOnTypeFormattingTest(HtmlFormattingFixture fixture, ITestOutputHelper testOutput) + : FormattingTestBase(fixture.Service, testOutput) { [Fact] public async Task FormatsIfStatementInComponent() diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentOnTypeFormattingEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentOnTypeFormattingEndpointTest.cs index 7d313bfffa3..a91c2f4821c 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentOnTypeFormattingEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentOnTypeFormattingEndpointTest.cs @@ -4,8 +4,10 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; using Xunit; using Xunit.Abstractions; @@ -81,7 +83,7 @@ public async Task Handle_OnTypeFormatting_RemapFailed_ReturnsNull() var uri = new Uri("file://path/test.razor"); var documentContext = CreateDocumentContext(uri, codeDocument); - var formattingService = new DummyRazorFormattingService(); + var formattingService = new DummyRazorFormattingService(RazorLanguageKind.CSharp); var optionsMonitor = GetOptionsMonitor(enableFormatting: true); var htmlFormatter = new TestHtmlFormatter(); @@ -183,7 +185,7 @@ public async Task Handle_OnTypeFormatting_UnexpectedTriggerCharacter_ReturnsNull var uri = new Uri("file://path/test.razor"); var documentContextFactory = CreateDocumentContextFactory(uri, codeDocument); - var formattingService = new DummyRazorFormattingService(); + var formattingService = new DummyRazorFormattingService(RazorLanguageKind.CSharp); var optionsMonitor = GetOptionsMonitor(enableFormatting: true); var htmlFormatter = new TestHtmlFormatter(); @@ -205,4 +207,41 @@ public async Task Handle_OnTypeFormatting_UnexpectedTriggerCharacter_ReturnsNull // Assert Assert.Null(result); } + + [Fact] + public async Task Handle_OnTypeFormatting_ExpectedTriggerCharacter_ReturnsNotNull() + { + // Arrange + TestCode content = """ + @code { + private string Goo {get;$$} + } + """; + var codeDocument = CreateCodeDocument(content.Text, [new SourceMapping(new SourceSpan(17, 0), new SourceSpan(17, 0))]); + var sourceText = SourceText.From(content.Text); + var uri = new Uri("file://path/test.razor"); + + var documentContextFactory = CreateDocumentContextFactory(uri, codeDocument); + var formattingService = new DummyRazorFormattingService(RazorLanguageKind.CSharp); + + var optionsMonitor = GetOptionsMonitor(enableFormatting: true); + var htmlFormatter = new TestHtmlFormatter(); + var endpoint = new DocumentOnTypeFormattingEndpoint( + formattingService, htmlFormatter, optionsMonitor, LoggerFactory); + var @params = new DocumentOnTypeFormattingParams() + { + TextDocument = new TextDocumentIdentifier { Uri = uri, }, + Character = ";", + Position = sourceText.GetPosition(content.Position), + Options = new FormattingOptions { InsertSpaces = true, TabSize = 4 } + }; + Assert.True(documentContextFactory.TryCreate(uri, out var documentContext)); + var requestContext = CreateRazorRequestContext(documentContext); + + // Act + await endpoint.HandleRequestAsync(@params, requestContext, DisposalToken); + + // Assert + Assert.True(formattingService.Called); + } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentRangeFormattingEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentRangeFormattingEndpointTest.cs index bd667e180a8..550db0cc4a2 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentRangeFormattingEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/DocumentRangeFormattingEndpointTest.cs @@ -79,7 +79,7 @@ public async Task Handle_UnsupportedCodeDocument_ReturnsNull() var endpoint = new DocumentRangeFormattingEndpoint(formattingService, htmlFormatter, optionsMonitor); var @params = new DocumentRangeFormattingParams() { - TextDocument = new TextDocumentIdentifier { Uri = uri, } + TextDocument = new TextDocumentIdentifier { Uri = uri, }, }; var requestContext = CreateRazorRequestContext(documentContext); @@ -107,4 +107,32 @@ public async Task Handle_FormattingDisabled_ReturnsNull() // Assert Assert.Null(result); } + + [Fact] + public async Task Handle_FormattingOnPasteDisabled_ReturnsNull() + { + // Arrange + var formattingService = new DummyRazorFormattingService(); + var optionsMonitor = GetOptionsMonitor(formatOnPaste: false); + var htmlFormatter = new TestHtmlFormatter(); + var endpoint = new DocumentRangeFormattingEndpoint(formattingService, htmlFormatter, optionsMonitor); + var @params = new DocumentRangeFormattingParams() + { + Options = new() + { + OtherOptions = new() + { + { "fromPaste", true } + } + } + }; + + var requestContext = CreateRazorRequestContext(documentContext: null); + + // Act + var result = await endpoint.HandleRequestAsync(@params, requestContext, DisposalToken); + + // Assert + Assert.Null(result); + } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs index 0d62aae1122..ed626bc07bd 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingContentValidationPassTest.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.LanguageServer.Test; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.CodeAnalysis.CSharp; @@ -31,7 +30,7 @@ public async Task Execute_NonDestructiveEdit_Allowed() [||]public class Foo { } } """; - using var context = CreateFormattingContext(source); + var context = CreateFormattingContext(source); var edits = ImmutableArray.Create(new TextChange(source.Span, " ")); var input = edits; var pass = GetPass(); @@ -52,7 +51,7 @@ public async Task Execute_DestructiveEdit_Rejected() [|public class Foo { } |]} """; - using var context = CreateFormattingContext(source); + var context = CreateFormattingContext(source); var edits = ImmutableArray.Create(new TextChange(source.Span, " ")); var input = edits; var pass = GetPass(); @@ -87,12 +86,10 @@ private static FormattingContext CreateFormattingContext(TestCode input, int tab }; var context = FormattingContext.Create( - uri, documentSnapshot, codeDocument, options, - new LspFormattingCodeDocumentProvider(), - TestAdhocWorkspaceFactory.Instance); + new LspFormattingCodeDocumentProvider()); return context; } @@ -108,9 +105,9 @@ private static (RazorCodeDocument, IDocumentSnapshot) CreateCodeDocumentAndSnaps }); var codeDocument = projectEngine.ProcessDesignTime(sourceDocument, fileKind, importSources: default, tagHelpers); - var documentSnapshot = new Mock(MockBehavior.Strict); + var documentSnapshot = new StrictMock(); documentSnapshot - .Setup(d => d.GetGeneratedOutputAsync(It.IsAny())) + .Setup(d => d.GetGeneratedOutputAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(codeDocument); documentSnapshot .Setup(d => d.TargetPath) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs index a667ec5855c..e7a66fad443 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingDiagnosticValidationPassTest.cs @@ -5,7 +5,6 @@ using System.Collections.Immutable; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.LanguageServer.Test; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.CodeAnalysis.CSharp; @@ -29,7 +28,7 @@ public async Task ExecuteAsync_NonDestructiveEdit_Allowed() [||]public class Foo { } } """; - using var context = CreateFormattingContext(source); + var context = CreateFormattingContext(source); var edits = ImmutableArray.Create(new TextChange(source.Span, " ")); var input = edits; var pass = GetPass(); @@ -51,7 +50,7 @@ public async Task ExecuteAsync_DestructiveEdit_Rejected() public class Foo { } } """; - using var context = CreateFormattingContext(source); + var context = CreateFormattingContext(source); var badEdit = new TextChange(source.Span, "@ "); // Creates a diagnostic var pass = GetPass(); @@ -85,12 +84,10 @@ private static FormattingContext CreateFormattingContext(TestCode input, int tab }; var context = FormattingContext.Create( - uri, documentSnapshot, codeDocument, options, - new LspFormattingCodeDocumentProvider(), - TestAdhocWorkspaceFactory.Instance); + new LspFormattingCodeDocumentProvider()); return context; } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerClient.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerClient.cs index f647e7d31d6..5779ad5aa30 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerClient.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerClient.cs @@ -18,8 +18,9 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; -internal class FormattingLanguageServerClient(ILoggerFactory loggerFactory) : IClientConnection +internal class FormattingLanguageServerClient(HtmlFormattingService htmlFormattingService, ILoggerFactory loggerFactory) : IClientConnection { + private readonly HtmlFormattingService _htmlFormattingService = htmlFormattingService; private readonly Dictionary _documents = []; private readonly ILoggerFactory _loggerFactory = loggerFactory; @@ -36,7 +37,7 @@ private async Task FormatAsync(DocumentOnTypeFo { var generatedHtml = GetGeneratedHtml(@params.TextDocument.Uri); - var edits = await HtmlFormatting.GetOnTypeFormattingEditsAsync(_loggerFactory, @params.TextDocument.Uri, generatedHtml, @params.Position, @params.Options.InsertSpaces, @params.Options.TabSize); + var edits = await _htmlFormattingService.GetOnTypeFormattingEditsAsync(_loggerFactory, @params.TextDocument.Uri, generatedHtml, @params.Position, @params.Options.InsertSpaces, @params.Options.TabSize); return new() { @@ -48,7 +49,7 @@ private async Task FormatAsync(DocumentFormatti { var generatedHtml = GetGeneratedHtml(@params.TextDocument.Uri); - var edits = await HtmlFormatting.GetDocumentFormattingEditsAsync(_loggerFactory, @params.TextDocument.Uri, generatedHtml, @params.Options.InsertSpaces, @params.Options.TabSize); + var edits = await _htmlFormattingService.GetDocumentFormattingEditsAsync(_loggerFactory, @params.TextDocument.Uri, generatedHtml, @params.Options.InsertSpaces, @params.Options.TabSize); return new() { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs index 10e9266ccef..da04938029b 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingLanguageServerTestBase.cs @@ -48,7 +48,8 @@ public Task> GetDocumentFormattingChangesAsync(Docume public Task> GetCSharpOnTypeFormattingChangesAsync(DocumentContext documentContext, RazorFormattingOptions options, int hostDocumentIndex, char triggerCharacter, CancellationToken cancellationToken) { - throw new NotImplementedException(); + Called = true; + return SpecializedTasks.EmptyImmutableArray(); } public Task TryGetCSharpSnippetFormattingEditAsync(DocumentContext documentContext, ImmutableArray edits, RazorFormattingOptions options, CancellationToken cancellationToken) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingTestBase.cs index 178eff98e56..7f2b29117d1 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/FormattingTestBase.cs @@ -36,10 +36,14 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; public class FormattingTestBase : RazorToolingIntegrationTestBase { - public FormattingTestBase(ITestOutputHelper testOutput) + private readonly HtmlFormattingService _htmlFormattingService; + + internal FormattingTestBase(HtmlFormattingService htmlFormattingService, ITestOutputHelper testOutput) : base(testOutput) { ITestOnlyLoggerExtensions.TestOnlyLoggingEnabled = true; + + _htmlFormattingService = htmlFormattingService; } private protected async Task RunFormattingTestAsync( @@ -96,7 +100,7 @@ private async Task RunFormattingTestInternalAsync(string input, string expected, var formattingService = await TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory, codeDocument, razorLSPOptions); var documentContext = new DocumentContext(uri, documentSnapshot, projectContext: null); - var client = new FormattingLanguageServerClient(LoggerFactory); + var client = new FormattingLanguageServerClient(_htmlFormattingService, LoggerFactory); client.AddCodeDocument(codeDocument); var htmlFormatter = new HtmlFormatter(client); @@ -161,7 +165,7 @@ private protected async Task RunOnTypeFormattingTestAsync( } else { - var client = new FormattingLanguageServerClient(LoggerFactory); + var client = new FormattingLanguageServerClient(_htmlFormattingService, LoggerFactory); client.AddCodeDocument(codeDocument); var htmlFormatter = new HtmlFormatter(client); @@ -170,7 +174,7 @@ private protected async Task RunOnTypeFormattingTestAsync( } // Assert - var edited = razorSourceText.WithChanges( changes); + var edited = razorSourceText.WithChanges(changes); var actual = edited.ToString(); AssertEx.EqualOrDiff(expected, actual); @@ -271,9 +275,9 @@ @using Microsoft.AspNetCore.Components.Web var importsPath = new Uri("file:///path/to/_Imports.razor").AbsolutePath; var importsSourceText = SourceText.From(DefaultImports); var importsDocument = RazorSourceDocument.Create(importsSourceText, RazorSourceDocumentProperties.Create(importsPath, importsPath)); - var importsSnapshot = new Mock(MockBehavior.Strict); + var importsSnapshot = new StrictMock(); importsSnapshot - .Setup(d => d.GetTextAsync()) + .Setup(d => d.GetTextAsync(It.IsAny())) .ReturnsAsync(importsSourceText); importsSnapshot .Setup(d => d.FilePath) @@ -284,11 +288,10 @@ @using Microsoft.AspNetCore.Components.Web var projectFileSystem = new TestRazorProjectFileSystem([ new TestRazorProjectItem(path, fileKind: fileKind), - new TestRazorProjectItem(importsPath, fileKind: FileKinds.ComponentImport), - ]); + new TestRazorProjectItem(importsPath, fileKind: FileKinds.ComponentImport)]); var projectEngine = RazorProjectEngine.Create( - new RazorConfiguration(RazorLanguageVersion.Latest, "TestConfiguration", Extensions: [], new LanguageServerFlags(forceRuntimeCodeGeneration)), + new RazorConfiguration(RazorLanguageVersion.Latest, "TestConfiguration", Extensions: [], LanguageServerFlags: new LanguageServerFlags(forceRuntimeCodeGeneration)), projectFileSystem, builder => { @@ -314,9 +317,9 @@ @using Microsoft.AspNetCore.Components.Web internal static IDocumentSnapshot CreateDocumentSnapshot(string path, ImmutableArray tagHelpers, string? fileKind, ImmutableArray importsDocuments, ImmutableArray imports, RazorProjectEngine projectEngine, RazorCodeDocument codeDocument, bool inGlobalNamespace = false) { - var documentSnapshot = new Mock(MockBehavior.Strict); + var documentSnapshot = new StrictMock(); documentSnapshot - .Setup(d => d.GetGeneratedOutputAsync(It.IsAny())) + .Setup(d => d.GetGeneratedOutputAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(codeDocument); documentSnapshot .Setup(d => d.FilePath) @@ -331,7 +334,7 @@ internal static IDocumentSnapshot CreateDocumentSnapshot(string path, ImmutableA .Setup(d => d.Project.Configuration) .Returns(projectEngine.Configuration); documentSnapshot - .Setup(d => d.GetTextAsync()) + .Setup(d => d.GetTextAsync(It.IsAny())) .ReturnsAsync(codeDocument.Source.Text); documentSnapshot .Setup(d => d.Project.GetTagHelpersAsync(It.IsAny())) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/HtmlFormattingCollection.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/HtmlFormattingCollection.cs new file mode 100644 index 00000000000..c8eacf6f87d --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/HtmlFormattingCollection.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis.Razor.Formatting; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; + +[CollectionDefinition(Name)] +public class HtmlFormattingCollection : ICollectionFixture +{ + public const string Name = nameof(HtmlFormattingCollection); +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/HtmlFormattingTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/HtmlFormattingTest.cs index 300f2fc42c1..593b32dd64e 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/HtmlFormattingTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/HtmlFormattingTest.cs @@ -14,7 +14,9 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; -public class HtmlFormattingTest(ITestOutputHelper testOutput) : FormattingTestBase(testOutput) +[Collection(HtmlFormattingCollection.Name)] +public class HtmlFormattingTest(HtmlFormattingFixture fixture, ITestOutputHelper testOutput) + : FormattingTestBase(fixture.Service, testOutput) { internal override bool UseTwoPhaseCompilation => true; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/RazorFormattingTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/RazorFormattingTest.cs index 27bb6dc6e7c..baa075aaa1f 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/RazorFormattingTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/RazorFormattingTest.cs @@ -7,12 +7,14 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.Razor.Formatting; using Xunit; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting; -public class RazorFormattingTest(ITestOutputHelper testOutput) : FormattingTestBase(testOutput) +[Collection(HtmlFormattingCollection.Name)] +public class RazorFormattingTest(HtmlFormattingFixture fixture, ITestOutputHelper testOutput) : FormattingTestBase(fixture.Service, testOutput) { [Fact] public async Task Section_BraceOnNextLine() @@ -705,6 +707,31 @@ await RunFormattingTestAsync( fileKind: FileKinds.Legacy); } + [Fact] + public async Task MultiLineComment_WithinHtml () + { + await RunFormattingTestAsync( + input: """ +
+ @*
+ This comment's opening at-star will be aligned, and the + indentation of the rest of its lines will be preserved. +
+ *@ +
+ """, + expected: """ +
+ @*
+ This comment's opening at-star will be aligned, and the + indentation of the rest of its lines will be preserved. +
+ *@ +
+ """, + fileKind: FileKinds.Legacy); + } + // Regression prevention tests: [Fact] public async Task Using() @@ -787,7 +814,7 @@ private void IncrementCount() } """, triggerCharacter: '}', - razorLSPOptions: RazorLSPOptions.Default with { FormatOnType = true }); + razorLSPOptions: RazorLSPOptions.Default); } [Fact] diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs index fc10917be71..217024e2dca 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Formatting_NetFx/TestRazorFormattingService.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; -using Microsoft.AspNetCore.Razor.LanguageServer.Test; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; using Microsoft.CodeAnalysis.Razor.Formatting; @@ -40,7 +39,8 @@ public static async Task CreateWithFullSupportAsync( } var formattingCodeDocumentProvider = new LspFormattingCodeDocumentProvider(); + var hostServicesProvider = new DefaultHostServicesProvider(); - return new RazorFormattingService(formattingCodeDocumentProvider, mappingService, TestAdhocWorkspaceFactory.Instance, loggerFactory); + return new RazorFormattingService(formattingCodeDocumentProvider, mappingService, hostServicesProvider, loggerFactory); } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/GeneratedDocumentPublisherTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/GeneratedDocumentPublisherTest.cs index e2fa1ae18e1..33d82d4784b 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/GeneratedDocumentPublisherTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/GeneratedDocumentPublisherTest.cs @@ -387,6 +387,31 @@ await _projectManager.UpdateAsync(updater => Assert.Equal(124, updateRequest.HostDocumentVersion); } + [Fact] + public async Task ProjectSnapshotManager_DocumentRemoved_ClearsContent() + { + // Arrange + var options = new TestLanguageServerFeatureOptions(includeProjectKeyInGeneratedFilePath: true); + var publisher = new GeneratedDocumentPublisher(_projectManager, _serverClient, options, LoggerFactory); + var sourceTextContent = "// The content"; + var initialSourceText = SourceText.From(sourceTextContent); + + publisher.PublishCSharp(s_hostProject.Key, s_hostDocument.FilePath, initialSourceText, 123); + + await _projectManager.UpdateAsync(updater => + { + updater.DocumentOpened(s_hostProject.Key, s_hostDocument.FilePath, initialSourceText); + }); + + // Act + await _projectManager.UpdateAsync(updater => + { + updater.DocumentRemoved(s_hostProject.Key, s_hostDocument); + }); + + Assert.Equal(0, publisher.GetTestAccessor().PublishedCSharpDataCount); + } + [Fact] public async Task ProjectSnapshotManager_ProjectRemoved_ClearsContent() { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/GeneratedDocumentSynchronizerTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/GeneratedDocumentSynchronizerTest.cs index bba790b36ae..cd99181070f 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/GeneratedDocumentSynchronizerTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/GeneratedDocumentSynchronizerTest.cs @@ -86,6 +86,19 @@ public void DocumentProcessed_CloseDocument_DoesntPublish() Assert.False(_publisher.PublishedHtml); } + [Fact] + public void DocumentProcessed_RemovedDocument_DoesntPublish() + { + var document = TestDocumentSnapshot.Create("/path/to/non.existent.file.razor"); + + // Act + _synchronizer.DocumentProcessed(_codeDocument, document); + + // Assert + Assert.False(_publisher.PublishedCSharp); + Assert.False(_publisher.PublishedHtml); + } + private class TestGeneratedDocumentPublisher : IGeneratedDocumentPublisher { public bool PublishedCSharp { get; private set; } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs index 246335632f0..6dc5f7eda36 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Hover/HoverServiceTest.cs @@ -9,15 +9,15 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Completion; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.AspNetCore.Razor.LanguageServer.Hover; -using Microsoft.AspNetCore.Razor.LanguageServer.Tooltip; using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.AspNetCore.Razor.Test.Common.Mef; using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; +using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Tooltip; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -25,11 +25,9 @@ using Moq; using Xunit; using Xunit.Abstractions; -using static Microsoft.AspNetCore.Razor.LanguageServer.Tooltip.DefaultVSLSPTagHelperTooltipFactory; namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.Hover; -[UseExportProvider] public class HoverServiceTest(ITestOutputHelper testOutput) : TagHelperServiceTestBase(testOutput) { private static VSInternalClientCapabilities CreateMarkDownCapabilities() @@ -670,9 +668,9 @@ public async Task GetHoverInfo_TagHelper_Element_VSClient_ReturnVSHover() var innerContainer = ((ContainerElement)containerElements[0]).Elements.ToList(); var classifiedTextElement = (ClassifiedTextElement)innerContainer[1]; Assert.Equal(2, innerContainer.Count); - Assert.Equal(ClassGlyph, innerContainer[0]); + Assert.Equal(ClassifiedTagHelperTooltipFactory.ClassGlyph, innerContainer[0]); Assert.Collection(classifiedTextElement.Runs, - run => DefaultVSLSPTagHelperTooltipFactoryTest.AssertExpectedClassification(run, "Test1TagHelper", VSPredefinedClassificationTypeNames.Type)); + run => run.AssertExpectedClassification("Test1TagHelper", ClassifiedTagHelperTooltipFactory.TypeClassificationName)); } [Fact] @@ -713,13 +711,13 @@ public async Task GetHoverInfo_TagHelper_Attribute_VSClient_ReturnVSHover() var innerContainer = ((ContainerElement)containerElements[0]).Elements.ToList(); var classifiedTextElement = (ClassifiedTextElement)innerContainer[1]; Assert.Equal(2, innerContainer.Count); - Assert.Equal(PropertyGlyph, innerContainer[0]); + Assert.Equal(ClassifiedTagHelperTooltipFactory.PropertyGlyph, innerContainer[0]); Assert.Collection(classifiedTextElement.Runs, - run => DefaultVSLSPTagHelperTooltipFactoryTest.AssertExpectedClassification(run, "bool", VSPredefinedClassificationTypeNames.Keyword), - run => DefaultVSLSPTagHelperTooltipFactoryTest.AssertExpectedClassification(run, " ", VSPredefinedClassificationTypeNames.WhiteSpace), - run => DefaultVSLSPTagHelperTooltipFactoryTest.AssertExpectedClassification(run, "Test1TagHelper", VSPredefinedClassificationTypeNames.Type), - run => DefaultVSLSPTagHelperTooltipFactoryTest.AssertExpectedClassification(run, ".", VSPredefinedClassificationTypeNames.Punctuation), - run => DefaultVSLSPTagHelperTooltipFactoryTest.AssertExpectedClassification(run, "BoolVal", VSPredefinedClassificationTypeNames.Identifier)); + run => run.AssertExpectedClassification("bool", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification(" ", ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification("Test1TagHelper", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("BoolVal", ClassificationTypeNames.Identifier)); } [Fact] @@ -929,14 +927,16 @@ public async Task Handle_Hover_SingleServer_AddTagHelper() var path = "C:/text.razor"; var codeDocument = CreateCodeDocument(code.Text, path, DefaultTagHelpers); var projectWorkspaceState = ProjectWorkspaceState.Create(DefaultTagHelpers); - var projectSnapshot = TestProjectSnapshot.Create("C:/project.csproj", projectWorkspaceState); + + var hostProject = TestHostProject.Create("C:/project.csproj"); + var projectSnapshot = TestMocks.CreateProjectSnapshot(hostProject, projectWorkspaceState); var documentSnapshotMock = new StrictMock(); documentSnapshotMock - .Setup(x => x.GetGeneratedOutputAsync(It.IsAny())) + .Setup(x => x.GetGeneratedOutputAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(codeDocument); documentSnapshotMock - .Setup(x => x.GetTextAsync()) + .Setup(x => x.GetTextAsync(It.IsAny())) .ReturnsAsync(codeDocument.Source.Text); documentSnapshotMock .SetupGet(x => x.FilePath) @@ -985,8 +985,6 @@ private HoverEndpoint CreateEndpoint( private HoverService GetHoverService(IDocumentMappingService? mappingService = null) { var projectManager = CreateProjectSnapshotManager(); - var lspTagHelperTooltipFactory = new DefaultLSPTagHelperTooltipFactory(projectManager); - var vsLspTagHelperTooltipFactory = new DefaultVSLSPTagHelperTooltipFactory(projectManager); var clientCapabilities = CreateMarkDownCapabilities(); clientCapabilities.SupportsVisualStudioExtensions = true; @@ -994,7 +992,7 @@ private HoverService GetHoverService(IDocumentMappingService? mappingService = n mappingService ??= StrictMock.Of(); - return new HoverService(lspTagHelperTooltipFactory, vsLspTagHelperTooltipFactory, mappingService, clientCapabilitiesService); + return new HoverService(projectManager, mappingService, clientCapabilitiesService); } private class HoverLanguageServer : IClientConnection diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Implementation/ImplementationEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Implementation/ImplementationEndpointTest.cs index 6ff8c8f61ac..ca7a424bc86 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Implementation/ImplementationEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Implementation/ImplementationEndpointTest.cs @@ -112,8 +112,8 @@ private async Task VerifyCSharpGoToImplementationAsync(string input) var result = await endpoint.HandleRequestAsync(request, requestContext, DisposalToken); // Assert - Assert.NotNull(result.First); - var locations = result.First; + Assert.NotNull(result.Value.First); + var locations = result.Value.First; Assert.Equal(expectedSpans.Length, locations.Length); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs index 9b964c0898a..285635a557a 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/InlayHints/InlayHintEndpointTest.cs @@ -73,8 +73,8 @@ public Task InlayHints_ComponentAttributes() """, toolTipMap: new Dictionary - { - }, + { + }, output: """
@@ -85,6 +85,42 @@ public Task InlayHints_ComponentAttributes() """); + [Theory] + [InlineData(0, 0, 0, 20)] + [InlineData(0, 0, 2, 0)] + [InlineData(2, 0, 4, 0)] + public async Task InlayHints_InvalidRange(int startLine, int starChar, int endLine, int endChar) + { + var input = """ +
+ """; + var razorFilePath = "C:/path/to/file.razor"; + var codeDocument = CreateCodeDocument(input, filePath: razorFilePath); + + var languageServer = await CreateLanguageServerAsync(codeDocument, razorFilePath); + + var service = new InlayHintService(DocumentMappingService); + + var endpoint = new InlayHintEndpoint(service, languageServer); + + var request = new InlayHintParams() + { + TextDocument = new VSTextDocumentIdentifier + { + Uri = new Uri(razorFilePath) + }, + Range = VsLspFactory.CreateRange(startLine, starChar, endLine, endChar) + }; + Assert.True(DocumentContextFactory.TryCreate(request.TextDocument, out var documentContext)); + var requestContext = CreateRazorRequestContext(documentContext); + + // Act + var hints = await endpoint.HandleRequestAsync(request, requestContext, DisposalToken); + + // Assert + Assert.Null(hints); + } + private async Task VerifyInlayHintsAsync(string input, Dictionary toolTipMap, string output) { TestFileMarkupParser.GetSpans(input, out input, out ImmutableDictionary> spansDict); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/MapCode/MapCodeTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/MapCode/MapCodeTest.cs index 31ea7e1b81e..0fa73622a5a 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/MapCode/MapCodeTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/MapCode/MapCodeTest.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Razor.LanguageServer.MapCode; using Microsoft.AspNetCore.Razor.Telemetry; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.AspNetCore.Razor.Test.Common.Mef; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Testing; @@ -22,7 +21,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.MapCode; -[UseExportProvider] public class MapCodeTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) { private const string RazorFilePath = "C:/path/to/file.razor"; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/OpenDocumentGeneratorTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/OpenDocumentGeneratorTest.cs index b5041a4ee7e..7c05c89c158 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/OpenDocumentGeneratorTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/OpenDocumentGeneratorTest.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; +using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -39,7 +40,7 @@ await projectManager.UpdateAsync(updater => { updater.ProjectAdded(_hostProject1); updater.ProjectAdded(_hostProject2); - updater.DocumentAdded(_hostProject1.Key, _documents[0], null!); + updater.DocumentAdded(_hostProject1.Key, _documents[0], _documents[0].CreateEmptyTextLoader()); updater.DocumentOpened(_hostProject1.Key, _documents[0].FilePath, SourceText.From(string.Empty)); }); @@ -49,7 +50,7 @@ await projectManager.UpdateAsync(updater => await projectManager.UpdateAsync(updater => { updater.DocumentRemoved(_hostProject1.Key, _documents[0]); - updater.DocumentAdded(_hostProject2.Key, _documents[0], null!); + updater.DocumentAdded(_hostProject2.Key, _documents[0], _documents[0].CreateEmptyTextLoader()); }); // Assert @@ -72,7 +73,7 @@ await projectManager.UpdateAsync(updater => updater.ProjectAdded(_hostProject2); // Act - updater.DocumentAdded(_hostProject1.Key, _documents[0], null!); + updater.DocumentAdded(_hostProject1.Key, _documents[0], _documents[0].CreateEmptyTextLoader()); }); // Assert @@ -91,7 +92,7 @@ await projectManager.UpdateAsync(updater => { updater.ProjectAdded(_hostProject1); updater.ProjectAdded(_hostProject2); - updater.DocumentAdded(_hostProject1.Key, _documents[0], null!); + updater.DocumentAdded(_hostProject1.Key, _documents[0], _documents[0].CreateEmptyTextLoader()); // Act updater.DocumentChanged(_hostProject1.Key, _documents[0].FilePath, SourceText.From("new")); @@ -113,7 +114,7 @@ await projectManager.UpdateAsync(updater => { updater.ProjectAdded(_hostProject1); updater.ProjectAdded(_hostProject2); - updater.DocumentAdded(_hostProject1.Key, _documents[0], null!); + updater.DocumentAdded(_hostProject1.Key, _documents[0], _documents[0].CreateEmptyTextLoader()); updater.DocumentOpened(_hostProject1.Key, _documents[0].FilePath, SourceText.From(string.Empty)); // Act @@ -138,7 +139,7 @@ await projectManager.UpdateAsync(updater => { updater.ProjectAdded(_hostProject1); updater.ProjectAdded(_hostProject2); - updater.DocumentAdded(_hostProject1.Key, _documents[0], null!); + updater.DocumentAdded(_hostProject1.Key, _documents[0], _documents[0].CreateEmptyTextLoader()); // Act updater.ProjectWorkspaceStateChanged(_hostProject1.Key, @@ -161,7 +162,7 @@ await projectManager.UpdateAsync(updater => { updater.ProjectAdded(_hostProject1); updater.ProjectAdded(_hostProject2); - updater.DocumentAdded(_hostProject1.Key, _documents[0], null!); + updater.DocumentAdded(_hostProject1.Key, _documents[0], _documents[0].CreateEmptyTextLoader()); updater.DocumentOpened(_hostProject1.Key, _documents[0].FilePath, SourceText.From(string.Empty)); // Act diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProjectSystem/IProjectSnapshotManagerExtensionsTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProjectSystem/IProjectSnapshotManagerExtensionsTest.cs index 525614171b6..c124083560f 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProjectSystem/IProjectSnapshotManagerExtensionsTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/ProjectSystem/IProjectSnapshotManagerExtensionsTest.cs @@ -39,12 +39,10 @@ public async Task TryResolveDocumentInAnyProject_AsksMiscellaneousProjectForDocu await projectManager.UpdateAsync(updater => { - var miscProject = projectManager.GetMiscellaneousProject(); - var hostProject = new HostProject(miscProject.FilePath, miscProject.IntermediateOutputPath, FallbackRazorConfiguration.Latest, miscProject.RootNamespace); - updater.DocumentAdded( - hostProject.Key, - new HostDocument(normalizedFilePath, "document.cshtml"), - new EmptyTextLoader(normalizedFilePath)); + var hostProject = MiscFilesHostProject.Instance with { Configuration = FallbackRazorConfiguration.Latest }; + var hostDocument = new HostDocument(normalizedFilePath, targetPath: "document.cshtml"); + + updater.DocumentAdded(hostProject.Key, hostDocument, hostDocument.CreateEmptyTextLoader()); }); // Act @@ -110,11 +108,13 @@ public async Task TryResolveAllProjects_UnrelatedProject_ReturnsFalse() { // Arrange var documentFilePath = "C:/path/to/document.cshtml"; + var hostProject = TestHostProject.Create("C:/other/path/to/project.csproj"); + var projectManager = CreateProjectSnapshotManager(); await projectManager.UpdateAsync(updater => { - updater.ProjectAdded(TestProjectSnapshot.Create("C:/other/path/to/project.csproj").HostProject); + updater.ProjectAdded(hostProject); }); // Act @@ -125,20 +125,23 @@ await projectManager.UpdateAsync(updater => public async Task TryResolveAllProjects_OwnerProjectWithOthers_ReturnsTrue() { // Arrange - var documentFilePath = "C:/path/to/document.cshtml"; + var hostProject = TestHostProject.Create("C:/path/to/project.csproj"); + var hostDocument = TestHostDocument.Create(hostProject, "C:/path/to/document.cshtml"); + var otherHostProject = TestHostProject.Create("C:/path/to/other/project.csproj"); + var projectManager = CreateProjectSnapshotManager(); var expectedProject = await projectManager.UpdateAsync(updater => { - var expectedProject = updater.CreateAndAddProject("C:/path/to/project.csproj"); - updater.CreateAndAddProject("C:/path/to/other/project.csproj"); - updater.CreateAndAddDocument(expectedProject, documentFilePath); + updater.ProjectAdded(hostProject); + updater.ProjectAdded(otherHostProject); + updater.DocumentAdded(hostProject.Key, hostDocument, hostDocument.CreateEmptyTextLoader()); - return expectedProject; + return updater.GetLoadedProject(hostProject.Key); }); // Act - Assert.True(projectManager.TryResolveAllProjects(documentFilePath, out var projects)); + Assert.True(projectManager.TryResolveAllProjects(hostDocument.FilePath, out var projects)); // Assert var project = Assert.Single(projects); @@ -149,18 +152,21 @@ public async Task TryResolveAllProjects_OwnerProjectWithOthers_ReturnsTrue() public async Task TryResolveAllProjects_MiscellaneousOwnerProjectWithOthers_ReturnsTrue() { // Arrange - var documentFilePath = Path.Combine(MiscFilesHostProject.Instance.DirectoryPath, "file.cshtml"); + var miscFilesHostProject = MiscFilesHostProject.Instance; + var documentFilePath = Path.Combine(miscFilesHostProject.DirectoryPath, "file.cshtml"); documentFilePath = FilePathNormalizer.Normalize(documentFilePath); + var hostDocument = TestHostDocument.Create(miscFilesHostProject, documentFilePath); + var hostProject = TestHostProject.Create("C:/path/to/project.csproj"); + var projectManager = CreateProjectSnapshotManager(); var miscProject = await projectManager.UpdateAsync(updater => { - var miscProject = (ProjectSnapshot)projectManager.GetMiscellaneousProject(); - updater.CreateAndAddDocument(miscProject, documentFilePath); - updater.CreateAndAddProject("C:/path/to/project.csproj"); + updater.DocumentAdded(miscFilesHostProject.Key, hostDocument, hostDocument.CreateEmptyTextLoader()); + updater.ProjectAdded(hostProject); - return miscProject; + return updater.GetLoadedProject(miscFilesHostProject.Key); }); // Act @@ -175,19 +181,21 @@ public async Task TryResolveAllProjects_MiscellaneousOwnerProjectWithOthers_Retu public async Task TryResolveAllProjects_OwnerProjectDifferentCasing_ReturnsTrue() { // Arrange - var documentFilePath = "c:/path/to/document.cshtml"; + var hostProject = TestHostProject.Create("C:/Path/To/project.csproj"); + var hostDocument = TestHostDocument.Create(hostProject, "c:/path/to/document.cshtml"); + var projectManager = CreateProjectSnapshotManager(); var ownerProject = await projectManager.UpdateAsync(updater => { - var ownerProject = updater.CreateAndAddProject("C:/Path/To/project.csproj"); - updater.CreateAndAddDocument(ownerProject, documentFilePath); + updater.ProjectAdded(hostProject); + updater.DocumentAdded(hostProject.Key, hostDocument, hostDocument.CreateEmptyTextLoader()); - return ownerProject; + return updater.GetLoadedProject(hostProject.Key); }); // Act - Assert.True(projectManager.TryResolveAllProjects(documentFilePath, out var projects)); + Assert.True(projectManager.TryResolveAllProjects(hostDocument.FilePath, out var projects)); // Assert var project = Assert.Single(projects); @@ -222,32 +230,36 @@ public void GetMiscellaneousProject_ProjectNotLoaded_CreatesProjectAndReturnsCre Assert.Equal(MiscFilesHostProject.Instance.FilePath, project.FilePath); } - private async Task CreateProjectManagerAsync(string filePath, bool addToMiscellaneous = false) + private async Task CreateProjectManagerAsync(string documentFilePath, bool addToMiscellaneous = false) { - filePath = FilePathNormalizer.Normalize(filePath); + documentFilePath = FilePathNormalizer.Normalize(documentFilePath); var projectManager = CreateProjectSnapshotManager(); + HostProject hostProject; + if (addToMiscellaneous) { - await projectManager.UpdateAsync(updater => - { - var miscProject = (ProjectSnapshot)projectManager.GetMiscellaneousProject(); - updater.CreateAndAddDocument(miscProject, filePath); - }); + hostProject = MiscFilesHostProject.Instance; } else { - var projectDirectory = FilePathNormalizer.GetNormalizedDirectoryName(filePath); - var projectSnapshot = TestProjectSnapshot.Create(Path.Combine(projectDirectory, "proj.csproj")); + var projectDirectory = FilePathNormalizer.GetNormalizedDirectoryName(documentFilePath); + hostProject = TestHostProject.Create(Path.Combine(projectDirectory, "proj.csproj")); await projectManager.UpdateAsync(updater => { - updater.ProjectAdded(projectSnapshot.HostProject); - updater.CreateAndAddDocument(projectSnapshot, filePath); + updater.ProjectAdded(hostProject); }); } + var hostDocument = TestHostDocument.Create(hostProject, documentFilePath); + + await projectManager.UpdateAsync(updater => + { + updater.DocumentAdded(hostProject.Key, hostDocument, hostDocument.CreateEmptyTextLoader()); + }); + return projectManager; } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorComponentSearchEngineTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorComponentSearchEngineTest.cs index 3f372b9f668..9dfc3afdccd 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorComponentSearchEngineTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorComponentSearchEngineTest.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Workspaces; @@ -41,7 +40,7 @@ public class RazorComponentSearchEngineTest(ITestOutputHelper testOutput) : Lang private static readonly string s_componentFilePath3 = Path.Combine(s_project2BasePath, "Component3.razor"); #nullable disable - private TestProjectSnapshotManager _projectManager; + private IProjectSnapshotManager _projectManager; #nullable enable protected override async Task InitializeAsync() @@ -65,8 +64,9 @@ protected override async Task InitializeAsync() remoteTextLoaderFactoryMock.Object, _projectManager, LoggerFactory); + AddDisposable(projectService); - await projectService.AddProjectAsync( + await projectService.GetTestAccessor().AddProjectAsync( s_projectFilePath1, s_intermediateOutputPath1, RazorConfiguration.Default, @@ -80,7 +80,7 @@ await projectService.AddProjectAsync( await projectService.AddDocumentToPotentialProjectsAsync(s_componentFilePath2, DisposalToken); await projectService.UpdateDocumentAsync(s_componentFilePath2, SourceText.From("@namespace Test"), DisposalToken); - await projectService.AddProjectAsync( + await projectService.GetTestAccessor().AddProjectAsync( s_projectFilePath2, s_intermediateOutputPath2, RazorConfiguration.Default, @@ -98,12 +98,11 @@ public async Task Handle_SearchFound_GenericComponent() // Arrange var tagHelperDescriptor1 = CreateRazorComponentTagHelperDescriptor("First", RootNamespace1, "Component1", typeName: "Component1"); var tagHelperDescriptor2 = CreateRazorComponentTagHelperDescriptor("Second", RootNamespace2, "Component3", typeName: "Component3"); - var searchEngine = new RazorComponentSearchEngine(_projectManager, LoggerFactory); - var snapshot = StrictMock.Of(); + var searchEngine = new RazorComponentSearchEngine(LoggerFactory); // Act - var documentSnapshot1 = await searchEngine.TryLocateComponentAsync(snapshot, tagHelperDescriptor1); - var documentSnapshot2 = await searchEngine.TryLocateComponentAsync(snapshot, tagHelperDescriptor2); + var documentSnapshot1 = await searchEngine.TryLocateComponentAsync(tagHelperDescriptor1, _projectManager.GetQueryOperations(), DisposalToken); + var documentSnapshot2 = await searchEngine.TryLocateComponentAsync(tagHelperDescriptor2, _projectManager.GetQueryOperations(), DisposalToken); // Assert Assert.NotNull(documentSnapshot1); @@ -118,12 +117,11 @@ public async Task Handle_SearchFound() // Arrange var tagHelperDescriptor1 = CreateRazorComponentTagHelperDescriptor("First", RootNamespace1, "Component1"); var tagHelperDescriptor2 = CreateRazorComponentTagHelperDescriptor("Second", RootNamespace2, "Component3"); - var searchEngine = new RazorComponentSearchEngine(_projectManager, LoggerFactory); - var snapshot = StrictMock.Of(); + var searchEngine = new RazorComponentSearchEngine(LoggerFactory); // Act - var documentSnapshot1 = await searchEngine.TryLocateComponentAsync(snapshot, tagHelperDescriptor1); - var documentSnapshot2 = await searchEngine.TryLocateComponentAsync(snapshot, tagHelperDescriptor2); + var documentSnapshot1 = await searchEngine.TryLocateComponentAsync(tagHelperDescriptor1, _projectManager.GetQueryOperations(), DisposalToken); + var documentSnapshot2 = await searchEngine.TryLocateComponentAsync(tagHelperDescriptor2, _projectManager.GetQueryOperations(), DisposalToken); // Assert Assert.NotNull(documentSnapshot1); @@ -137,11 +135,10 @@ public async Task Handle_SearchFound_SetNamespace() { // Arrange var tagHelperDescriptor = CreateRazorComponentTagHelperDescriptor("First", "Test", "Component2"); - var searchEngine = new RazorComponentSearchEngine(_projectManager, LoggerFactory); - var snapshot = StrictMock.Of(); + var searchEngine = new RazorComponentSearchEngine(LoggerFactory); // Act - var documentSnapshot = await searchEngine.TryLocateComponentAsync(snapshot, tagHelperDescriptor); + var documentSnapshot = await searchEngine.TryLocateComponentAsync(tagHelperDescriptor, _projectManager.GetQueryOperations(), DisposalToken); // Assert Assert.NotNull(documentSnapshot); @@ -153,11 +150,10 @@ public async Task Handle_SearchMissing_IncorrectAssembly() { // Arrange var tagHelperDescriptor = CreateRazorComponentTagHelperDescriptor("Third", RootNamespace1, "Component3"); - var searchEngine = new RazorComponentSearchEngine(_projectManager, LoggerFactory); - var snapshot = StrictMock.Of(); + var searchEngine = new RazorComponentSearchEngine(LoggerFactory); // Act - var documentSnapshot = await searchEngine.TryLocateComponentAsync(snapshot, tagHelperDescriptor); + var documentSnapshot = await searchEngine.TryLocateComponentAsync(tagHelperDescriptor, _projectManager.GetQueryOperations(), DisposalToken); // Assert Assert.Null(documentSnapshot); @@ -168,11 +164,10 @@ public async Task Handle_SearchMissing_IncorrectNamespace() { // Arrange var tagHelperDescriptor = CreateRazorComponentTagHelperDescriptor("First", RootNamespace1, "Component2"); - var searchEngine = new RazorComponentSearchEngine(_projectManager, LoggerFactory); - var snapshot = StrictMock.Of(); + var searchEngine = new RazorComponentSearchEngine(LoggerFactory); // Act - var documentSnapshot = await searchEngine.TryLocateComponentAsync(snapshot, tagHelperDescriptor); + var documentSnapshot = await searchEngine.TryLocateComponentAsync(tagHelperDescriptor, _projectManager.GetQueryOperations(), DisposalToken); // Assert Assert.Null(documentSnapshot); @@ -183,11 +178,10 @@ public async Task Handle_SearchMissing_IncorrectComponent() { // Arrange var tagHelperDescriptor = CreateRazorComponentTagHelperDescriptor("First", RootNamespace1, "Component3"); - var searchEngine = new RazorComponentSearchEngine(_projectManager, LoggerFactory); - var snapshot = StrictMock.Of(); + var searchEngine = new RazorComponentSearchEngine(LoggerFactory); // Act - var documentSnapshot = await searchEngine.TryLocateComponentAsync(snapshot, tagHelperDescriptor); + var documentSnapshot = await searchEngine.TryLocateComponentAsync(tagHelperDescriptor, _projectManager.GetQueryOperations(), DisposalToken); // Assert Assert.Null(documentSnapshot); @@ -198,11 +192,10 @@ public async Task Handle_FilePathAndAssemblyNameDifferent() { // Arrange var tagHelperDescriptor = CreateRazorComponentTagHelperDescriptor("AssemblyName", "Test", "Component2"); - var searchEngine = new RazorComponentSearchEngine(_projectManager, LoggerFactory); - var snapshot = StrictMock.Of(); + var searchEngine = new RazorComponentSearchEngine(LoggerFactory); // Act - var documentSnapshot = await searchEngine.TryLocateComponentAsync(snapshot, tagHelperDescriptor); + var documentSnapshot = await searchEngine.TryLocateComponentAsync(tagHelperDescriptor, _projectManager.GetQueryOperations(), DisposalToken); // Assert Assert.NotNull(documentSnapshot); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLSPOptionsMonitorTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLSPOptionsMonitorTest.cs index 923c290ca46..973d319eb9e 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLSPOptionsMonitorTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLSPOptionsMonitorTest.cs @@ -23,7 +23,7 @@ public RazorLSPOptionsMonitorTest(ITestOutputHelper testOutput) public async Task UpdateAsync_Invokes_OnChangeRegistration() { // Arrange - var expectedOptions = new RazorLSPOptions(EnableFormatting: false, AutoClosingTags: true, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, FormatOnType: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: true); + var expectedOptions = new RazorLSPOptions(FormattingFlags.Disabled, AutoClosingTags: true, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: true); var configService = Mock.Of( f => f.GetLatestOptionsAsync(DisposalToken) == Task.FromResult(expectedOptions), MockBehavior.Strict); @@ -45,7 +45,7 @@ public async Task UpdateAsync_Invokes_OnChangeRegistration() public async Task UpdateAsync_DoesNotInvoke_OnChangeRegistration_AfterDispose() { // Arrange - var expectedOptions = new RazorLSPOptions(EnableFormatting: false, AutoClosingTags: true, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, FormatOnType: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: true); + var expectedOptions = new RazorLSPOptions(FormattingFlags.Disabled, AutoClosingTags: true, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: true); var configService = Mock.Of( f => f.GetLatestOptionsAsync(DisposalToken) == Task.FromResult(expectedOptions), MockBehavior.Strict); @@ -91,7 +91,7 @@ public async Task UpdateAsync_ConfigReturnsNull_DoesNotInvoke_OnChangeRegistrati public void InitializedOptionsAreCurrent() { // Arrange - var expectedOptions = new RazorLSPOptions(EnableFormatting: false, AutoClosingTags: true, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, FormatOnType: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: true); + var expectedOptions = new RazorLSPOptions(FormattingFlags.Disabled, AutoClosingTags: true, InsertSpaces: true, TabSize: 4, AutoShowCompletion: true, AutoListParams: true, AutoInsertAttributeQuotes: true, ColorBackground: false, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: true); var configService = Mock.Of( f => f.GetLatestOptionsAsync(DisposalToken) == Task.FromResult(expectedOptions), MockBehavior.Strict); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLanguageServerTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLanguageServerTest.cs index 3fc2585b22d..700d8ca7a20 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLanguageServerTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorLanguageServerTest.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Razor.Telemetry; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CommonLanguageServerProtocol.Framework; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -44,11 +45,16 @@ public async Task LocaleIsSetCorrectly() await queue.ExecuteAsync(initializeParams, Methods.InitializeName, server.GetLspServices(), DisposalToken); // We have to send one more request, because culture is set before any request starts, but the first initialize request has to - // be started in order to set the culture. - // The request isn't actually valid, so we wrap it in a try catch, but we don't care for this test + // be started in order to set the culture. The request must be valid because the culture is set in `BeforeRequest` but it doesn't + // have to succeed. try { - await queue.ExecuteAsync(JsonSerializer.SerializeToElement(new object()), VSInternalMethods.DocumentPullDiagnosticName, server.GetLspServices(), DisposalToken); + var namedPipeParams = new RazorNamedPipeConnectParams() + { + PipeName = "" + }; + + await queue.ExecuteAsync(JsonSerializer.SerializeToElement(namedPipeParams), CustomMessageNames.RazorNamedPipeConnectEndpointName, server.GetLspServices(), DisposalToken); } catch { } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorProjectServiceTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorProjectServiceTest.cs index 5c10d288509..a12f848949f 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorProjectServiceTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorProjectServiceTest.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; -using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -33,6 +31,7 @@ public class RazorProjectServiceTest(ITestOutputHelper testOutput) : LanguageSer #nullable disable private TestProjectSnapshotManager _projectManager; private TestRazorProjectService _projectService; + private IRazorProjectInfoListener _projectInfoListener; #nullable enable protected override Task InitializeAsync() @@ -51,11 +50,13 @@ protected override Task InitializeAsync() _projectManager, LoggerFactory); + _projectInfoListener = _projectService; + return Task.CompletedTask; } [Fact] - public async Task UpdateProject_UpdatesProjectWorkspaceState() + public async Task IProjectInfoListener_UpdatedAsync_UpdatesProjectWorkspaceState() { // Arrange var hostProject = new HostProject("C:/path/to/project.csproj", "C:/path/to/obj", RazorConfiguration.Default, "TestRootNamespace"); @@ -68,13 +69,14 @@ await _projectManager.UpdateAsync(updater => var projectWorkspaceState = ProjectWorkspaceState.Create(LanguageVersion.LatestMajor); // Act - await _projectService.UpdateProjectAsync( + await _projectInfoListener.UpdatedAsync(new RazorProjectInfo( hostProject.Key, + hostProject.FilePath, hostProject.Configuration, hostProject.RootNamespace, hostProject.DisplayName, projectWorkspaceState, - documents: [], + documents: []), DisposalToken); // Assert @@ -83,7 +85,7 @@ await _projectService.UpdateProjectAsync( } [Fact] - public async Task UpdateProject_UpdatingDocument_MapsRelativeFilePathToActualDocument() + public async Task IProjectInfoListener_UpdatedAsync_UpdatingDocument_MapsRelativeFilePathToActualDocument() { // Arrange var hostProject = new HostProject("C:/path/to/project.csproj", "C:/path/to/obj", RazorConfiguration.Default, "TestRootNamespace"); @@ -98,13 +100,14 @@ await _projectManager.UpdateAsync(updater => var newDocument = new DocumentSnapshotHandle("file.cshtml", "file.cshtml", FileKinds.Component); // Act - await _projectService.UpdateProjectAsync( + await _projectInfoListener.UpdatedAsync(new RazorProjectInfo( hostProject.Key, + hostProject.FilePath, hostProject.Configuration, hostProject.RootNamespace, hostProject.DisplayName, ProjectWorkspaceState.Default, - [newDocument], + [newDocument]), DisposalToken); // Assert @@ -115,7 +118,7 @@ await _projectService.UpdateProjectAsync( } [Fact] - public async Task UpdateProject_AddsNewDocuments() + public async Task IProjectInfoListener_UpdatedAsync_AddsNewDocuments() { // Arrange var hostProject = new HostProject("C:/path/to/project.csproj", "C:/path/to/obj", RazorConfiguration.Default, "TestRootNamespace"); @@ -131,13 +134,14 @@ await _projectManager.UpdateAsync(updater => var newDocument = new DocumentSnapshotHandle("C:/path/to/file2.cshtml", "file2.cshtml", FileKinds.Legacy); // Act - await _projectService.UpdateProjectAsync( + await _projectInfoListener.UpdatedAsync(new RazorProjectInfo( hostProject.Key, + hostProject.FilePath, hostProject.Configuration, hostProject.RootNamespace, hostProject.DisplayName, ProjectWorkspaceState.Default, - [oldDocument, newDocument], + [oldDocument, newDocument]), DisposalToken); // Assert @@ -147,7 +151,7 @@ await _projectService.UpdateProjectAsync( } [Fact] - public async Task UpdateProject_MovesDocumentsFromMisc() + public async Task IProjectInfoListener_UpdatedAsync_MovesDocumentsFromMisc() { // Arrange var hostProject = new HostProject("C:/path/to/project.csproj", "C:/path/to/obj", RazorConfiguration.Default, "TestRootNamespace"); @@ -166,13 +170,14 @@ await _projectManager.UpdateAsync(updater => var addedDocument = new DocumentSnapshotHandle(hostDocument.FilePath, hostDocument.TargetPath, hostDocument.FileKind); // Act - await _projectService.UpdateProjectAsync( + await _projectInfoListener.UpdatedAsync(new RazorProjectInfo( hostProject.Key, + hostProject.FilePath, hostProject.Configuration, hostProject.RootNamespace, hostProject.DisplayName, ProjectWorkspaceState.Default, - [addedDocument], + [addedDocument]), DisposalToken); // Assert @@ -184,7 +189,7 @@ await _projectService.UpdateProjectAsync( } [Fact] - public async Task UpdateProject_MovesDocumentsFromMisc_ViaService() + public async Task IProjectInfoListener_UpdatedAsync_MovesDocumentsFromMisc_ViaService() { // Arrange const string DocumentFilePath = "C:/path/to/file.cshtml"; @@ -192,7 +197,7 @@ public async Task UpdateProject_MovesDocumentsFromMisc_ViaService() const string IntermediateOutputPath = "C:/path/to/obj"; const string RootNamespace = "TestRootNamespace"; - var ownerProjectKey = await _projectService.AddProjectAsync( + var ownerProjectKey = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); await _projectService.AddDocumentToMiscProjectAsync(DocumentFilePath, DisposalToken); @@ -204,13 +209,14 @@ public async Task UpdateProject_MovesDocumentsFromMisc_ViaService() var addedDocument = new DocumentSnapshotHandle(DocumentFilePath, DocumentFilePath, FileKinds.Legacy); // Act - await _projectService.UpdateProjectAsync( + await _projectInfoListener.UpdatedAsync(new RazorProjectInfo( project.Key, + project.FilePath, project.Configuration, project.RootNamespace, project.DisplayName, ProjectWorkspaceState.Default, - [addedDocument], + [addedDocument]), DisposalToken); // Assert @@ -222,7 +228,7 @@ await _projectService.UpdateProjectAsync( } [Fact] - public async Task UpdateProject_MovesExistingDocumentToMisc() + public async Task IProjectInfoListener_UpdatedAsync_MovesExistingDocumentToMisc() { // Arrange var hostProject = new HostProject("C:/path/to/project.csproj", "C:/path/to/obj", RazorConfiguration.Default, "TestRootNamespace"); @@ -243,13 +249,14 @@ public async Task UpdateProject_MovesExistingDocumentToMisc() var newDocument = new DocumentSnapshotHandle("C:/path/to/file2.cshtml", "file2.cshtml", FileKinds.Legacy); // Act - await _projectService.UpdateProjectAsync( + await _projectInfoListener.UpdatedAsync(new RazorProjectInfo( hostProject.Key, + hostProject.FilePath, hostProject.Configuration, hostProject.RootNamespace, hostProject.DisplayName, ProjectWorkspaceState.Default, - [newDocument], + [newDocument]), DisposalToken); // Assert @@ -261,7 +268,7 @@ await _projectService.UpdateProjectAsync( } [Fact] - public async Task UpdateProject_KnownDocuments() + public async Task IProjectInfoListener_UpdatedAsync_KnownDocuments() { // Arrange var hostProject = new HostProject("path/to/project.csproj", "path/to/obj", RazorConfiguration.Default, "TestRootNamespace"); @@ -278,20 +285,21 @@ await _projectManager.UpdateAsync(updater => using var listener = _projectManager.ListenToNotifications(); // Act & Assert - await _projectService.UpdateProjectAsync( + await _projectInfoListener.UpdatedAsync(new RazorProjectInfo( hostProject.Key, + hostProject.FilePath, hostProject.Configuration, hostProject.RootNamespace, hostProject.DisplayName, ProjectWorkspaceState.Default, - [newDocument], + [newDocument]), DisposalToken); listener.AssertNoNotifications(); } [Fact] - public async Task UpdateProject_UpdatesLegacyDocumentsAsComponents() + public async Task IProjectInfoListener_UpdatedAsync_UpdatesLegacyDocumentsAsComponents() { // Arrange var hostProject = new HostProject("C:/path/to/project.csproj", "C:/path/to/obj", RazorConfiguration.Default, "TestRootNamespace"); @@ -306,13 +314,14 @@ await _projectManager.UpdateAsync(updater => var newDocument = new DocumentSnapshotHandle(legacyDocument.FilePath, legacyDocument.TargetPath, FileKinds.Component); // Act - await _projectService.UpdateProjectAsync( + await _projectInfoListener.UpdatedAsync(new RazorProjectInfo( hostProject.Key, + hostProject.FilePath, hostProject.Configuration, hostProject.RootNamespace, hostProject.DisplayName, ProjectWorkspaceState.Default, - [newDocument], + [newDocument]), DisposalToken); // Assert @@ -323,14 +332,14 @@ await _projectService.UpdateProjectAsync( } [Fact] - public async Task UpdateProject_SameConfigurationDifferentRootNamespace_UpdatesRootNamespace() + public async Task IProjectInfoListener_UpdatedAsync_SameConfigurationDifferentRootNamespace_UpdatesRootNamespace() { // Arrange const string ProjectFilePath = "C:/path/to/project.csproj"; const string IntermediateOutputPath = "C:/path/to/obj"; const string NewRootNamespace = "NewRootNamespace"; - var ownerProjectKey = await _projectService.AddProjectAsync( + var ownerProjectKey = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, rootNamespace: null, displayName: null, DisposalToken); var ownerProject = _projectManager.GetLoadedProject(ownerProjectKey); @@ -338,13 +347,14 @@ public async Task UpdateProject_SameConfigurationDifferentRootNamespace_UpdatesR using var listener = _projectManager.ListenToNotifications(); // Act - await _projectService.UpdateProjectAsync( + await _projectInfoListener.UpdatedAsync(new RazorProjectInfo( ownerProject.Key, + ownerProject.FilePath, ownerProject.Configuration, NewRootNamespace, ownerProject.DisplayName, ProjectWorkspaceState.Default, - documents: [], + documents: []), DisposalToken); var notification = Assert.Single(listener); @@ -355,14 +365,14 @@ await _projectService.UpdateProjectAsync( } [Fact] - public async Task UpdateProject_SameConfigurationAndRootNamespaceNoops() + public async Task IProjectInfoListener_UpdatedAsync_SameConfigurationAndRootNamespaceNoops() { // Arrange const string ProjectFilePath = "C:/path/to/project.csproj"; const string IntermediateOutputPath = "C:/path/to/obj"; const string RootNamespace = "TestRootNamespace"; - var ownerProjectKey = await _projectService.AddProjectAsync( + var ownerProjectKey = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); var ownerProject = _projectManager.GetLoadedProject(ownerProjectKey); @@ -370,58 +380,28 @@ public async Task UpdateProject_SameConfigurationAndRootNamespaceNoops() using var listener = _projectManager.ListenToNotifications(); // Act & Assert - await _projectService.UpdateProjectAsync( + await _projectInfoListener.UpdatedAsync(new RazorProjectInfo( ownerProject.Key, + ownerProject.FilePath, ownerProject.Configuration, ownerProject.RootNamespace, displayName: "", ProjectWorkspaceState.Default, - documents: [], + documents: []), DisposalToken); listener.AssertNoNotifications(); } [Fact] - public async Task UpdateProject_NullConfigurationUsesDefault() - { - // Arrange - const string ProjectFilePath = "C:/path/to/project.csproj"; - const string IntermediateOutputPath = "C:/path/to/obj"; - const string RootNamespace = "TestRootNamespace"; - - var ownerProjectKey = await _projectService.AddProjectAsync( - ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); - - var ownerProject = _projectManager.GetLoadedProject(ownerProjectKey); - - using var listener = _projectManager.ListenToNotifications(); - - // Act - await _projectService.UpdateProjectAsync( - ownerProject.Key, - configuration: null, - "TestRootNamespace", - displayName: "", - ProjectWorkspaceState.Default, - documents: [], - DisposalToken); - - // Assert - var notification = Assert.Single(listener); - Assert.NotNull(notification.Newer); - Assert.Same(FallbackRazorConfiguration.Latest, notification.Newer.Configuration); - } - - [Fact] - public async Task UpdateProject_ChangesProjectToUseProvidedConfiguration() + public async Task IProjectInfoListener_UpdatedAsync_ChangesProjectToUseProvidedConfiguration() { // Arrange const string ProjectFilePath = "C:/path/to/project.csproj"; const string IntermediateOutputPath = "C:/path/to/obj"; const string RootNamespace = "TestRootNamespace"; - var ownerProjectKey = await _projectService.AddProjectAsync( + var ownerProjectKey = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); var ownerProject = _projectManager.GetLoadedProject(ownerProjectKey); @@ -429,13 +409,14 @@ public async Task UpdateProject_ChangesProjectToUseProvidedConfiguration() using var listener = _projectManager.ListenToNotifications(); // Act - await _projectService.UpdateProjectAsync( + await _projectInfoListener.UpdatedAsync(new RazorProjectInfo( ownerProject.Key, + ownerProject.FilePath, FallbackRazorConfiguration.MVC_1_1, "TestRootNamespace", displayName: "", ProjectWorkspaceState.Default, - documents: [], + documents: []), DisposalToken); // Assert @@ -444,25 +425,6 @@ await _projectService.UpdateProjectAsync( Assert.Same(FallbackRazorConfiguration.MVC_1_1, notification.Newer.Configuration); } - [Fact] - public async Task UpdateProject_UntrackedProjectNoops() - { - // Arrange - using var listener = _projectManager.ListenToNotifications(); - - // Act & Assert - await _projectService.UpdateProjectAsync( - TestProjectKey.Create("C:/path/to/obj"), - FallbackRazorConfiguration.MVC_1_1, - "TestRootNamespace", - displayName: "", - ProjectWorkspaceState.Default, - documents: [], - DisposalToken); - - listener.AssertNoNotifications(); - } - [Fact] public async Task CloseDocument_ClosesDocumentInOwnerProject() { @@ -472,7 +434,7 @@ public async Task CloseDocument_ClosesDocumentInOwnerProject() const string RootNamespace = "TestRootNamespace"; const string DocumentFilePath = "C:/path/to/document.cshtml"; - var ownerProjectKey = await _projectService.AddProjectAsync( + var ownerProjectKey = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); await _projectService.OpenDocumentAsync(DocumentFilePath, s_emptyText, DisposalToken); @@ -503,9 +465,9 @@ public async Task CloseDocument_ClosesDocumentInAllOwnerProjects() const string RootNamespace = "TestRootNamespace"; const string DocumentFilePath = "C:/path/to/document.cshtml"; - var ownerProjectKey1 = await _projectService.AddProjectAsync( + var ownerProjectKey1 = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath1, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); - var ownerProjectKey2 = await _projectService.AddProjectAsync( + var ownerProjectKey2 = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath2, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); await _projectService.OpenDocumentAsync(DocumentFilePath, s_emptyText, DisposalToken); @@ -562,7 +524,7 @@ public async Task OpenDocument_OpensAlreadyAddedDocumentInOwnerProject() const string RootNamespace = "TestRootNamespace"; const string DocumentFilePath = "C:/path/to/document.cshtml"; - var ownerProjectKey = await _projectService.AddProjectAsync( + var ownerProjectKey = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); @@ -592,9 +554,9 @@ public async Task OpenDocument_OpensAlreadyAddedDocumentInAllOwnerProjects() const string RootNamespace = "TestRootNamespace"; const string DocumentFilePath = "C:/path/to/document.cshtml"; - var ownerProjectKey1 = await _projectService.AddProjectAsync( + var ownerProjectKey1 = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath1, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); - var ownerProjectKey2 = await _projectService.AddProjectAsync( + var ownerProjectKey2 = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath2, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); @@ -649,7 +611,7 @@ public async Task OpenDocument_OpensAndAddsDocumentToMiscellaneousProject() const string RootNamespace = "TestRootNamespace"; const string DocumentFilePath = "C:/path/to/document.cshtml"; - var ownerProjectKey = await _projectService.AddProjectAsync( + var ownerProjectKey = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); var miscProject = _projectManager.GetMiscellaneousProject(); @@ -693,7 +655,7 @@ public async Task AddDocument_AddsDocumentToOwnerProject() const string RootNamespace = "TestRootNamespace"; const string DocumentFilePath = "C:/path/to/document.cshtml"; - var ownerProjectKey = await _projectService.AddProjectAsync( + var ownerProjectKey = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); var ownerProject = _projectManager.GetLoadedProject(ownerProjectKey); @@ -739,7 +701,7 @@ public async Task AddDocumentToMiscProjectAsync_IgnoresKnownDocument() const string RootNamespace = "TestRootNamespace"; const string DocumentFilePath = "C:/path/to/document.cshtml"; - await _projectService.AddProjectAsync( + await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); @@ -780,7 +742,7 @@ public async Task RemoveDocument_RemovesDocumentFromOwnerProject() const string RootNamespace = "TestRootNamespace"; const string DocumentFilePath = "C:/path/to/document.cshtml"; - var ownerProjectKey = await _projectService.AddProjectAsync( + var ownerProjectKey = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); @@ -808,9 +770,9 @@ public async Task RemoveDocument_RemovesDocumentFromAllOwnerProjects() const string RootNamespace = "TestRootNamespace"; const string DocumentFilePath = "C:/path/to/document.cshtml"; - var ownerProjectKey1 = await _projectService.AddProjectAsync( + var ownerProjectKey1 = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath1, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); - var ownerProjectKey2 = await _projectService.AddProjectAsync( + var ownerProjectKey2 = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath2, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); @@ -839,7 +801,7 @@ public async Task RemoveOpenDocument_RemovesDocumentFromOwnerProject_MovesToMisc const string RootNamespace = "TestRootNamespace"; const string DocumentFilePath = "C:/path/to/document.cshtml"; - var ownerProjectKey = await _projectService.AddProjectAsync( + var ownerProjectKey = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); await _projectService.OpenDocumentAsync(DocumentFilePath, s_emptyText, DisposalToken); @@ -925,7 +887,7 @@ public async Task UpdateDocument_ChangesDocumentInOwnerProject() const string RootNamespace = "TestRootNamespace"; const string DocumentFilePath = "C:/path/to/document.cshtml"; - var ownerProjectKey = await _projectService.AddProjectAsync( + var ownerProjectKey = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); await _projectService.OpenDocumentAsync(DocumentFilePath, s_emptyText, DisposalToken); @@ -956,9 +918,9 @@ public async Task UpdateDocument_ChangesDocumentInAllOwnerProjects() const string RootNamespace = "TestRootNamespace"; const string DocumentFilePath = "C:/path/to/document.cshtml"; - var ownerProjectKey1 = await _projectService.AddProjectAsync( + var ownerProjectKey1 = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath1, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); - var ownerProjectKey2 = await _projectService.AddProjectAsync( + var ownerProjectKey2 = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath2, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); await _projectService.OpenDocumentAsync(DocumentFilePath, s_emptyText, DisposalToken); @@ -1015,7 +977,7 @@ public async Task UpdateDocument_DocumentVersionUpdated() const string RootNamespace = "TestRootNamespace"; const string DocumentFilePath = "C:/path/to/document.cshtml"; - var ownerProjectKey = await _projectService.AddProjectAsync( + var ownerProjectKey = await _projectService.GetTestAccessor().AddProjectAsync( ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); await _projectService.AddDocumentToPotentialProjectsAsync(DocumentFilePath, DisposalToken); @@ -1035,45 +997,7 @@ public async Task UpdateDocument_DocumentVersionUpdated() } [Fact] - public async Task UpdateDocument_ThrowsForUnknownDocument() - { - // Arrange - const string ProjectFilePath = "C:/path/to/project.csproj"; - const string IntermediateOutputPath = "C:/path/to/obj"; - const string RootNamespace = "TestRootNamespace"; - const string DocumentFilePath = "C:/path/to/document.cshtml"; - - await _projectService.AddProjectAsync( - ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, RootNamespace, displayName: null, DisposalToken); - - // Act - await Assert.ThrowsAnyAsync(() => - { - return _projectService.UpdateDocumentAsync(DocumentFilePath, s_emptyText.Replace(0, 0, "Hello World"), DisposalToken); - }); - } - - [Fact] - public async Task AddProject_AddsProjectWithDefaultConfiguration() - { - // Arrange - const string ProjectFilePath = "C:/path/to/project.csproj"; - const string IntermediateOutputPath = "C:/path/to/obj"; - - // Act - var projectKey = await _projectService.AddProjectAsync( - ProjectFilePath, IntermediateOutputPath, configuration: null, rootNamespace: null, displayName: null, DisposalToken); - - var project = _projectManager.GetLoadedProject(projectKey); - - // Assert - Assert.Equal(ProjectFilePath, project.FilePath); - Assert.Same(FallbackRazorConfiguration.Latest, project.Configuration); - Assert.Null(project.RootNamespace); - } - - [Fact] - public async Task AddProject_AddsProjectWithSpecifiedConfiguration() + public async Task IRazorProjectInfoListener_UpdatedAsync_AddsProjectWithSpecifiedConfiguration() { // Arrange const string ProjectFilePath = "C:/path/to/project.csproj"; @@ -1081,10 +1005,18 @@ public async Task AddProject_AddsProjectWithSpecifiedConfiguration() const string RootNamespace = "My.Root.Namespace"; var configuration = new RazorConfiguration(RazorLanguageVersion.Version_1_0, "TestName", Extensions: []); + var projectKey = new ProjectKey(IntermediateOutputPath); // Act - var projectKey = await _projectService.AddProjectAsync( - ProjectFilePath, IntermediateOutputPath, configuration, RootNamespace, displayName: null, DisposalToken); + await _projectInfoListener.UpdatedAsync(new RazorProjectInfo( + projectKey, + ProjectFilePath, + configuration, + RootNamespace, + "ProjectDisplayName", + ProjectWorkspaceState.Default, + documents: []), + DisposalToken); var project = _projectManager.GetLoadedProject(projectKey); @@ -1094,132 +1026,6 @@ public async Task AddProject_AddsProjectWithSpecifiedConfiguration() Assert.Equal(RootNamespace, project.RootNamespace); } - [Fact] - public async Task AddProject_DoesNotMigrateMiscellaneousDocumentIfNewProjectNotACandidate() - { - // Arrange - const string ProjectFilePath = "C:/other-path/to/project.csproj"; - const string IntermediateOutputPath = "C:/other-path/to/obj"; - const string DocumentFilePath1 = "C:/path/to/document1.cshtml"; - const string DocumentFilePath2 = "C:/path/to/document2.cshtml"; - - var miscProject = _projectManager.GetMiscellaneousProject(); - - await _projectManager.UpdateAsync(updater => - { - updater.DocumentAdded(miscProject.Key, - new HostDocument(DocumentFilePath1, "document1.cshtml"), CreateEmptyTextLoader()); - updater.DocumentAdded(miscProject.Key, - new HostDocument(DocumentFilePath2, "document2.cshtml"), CreateEmptyTextLoader()); - }); - - using var listener = _projectManager.ListenToNotifications(); - - // Act - var newProjectKey = await _projectService.AddProjectAsync( - ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, rootNamespace: null, displayName: null, DisposalToken); - - // Assert - listener.AssertNotifications( - x => x.ProjectAdded(ProjectFilePath, newProjectKey)); - } - - [Fact] - public async Task AddProject_MigratesMiscellaneousDocumentsToNewOwnerProject() - { - // Arrange - const string ProjectFilePath = "C:/path/to/project.csproj"; - const string IntermediateOutputPath = "C:/path/to/obj"; - const string DocumentFilePath1 = "C:/path/to/document1.cshtml"; - const string DocumentFilePath2 = "C:/path/to/document2.cshtml"; - - var miscProject = _projectManager.GetMiscellaneousProject(); - - await _projectManager.UpdateAsync(updater => - { - updater.DocumentAdded(miscProject.Key, - new HostDocument(DocumentFilePath1, "document1.cshtml"), CreateEmptyTextLoader()); - updater.DocumentAdded(miscProject.Key, - new HostDocument(DocumentFilePath2, "document2.cshtml"), CreateEmptyTextLoader()); - }); - - using var listener = _projectManager.ListenToNotifications(); - - // Act - var newProjectKey = await _projectService.AddProjectAsync( - ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, rootNamespace: null, displayName: null, DisposalToken); - - // Assert - - // AddProject iterates through a dictionary to migrate documents, so the order of the documents is not deterministic. In the real world - // the adds and removes are interleaved per document - listener.OrderBy(e => e.Kind).ThenBy(e => e.DocumentFilePath).AssertNotifications( - x => x.ProjectAdded(ProjectFilePath, newProjectKey), - x => x.DocumentAdded(DocumentFilePath1, newProjectKey), - x => x.DocumentAdded(DocumentFilePath2, newProjectKey), - x => x.DocumentRemoved(DocumentFilePath1, miscProject.Key), - x => x.DocumentRemoved(DocumentFilePath2, miscProject.Key)); - } - - [Fact] - public async Task AddProject_MigratesMiscellaneousDocumentsToNewOwnerProject_FixesTargetPath() - { - // Arrange - const string ProjectFilePath = "C:/path/to/project.csproj"; - const string IntermediateOutputPath = "C:/path/to/obj"; - const string DocumentFilePath1 = "C:/path/to/document1.cshtml"; - const string DocumentFilePath2 = "C:/path/to/document2.cshtml"; - - var miscProject = _projectManager.GetMiscellaneousProject(); - - await _projectManager.UpdateAsync(updater => - { - updater.DocumentAdded(miscProject.Key, - new HostDocument(DocumentFilePath1, "C:/path/to/document1.cshtml"), CreateEmptyTextLoader()); - updater.DocumentAdded(miscProject.Key, - new HostDocument(DocumentFilePath2, "C:/path/to/document2.cshtml"), CreateEmptyTextLoader()); - }); - - // Act - var newProjectKey = await _projectService.AddProjectAsync( - ProjectFilePath, IntermediateOutputPath, RazorConfiguration.Default, rootNamespace: null, displayName: null, DisposalToken); - - // Assert - var newProject = _projectManager.GetLoadedProject(newProjectKey); - Assert.Equal("document1.cshtml", newProject.GetDocument(DocumentFilePath1)!.TargetPath); - Assert.Equal("document2.cshtml", newProject.GetDocument(DocumentFilePath2)!.TargetPath); - } - - [Fact] - public async Task AddOrUpdateProjectAsync_MigratesMiscellaneousDocumentsToNewOwnerProject_MaintainsTextState() - { - // Arrange - const string ProjectFilePath = "C:/path/to/project.csproj"; - const string IntermediateOutputPath = "C:/path/to/obj"; - const string DocumentFilePath1 = "C:/path/to/document1.cshtml"; - - var miscProject = _projectManager.GetMiscellaneousProject(); - - await _projectManager.UpdateAsync(updater => - { - updater.DocumentAdded(miscProject.Key, - new HostDocument(DocumentFilePath1, "other/document1.cshtml"), CreateTextLoader(SourceText.From("Hello"))); - }); - - var projectKey = new ProjectKey(IntermediateOutputPath); - - var documentHandles = ImmutableArray.Create(new DocumentSnapshotHandle(DocumentFilePath1, "document1.cshtml", "mvc")); - - // Act - await _projectService.AddOrUpdateProjectAsync( - projectKey, ProjectFilePath, RazorConfiguration.Default, rootNamespace: null, displayName: null, ProjectWorkspaceState.Default, documentHandles, DisposalToken); - - // Assert - var newProject = _projectManager.GetLoadedProject(projectKey); - var documentText = await newProject.GetDocument(DocumentFilePath1)!.GetTextAsync(); - Assert.Equal("Hello", documentText.ToString()); - } - private static TextLoader CreateTextLoader(SourceText text) { var textLoaderMock = new StrictMock(); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointDelegationTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointDelegationTest.cs index e96dbb8b141..0d587573d67 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointDelegationTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointDelegationTest.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Test.Common.Mef; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Rename; using Microsoft.CodeAnalysis.Razor.Workspaces; @@ -19,7 +18,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Refactoring; -[UseExportProvider] public class RenameEndpointDelegationTest(ITestOutputHelper testOutput) : SingleServerDelegatingEndpointTestBase(testOutput) { [Fact] @@ -59,21 +57,22 @@ public async Task Handle_Rename_SingleServer_CSharpEditsAreMapped() await projectManager.UpdateAsync(updater => { updater.ProjectAdded(new( - projectFilePath: "C:/path/to/project.csproj", + filePath: "C:/path/to/project.csproj", intermediateOutputPath: "C:/path/to/obj", - razorConfiguration: RazorConfiguration.Default, + configuration: RazorConfiguration.Default, rootNamespace: "project")); }); - var searchEngine = new RazorComponentSearchEngine(projectManager, LoggerFactory); + var searchEngine = new RazorComponentSearchEngine(LoggerFactory); - var renameService = new RenameService(searchEngine, projectManager, LanguageServerFeatureOptions); + var renameService = new RenameService(searchEngine, LanguageServerFeatureOptions); var endpoint = new RenameEndpoint( renameService, LanguageServerFeatureOptions, DocumentMappingService, EditMappingService, + projectManager, languageServer, LoggerFactory); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs index 1ff45c926a5..4c78d3dd3db 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Refactoring/RenameEndpointTest.cs @@ -17,7 +17,6 @@ using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.AspNetCore.Razor.Test.Common.Mef; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -33,7 +32,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Refactoring; -[UseExportProvider] public class RenameEndpointTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) { private static readonly string s_project1BasePath = PathUtilities.CreateRootedPath("First"); @@ -656,7 +654,7 @@ public async Task Handle_Rename_SingleServer_DoesNotDelegateForRazor() projectManager, LoggerFactory)); - var projectKey1 = await projectService.AddProjectAsync( + var projectKey1 = await projectService.GetTestAccessor().AddProjectAsync( s_projectFilePath1, s_intermediateOutputPath1, RazorConfiguration.Default, RootNamespace1, displayName: null, DisposalToken); await projectManager.UpdateAsync(updater => @@ -678,7 +676,7 @@ await projectManager.UpdateAsync(updater => await projectService.UpdateDocumentAsync(s_componentFilePath1337, SourceText.From(ComponentText1337), DisposalToken); await projectService.UpdateDocumentAsync(s_indexFilePath1, SourceText.From(IndexText1), DisposalToken); - var projectKey2 = await projectService.AddProjectAsync( + var projectKey2 = await projectService.GetTestAccessor().AddProjectAsync( s_projectFilePath2, s_intermediateOutputPath2, RazorConfiguration.Default, RootNamespace2, displayName: null, DisposalToken); await projectManager.UpdateAsync(updater => @@ -694,7 +692,7 @@ await projectManager.UpdateAsync(updater => await projectService.UpdateDocumentAsync(s_componentFilePath4, SourceText.From(ComponentText4), DisposalToken); await projectService.UpdateDocumentAsync(s_componentWithParamFilePath, SourceText.From(ComponentWithParamText), DisposalToken); - var searchEngine = new RazorComponentSearchEngine(projectManager, LoggerFactory); + var searchEngine = new RazorComponentSearchEngine(LoggerFactory); options ??= StrictMock.Of(static o => o.SupportsFileManipulation == true && o.SingleServerSupport == false && @@ -716,12 +714,13 @@ await projectManager.UpdateAsync(updater => clientConnection ??= StrictMock.Of(); - var renameService = new RenameService(searchEngine, projectManager, options); + var renameService = new RenameService(searchEngine, options); var endpoint = new RenameEndpoint( renameService, options, documentMappingService, editMappingService, + projectManager, clientConnection, LoggerFactory); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs index 455da63ae60..6ef88b7406d 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/SemanticTokensTest.cs @@ -19,7 +19,6 @@ using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.AspNetCore.Razor.Test.Common.Mef; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; @@ -35,7 +34,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic; -[UseExportProvider] public partial class SemanticTokensTest(ITestOutputHelper testOutput) : TagHelperServiceTestBase(testOutput) { private readonly Mock _clientConnection = new(MockBehavior.Strict); @@ -50,13 +48,13 @@ public partial class SemanticTokensTest(ITestOutputHelper testOutput) : TagHelpe } }; - private static readonly Regex s_matchNewLines = MyRegex(); + private static readonly Regex s_matchNewLines = NewLineRegex(); #if NET [GeneratedRegex("\r\n")] - private static partial Regex MyRegex(); + private static partial Regex NewLineRegex(); #else - private static Regex MyRegex() => new Regex("\r\n|\r|\n"); + private static Regex NewLineRegex() => new Regex("\r\n|\r|\n"); #endif [Theory] @@ -656,6 +654,26 @@ public void M() await VerifySemanticTokensAsync(documentText, precise, isRazorFile: true); } + [Theory] + [CombinatorialData] + public async Task GetSemanticTokens_Legacy_Model(bool precise) + { + var documentText = """ + @using System + @model SampleApp.Pages.ErrorModel + +
+ + @{ + @Model.ToString(); + } + +
+ """; + + await VerifySemanticTokensAsync(documentText, precise, isRazorFile: false); + } + [Theory] [CombinatorialData] public async Task GetSemanticTokens_CSharp_LargeFile(bool precise) @@ -937,10 +955,10 @@ private static DocumentContext CreateDocumentContext( .SetupGet(x => x.Project) .Returns(projectSnapshot.Object); documentSnapshotMock - .Setup(x => x.GetGeneratedOutputAsync(It.IsAny())) + .Setup(x => x.GetGeneratedOutputAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(document); documentSnapshotMock - .Setup(x => x.GetTextAsync()) + .Setup(x => x.GetTextAsync(It.IsAny())) .ReturnsAsync(document.Source.Text); documentSnapshotMock .SetupGet(x => x.Version) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/TestFiles/GetSemanticTokens_Legacy_Model.semantic.txt b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/TestFiles/GetSemanticTokens_Legacy_Model.semantic.txt new file mode 100644 index 00000000000..96a45b243e5 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Semantic/TestFiles/GetSemanticTokens_Legacy_Model.semantic.txt @@ -0,0 +1,28 @@ +//line,characterPos,length,tokenType,modifier,text +0 0 1 razorTransition 0 [@] +0 1 5 keyword 0 [using] +0 6 6 namespace name 0 [System] +1 0 1 razorTransition 0 [@] +0 1 5 razorDirective 0 [model] +0 6 9 variable 0 [SampleApp] +0 9 1 operator 0 [.] +0 1 5 variable 0 [Pages] +0 5 1 operator 0 [.] +0 1 10 variable 0 [ErrorModel] +2 0 1 markupTagDelimiter 0 [<] +0 1 3 markupElement 0 [div] +0 3 1 markupTagDelimiter 0 [>] +2 4 1 razorTransition 0 [@] +0 1 1 razorTransition 0 [{] +1 8 1 razorTransition 0 [@] +0 1 5 variable 0 [Model] +0 5 1 operator 0 [.] +0 1 8 variable 0 [ToString] +0 8 1 punctuation 0 [(] +0 1 1 punctuation 0 [)] +0 1 1 punctuation 0 [;] +1 4 1 razorTransition 0 [}] +2 0 1 markupTagDelimiter 0 [<] +0 1 1 markupTagDelimiter 0 [/] +0 1 3 markupElement 0 [div] +0 3 1 markupTagDelimiter 0 [>] diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.cs index 49c4fbb7423..c2c8405e394 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SingleServerDelegatingEndpointTestBase.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.AspNetCore.Razor.Test.Common.Mef; using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -19,7 +18,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer; -[UseExportProvider] public abstract partial class SingleServerDelegatingEndpointTestBase(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) { private protected IDocumentContextFactory? DocumentContextFactory { get; private set; } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TestAdhocWorkspaceFactory.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TestAdhocWorkspaceFactory.cs deleted file mode 100644 index 6011f4a4982..00000000000 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TestAdhocWorkspaceFactory.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System.Linq; -using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Razor.Workspaces; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.Test; - -internal class TestAdhocWorkspaceFactory : IAdhocWorkspaceFactory -{ - public static readonly TestAdhocWorkspaceFactory Instance = new(); - - private TestAdhocWorkspaceFactory() - { - } - - public AdhocWorkspace Create(params IWorkspaceService[] workspaceServices) - { - var services = TestServices.Create(workspaceServices, Enumerable.Empty()); - var workspace = TestWorkspace.Create(services); - return workspace; - } -} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TestRazorProjectService.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TestRazorProjectService.cs index e4f29f50c72..026a36c8242 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TestRazorProjectService.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TestRazorProjectService.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.Common; +using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.AspNetCore.Razor.Serialization; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Utilities; @@ -52,8 +53,8 @@ public async Task AddDocumentToPotentialProjectsAsync(string textDocumentPath, C .Select(d => new DocumentSnapshotHandle(d, d, FileKinds.GetFileKindFromFilePath(d))) .ToImmutableArray(); - await this.UpdateProjectAsync(projectSnapshot.Key, projectSnapshot.Configuration, projectSnapshot.RootNamespace, projectSnapshot.DisplayName, projectSnapshot.ProjectWorkspaceState, - documents, cancellationToken).ConfigureAwait(false); + await ((IRazorProjectInfoListener)this).UpdatedAsync(new RazorProjectInfo(projectSnapshot.Key, projectSnapshot.FilePath, projectSnapshot.Configuration, projectSnapshot.RootNamespace, projectSnapshot.DisplayName, projectSnapshot.ProjectWorkspaceState, + documents), cancellationToken).ConfigureAwait(false); } } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/WorkspaceDiagnosticRefreshTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/WorkspaceDiagnosticRefreshTest.cs new file mode 100644 index 00000000000..f4ea2ed099f --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/WorkspaceDiagnosticRefreshTest.cs @@ -0,0 +1,191 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; +using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Moq; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.Test; + +public class WorkspaceDiagnosticRefreshTest(ITestOutputHelper testOutputHelper) : LanguageServerTestBase(testOutputHelper) +{ + private static readonly TimeSpan s_delay = TimeSpan.FromMilliseconds(10); + + [Fact] + public async Task WorkspaceRefreshSent() + { + var projectSnapshotManager = CreateProjectSnapshotManager(); + var clientConnection = new StrictMock(); + clientConnection + .Setup(c => c.SendNotificationAsync(Methods.WorkspaceDiagnosticRefreshName, It.IsAny())) + .Returns(Task.CompletedTask) + .Verifiable(); + + using var publisher = new WorkspaceDiagnosticsRefresher( + projectSnapshotManager, + new TestClientCapabilitiesService(new() + { + Workspace = new() + { + Diagnostics = new() + { + RefreshSupport = true + } + } + }), + clientConnection.Object, + s_delay); + + var testAccessor = publisher.GetTestAccessor(); + + await projectSnapshotManager.UpdateAsync( + static updater => + { + var hostProject = TestHostProject.Create("C:/path/to/project.csproj"); + updater.ProjectAdded(hostProject); + }); + + await testAccessor.WaitForRefreshAsync(); + + clientConnection.Verify(); + } + + [Fact] + public async Task WorkspaceRefreshSent_MultipleTimes() + { + var projectSnapshotManager = CreateProjectSnapshotManager(); + var clientConnection = new StrictMock(); + clientConnection + .Setup(c => c.SendNotificationAsync(Methods.WorkspaceDiagnosticRefreshName, It.IsAny())) + .Returns(Task.CompletedTask); + + using var publisher = new WorkspaceDiagnosticsRefresher( + projectSnapshotManager, + new TestClientCapabilitiesService(new() + { + Workspace = new() + { + Diagnostics = new() + { + RefreshSupport = true + } + } + }), + clientConnection.Object, + s_delay); + + var testAccessor = publisher.GetTestAccessor(); + + var hostProject = TestHostProject.Create("C:/path/to/project.csproj"); + + var directory = Path.GetDirectoryName(hostProject.FilePath); + Assert.NotNull(directory); + + var hostDocument = TestHostDocument.Create(hostProject, Path.Combine(directory, "directory.razor")); + + await projectSnapshotManager.UpdateAsync( + updater => + { + updater.ProjectAdded(hostProject); + }); + + await testAccessor.WaitForRefreshAsync(); + + await projectSnapshotManager.UpdateAsync( + updater => + { + updater.DocumentAdded(hostProject.Key, hostDocument, hostDocument.CreateEmptyTextLoader()); + }); + + await testAccessor.WaitForRefreshAsync(); + + clientConnection.Verify( + c => c.SendNotificationAsync(Methods.WorkspaceDiagnosticRefreshName, It.IsAny()), + Times.Exactly(2)); + } + + [Fact] + public async Task WorkspaceRefreshNotSent_ClientDoesNotSupport() + { + var projectSnapshotManager = CreateProjectSnapshotManager(); + var clientConnection = new StrictMock(); + + using var publisher = new WorkspaceDiagnosticsRefresher( + projectSnapshotManager, + new TestClientCapabilitiesService(new() + { + Workspace = new() + { + Diagnostics = new() + { + RefreshSupport = false + } + } + }), + clientConnection.Object, + s_delay); + + var testAccessor = publisher.GetTestAccessor(); + + await projectSnapshotManager.UpdateAsync( + updater => + { + var hostProject = TestHostProject.Create("C:/path/to/project.csproj"); + updater.ProjectAdded(hostProject); + }); + + await testAccessor.WaitForRefreshAsync(); + + clientConnection + .Verify(c => c.SendNotificationAsync(It.IsAny(), It.IsAny()), + Times.Never); + } + + [Fact] + public async Task WorkspaceRefreshNotSent_RefresherDisposed() + { + var projectSnapshotManager = CreateProjectSnapshotManager(); + var clientConnection = new StrictMock(); + + var publisher = new WorkspaceDiagnosticsRefresher( + projectSnapshotManager, + new TestClientCapabilitiesService(new() + { + Workspace = new() + { + Diagnostics = new() + { + RefreshSupport = false + } + } + }), + clientConnection.Object, + s_delay); + + var testAccessor = publisher.GetTestAccessor(); + + publisher.Dispose(); + + await projectSnapshotManager.UpdateAsync( + static updater => + { + var hostProject = TestHostProject.Create("C:/path/to/project.csproj"); + updater.ProjectAdded(hostProject); + }); + + await testAccessor.WaitForRefreshAsync(); + + clientConnection + .Verify(c => c.SendNotificationAsync(It.IsAny(), It.IsAny()), + Times.Never); + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/WrapWithTag/WrapWithTagEndpointTests.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/WrapWithTag/WrapWithTagEndpointTests.cs index cb174425556..53072c32790 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/WrapWithTag/WrapWithTagEndpointTests.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/WrapWithTag/WrapWithTagEndpointTests.cs @@ -68,7 +68,7 @@ public async Task Handle_CSharp_ReturnsNull() var wrapWithDivParams = new WrapWithTagParams(new() { Uri = uri }) { - Range = VsLspFactory.CreateSingleLineRange(start: (0, 0), length: 2), + Range = VsLspFactory.CreateSingleLineRange(start: (0, 1), length: 2), }; var requestContext = CreateRazorRequestContext(documentContext); @@ -112,6 +112,42 @@ public async Task Handle_CSharp_WholeImplicitStatement_ReturnsResult() Mock.Get(clientConnection).Verify(); } + [Fact] + public async Task Handle_RazorBlockStart_ReturnsResult() + { + // Arrange + var input = new TestCode(""" + [|@if (true) { } +
+
|] + """); + var codeDocument = CreateCodeDocument(input.Text); + var uri = new Uri("file://path/test.razor"); + var documentContext = CreateDocumentContext(uri, codeDocument); + var response = new WrapWithTagResponse(); + + var clientConnection = TestMocks.CreateClientConnection(builder => + { + builder.SetupSendRequest(LanguageServerConstants.RazorWrapWithTagEndpoint, response: new(), verifiable: true); + }); + + var endpoint = new WrapWithTagEndpoint(clientConnection, LoggerFactory); + + var range = codeDocument.Source.Text.GetRange(input.Span); + var wrapWithDivParams = new WrapWithTagParams(new TextDocumentIdentifier { Uri = uri }) + { + Range = range + }; + var requestContext = CreateRazorRequestContext(documentContext); + + // Act + var result = await endpoint.HandleRequestAsync(wrapWithDivParams, requestContext, DisposalToken); + + // Assert + Assert.NotNull(result); + Mock.Get(clientConnection).Verify(); + } + [Fact] public async Task Handle_CSharp_PartOfImplicitStatement_ReturnsNull() { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.ProjectEngineHost.Test/ProjectEngineFactoryProviderTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.ProjectEngineHost.Test/ProjectEngineFactoryProviderTest.cs index 4072d5b13d7..6b0c2f43966 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.ProjectEngineHost.Test/ProjectEngineFactoryProviderTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.ProjectEngineHost.Test/ProjectEngineFactoryProviderTest.cs @@ -103,7 +103,7 @@ public void Create_CreatesDesignTimeTemplateEngine_ForVersion2_1() Assert.Single(engine.Engine.Features.OfType()); Assert.Single(engine.Engine.Features.OfType()); - Assert.Single(engine.Engine.Features.OfType()); + Assert.Single(engine.Engine.Features.OfType()); } [Fact] @@ -122,7 +122,7 @@ public void Create_CreatesDesignTimeTemplateEngine_ForVersion2_0() Assert.Single(engine.Engine.Features.OfType()); Assert.Single(engine.Engine.Features.OfType()); Assert.Single(engine.Engine.Features.OfType()); - Assert.Single(engine.Engine.Features.OfType()); + Assert.Single(engine.Engine.Features.OfType()); } [Fact] @@ -141,7 +141,7 @@ public void Create_CreatesTemplateEngine_ForVersion1_1() Assert.Single(engine.Engine.Features.OfType()); Assert.Single(engine.Engine.Features.OfType()); Assert.Single(engine.Engine.Features.OfType()); - Assert.Single(engine.Engine.Features.OfType()); + Assert.Single(engine.Engine.Features.OfType()); } [Fact] @@ -166,10 +166,10 @@ public void Create_DoesNotSupportViewComponentTagHelpers_ForVersion1_0() Assert.Empty(engine.Engine.Features.OfType()); Assert.Empty(engine.Engine.Features.OfType()); - Assert.Empty(engine.Engine.Features.OfType()); + Assert.Empty(engine.Engine.Features.OfType()); Assert.Empty(engine.Engine.Features.OfType()); - Assert.Empty(engine.Engine.Features.OfType()); + Assert.Empty(engine.Engine.Features.OfType()); } [Fact] diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/AssertExtensions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/AssertExtensions.cs new file mode 100644 index 00000000000..eeab228a043 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/AssertExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using Microsoft.VisualStudio.Text.Adornments; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Test.Common; + +internal static class AssertExtensions +{ + internal static void AssertExpectedClassification( + this ClassifiedTextRun run, + string expectedText, + string expectedClassificationType, + ClassifiedTextRunStyle expectedClassificationStyle = ClassifiedTextRunStyle.Plain) + { + Assert.Equal(expectedText, run.Text); + Assert.Equal(expectedClassificationType, run.ClassificationTypeName); + Assert.Equal(expectedClassificationStyle, run.Style); + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Editor_NetFx/TestTelemetryReporter.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Editor_NetFx/TestTelemetryReporter.cs index 1c97dd65cc7..cf23f1b6643 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Editor_NetFx/TestTelemetryReporter.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Editor_NetFx/TestTelemetryReporter.cs @@ -1,19 +1,42 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.VisualStudio.Razor.Telemetry; using Microsoft.VisualStudio.Telemetry; +using Xunit; +using static Microsoft.VisualStudio.Razor.Telemetry.AggregatingTelemetryLog; namespace Microsoft.VisualStudio.Editor.Razor.Test.Shared; internal class TestTelemetryReporter(ILoggerFactory loggerFactory) : VSTelemetryReporter(loggerFactory) { public List Events { get; } = []; + public List Metrics { get; } = []; + + public override bool IsEnabled => true; + + public override void ReportMetric(TelemetryInstrumentEvent metricEvent) + { + Metrics.Add(metricEvent); + } protected override void Report(TelemetryEvent telemetryEvent) { Events.Add(telemetryEvent); } + + /// + /// This exists because both the remote and workspace projects are referenced by the test project, + /// so using directly is impossibly ambiguous. I'm sure there's a + /// clever way to fix this that isn't writing this method and I'm very happy if you, the reader, come along + /// and make it so. However, I unfortunately do not have that insight nor the drive to do so. This works fine for + /// asserting types without changing the project dependencies to accommodate testing. + /// + public void AssertMetrics(params Action[] elementInspectors) + { + Assert.Collection(Metrics, elementInspectors); + } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Formatting_NetFx/HtmlFormattingFixture.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Formatting_NetFx/HtmlFormattingFixture.cs new file mode 100644 index 00000000000..13881a19c93 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Formatting_NetFx/HtmlFormattingFixture.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.CodeAnalysis.Razor.Formatting; + +public class HtmlFormattingFixture : IDisposable +{ + private readonly HtmlFormattingService _htmlFormattingService = new(); + + internal HtmlFormattingService Service => _htmlFormattingService; + + public void Dispose() + { + _htmlFormattingService.Dispose(); + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Formatting_NetFx/HtmlFormatting.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Formatting_NetFx/HtmlFormattingService.cs similarity index 78% rename from src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Formatting_NetFx/HtmlFormatting.cs rename to src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Formatting_NetFx/HtmlFormattingService.cs index 1879dfadf1d..7c082b276cc 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Formatting_NetFx/HtmlFormatting.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Formatting_NetFx/HtmlFormattingService.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Razor.Test.Common.Mef; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Composition; using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Utilities; @@ -17,9 +18,21 @@ namespace Microsoft.CodeAnalysis.Razor.Formatting; -internal static class HtmlFormatting +internal sealed class HtmlFormattingService : IDisposable { - public static Task GetDocumentFormattingEditsAsync(ILoggerFactory loggerFactory, Uri uri, string generatedHtml, bool insertSpaces, int tabSize) + private ExportProvider? _exportProvider; + + private ExportProvider ExportProvider => _exportProvider ?? (_exportProvider = TestComposition.Editor.ExportProviderFactory.CreateExportProvider()); + + public void Dispose() + { + if (_exportProvider is not null) + { + _exportProvider.Dispose(); + } + } + + public Task GetDocumentFormattingEditsAsync(ILoggerFactory loggerFactory, Uri uri, string generatedHtml, bool insertSpaces, int tabSize) { var request = $$""" { @@ -37,7 +50,7 @@ internal static class HtmlFormatting return CallWebToolsApplyFormattedEditsHandlerAsync(loggerFactory, request, uri, generatedHtml); } - public static Task GetOnTypeFormattingEditsAsync(ILoggerFactory loggerFactory, Uri uri, string generatedHtml, Position position, bool insertSpaces, int tabSize) + public Task GetOnTypeFormattingEditsAsync(ILoggerFactory loggerFactory, Uri uri, string generatedHtml, Position position, bool insertSpaces, int tabSize) { var generatedHtmlSource = SourceText.From(generatedHtml, Encoding.UTF8); var absoluteIndex = generatedHtmlSource.GetRequiredAbsoluteIndex(position); @@ -64,10 +77,9 @@ internal static class HtmlFormatting return CallWebToolsApplyFormattedEditsHandlerAsync(loggerFactory, request, uri, generatedHtml); } - private static async Task CallWebToolsApplyFormattedEditsHandlerAsync(ILoggerFactory loggerFactory, string serializedValue, Uri documentUri, string generatedHtml) + private async Task CallWebToolsApplyFormattedEditsHandlerAsync(ILoggerFactory loggerFactory, string serializedValue, Uri documentUri, string generatedHtml) { - var exportProvider = TestComposition.Editor.ExportProviderFactory.CreateExportProvider(); - var contentTypeService = exportProvider.GetExportedValue(); + var contentTypeService = ExportProvider.GetExportedValue(); lock (contentTypeService) { @@ -77,7 +89,7 @@ internal static class HtmlFormatting } } - var textBufferFactoryService = (ITextBufferFactoryService3)exportProvider.GetExportedValue(); + var textBufferFactoryService = (ITextBufferFactoryService3)ExportProvider.GetExportedValue(); var bufferManager = WebTools.BufferManager.New(contentTypeService, textBufferFactoryService, []); var logger = loggerFactory.GetOrCreateLogger("ApplyFormattedEditsHandler"); var applyFormatEditsHandler = WebTools.ApplyFormatEditsHandler.New(textBufferFactoryService, bufferManager, logger); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Language/IntegrationTests/RazorToolingIntegrationTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Language/IntegrationTests/RazorToolingIntegrationTestBase.cs index 6126af7a104..6880e6747df 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Language/IntegrationTests/RazorToolingIntegrationTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Language/IntegrationTests/RazorToolingIntegrationTestBase.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.AspNetCore.Razor.Test.Common.Mef; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Razor; @@ -23,7 +22,6 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests; -[UseExportProvider] public class RazorToolingIntegrationTestBase : ToolingTestBase { internal const string ArbitraryWindowsPath = "x:\\dir\\subdir\\Test"; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServer.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServer.cs index 2b20c842cf4..f095947c978 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServer.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServer.cs @@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; public sealed class CSharpTestLspServer : IAsyncDisposable { private readonly AdhocWorkspace _testWorkspace; - private readonly IRazorLanguageServerTarget _languageServer; + private readonly ExportProvider _exportProvider; private readonly JsonRpc _clientRpc; private readonly JsonRpc _serverRpc; @@ -41,6 +41,7 @@ private CSharpTestLspServer( CancellationToken cancellationToken) { _testWorkspace = testWorkspace; + _exportProvider = exportProvider; _cancellationToken = cancellationToken; var (clientStream, serverStream) = FullDuplexStream.CreatePair(); @@ -67,7 +68,7 @@ private CSharpTestLspServer( _clientRpc.StartListening(); - _languageServer = CreateLanguageServer(_serverRpc, _serverMessageFormatter.JsonSerializerOptions, testWorkspace, languageServerFactory, exportProvider, serverCapabilities); + _ = CreateLanguageServer(_serverRpc, _serverMessageFormatter.JsonSerializerOptions, testWorkspace, languageServerFactory, exportProvider, serverCapabilities); static SystemTextJsonFormatter CreateSystemTextJsonMessageFormatter(AbstractRazorLanguageServerFactoryWrapper languageServerFactory) { @@ -146,6 +147,7 @@ internal Task ExecuteRequestAsync( public async ValueTask DisposeAsync() { _testWorkspace.Dispose(); + _exportProvider.Dispose(); _clientRpc.Dispose(); _clientMessageFormatter.Dispose(); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServerHelpers.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServerHelpers.cs index ecf351fe1e8..b91d512c39c 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServerHelpers.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/CSharpTestLspServerHelpers.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -14,9 +13,7 @@ using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; using Microsoft.AspNetCore.Razor.Threading; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.ExternalAccess.Razor; -using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; @@ -109,8 +106,7 @@ private static AdhocWorkspace CreateCSharpTestWorkspace( IRazorSpanMappingService razorSpanMappingService, bool multiTargetProject) { - var hostServices = MefHostServices.Create(exportProvider.AsCompositionContext()); - var workspace = TestWorkspace.Create(hostServices); + var workspace = TestWorkspace.CreateWithDiagnosticAnalyzers(exportProvider); // Add project and solution to workspace var projectInfoNet60 = ProjectInfo.Create( @@ -135,14 +131,10 @@ private static AdhocWorkspace CreateCSharpTestWorkspace( ? [projectInfoNet60, projectInfoNet80] : [projectInfoNet80]; - var solutionInfo = SolutionInfo.Create( - id: SolutionId.CreateNewId("TestSolution"), - version: VersionStamp.Default, - projects: projectInfos); - - workspace.AddSolution(solutionInfo); - - AddAnalyzersToWorkspace(workspace, exportProvider); + foreach (var projectInfo in projectInfos) + { + workspace.AddProject(projectInfo); + } // Add document to workspace. We use an IVT method to create the DocumentInfo variable because there's // a special constructor in Roslyn that will help identify the document as belonging to Razor. @@ -172,30 +164,6 @@ private static AdhocWorkspace CreateCSharpTestWorkspace( return workspace; } - private static void AddAnalyzersToWorkspace(Workspace workspace, ExportProvider exportProvider) - { - var analyzerLoader = RazorTestAnalyzerLoader.CreateAnalyzerAssemblyLoader(); - - var analyzerPaths = new DirectoryInfo(AppContext.BaseDirectory).GetFiles("*.dll") - .Where(f => f.Name.StartsWith("Microsoft.CodeAnalysis.", StringComparison.Ordinal) && !f.Name.Contains("LanguageServer") && !f.Name.Contains("Test.Utilities")) - .Select(f => f.FullName) - .ToImmutableArray(); - var references = new List(); - foreach (var analyzerPath in analyzerPaths) - { - if (File.Exists(analyzerPath)) - { - references.Add(new AnalyzerFileReference(analyzerPath, analyzerLoader)); - } - } - - workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences(references)); - - // Make sure Roslyn is producing diagnostics for our workspace - var razorTestAnalyzerLoader = exportProvider.GetExportedValue(); - razorTestAnalyzerLoader.InitializeDiagnosticsServices(workspace); - } - private record CSharpFile(Uri DocumentUri, SourceText CSharpSourceText); private class EmptyMappingService : IRazorSpanMappingService diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/LanguageServerTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/LanguageServerTestBase.cs index 4ed6d98fc3e..72cdaffb7fd 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/LanguageServerTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/LanguageServerTestBase.cs @@ -48,10 +48,7 @@ protected LanguageServerTestBase(ITestOutputHelper testOutput) FilePathService = new LSPFilePathService(TestLanguageServerFeatureOptions.Instance); } - private protected TestProjectSnapshotManager CreateProjectSnapshotManager() - => CreateProjectSnapshotManager(ProjectEngineFactories.DefaultProvider); - - private protected TestProjectSnapshotManager CreateProjectSnapshotManager( + private protected override TestProjectSnapshotManager CreateProjectSnapshotManager( IProjectEngineFactoryProvider projectEngineFactoryProvider) => new( projectEngineFactoryProvider, @@ -113,7 +110,7 @@ private protected static IDocumentContextFactory CreateDocumentContextFactory(Ur private protected static DocumentContext CreateDocumentContext(Uri documentPath, RazorCodeDocument codeDocument) { - return TestDocumentContext.From(documentPath.GetAbsoluteOrUNCPath(), codeDocument); + return TestDocumentContext.Create(documentPath.GetAbsoluteOrUNCPath(), codeDocument); } private protected static IDocumentContextFactory CreateDocumentContextFactory( @@ -133,15 +130,56 @@ private protected static DocumentContext CreateDocumentContext(Uri uri, IDocumen return new DocumentContext(uri, snapshot, projectContext: null); } - private protected static RazorLSPOptionsMonitor GetOptionsMonitor(bool enableFormatting = true, bool autoShowCompletion = true, bool autoListParams = true, bool formatOnType = true, bool autoInsertAttributeQuotes = true, bool colorBackground = false, bool codeBlockBraceOnNextLine = false, bool commitElementsWithSpace = true) + private protected static RazorLSPOptionsMonitor GetOptionsMonitor( + bool enableFormatting = true, + bool autoShowCompletion = true, + bool autoListParams = true, + bool formatOnType = true, + bool autoInsertAttributeQuotes = true, + bool colorBackground = false, + bool codeBlockBraceOnNextLine = false, + bool commitElementsWithSpace = true, + bool formatOnPaste = true) { var configService = StrictMock.Of(); - var options = new RazorLSPOptions(enableFormatting, true, InsertSpaces: true, TabSize: 4, autoShowCompletion, autoListParams, formatOnType, autoInsertAttributeQuotes, colorBackground, codeBlockBraceOnNextLine, commitElementsWithSpace); + var options = new RazorLSPOptions( + GetFormattingFlags(enableFormatting, formatOnType, formatOnPaste), + true, + InsertSpaces: true, + TabSize: 4, + autoShowCompletion, + autoListParams, + autoInsertAttributeQuotes, + colorBackground, + codeBlockBraceOnNextLine, + commitElementsWithSpace); var optionsMonitor = new RazorLSPOptionsMonitor(configService, options); return optionsMonitor; } + private static FormattingFlags GetFormattingFlags(bool enableFormatting, bool formatOnType, bool formatOnPaste) + { + var flags = FormattingFlags.Disabled; + + if (enableFormatting) + { + flags |= FormattingFlags.Enabled; + } + + if (formatOnType) + { + flags |= FormattingFlags.OnType; + } + + if (formatOnPaste) + { + flags |= FormattingFlags.OnPaste; + } + + return flags; + } + private class ThrowingRazorSpanMappingService : IRazorSpanMappingService { public Task> MapSpansAsync(Document document, IEnumerable spans, CancellationToken cancellationToken) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/TestDocumentContext.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/TestDocumentContext.cs index e6e60acffe2..eeaea0b6411 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/TestDocumentContext.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/TestDocumentContext.cs @@ -19,30 +19,27 @@ public static DocumentContext Create(Uri uri, string text) return new DocumentContext(uri, snapshot, projectContext: null); } - public static DocumentContext From(string filePath, RazorCodeDocument codeDocument, int hostDocumentVersion) + public static DocumentContext Create(string filePath, RazorCodeDocument codeDocument, int hostDocumentVersion) { - var content = codeDocument.Source.Text.ToString(); - var documentSnapshot = TestDocumentSnapshot.Create(filePath, content, hostDocumentVersion); - documentSnapshot.With(codeDocument); + var documentSnapshot = TestDocumentSnapshot.Create(filePath, codeDocument, hostDocumentVersion); var uri = new Uri(filePath); return new DocumentContext(uri, documentSnapshot, projectContext: null); } - public static DocumentContext From(string filePath, RazorCodeDocument codeDocument) + public static DocumentContext Create(string filePath, RazorCodeDocument codeDocument) { - var content = codeDocument.Source.Text.ToString(); - var documentSnapshot = TestDocumentSnapshot.Create(filePath, content); - documentSnapshot.With(codeDocument); + var documentSnapshot = TestDocumentSnapshot.Create(filePath, codeDocument); var uri = new Uri(filePath); return new DocumentContext(uri, documentSnapshot, projectContext: null); } - public static DocumentContext From(string filePath) + public static DocumentContext Create(string filePath) { var properties = RazorSourceDocumentProperties.Create(filePath, filePath); var sourceDocument = RazorSourceDocument.Create(content: string.Empty, properties); var codeDocument = RazorCodeDocument.Create(sourceDocument); - return From(filePath, codeDocument); + + return Create(filePath, codeDocument); } public static DocumentContext From(string filePath, int hostDocumentVersion) @@ -50,6 +47,7 @@ public static DocumentContext From(string filePath, int hostDocumentVersion) var properties = RazorSourceDocumentProperties.Create(filePath, filePath); var sourceDocument = RazorSourceDocument.Create(content: string.Empty, properties); var codeDocument = RazorCodeDocument.Create(sourceDocument); - return From(filePath, codeDocument, hostDocumentVersion); + + return Create(filePath, codeDocument, hostDocumentVersion); } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/TestDocumentContextFactory.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/TestDocumentContextFactory.cs index 7a0aa4ba27d..90049c04c1e 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/TestDocumentContextFactory.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/LanguageServer/TestDocumentContextFactory.cs @@ -35,7 +35,7 @@ public virtual bool TryCreate( return false; } - context = TestDocumentContext.From(FilePath, CodeDocument); + context = TestDocumentContext.Create(FilePath, CodeDocument); return true; } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Mef/ExportProviderCache.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Mef/ExportProviderCache.cs index 2b2a62815a7..e8937fd4a96 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Mef/ExportProviderCache.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Mef/ExportProviderCache.cs @@ -2,14 +2,10 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using System.Reflection; -using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.VisualStudio.Composition; namespace Microsoft.AspNetCore.Razor.Test.Common.Mef; @@ -18,60 +14,6 @@ public static class ExportProviderCache { private static readonly PartDiscovery s_partDiscovery = CreatePartDiscovery(Resolver.DefaultInstance); - private static readonly TestComposition s_defaultHostExportProviderComposition = TestComposition.Empty - .AddAssemblies(MefHostServices.DefaultAssemblies); - private static readonly ConcurrentDictionary s_scopes = new(); - private const string DefaultScope = "default"; - - private static readonly object s_lock = new(); - - internal static bool Enabled { get; private set; } - - internal static ExportProvider[] ExportProvidersForCleanup - { - get - { - var scopes = s_scopes.Values.ToArray(); - var defaultScope = scopes.Where(scope => scope.Name == DefaultScope); - var allButDefault = scopes.Where(scope => scope.Name != DefaultScope); - - // Make sure to return the default scope as the last element - return allButDefault.Concat(defaultScope) - .Where(scope => scope._currentExportProvider is { }) - .Select(scope => scope._currentExportProvider!) - .ToArray(); - } - } - - internal static void SetEnabled_OnlyUseExportProviderAttributeCanCall(bool value) - { - lock (s_lock) - { - Enabled = value; - if (!Enabled) - { - foreach (var scope in s_scopes.Values.ToArray()) - { - scope.Clear(); - } - } - } - } - - /// - /// Use to create for default instances of . - /// - public static IExportProviderFactory GetOrCreateExportProviderFactory(IEnumerable assemblies) - { - if (assemblies is ImmutableArray assembliesArray && - assembliesArray == MefHostServices.DefaultAssemblies) - { - return s_defaultHostExportProviderComposition.ExportProviderFactory; - } - - return CreateExportProviderFactory(CreateAssemblyCatalog(assemblies), scopeName: DefaultScope); - } - public static ComposableCatalog CreateAssemblyCatalog(IEnumerable assemblies, Resolver? resolver = null) { var discovery = resolver is null ? s_partDiscovery : CreatePartDiscovery(resolver); @@ -125,151 +67,48 @@ bool IsExcludedPart(ComposablePartDefinition part) } } - public static IExportProviderFactory CreateExportProviderFactory(ComposableCatalog catalog, string? scopeName = null) + public static IExportProviderFactory CreateExportProviderFactory(ComposableCatalog catalog) { - var scope = s_scopes.GetOrAdd(scopeName ?? DefaultScope, scopeName => new Scope(scopeName)); var configuration = CompositionConfiguration.Create(catalog.WithCompositionService()); + ValidateConfiguration(configuration); + var runtimeComposition = RuntimeComposition.CreateRuntimeComposition(configuration); var exportProviderFactory = runtimeComposition.CreateExportProviderFactory(); - return new SingleExportProviderFactory(scope, catalog, configuration, exportProviderFactory); + return exportProviderFactory; } - private sealed class SingleExportProviderFactory : IExportProviderFactory + private static void ValidateConfiguration(CompositionConfiguration configuration) { - private readonly Scope _scope; - private readonly ComposableCatalog _catalog; - private readonly CompositionConfiguration _configuration; - private readonly IExportProviderFactory _exportProviderFactory; - - public SingleExportProviderFactory(Scope scope, ComposableCatalog catalog, CompositionConfiguration configuration, IExportProviderFactory exportProviderFactory) + foreach (var errorCollection in configuration.CompositionErrors) { - _scope = scope; - _catalog = catalog; - _configuration = configuration; - _exportProviderFactory = exportProviderFactory; - } - - private ExportProvider GetOrCreateExportProvider() - { - if (!Enabled) - { - // The [UseExportProvider] attribute on tests ensures that the pre- and post-conditions of methods - // in this type are met during test conditions. - throw new InvalidOperationException($"{nameof(ExportProviderCache)} may only be used from tests marked with {nameof(UseExportProviderAttribute)}"); - } - - var expectedCatalog = Interlocked.CompareExchange(ref _scope._expectedCatalog, _catalog, null) ?? _catalog; - RequireForSingleExportProvider(expectedCatalog == _catalog); - - var expected = _scope._expectedProviderForCatalog; - if (expected is null) + foreach (var error in errorCollection) { - foreach (var errorCollection in _configuration.CompositionErrors) + foreach (var part in error.Parts) { - foreach (var error in errorCollection) + foreach (var pair in part.SatisfyingExports) { - foreach (var part in error.Parts) + var (importBinding, exportBindings) = (pair.Key, pair.Value); + if (exportBindings.Count <= 1) { - foreach (var pair in part.SatisfyingExports) - { - var (importBinding, exportBindings) = (pair.Key, pair.Value); - if (exportBindings.Count <= 1) - { - // Ignore composition errors for missing parts - continue; - } + // Ignore composition errors for missing parts + continue; + } - if (importBinding.ImportDefinition.Cardinality != ImportCardinality.ZeroOrMore) - { - // This failure occurs when a binding fails because multiple exports were - // provided but only a single one (at most) is expected. This typically occurs - // when a test ExportProvider is created with a mock implementation without - // first removing a value provided by default. - throw new InvalidOperationException( - "Failed to construct the MEF catalog for testing. Multiple exports were found for a part for which only one export is expected:" + Environment.NewLine - + error.Message); - } - } + if (importBinding.ImportDefinition.Cardinality != ImportCardinality.ZeroOrMore) + { + // This failure occurs when a binding fails because multiple exports were + // provided but only a single one (at most) is expected. This typically occurs + // when a test ExportProvider is created with a mock implementation without + // first removing a value provided by default. + throw new InvalidOperationException( + "Failed to construct the MEF catalog for testing. Multiple exports were found for a part for which only one export is expected:" + Environment.NewLine + + error.Message); } } } - - expected = _exportProviderFactory.CreateExportProvider(); - expected = Interlocked.CompareExchange(ref _scope._expectedProviderForCatalog, expected, null) ?? expected; - Interlocked.CompareExchange(ref _scope._currentExportProvider, expected, null); - } - - var exportProvider = _scope._currentExportProvider; - RequireForSingleExportProvider(exportProvider == expected); - - return exportProvider!; - } - - ExportProvider IExportProviderFactory.CreateExportProvider() - { - // Currently this implementation deviates from the typical behavior of IExportProviderFactory. For the - // duration of a single test, an instance of SingleExportProviderFactory will continue returning the - // same ExportProvider instance each time this method is called. - // - // It may be clearer to refactor the implementation to only allow one call to CreateExportProvider in - // the context of a single test. https://github.com/dotnet/roslyn/issues/25863 - lock (s_lock) - { - return GetOrCreateExportProvider(); } } - - private void RequireForSingleExportProvider(bool condition) - { - if (!condition) - { - // The ExportProvider provides services that act as singleton instances in the context of an - // application (this include cases of multiple exports, where the 'singleton' is the list of all - // exports matching the contract). When reasoning about the behavior of test code, it is valuable to - // know service instances will be used in a consistent manner throughout the execution of a test, - // regardless of whether they are passed as arguments or obtained through requests to the - // ExportProvider. - // - // Restricting a test to a single ExportProvider guarantees that objects that *look* like singletons - // will *behave* like singletons for the duration of the test. Each test is expected to create and - // use its ExportProvider in a consistent manner. - // - // A test that validates remote services is allowed to create a couple of ExportProviders: - // one for local workspace and the other for the remote one. - // - // When this exception is thrown by a test, it typically means one of the following occurred: - // - // * A test failed to pass an ExportProvider via an optional argument to a method, resulting in the - // method attempting to create a default ExportProvider which did not match the one assigned to - // the test. - // * A test attempted to perform multiple test sequences in the context of a single test method, - // rather than break up the test into distinct tests for each case. - // * A test referenced different predefined ExportProvider instances within the context of a test. - // Each test is expected to use the same ExportProvider throughout the test. - throw new InvalidOperationException($"Only one {_scope.Name} {nameof(ExportProvider)} can be created in the context of a single test."); - } - } - } - - private sealed class Scope - { - public readonly string Name; - public ExportProvider? _currentExportProvider; - public ComposableCatalog? _expectedCatalog; - public ExportProvider? _expectedProviderForCatalog; - - public Scope(string name) - { - Name = name; - } - - public void Clear() - { - _currentExportProvider = null; - _expectedCatalog = null; - _expectedProviderForCatalog = null; - } } private sealed class SimpleAssemblyLoader : IAssemblyLoader diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Mef/ExportProviderExtensions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Mef/ExportProviderExtensions.cs index 05540f3862b..18d5c5dd654 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Mef/ExportProviderExtensions.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Mef/ExportProviderExtensions.cs @@ -3,12 +3,12 @@ using System; using System.Collections.Generic; -using System.Composition.Hosting.Core; using System.Composition; +using System.Composition.Hosting.Core; using System.Diagnostics.CodeAnalysis; using System.Linq; -using Microsoft.VisualStudio.Composition; using System.Reflection; +using Microsoft.VisualStudio.Composition; namespace Microsoft.AspNetCore.Razor.Test.Common.Mef; @@ -31,43 +31,55 @@ public CompositionContextShim(ExportProvider exportProvider) public override bool TryGetExport(CompositionContract contract, [NotNullWhen(true)] out object? export) { var importMany = contract.MetadataConstraints.Contains(new KeyValuePair("IsImportMany", true)); - var (contractType, metadataType) = GetContractType(contract.ContractType, importMany); + var (contractType, metadataType, isLazy) = GetContractType(contract.ContractType, importMany); - if (metadataType != null) + var method = (metadataType, isLazy) switch { - var methodInfo = (from method in _exportProvider.GetType().GetTypeInfo().GetMethods() - where method.Name == nameof(ExportProvider.GetExports) - where method.IsGenericMethod && method.GetGenericArguments().Length == 2 - where method.GetParameters().Length == 1 && method.GetParameters()[0].ParameterType == typeof(string) - select method).Single(); - var parameterizedMethod = methodInfo.MakeGenericMethod(contractType, metadataType); - export = parameterizedMethod.Invoke(_exportProvider, new[] { contract.ContractName }); - Assumes.NotNull(export); + (not null, true) => GetExportProviderGenericMethod(nameof(ExportProvider.GetExports), contractType, metadataType), + (null, true) => GetExportProviderGenericMethod(nameof(ExportProvider.GetExports), contractType), + (null, false) => GetExportProviderGenericMethod(nameof(ExportProvider.GetExportedValues), contractType), + _ => null + }; + + if (method is null) + { + export = null; + return false; } - else + + export = method.Invoke(_exportProvider, [contract.ContractName]); + Assumes.NotNull(export); + + return true; + + static MethodInfo GetExportProviderGenericMethod(string methodName, params Type[] typeArguments) { - var methodInfo = (from method in _exportProvider.GetType().GetTypeInfo().GetMethods() - where method.Name == nameof(ExportProvider.GetExports) - where method.IsGenericMethod && method.GetGenericArguments().Length == 1 + var methodInfo = (from method in typeof(ExportProvider).GetTypeInfo().GetMethods() + where method.Name == methodName + where method.IsGenericMethod && method.GetGenericArguments().Length == typeArguments.Length where method.GetParameters().Length == 1 && method.GetParameters()[0].ParameterType == typeof(string) select method).Single(); - var parameterizedMethod = methodInfo.MakeGenericMethod(contractType); - export = parameterizedMethod.Invoke(_exportProvider, new[] { contract.ContractName }); - Assumes.NotNull(export); - } - return true; + return methodInfo.MakeGenericMethod(typeArguments); + } } - private static (Type exportType, Type? metadataType) GetContractType(Type contractType, bool importMany) + private static (Type exportType, Type? metadataType, bool isLazy) GetContractType(Type contractType, bool importMany) { - if (importMany && contractType.IsConstructedGenericType) + if (importMany) { - if (contractType.GetGenericTypeDefinition() == typeof(IList<>) - || contractType.GetGenericTypeDefinition() == typeof(ICollection<>) - || contractType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + if (contractType.IsConstructedGenericType) + { + if (contractType.GetGenericTypeDefinition() == typeof(IList<>) + || contractType.GetGenericTypeDefinition() == typeof(ICollection<>) + || contractType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + contractType = contractType.GenericTypeArguments[0]; + } + } + else if (contractType.IsArray) { - contractType = contractType.GenericTypeArguments[0]; + contractType = contractType.GetElementType().AssumeNotNull(); } } @@ -75,11 +87,11 @@ private static (Type exportType, Type? metadataType) GetContractType(Type contra { if (contractType.GetGenericTypeDefinition() == typeof(Lazy<>)) { - return (contractType.GenericTypeArguments[0], null); + return (contractType.GenericTypeArguments[0], null, true); } else if (contractType.GetGenericTypeDefinition() == typeof(Lazy<,>)) { - return (contractType.GenericTypeArguments[0], contractType.GenericTypeArguments[1]); + return (contractType.GenericTypeArguments[0], contractType.GenericTypeArguments[1], true); } else { @@ -87,7 +99,7 @@ private static (Type exportType, Type? metadataType) GetContractType(Type contra } } - throw new NotSupportedException(); + return (contractType, null, false); } } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Mef/TestComposition.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Mef/TestComposition.cs index 4144af3544b..d73f291ca9c 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Mef/TestComposition.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Mef/TestComposition.cs @@ -22,8 +22,7 @@ public sealed partial class TestComposition public static readonly TestComposition Empty = new( ImmutableHashSet.Empty, ImmutableHashSet.Empty, - ImmutableHashSet.Empty, - scope: null); + ImmutableHashSet.Empty); public static readonly TestComposition Roslyn = Empty .AddAssemblies(MefHostServices.DefaultAssemblies) @@ -102,32 +101,17 @@ public override int GetHashCode() ///
public readonly ImmutableHashSet Parts; - /// - /// The scope in which to create the export provider, or to use the default scope. - /// - public readonly string? Scope; - private readonly Lazy _exportProviderFactory; - private TestComposition(ImmutableHashSet assemblies, ImmutableHashSet parts, ImmutableHashSet excludedPartTypes, string? scope) + private TestComposition(ImmutableHashSet assemblies, ImmutableHashSet parts, ImmutableHashSet excludedPartTypes) { Assemblies = assemblies; Parts = parts; ExcludedPartTypes = excludedPartTypes; - Scope = scope; _exportProviderFactory = new Lazy(GetOrCreateFactory); } -#if false -/// -/// Returns a new instance of for the composition. This will either be a MEF composition or VS MEF composition host, -/// depending on what layer the composition is for. Editor Features and VS layers use VS MEF composition while anything else uses System.Composition. -/// -public HostServices GetHostServices() - => VisualStudioMefHostServices.Create(ExportProviderFactory.CreateExportProvider()); -#endif - /// /// VS MEF . /// @@ -145,7 +129,7 @@ private IExportProviderFactory GetOrCreateFactory() } } - var newFactory = ExportProviderCache.CreateExportProviderFactory(GetCatalog(), Scope); + var newFactory = ExportProviderCache.CreateExportProviderFactory(GetCatalog()); lock (s_factoryCache) { @@ -215,7 +199,7 @@ public TestComposition WithAssemblies(ImmutableHashSet assemblies) var testAssembly = assemblies.FirstOrDefault(IsTestAssembly); Verify.Operation(testAssembly is null, $"Test assemblies are not allowed in test composition: {testAssembly}. Specify explicit test parts instead."); - return new TestComposition(assemblies, Parts, ExcludedPartTypes, Scope); + return new TestComposition(assemblies, Parts, ExcludedPartTypes); static bool IsTestAssembly(Assembly assembly) { @@ -230,13 +214,10 @@ static bool IsTestAssembly(Assembly assembly) } public TestComposition WithParts(ImmutableHashSet parts) - => parts == Parts ? this : new TestComposition(Assemblies, parts, ExcludedPartTypes, Scope); + => parts == Parts ? this : new TestComposition(Assemblies, parts, ExcludedPartTypes); public TestComposition WithExcludedPartTypes(ImmutableHashSet excludedPartTypes) - => excludedPartTypes == ExcludedPartTypes ? this : new TestComposition(Assemblies, Parts, excludedPartTypes, Scope); - - public TestComposition WithScope(string? scope) - => scope == Scope ? this : new TestComposition(Assemblies, Parts, ExcludedPartTypes, scope); + => excludedPartTypes == ExcludedPartTypes ? this : new TestComposition(Assemblies, Parts, excludedPartTypes); /// /// Use for VS MEF composition troubleshooting. diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Mef/UseExportProviderAttribute.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Mef/UseExportProviderAttribute.cs deleted file mode 100644 index 8f2d7ba28f3..00000000000 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Mef/UseExportProviderAttribute.cs +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Reflection; -using Microsoft.CodeAnalysis.Host; -using Microsoft.VisualStudio.Composition; -using Xunit.Sdk; - -namespace Microsoft.AspNetCore.Razor.Test.Common.Mef; - -/// -/// This attribute supports tests that need to use a MEF container () directly or -/// indirectly during the test sequence. It ensures production code uniformly handles the export provider created -/// during a test, and cleans up the state before the test completes. -/// -/// -/// This attribute serves several important functions for tests that use state variables which are otherwise -/// shared at runtime: -/// -/// Ensures implementations all use the same , which is -/// the one created by the test. -/// Clears static cached values in production code holding instances of , or any -/// object obtained from it or one of its related interfaces such as . -/// Isolates tests by waiting for asynchronous operations to complete before a test is considered -/// complete. -/// When required, provides a separate for the -/// executing in the test process. If this provider is created during testing, it is cleaned up with the primary -/// export provider during test teardown. -/// -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] -public class UseExportProviderAttribute : BeforeAfterTestAttribute -{ - /// - /// Asynchronous operations are expected to be cancelled at the end of the test that started them. Operations - /// cancelled by the test are cleaned up immediately. The remaining operations are given an opportunity to run - /// to completion. If this timeout is exceeded by the asynchronous operations running after a test completes, - /// the test is failed. - /// - private static readonly TimeSpan s_cleanupTimeout = TimeSpan.FromMinutes(1); - -#if false -private MefHostServices? _hostServices; -#endif - - public override void Before(MethodInfo? methodUnderTest) - { -#if false - MefHostServices.TestAccessor.HookServiceCreation(CreateMefHostServices); - - // make sure we enable this for all unit tests - AsynchronousOperationListenerProvider.Enable(enable: true, diagnostics: true); -#endif - - ExportProviderCache.SetEnabled_OnlyUseExportProviderAttributeCanCall(true); - } - - /// - /// To the extent reasonably possible, this method resets the state of the test environment to the same state as - /// it started, ensuring that tests running in sequence cannot influence the outcome of later tests. - /// - /// - /// The test cleanup runs in two primary steps: - /// - /// Waiting for asynchronous operations started by the test to complete. - /// Disposing of mutable resources created by the test. - /// Clearing static state variables related to the use of MEF during a test. - /// - /// - public override void After(MethodInfo? methodUnderTest) - { - try - { - foreach (var exportProvider in ExportProviderCache.ExportProvidersForCleanup) - { - DisposeExportProvider(exportProvider); - } - } - finally - { -#if false - // Replace hooks with ones that always throw exceptions. These hooks detect cases where code executing - // after the end of a test attempts to create an ExportProvider. - MefHostServices.TestAccessor.HookServiceCreation(DenyMefHostServicesCreationBetweenTests); -#endif - - // Reset static state variables. -#if false - _hostServices = null; -#endif - ExportProviderCache.SetEnabled_OnlyUseExportProviderAttributeCanCall(false); - } - } - - private static void DisposeExportProvider(ExportProvider? exportProvider) - { - if (exportProvider is null) - { - return; - } - - // Dispose of the export provider, including calling Dispose for any IDisposable services created during the test. - using var _ = exportProvider; - -#if false - if (exportProvider.GetExportedValues().SingleOrDefault() is { } listenerProvider) - { - if (exportProvider.GetExportedValues().SingleOrDefault()?.HasMainThread ?? false) - { - // Immediately clear items from the foreground notification service for which cancellation is - // requested. This service maintains a queue separately from Tasks, and work items scheduled for - // execution after a delay are not immediately purged when cancellation is requested. This code - // instructs the service to walk the list of queued work items and immediately cancel and purge any - // which are already cancelled. - var foregroundNotificationService = exportProvider.GetExportedValues().SingleOrDefault() as ForegroundNotificationService; - foregroundNotificationService?.ReleaseCancelledItems(); - } - - // Verify the synchronization context was not used incorrectly - var testExportJoinableTaskContext = exportProvider.GetExportedValues().SingleOrDefault(); - var denyExecutionSynchronizationContext = testExportJoinableTaskContext?.SynchronizationContext as TestExportJoinableTaskContext.DenyExecutionSynchronizationContext; - - // Join remaining operations with a timeout - using (var timeoutTokenSource = new CancellationTokenSource(s_cleanupTimeout)) - { - if (denyExecutionSynchronizationContext is object) - { - // Immediately cancel the test if the synchronization context is improperly used - denyExecutionSynchronizationContext.InvalidSwitch += delegate { timeoutTokenSource.CancelAfter(0); }; - denyExecutionSynchronizationContext.ThrowIfSwitchOccurred(); - } - - try - { - // This attribute cleans up the in-process and out-of-process export providers separately, so we - // don't need to provide a workspace when waiting for operations to complete. - var waiter = ((AsynchronousOperationListenerProvider)listenerProvider).WaitAllDispatcherOperationAndTasksAsync(workspace: null); - waiter.JoinUsingDispatcher(timeoutTokenSource.Token); - } - catch (OperationCanceledException ex) when (timeoutTokenSource.IsCancellationRequested) - { - // If the failure was caused by an invalid thread change, throw that exception - denyExecutionSynchronizationContext?.ThrowIfSwitchOccurred(); - - var messageBuilder = new StringBuilder("Failed to clean up listeners in a timely manner."); - foreach (var token in ((AsynchronousOperationListenerProvider)listenerProvider).GetTokens()) - { - messageBuilder.AppendLine().Append($" {token}"); - } - - throw new TimeoutException(messageBuilder.ToString(), ex); - } - } - - denyExecutionSynchronizationContext?.ThrowIfSwitchOccurred(); - - foreach (var testErrorHandler in exportProvider.GetExportedValues()) - { - var exceptions = testErrorHandler.Exceptions; - if (exceptions.Count > 0) - { - throw new AggregateException("Tests threw unexpected exceptions", exceptions); - } - } - } -#endif - } - -#if false -private MefHostServices CreateMefHostServices(IEnumerable assemblies) -{ - ExportProvider exportProvider; - - if (assemblies is ImmutableArray array && - array == MefHostServices.DefaultAssemblies && - ExportProviderCache.LocalExportProviderForCleanup != null) - { - if (_hostServices != null) - { - return _hostServices; - } - - exportProvider = ExportProviderCache.LocalExportProviderForCleanup; - } - else - { - exportProvider = ExportProviderCache.GetOrCreateExportProviderFactory(assemblies).CreateExportProvider(); - } - - Interlocked.CompareExchange( - ref _hostServices, - new ExportProviderMefHostServices(exportProvider), - null); - - return _hostServices; -} - -private static MefHostServices DenyMefHostServicesCreationBetweenTests(IEnumerable assemblies) -{ - // If you hit this, one of three situations occurred: - // - // 1. A test method that uses ExportProvider is not marked with UseExportProviderAttribute (can also be - // applied to the containing type or a base type. - // 2. A test attempted to create an ExportProvider during the test cleanup operations after the - // ExportProvider was already disposed. - // 3. A test attempted to use an ExportProvider in the constructor of the test, or during the initialization - // of a field in the test class. - throw new InvalidOperationException("Cannot create host services after test tear down."); -} - -private class ExportProviderMefHostServices : MefHostServices, IMefHostExportProvider -{ - private readonly VisualStudioMefHostServices _vsHostServices; - - public ExportProviderMefHostServices(ExportProvider exportProvider) - : base(new ContainerConfiguration().CreateContainer()) - { - _vsHostServices = VisualStudioMefHostServices.Create(exportProvider); - } - - protected internal override HostWorkspaceServices CreateWorkspaceServices(Workspace workspace) - => _vsHostServices.CreateWorkspaceServices(workspace); - - IEnumerable> IMefHostExportProvider.GetExports() - => _vsHostServices.GetExports(); - - IEnumerable> IMefHostExportProvider.GetExports() - => _vsHostServices.GetExports(); -} -#endif -} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Microsoft.AspNetCore.Razor.Test.Common.Tooling.csproj b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Microsoft.AspNetCore.Razor.Test.Common.Tooling.csproj index 89019499fa8..6683bf95cdb 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Microsoft.AspNetCore.Razor.Test.Common.Tooling.csproj +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Microsoft.AspNetCore.Razor.Test.Common.Tooling.csproj @@ -89,10 +89,6 @@ - - - - diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/Extensions.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/Extensions.cs deleted file mode 100644 index 455366453bf..00000000000 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/Extensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using Microsoft.AspNetCore.Razor.LanguageServer; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; - -namespace Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; - -internal static class Extensions -{ - public static TestDocumentSnapshot CreateAndAddDocument(this ProjectSnapshotManager.Updater updater, ProjectSnapshot projectSnapshot, string filePath) - { - var documentSnapshot = TestDocumentSnapshot.Create(projectSnapshot, filePath); - updater.DocumentAdded(projectSnapshot.Key, documentSnapshot.HostDocument, new DocumentSnapshotTextLoader(documentSnapshot)); - - return documentSnapshot; - } - - public static TestProjectSnapshot CreateAndAddProject(this ProjectSnapshotManager.Updater updater, string filePath) - { - var projectSnapshot = TestProjectSnapshot.Create(filePath); - updater.ProjectAdded(projectSnapshot.HostProject); - - return projectSnapshot; - } -} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestDocumentSnapshot.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestDocumentSnapshot.cs index 645bec36bae..07cc9ddcf81 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestDocumentSnapshot.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestDocumentSnapshot.cs @@ -1,113 +1,123 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; -using System.IO; +using System.Diagnostics.CodeAnalysis; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.ProjectSystem; -using Microsoft.AspNetCore.Razor.Utilities; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Text; namespace Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; -internal class TestDocumentSnapshot : DocumentSnapshot +internal sealed class TestDocumentSnapshot : IDocumentSnapshot { - private RazorCodeDocument? _codeDocument; + public DocumentSnapshot RealSnapshot { get; } - public static TestDocumentSnapshot Create(string filePath) - => Create(filePath, string.Empty); + private readonly RazorCodeDocument? _codeDocument; - public static TestDocumentSnapshot Create(string filePath, VersionStamp textVersion) - => Create(filePath, string.Empty, textVersion); + private TestDocumentSnapshot(TestProjectSnapshot project, DocumentState state, RazorCodeDocument? codeDocument = null) + { + RealSnapshot = new DocumentSnapshot(project.RealSnapshot, state); + _codeDocument = codeDocument; + } - public static TestDocumentSnapshot Create(string filePath, string text, int version = 0) - => Create(filePath, text, VersionStamp.Default, version: version); + public static TestDocumentSnapshot Create(string filePath) + => Create(filePath, text: string.Empty, ProjectWorkspaceState.Default, version: 0); - public static TestDocumentSnapshot Create(string filePath, string text, VersionStamp textVersion, ProjectWorkspaceState? projectWorkspaceState = null, int version = 0) - => Create(filePath, text, textVersion, TestProjectSnapshot.Create(filePath + ".csproj", projectWorkspaceState), version); + public static TestDocumentSnapshot Create(string filePath, string text, int version = 0) + => Create(filePath, text, ProjectWorkspaceState.Default, version); - public static TestDocumentSnapshot Create(string filePath, string text, VersionStamp textVersion, TestProjectSnapshot projectSnapshot, int version) + public static TestDocumentSnapshot Create(string filePath, string text, ProjectWorkspaceState projectWorkspaceState, int version = 0) { - var targetPath = Path.GetDirectoryName(projectSnapshot.FilePath) is string projectDirectory && filePath.StartsWith(projectDirectory) - ? filePath[projectDirectory.Length..] - : filePath; + var project = TestProjectSnapshot.Create(filePath + ".csproj", projectWorkspaceState); + var hostDocument = TestHostDocument.Create(project.HostProject, filePath); - var hostDocument = new HostDocument(filePath, targetPath); var sourceText = SourceText.From(text); - var documentState = new DocumentState( - hostDocument, - SourceText.From(text), - textVersion, - version, - () => Task.FromResult(TextAndVersion.Create(sourceText, textVersion))); - var testDocument = new TestDocumentSnapshot(projectSnapshot, documentState); - - return testDocument; + var textVersion = VersionStamp.Default; + + var documentState = new DocumentState(hostDocument, version, sourceText, textVersion); + + return new TestDocumentSnapshot(project, documentState); } - internal static TestDocumentSnapshot Create(ProjectSnapshot projectSnapshot, string filePath, string text = "", VersionStamp? version = null) + public static TestDocumentSnapshot Create(string filePath, RazorCodeDocument codeDocument, int version = 0) + => Create(filePath, codeDocument, ProjectWorkspaceState.Default, version); + + public static TestDocumentSnapshot Create(string filePath, RazorCodeDocument codeDocument, ProjectWorkspaceState projectWorkspaceState, int version = 0) { - version ??= VersionStamp.Default; + var project = TestProjectSnapshot.Create(filePath + ".csproj", projectWorkspaceState); + var hostDocument = TestHostDocument.Create(project.HostProject, filePath); - var targetPath = FilePathNormalizer.Normalize(filePath); - var projectDirectory = FilePathNormalizer.GetNormalizedDirectoryName(projectSnapshot.FilePath); - if (targetPath.StartsWith(projectDirectory)) - { - targetPath = targetPath[projectDirectory.Length..]; - } + var sourceText = codeDocument.Source.Text; + var textVersion = VersionStamp.Default; - var hostDocument = new HostDocument(filePath, targetPath); - var sourceText = SourceText.From(text); - var documentState = new DocumentState( - hostDocument, - SourceText.From(text), - version, - version: 1, - () => Task.FromResult(TextAndVersion.Create(sourceText, version.Value))); - var testDocument = new TestDocumentSnapshot(projectSnapshot, documentState); - - return testDocument; + var documentState = new DocumentState(hostDocument, version, sourceText, textVersion); + + return new TestDocumentSnapshot(project, documentState, codeDocument); } - public TestDocumentSnapshot(ProjectSnapshot projectSnapshot, DocumentState documentState) - : base(projectSnapshot, documentState) + public HostDocument HostDocument => RealSnapshot.HostDocument; + + public string? FileKind => RealSnapshot.FileKind; + public string? FilePath => RealSnapshot.FilePath; + public string? TargetPath => RealSnapshot.TargetPath; + public IProjectSnapshot Project => RealSnapshot.Project; + public int Version => RealSnapshot.Version; + + public ValueTask GetGeneratedOutputAsync( + bool forceDesignTimeGeneratedOutput, + CancellationToken cancellationToken) { + return _codeDocument is null + ? RealSnapshot.GetGeneratedOutputAsync(forceDesignTimeGeneratedOutput, cancellationToken) + : new(_codeDocument); } - public HostDocument HostDocument => State.HostDocument; - - public override Task GetGeneratedOutputAsync(bool _) + public ValueTask GetTextAsync(CancellationToken cancellationToken) { - if (_codeDocument is null) - { - throw new ArgumentNullException(nameof(_codeDocument)); - } + return _codeDocument is null + ? RealSnapshot.GetTextAsync(cancellationToken) + : new(_codeDocument.Source.Text); + } + + public ValueTask GetTextVersionAsync(CancellationToken cancellationToken) + => RealSnapshot.GetTextVersionAsync(cancellationToken); - return Task.FromResult(_codeDocument); + public ValueTask GetCSharpSyntaxTreeAsync(CancellationToken cancellationToken) + { + return _codeDocument is null + ? RealSnapshot.GetCSharpSyntaxTreeAsync(cancellationToken) + : new(DocumentSnapshot.GetOrParseCSharpSyntaxTree(_codeDocument, cancellationToken)); } - public override bool TryGetGeneratedOutput(out RazorCodeDocument result) + public bool TryGetGeneratedOutput([NotNullWhen(true)] out RazorCodeDocument? result) { - if (_codeDocument is null) + if (_codeDocument is { } codeDocument) { - throw new InvalidOperationException($"You must call {nameof(With)} to set the code document for this document snapshot."); + result = codeDocument; + return true; } - result = _codeDocument; - return true; + return RealSnapshot.TryGetGeneratedOutput(out result); } - public TestDocumentSnapshot With(RazorCodeDocument codeDocument) + public bool TryGetText([NotNullWhen(true)] out SourceText? result) { - if (codeDocument is null) + if (_codeDocument is { } codeDocument) { - throw new ArgumentNullException(nameof(codeDocument)); + result = codeDocument.Source.Text; + return true; } - _codeDocument = codeDocument; - return this; + return RealSnapshot.TryGetText(out result); } + + public bool TryGetTextVersion(out VersionStamp result) + => RealSnapshot.TryGetTextVersion(out result); + + public IDocumentSnapshot WithText(SourceText text) + => RealSnapshot.WithText(text); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestHostDocument.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestHostDocument.cs new file mode 100644 index 00000000000..326d4c0d7e0 --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestHostDocument.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Razor.Utilities; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; + +internal static class TestHostDocument +{ + public static HostDocument Create(HostProject hostProject, string documentFilePath) + { + var targetPath = FilePathNormalizer.Normalize(documentFilePath); + var projectDirectory = FilePathNormalizer.GetNormalizedDirectoryName(hostProject.FilePath); + if (targetPath.StartsWith(projectDirectory)) + { + targetPath = targetPath[projectDirectory.Length..]; + } + + return new(documentFilePath, targetPath); + } + + public static TextLoader CreateEmptyTextLoader(this HostDocument hostDocument) + => TestMocks.CreateTextLoader(hostDocument.FilePath, string.Empty); +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestHostProject.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestHostProject.cs new file mode 100644 index 00000000000..0f3ac548c4e --- /dev/null +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestHostProject.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.IO; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; + +internal static class TestHostProject +{ + public static HostProject Create(string filePath) + => Create(filePath, intermediateOutputPath: Path.Combine(Path.GetDirectoryName(filePath) ?? @"\\path", "obj")); + + public static HostProject Create(string filePath, string intermediateOutputPath) + => new(filePath, intermediateOutputPath, RazorConfiguration.Default, rootNamespace: "TestRootNamespace"); +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestProjectSnapshot.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestProjectSnapshot.cs index 36da196783d..731bf4a98dd 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestProjectSnapshot.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestProjectSnapshot.cs @@ -1,8 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; -using System.IO; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.ProjectEngineHost; @@ -10,58 +12,57 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Text; using Microsoft.NET.Sdk.Razor.SourceGenerators; namespace Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; -internal class TestProjectSnapshot : ProjectSnapshot +internal sealed class TestProjectSnapshot : IProjectSnapshot { - public static TestProjectSnapshot Create(string filePath, ProjectWorkspaceState? projectWorkspaceState = null) - => Create(filePath, [], projectWorkspaceState); - - public static TestProjectSnapshot Create(string filePath, string[] documentFilePaths, ProjectWorkspaceState? projectWorkspaceState = null) - => Create(filePath, Path.Combine(Path.GetDirectoryName(filePath) ?? @"\\path", "obj"), documentFilePaths, RazorConfiguration.Default, projectWorkspaceState); + public ProjectSnapshot RealSnapshot { get; } - public static TestProjectSnapshot Create( - string filePath, - string intermediateOutputPath, - string[] documentFilePaths, - RazorConfiguration configuration, - ProjectWorkspaceState? projectWorkspaceState = null, - string? displayName = null) + private TestProjectSnapshot(ProjectState state) { - var hostProject = new HostProject(filePath, intermediateOutputPath, configuration, "TestRootNamespace", displayName); - var state = ProjectState.Create(ProjectEngineFactories.DefaultProvider, hostProject, projectWorkspaceState ?? ProjectWorkspaceState.Default); - - foreach (var documentFilePath in documentFilePaths) - { - var hostDocument = new HostDocument(documentFilePath, documentFilePath); - state = state.WithAddedHostDocument(hostDocument, () => Task.FromResult(TextAndVersion.Create(SourceText.From(string.Empty), VersionStamp.Default))); - } - - var testProject = new TestProjectSnapshot(state); - - return testProject; + RealSnapshot = new ProjectSnapshot(state); } - private TestProjectSnapshot(ProjectState state) - : base(state) + public static TestProjectSnapshot Create(string filePath, ProjectWorkspaceState? projectWorkspaceState = null) { - } + var hostProject = TestHostProject.Create(filePath); + projectWorkspaceState ??= ProjectWorkspaceState.Default; - public override VersionStamp Version => throw new NotImplementedException(); + var state = ProjectState.Create(ProjectEngineFactories.DefaultProvider, hostProject, projectWorkspaceState); - public override IDocumentSnapshot? GetDocument(string filePath) - { - return base.GetDocument(filePath); + return new TestProjectSnapshot(state); } - public override RazorProjectEngine GetProjectEngine() - { - return RazorProjectEngine.Create( - RazorConfiguration.Default, + public HostProject HostProject => RealSnapshot.HostProject; + + public ProjectKey Key => RealSnapshot.Key; + public RazorConfiguration Configuration => RealSnapshot.Configuration; + public IEnumerable DocumentFilePaths => RealSnapshot.DocumentFilePaths; + public string FilePath => RealSnapshot.FilePath; + public string IntermediateOutputPath => RealSnapshot.IntermediateOutputPath; + public string? RootNamespace => RealSnapshot.RootNamespace; + public string DisplayName => RealSnapshot.DisplayName; + public LanguageVersion CSharpLanguageVersion => RealSnapshot.CSharpLanguageVersion; + public ProjectWorkspaceState ProjectWorkspaceState => RealSnapshot.ProjectWorkspaceState; + public VersionStamp Version => RealSnapshot.Version; + + public RazorProjectEngine GetProjectEngine() + => RazorProjectEngine.Create( + Configuration, RazorProjectFileSystem.Create("C:/"), b => b.Features.Add(new ConfigureRazorParserOptions(useRoslynTokenizer: true, CSharpParseOptions.Default))); - } + + public ValueTask> GetTagHelpersAsync(CancellationToken cancellationToken) + => RealSnapshot.GetTagHelpersAsync(cancellationToken); + + public bool ContainsDocument(string filePath) + => RealSnapshot.ContainsDocument(filePath); + + public IDocumentSnapshot? GetDocument(string filePath) + => RealSnapshot.GetDocument(filePath); + + public bool TryGetDocument(string filePath, [NotNullWhen(true)] out IDocumentSnapshot? document) + => RealSnapshot.TryGetDocument(filePath, out document); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestProjectSnapshotManager.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestProjectSnapshotManager.cs index 01f551ba04c..06ae2133870 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestProjectSnapshotManager.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ProjectSystem/TestProjectSnapshotManager.cs @@ -2,14 +2,11 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer; using Microsoft.AspNetCore.Razor.ProjectEngineHost; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Workspaces; namespace Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; @@ -18,28 +15,10 @@ internal partial class TestProjectSnapshotManager( ILoggerFactory loggerFactory, CancellationToken disposalToken, Action? initializer = null) - : ProjectSnapshotManager(projectEngineFactoryProvider, loggerFactory, initializer), IProjectCollectionResolver + : ProjectSnapshotManager(projectEngineFactoryProvider, loggerFactory, initializer) { private readonly CancellationToken _disposalToken = disposalToken; - public Task CreateAndAddDocumentAsync(ProjectSnapshot projectSnapshot, string filePath) - { - return UpdateAsync( - updater => - { - var documentSnapshot = TestDocumentSnapshot.Create(projectSnapshot, filePath); - updater.DocumentAdded(projectSnapshot.Key, documentSnapshot.HostDocument, new DocumentSnapshotTextLoader(documentSnapshot)); - - return documentSnapshot; - }, - _disposalToken); - } - - public IEnumerable EnumerateProjects(IDocumentSnapshot snapshot) - { - return GetProjects(); - } - public Listener ListenToNotifications() => new(this); public Task UpdateAsync(Action updater) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/TestCode.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/TestCode.cs index 6930bda0d33..9374bd108ab 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/TestCode.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/TestCode.cs @@ -47,6 +47,9 @@ public TextSpan Span public ImmutableArray Spans => GetNamedSpans(string.Empty); + public ImmutableDictionary> NamedSpans + => _nameToSpanMap; + public ImmutableArray GetNamedSpans(string name) => _nameToSpanMap[name]; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/TestMocks.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/TestMocks.cs index c8f8e987ec4..9abf55ceff3 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/TestMocks.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/TestMocks.cs @@ -3,12 +3,12 @@ using System; using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Text; using Moq; -using Moq.Language.Flow; namespace Microsoft.AspNetCore.Razor.Test.Common; @@ -31,6 +31,21 @@ public static TextLoader CreateTextLoader(string filePath, SourceText text) return mock.Object; } + public static TextLoader CreateTextLoader(string text, VersionStamp version) + => CreateTextLoader(SourceText.From(text), version); + + public static TextLoader CreateTextLoader(SourceText text, VersionStamp version) + { + var mock = new StrictMock(); + + var textAndVersion = TextAndVersion.Create(text, version); + + mock.Setup(x => x.LoadTextAndVersionAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(textAndVersion); + + return mock.Object; + } + public interface IClientConnectionBuilder { void SetupSendRequest(string method, TResponse response, bool verifiable = false); @@ -84,4 +99,34 @@ public static void VerifySendRequest(this Mock(this Mock mock, string method, TParams @params, Func times) => mock.Verify(x => x.SendRequestAsync(method, @params, It.IsAny()), times); + + public static IProjectSnapshot CreateProjectSnapshot(HostProject hostProject, ProjectWorkspaceState? projectWorkspaceState = null) + { + var mock = new StrictMock(); + + mock.SetupGet(x => x.Key) + .Returns(hostProject.Key); + mock.SetupGet(x => x.FilePath) + .Returns(hostProject.FilePath); + mock.SetupGet(x => x.IntermediateOutputPath) + .Returns(hostProject.IntermediateOutputPath); + mock.SetupGet(x => x.Configuration) + .Returns(hostProject.Configuration); + mock.SetupGet(x => x.RootNamespace) + .Returns(hostProject.RootNamespace); + mock.SetupGet(x => x.DisplayName) + .Returns(hostProject.DisplayName); + + if (projectWorkspaceState is not null) + { + mock.SetupGet(x => x.ProjectWorkspaceState) + .Returns(projectWorkspaceState); + mock.SetupGet(x => x.CSharpLanguageVersion) + .Returns(projectWorkspaceState.CSharpLanguageVersion); + mock.Setup(x => x.GetTagHelpersAsync(It.IsAny())) + .ReturnsAsync(projectWorkspaceState.TagHelpers); + } + + return mock.Object; + } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ToolingTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ToolingTestBase.cs index 15885cad28e..09d784274ab 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ToolingTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/ToolingTestBase.cs @@ -5,7 +5,9 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.ProjectEngineHost; using Microsoft.AspNetCore.Razor.Test.Common.Logging; +using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.VisualStudio.Threading; using Xunit; @@ -214,4 +216,13 @@ protected void AddDisposables(IEnumerable disposables) /// protected void AddDisposables(params IAsyncDisposable[] disposables) => AddDisposables((IEnumerable)disposables); + + private protected virtual TestProjectSnapshotManager CreateProjectSnapshotManager() + => CreateProjectSnapshotManager(ProjectEngineFactories.DefaultProvider); + + private protected virtual TestProjectSnapshotManager CreateProjectSnapshotManager(IProjectEngineFactoryProvider projectEngineFactoryProvider) + => new( + projectEngineFactoryProvider, + LoggerFactory, + DisposalToken); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/VisualStudio_NetFx/VisualStudioTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/VisualStudio_NetFx/VisualStudioTestBase.cs index 3ba85943b1e..0e4765bd856 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/VisualStudio_NetFx/VisualStudioTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/VisualStudio_NetFx/VisualStudioTestBase.cs @@ -2,17 +2,10 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using Microsoft.AspNetCore.Razor.Language.Legacy; -using Microsoft.AspNetCore.Razor.ProjectEngineHost; -using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Razor.Test.Common.VisualStudio; public abstract class VisualStudioTestBase(ITestOutputHelper testOutput) : ToolingParserTestBase(testOutput) { - private protected TestProjectSnapshotManager CreateProjectSnapshotManager() - => CreateProjectSnapshotManager(ProjectEngineFactories.DefaultProvider); - - private protected TestProjectSnapshotManager CreateProjectSnapshotManager(IProjectEngineFactoryProvider projectEngineFactoryProvider) - => new(projectEngineFactoryProvider, LoggerFactory, DisposalToken); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/DocumentExcerptServiceTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/DocumentExcerptServiceTestBase.cs index d6370f7eb2a..f013c6cab13 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/DocumentExcerptServiceTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/DocumentExcerptServiceTestBase.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.ProjectSystem; @@ -40,9 +41,11 @@ public static (SourceText sourceText, TextSpan span) CreateText(string text) // Adds the text to a ProjectSnapshot, generates code, and updates the workspace. private (IDocumentSnapshot primary, Document secondary) InitializeDocument(SourceText sourceText) { - var project = new ProjectSnapshot( - ProjectState.Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) - .WithAddedHostDocument(_hostDocument, () => Task.FromResult(TextAndVersion.Create(sourceText, VersionStamp.Create())))); + var state = ProjectState + .Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) + .WithAddedHostDocument(_hostDocument, TestMocks.CreateTextLoader(sourceText, VersionStamp.Create())); + + var project = new ProjectSnapshot(state); var primary = project.GetDocument(_hostDocument.FilePath).AssumeNotNull(); @@ -66,9 +69,9 @@ public static (SourceText sourceText, TextSpan span) CreateText(string text) // Maps a span in the primary buffer to the secondary buffer. This is only valid for C# code // that appears in the primary buffer. - private static async Task GetSecondarySpanAsync(IDocumentSnapshot primary, TextSpan primarySpan, Document secondary) + private static async Task GetSecondarySpanAsync(IDocumentSnapshot primary, TextSpan primarySpan, Document secondary, CancellationToken cancellationToken) { - var output = await primary.GetGeneratedOutputAsync(); + var output = await primary.GetGeneratedOutputAsync(cancellationToken); foreach (var mapping in output.GetCSharpDocument().SourceMappings) { @@ -78,8 +81,8 @@ private static async Task GetSecondarySpanAsync(IDocumentSnapshot prim var offset = mapping.GeneratedSpan.AbsoluteIndex - mapping.OriginalSpan.AbsoluteIndex; var secondarySpan = new TextSpan(primarySpan.Start + offset, primarySpan.Length); Assert.Equal( - (await primary.GetTextAsync()).GetSubText(primarySpan).ToString(), - (await secondary.GetTextAsync()).GetSubText(secondarySpan).ToString()); + (await primary.GetTextAsync(cancellationToken)).ToString(primarySpan), + (await secondary.GetTextAsync(cancellationToken)).ToString(secondarySpan)); return secondarySpan; } } @@ -87,19 +90,19 @@ private static async Task GetSecondarySpanAsync(IDocumentSnapshot prim throw new InvalidOperationException("Could not map the primary span to the generated code."); } - public async Task<(Document generatedDocument, SourceText razorSourceText, TextSpan primarySpan, TextSpan generatedSpan)> InitializeAsync(string razorSource) + public async Task<(Document generatedDocument, SourceText razorSourceText, TextSpan primarySpan, TextSpan generatedSpan)> InitializeAsync(string razorSource, CancellationToken cancellationToken) { var (razorSourceText, primarySpan) = CreateText(razorSource); var (primary, generatedDocument) = InitializeDocument(razorSourceText); - var generatedSpan = await GetSecondarySpanAsync(primary, primarySpan, generatedDocument); + var generatedSpan = await GetSecondarySpanAsync(primary, primarySpan, generatedDocument, cancellationToken); return (generatedDocument, razorSourceText, primarySpan, generatedSpan); } - internal async Task<(IDocumentSnapshot primary, Document generatedDocument, TextSpan generatedSpan)> InitializeWithSnapshotAsync(string razorSource) + internal async Task<(IDocumentSnapshot primary, Document generatedDocument, TextSpan generatedSpan)> InitializeWithSnapshotAsync(string razorSource, CancellationToken cancellationToken) { var (razorSourceText, primarySpan) = CreateText(razorSource); var (primary, generatedDocument) = InitializeDocument(razorSourceText); - var generatedSpan = await GetSecondarySpanAsync(primary, primarySpan, generatedDocument); + var generatedSpan = await GetSecondarySpanAsync(primary, primarySpan, generatedDocument, cancellationToken); return (primary, generatedDocument, generatedSpan); } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/TestWorkspace.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/TestWorkspace.cs index cae18632e56..57042c42c99 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/TestWorkspace.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/TestWorkspace.cs @@ -2,8 +2,17 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using Microsoft.AspNetCore.Razor.Test.Common.Mef; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.VisualStudio.Composition; namespace Microsoft.AspNetCore.Razor.Test.Common.Workspaces; @@ -14,6 +23,17 @@ public static class TestWorkspace public static Workspace Create(Action? configure = null) => Create(services: null, configure: configure); + public static AdhocWorkspace CreateWithDiagnosticAnalyzers(ExportProvider exportProvider) + { + var hostServices = MefHostServices.Create(exportProvider.AsCompositionContext()); + + var workspace = Create(hostServices); + + AddAnalyzersToWorkspace(workspace, exportProvider); + + return workspace; + } + public static AdhocWorkspace Create(HostServices? services, Action? configure = null) { lock (s_workspaceLock) @@ -27,4 +47,24 @@ public static AdhocWorkspace Create(HostServices? services, Action f.Name.StartsWith("Microsoft.CodeAnalysis.", StringComparison.Ordinal) && !f.Name.Contains("LanguageServer") && !f.Name.Contains("Test.Utilities")) + .Select(f => f.FullName) + .ToImmutableArray(); + var references = new List(); + foreach (var analyzerPath in analyzerPaths) + { + if (File.Exists(analyzerPath)) + { + references.Add(new AnalyzerFileReference(analyzerPath, analyzerLoader)); + } + } + + workspace.TryApplyChanges(workspace.CurrentSolution.WithAnalyzerReferences(references)); + } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/WorkspaceTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/WorkspaceTestBase.cs index a30b8c69a59..fb683bd8fe0 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/WorkspaceTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/Workspaces/WorkspaceTestBase.cs @@ -57,12 +57,9 @@ private protected IProjectEngineFactoryProvider ProjectEngineFactoryProvider } } - private protected TestProjectSnapshotManager CreateProjectSnapshotManager() + private protected override TestProjectSnapshotManager CreateProjectSnapshotManager() => CreateProjectSnapshotManager(ProjectEngineFactoryProvider); - private protected TestProjectSnapshotManager CreateProjectSnapshotManager(IProjectEngineFactoryProvider projectEngineFactoryProvider) - => new(projectEngineFactoryProvider, LoggerFactory, DisposalToken); - protected virtual void ConfigureWorkspace(AdhocWorkspace workspace) { } diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/IProjectSnapshotManagerExtensions.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/IProjectSnapshotManagerExtensions.cs new file mode 100644 index 00000000000..37913fdaf0b --- /dev/null +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/IProjectSnapshotManagerExtensions.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Razor.Workspaces.Test; + +internal static class IProjectSnapshotManagerExtensions +{ + public static ISolutionQueryOperations GetQueryOperations(this IProjectSnapshotManager projectManager) + => new TestSolutionQueryOperations(projectManager); +} + +file sealed class TestSolutionQueryOperations(IProjectSnapshotManager projectManager) : ISolutionQueryOperations +{ + private readonly IProjectSnapshotManager _projectManager = projectManager; + + public IEnumerable GetProjects() + { + return _projectManager.GetProjects(); + } + + public ImmutableArray GetProjectsContainingDocument(string documentFilePath) + { + using var projects = new PooledArrayBuilder(); + + foreach (var project in _projectManager.GetProjects()) + { + if (project.ContainsDocument(documentFilePath)) + { + projects.Add(project); + } + } + + return projects.DrainToImmutable(); + } +} diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj index e8a6418c986..232456c3724 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Microsoft.CodeAnalysis.Razor.Workspaces.Test.csproj @@ -13,7 +13,6 @@ - diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultDocumentSnapshotTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultDocumentSnapshotTest.cs index 44f5eef3136..8fb1c912ad6 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultDocumentSnapshotTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultDocumentSnapshotTest.cs @@ -15,15 +15,16 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; public class DefaultDocumentSnapshotTest : WorkspaceTestBase { + private static readonly HostDocument s_componentHostDocument = TestProjectData.SomeProjectComponentFile1; + private static readonly HostDocument s_componentCshtmlHostDocument = TestProjectData.SomeProjectCshtmlComponentFile5; + private static readonly HostDocument s_legacyHostDocument = TestProjectData.SomeProjectFile1; + private static readonly HostDocument s_nestedComponentHostDocument = TestProjectData.SomeProjectNestedComponentFile3; + private readonly SourceText _sourceText; private readonly VersionStamp _version; - private readonly HostDocument _componentHostDocument; - private readonly HostDocument _componentCshtmlHostDocument; - private readonly HostDocument _legacyHostDocument; private readonly DocumentSnapshot _componentDocument; private readonly DocumentSnapshot _componentCshtmlDocument; private readonly DocumentSnapshot _legacyDocument; - private readonly HostDocument _nestedComponentHostDocument; private readonly DocumentSnapshot _nestedComponentDocument; public DefaultDocumentSnapshotTest(ITestOutputHelper testOutput) @@ -32,27 +33,21 @@ public DefaultDocumentSnapshotTest(ITestOutputHelper testOutput) _sourceText = SourceText.From("

Hello World

"); _version = VersionStamp.Create(); - // Create a new HostDocument to avoid mutating the code container - _componentCshtmlHostDocument = new HostDocument(TestProjectData.SomeProjectCshtmlComponentFile5); - _componentHostDocument = new HostDocument(TestProjectData.SomeProjectComponentFile1); - _legacyHostDocument = new HostDocument(TestProjectData.SomeProjectFile1); - _nestedComponentHostDocument = new HostDocument(TestProjectData.SomeProjectNestedComponentFile3); - var projectState = ProjectState.Create(ProjectEngineFactoryProvider, TestProjectData.SomeProject, ProjectWorkspaceState.Default); var project = new ProjectSnapshot(projectState); - var textAndVersion = TextAndVersion.Create(_sourceText, _version); + var textLoader = TestMocks.CreateTextLoader(_sourceText, _version); - var documentState = DocumentState.Create(_legacyHostDocument, () => Task.FromResult(textAndVersion)); + var documentState = DocumentState.Create(s_legacyHostDocument, textLoader); _legacyDocument = new DocumentSnapshot(project, documentState); - documentState = DocumentState.Create(_componentHostDocument, () => Task.FromResult(textAndVersion)); + documentState = DocumentState.Create(s_componentHostDocument, textLoader); _componentDocument = new DocumentSnapshot(project, documentState); - documentState = DocumentState.Create(_componentCshtmlHostDocument, () => Task.FromResult(textAndVersion)); + documentState = DocumentState.Create(s_componentCshtmlHostDocument, textLoader); _componentCshtmlDocument = new DocumentSnapshot(project, documentState); - documentState = DocumentState.Create(_nestedComponentHostDocument, () => Task.FromResult(textAndVersion)); + documentState = DocumentState.Create(s_nestedComponentHostDocument, textLoader); _nestedComponentDocument = new DocumentSnapshot(project, documentState); } @@ -60,7 +55,7 @@ public DefaultDocumentSnapshotTest(ITestOutputHelper testOutput) public async Task GCCollect_OutputIsNoLongerCached() { // Arrange - await Task.Run(async () => { await _legacyDocument.GetGeneratedOutputAsync(); }); + await Task.Run(async () => { await _legacyDocument.GetGeneratedOutputAsync(DisposalToken); }); // Act @@ -75,7 +70,7 @@ public async Task GCCollect_OutputIsNoLongerCached() public async Task RegeneratingWithReference_CachesOutput() { // Arrange - var output = await _legacyDocument.GetGeneratedOutputAsync(); + var output = await _legacyDocument.GetGeneratedOutputAsync(DisposalToken); // Mostly doing this to ensure "var output" doesn't get optimized out Assert.NotNull(output); @@ -91,7 +86,7 @@ public async Task RegeneratingWithReference_CachesOutput() public async Task GetGeneratedOutputAsync_CshtmlComponent_ContainsComponentImports() { // Act - var codeDocument = await _componentCshtmlDocument.GetGeneratedOutputAsync(); + var codeDocument = await _componentCshtmlDocument.GetGeneratedOutputAsync(DisposalToken); // Assert Assert.Contains("using global::Microsoft.AspNetCore.Components", codeDocument.GetCSharpSourceText().ToString(), StringComparison.Ordinal); @@ -101,7 +96,7 @@ public async Task GetGeneratedOutputAsync_CshtmlComponent_ContainsComponentImpor public async Task GetGeneratedOutputAsync_Component() { // Act - var codeDocument = await _componentDocument.GetGeneratedOutputAsync(); + var codeDocument = await _componentDocument.GetGeneratedOutputAsync(DisposalToken); // Assert Assert.Contains("ComponentBase", codeDocument.GetCSharpSourceText().ToString(), StringComparison.Ordinal); @@ -111,7 +106,7 @@ public async Task GetGeneratedOutputAsync_Component() public async Task GetGeneratedOutputAsync_NestedComponentDocument_SetsCorrectNamespaceAndClassName() { // Act - var codeDocument = await _nestedComponentDocument.GetGeneratedOutputAsync(); + var codeDocument = await _nestedComponentDocument.GetGeneratedOutputAsync(DisposalToken); // Assert Assert.Contains("ComponentBase", codeDocument.GetCSharpSourceText().ToString(), StringComparison.Ordinal); @@ -125,7 +120,7 @@ public async Task GetGeneratedOutputAsync_NestedComponentDocument_SetsCorrectNam public async Task GetGeneratedOutputAsync_Legacy() { // Act - var codeDocument = await _legacyDocument.GetGeneratedOutputAsync(); + var codeDocument = await _legacyDocument.GetGeneratedOutputAsync(DisposalToken); // Assert Assert.Contains("Template", codeDocument.GetCSharpSourceText().ToString(), StringComparison.Ordinal); diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs index b661352b3e0..9cb9d299dfa 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DefaultProjectSnapshotTest.cs @@ -21,7 +21,7 @@ public class DefaultProjectSnapshotTest : WorkspaceTestBase public DefaultProjectSnapshotTest(ITestOutputHelper testOutput) : base(testOutput) { - _hostProject = new HostProject(TestProjectData.SomeProject.FilePath, TestProjectData.SomeProject.IntermediateOutputPath, FallbackRazorConfiguration.MVC_2_0, TestProjectData.SomeProject.RootNamespace); + _hostProject = TestProjectData.SomeProject with { Configuration = FallbackRazorConfiguration.MVC_2_0 }; _projectWorkspaceState = ProjectWorkspaceState.Create(ImmutableArray.Create( TagHelperDescriptorBuilder.Create("TestTagHelper", "TestAssembly").Build())); diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DocumentStateTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DocumentStateTest.cs index b5296525800..2632887a2ed 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DocumentStateTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/DocumentStateTest.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis.Text; @@ -13,7 +12,7 @@ namespace Microsoft.CodeAnalysis.Razor.ProjectSystem; public class DocumentStateTest : ToolingTestBase { private readonly HostDocument _hostDocument; - private readonly Func> _textLoader; + private readonly TextLoader _textLoader; private readonly SourceText _text; public DocumentStateTest(ITestOutputHelper testOutput) @@ -21,7 +20,7 @@ public DocumentStateTest(ITestOutputHelper testOutput) { _hostDocument = TestProjectData.SomeProjectFile1; _text = SourceText.From("Hello, world!"); - _textLoader = () => Task.FromResult(TextAndVersion.Create(_text, VersionStamp.Create())); + _textLoader = TestMocks.CreateTextLoader(_text, VersionStamp.Create()); } [Fact] @@ -31,7 +30,7 @@ public async Task DocumentState_CreatedNew_HasEmptyText() var state = DocumentState.Create(_hostDocument, DocumentState.EmptyLoader); // Assert - var text = await state.GetTextAsync(); + var text = await state.GetTextAsync(DisposalToken); Assert.Equal(0, text.Length); } @@ -45,7 +44,7 @@ public async Task DocumentState_WithText_CreatesNewState() var state = original.WithText(_text, VersionStamp.Create()); // Assert - var text = await state.GetTextAsync(); + var text = await state.GetTextAsync(DisposalToken); Assert.Same(_text, text); } @@ -59,7 +58,7 @@ public async Task DocumentState_WithTextLoader_CreatesNewState() var state = original.WithTextLoader(_textLoader); // Assert - var text = await state.GetTextAsync(); + var text = await state.GetTextAsync(DisposalToken); Assert.Same(_text, text); } @@ -85,7 +84,7 @@ public async Task DocumentState_WithConfigurationChange_CachesLoadedText() var original = DocumentState.Create(_hostDocument, DocumentState.EmptyLoader) .WithTextLoader(_textLoader); - await original.GetTextAsync(); + await original.GetTextAsync(DisposalToken); // Act var state = original.WithConfigurationChange(); @@ -117,7 +116,7 @@ public async Task DocumentState_WithImportsChange_CachesLoadedText() var original = DocumentState.Create(_hostDocument, DocumentState.EmptyLoader) .WithTextLoader(_textLoader); - await original.GetTextAsync(); + await original.GetTextAsync(DisposalToken); // Act var state = original.WithImportsChange(); @@ -149,7 +148,7 @@ public async Task DocumentState_WithProjectWorkspaceStateChange_CachesLoadedText var original = DocumentState.Create(_hostDocument, DocumentState.EmptyLoader) .WithTextLoader(_textLoader); - await original.GetTextAsync(); + await original.GetTextAsync(DisposalToken); // Act var state = original.WithProjectWorkspaceStateChange(); diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/GeneratedDocumentTextLoaderTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/GeneratedDocumentTextLoaderTest.cs index ee1ca19e87f..3f608cd19f2 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/GeneratedDocumentTextLoaderTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/GeneratedDocumentTextLoaderTest.cs @@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; -using Microsoft.CodeAnalysis.Text; using Xunit; using Xunit.Abstractions; @@ -32,7 +31,7 @@ public async Task LoadAsync_SpecifiesEncoding() // Arrange var project = new ProjectSnapshot( ProjectState.Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) - .WithAddedHostDocument(_hostDocument, () => Task.FromResult(TextAndVersion.Create(SourceText.From(""), VersionStamp.Create())))); + .WithAddedHostDocument(_hostDocument, TestMocks.CreateTextLoader("", VersionStamp.Create()))); var document = project.GetDocument(_hostDocument.FilePath); diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateGeneratedOutputTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateGeneratedOutputTest.cs index 0e0f2732f55..7e80b2537b7 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateGeneratedOutputTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateGeneratedOutputTest.cs @@ -2,13 +2,13 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System.Collections.Immutable; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Text; using Xunit; using Xunit.Abstractions; @@ -20,20 +20,17 @@ public class ProjectStateGeneratedOutputTest : WorkspaceTestBase private readonly HostProject _hostProject; private readonly HostProject _hostProjectWithConfigurationChange; private readonly ImmutableArray _someTagHelpers; - private readonly SourceText _text; public ProjectStateGeneratedOutputTest(ITestOutputHelper testOutput) : base(testOutput) { - _hostProject = new HostProject(TestProjectData.SomeProject.FilePath, TestProjectData.SomeProject.IntermediateOutputPath, FallbackRazorConfiguration.MVC_2_0, TestProjectData.SomeProject.RootNamespace); - _hostProjectWithConfigurationChange = new HostProject(TestProjectData.SomeProject.FilePath, TestProjectData.SomeProject.IntermediateOutputPath, FallbackRazorConfiguration.MVC_1_0, TestProjectData.SomeProject.RootNamespace); + _hostProject = TestProjectData.SomeProject with { Configuration = FallbackRazorConfiguration.MVC_2_0 }; + _hostProjectWithConfigurationChange = TestProjectData.SomeProject with { Configuration = FallbackRazorConfiguration.MVC_1_0 }; _someTagHelpers = ImmutableArray.Create( TagHelperDescriptorBuilder.Create("Test1", "TestAssembly").Build()); _hostDocument = TestProjectData.SomeProjectFile1; - - _text = SourceText.From("Hello, world!"); } protected override void ConfigureProjectEngine(RazorProjectEngineBuilder builder) @@ -49,13 +46,13 @@ public async Task HostDocumentAdded_CachesOutput() ProjectState.Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) .WithAddedHostDocument(_hostDocument, DocumentState.EmptyLoader); - var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument); + var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument, DisposalToken); // Act var state = original.WithAddedHostDocument(TestProjectData.AnotherProjectFile1, DocumentState.EmptyLoader); // Assert - var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument); + var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument, DisposalToken); Assert.Same(originalOutput, actualOutput); Assert.Equal(originalInputVersion, actualInputVersion); } @@ -68,13 +65,13 @@ public async Task HostDocumentAdded_Import_DoesNotCacheOutput() ProjectState.Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) .WithAddedHostDocument(_hostDocument, DocumentState.EmptyLoader); - var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument); + var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument, DisposalToken); // Act var state = original.WithAddedHostDocument(TestProjectData.SomeProjectImportFile, DocumentState.EmptyLoader); // Assert - var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument); + var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument, DisposalToken); Assert.NotSame(originalOutput, actualOutput); Assert.NotEqual(originalInputVersion, actualInputVersion); Assert.Equal(state.DocumentCollectionVersion, actualInputVersion); @@ -84,19 +81,19 @@ public async Task HostDocumentAdded_Import_DoesNotCacheOutput() public async Task HostDocumentChanged_DoesNotCacheOutput() { // Arrange - var original = - ProjectState.Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) + var original = ProjectState + .Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) .WithAddedHostDocument(_hostDocument, DocumentState.EmptyLoader) .WithAddedHostDocument(TestProjectData.SomeProjectImportFile, DocumentState.EmptyLoader); - var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument); + var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument, DisposalToken); // Act var version = VersionStamp.Create(); - var state = original.WithChangedHostDocument(_hostDocument, () => Task.FromResult(TextAndVersion.Create(SourceText.From("@using System"), version))); + var state = original.WithChangedHostDocument(_hostDocument, TestMocks.CreateTextLoader("@using System", version)); // Assert - var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument); + var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument, DisposalToken); Assert.NotSame(originalOutput, actualOutput); Assert.NotEqual(originalInputVersion, actualInputVersion); Assert.Equal(version, actualInputVersion); @@ -106,19 +103,19 @@ public async Task HostDocumentChanged_DoesNotCacheOutput() public async Task HostDocumentChanged_Import_DoesNotCacheOutput() { // Arrange - var original = - ProjectState.Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) + var original = ProjectState + .Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) .WithAddedHostDocument(_hostDocument, DocumentState.EmptyLoader) .WithAddedHostDocument(TestProjectData.SomeProjectImportFile, DocumentState.EmptyLoader); - var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument); + var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument, DisposalToken); // Act var version = VersionStamp.Create(); - var state = original.WithChangedHostDocument(TestProjectData.SomeProjectImportFile, () => Task.FromResult(TextAndVersion.Create(SourceText.From("@using System"), version))); + var state = original.WithChangedHostDocument(TestProjectData.SomeProjectImportFile, TestMocks.CreateTextLoader("@using System", version)); // Assert - var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument); + var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument, DisposalToken); Assert.NotSame(originalOutput, actualOutput); Assert.NotEqual(originalInputVersion, actualInputVersion); Assert.Equal(version, actualInputVersion); @@ -133,13 +130,13 @@ public async Task HostDocumentRemoved_Import_DoesNotCacheOutput() .WithAddedHostDocument(_hostDocument, DocumentState.EmptyLoader) .WithAddedHostDocument(TestProjectData.SomeProjectImportFile, DocumentState.EmptyLoader); - var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument); + var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument, DisposalToken); // Act var state = original.WithRemovedHostDocument(TestProjectData.SomeProjectImportFile); // Assert - var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument); + var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument, DisposalToken); Assert.NotSame(originalOutput, actualOutput); Assert.NotEqual(originalInputVersion, actualInputVersion); Assert.Equal(state.DocumentCollectionVersion, actualInputVersion); @@ -153,14 +150,14 @@ public async Task ProjectWorkspaceStateChange_CachesOutput_EvenWhenNewerProjectW ProjectState.Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) .WithAddedHostDocument(_hostDocument, DocumentState.EmptyLoader); - var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument); + var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument, DisposalToken); var changed = ProjectWorkspaceState.Default; // Act var state = original.WithProjectWorkspaceState(changed); // Assert - var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument); + var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument, DisposalToken); Assert.Same(originalOutput, actualOutput); Assert.Equal(originalInputVersion, actualInputVersion); } @@ -174,14 +171,14 @@ public async Task ProjectWorkspaceStateChange_WithTagHelperChange_DoesNotCacheOu ProjectState.Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) .WithAddedHostDocument(_hostDocument, DocumentState.EmptyLoader); - var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument); + var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument, DisposalToken); var changed = ProjectWorkspaceState.Create(_someTagHelpers); // Act var state = original.WithProjectWorkspaceState(changed); // Assert - var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument); + var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument, DisposalToken); Assert.NotSame(originalOutput, actualOutput); Assert.NotEqual(originalInputVersion, actualInputVersion); Assert.Equal(state.ProjectWorkspaceStateVersion, actualInputVersion); @@ -192,20 +189,20 @@ public async Task ProjectWorkspaceStateChange_WithProjectWorkspaceState_CSharpLa { // Arrange var csharp8ValidConfiguration = new RazorConfiguration(RazorLanguageVersion.Version_3_0, _hostProject.Configuration.ConfigurationName, _hostProject.Configuration.Extensions); - var hostProject = new HostProject(TestProjectData.SomeProject.FilePath, TestProjectData.SomeProject.IntermediateOutputPath, csharp8ValidConfiguration, TestProjectData.SomeProject.RootNamespace); + var hostProject = TestProjectData.SomeProject with { Configuration = csharp8ValidConfiguration }; var originalWorkspaceState = ProjectWorkspaceState.Create(_someTagHelpers, LanguageVersion.CSharp7); var original = ProjectState.Create(ProjectEngineFactoryProvider, hostProject, originalWorkspaceState) - .WithAddedHostDocument(_hostDocument, () => Task.FromResult(TextAndVersion.Create(SourceText.From("@DateTime.Now"), VersionStamp.Default))); + .WithAddedHostDocument(_hostDocument, TestMocks.CreateTextLoader("@DateTime.Now", VersionStamp.Default)); var changedWorkspaceState = ProjectWorkspaceState.Create(_someTagHelpers, LanguageVersion.CSharp8); - var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument); + var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument, DisposalToken); // Act var state = original.WithProjectWorkspaceState(changedWorkspaceState); // Assert - var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument); + var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument, DisposalToken); Assert.NotSame(originalOutput, actualOutput); Assert.NotEqual(originalInputVersion, actualInputVersion); Assert.Equal(state.ProjectWorkspaceStateVersion, actualInputVersion); @@ -219,29 +216,28 @@ public async Task ConfigurationChange_DoesNotCacheOutput() ProjectState.Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) .WithAddedHostDocument(_hostDocument, DocumentState.EmptyLoader); - var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument); + var (originalOutput, originalInputVersion) = await GetOutputAsync(original, _hostDocument, DisposalToken); // Act var state = original.WithHostProject(_hostProjectWithConfigurationChange); // Assert - var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument); + var (actualOutput, actualInputVersion) = await GetOutputAsync(state, _hostDocument, DisposalToken); Assert.NotSame(originalOutput, actualOutput); Assert.NotEqual(originalInputVersion, actualInputVersion); Assert.NotEqual(state.ProjectWorkspaceStateVersion, actualInputVersion); } - private static Task<(RazorCodeDocument, VersionStamp)> GetOutputAsync(ProjectState project, HostDocument hostDocument) + private static Task<(RazorCodeDocument, VersionStamp)> GetOutputAsync(ProjectState project, HostDocument hostDocument, CancellationToken cancellationToken) { var document = project.Documents[hostDocument.FilePath]; - return GetOutputAsync(project, document); + return GetOutputAsync(project, document, cancellationToken); } - private static Task<(RazorCodeDocument, VersionStamp)> GetOutputAsync(ProjectState project, DocumentState document) + private static Task<(RazorCodeDocument, VersionStamp)> GetOutputAsync(ProjectState project, DocumentState document, CancellationToken cancellationToken) { - var projectSnapshot = new ProjectSnapshot(project); var documentSnapshot = new DocumentSnapshot(projectSnapshot, document); - return document.GetGeneratedOutputAndVersionAsync(projectSnapshot, documentSnapshot); + return document.GetGeneratedOutputAndVersionAsync(projectSnapshot, documentSnapshot, cancellationToken); } } diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateTest.cs index 5f5c1a8e422..2658d893683 100644 --- a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/ProjectSystem/ProjectStateTest.cs @@ -22,14 +22,14 @@ public class ProjectStateTest : WorkspaceTestBase private readonly HostProject _hostProject; private readonly HostProject _hostProjectWithConfigurationChange; private readonly ProjectWorkspaceState _projectWorkspaceState; - private readonly Func> _textLoader; + private readonly TextLoader _textLoader; private readonly SourceText _text; public ProjectStateTest(ITestOutputHelper testOutput) : base(testOutput) { - _hostProject = new HostProject(TestProjectData.SomeProject.FilePath, TestProjectData.SomeProject.IntermediateOutputPath, FallbackRazorConfiguration.MVC_2_0, TestProjectData.SomeProject.RootNamespace); - _hostProjectWithConfigurationChange = new HostProject(TestProjectData.SomeProject.FilePath, TestProjectData.SomeProject.IntermediateOutputPath, FallbackRazorConfiguration.MVC_1_0, TestProjectData.SomeProject.RootNamespace); + _hostProject = TestProjectData.SomeProject with { Configuration = FallbackRazorConfiguration.MVC_2_0 }; + _hostProjectWithConfigurationChange = TestProjectData.SomeProject with { Configuration = FallbackRazorConfiguration.MVC_1_0 }; _projectWorkspaceState = ProjectWorkspaceState.Create( ImmutableArray.Create( TagHelperDescriptorBuilder.Create("TestTagHelper", "TestAssembly").Build())); @@ -44,7 +44,7 @@ public ProjectStateTest(ITestOutputHelper testOutput) }; _text = SourceText.From("Hello, world!"); - _textLoader = () => Task.FromResult(TextAndVersion.Create(_text, VersionStamp.Create())); + _textLoader = TestMocks.CreateTextLoader(_text, VersionStamp.Create()); } protected override void ConfigureProjectEngine(RazorProjectEngineBuilder builder) @@ -106,7 +106,7 @@ public async Task ProjectState_AddHostDocument_DocumentIsEmpty() var state = original.WithAddedHostDocument(_documents[0], DocumentState.EmptyLoader); // Assert - var text = await state.Documents[_documents[0].FilePath].GetTextAsync(); + var text = await state.Documents[_documents[0].FilePath].GetTextAsync(DisposalToken); Assert.Equal(0, text.Length); } @@ -278,7 +278,7 @@ public async Task ProjectState_WithChangedHostDocument_Loader() // Assert Assert.NotEqual(original.Version, state.Version); - var text = await state.Documents[_documents[1].FilePath].GetTextAsync(); + var text = await state.Documents[_documents[1].FilePath].GetTextAsync(DisposalToken); Assert.Same(_text, text); Assert.Equal(original.DocumentCollectionVersion, state.DocumentCollectionVersion); @@ -298,7 +298,7 @@ public async Task ProjectState_WithChangedHostDocument_Snapshot() // Assert Assert.NotEqual(original.Version, state.Version); - var text = await state.Documents[_documents[1].FilePath].GetTextAsync(); + var text = await state.Documents[_documents[1].FilePath].GetTextAsync(DisposalToken); Assert.Same(_text, text); Assert.Equal(original.DocumentCollectionVersion, state.DocumentCollectionVersion); @@ -573,11 +573,7 @@ public void ProjectState_WithHostProject_RootNamespaceChange_UpdatesConfiguratio var original = ProjectState.Create(ProjectEngineFactoryProvider, _hostProject, _projectWorkspaceState) .WithAddedHostDocument(_documents[2], DocumentState.EmptyLoader) .WithAddedHostDocument(_documents[1], DocumentState.EmptyLoader); - var hostProjectWithRootNamespaceChange = new HostProject( - original.HostProject.FilePath, - original.HostProject.IntermediateOutputPath, - original.HostProject.Configuration, - "ChangedRootNamespace"); + var hostProjectWithRootNamespaceChange = original.HostProject with { RootNamespace = "ChangedRootNamespace" }; // Force init _ = original.TagHelpers; @@ -991,7 +987,7 @@ private class TestDocumentState : DocumentState { public static TestDocumentState Create( HostDocument hostDocument, - Func>? loader = null, + TextLoader? loader = null, Action? onTextChange = null, Action? onTextLoaderChange = null, Action? onConfigurationChange = null, @@ -1016,13 +1012,13 @@ public static TestDocumentState Create( private TestDocumentState( HostDocument hostDocument, - Func>? loader, + TextLoader? loader, Action? onTextChange, Action? onTextLoaderChange, Action? onConfigurationChange, Action? onImportsChange, Action? onProjectWorkspaceStateChange) - : base(hostDocument, text: null, textVersion: null, version: 1, loader) + : base(hostDocument, version: 1, loader ?? EmptyLoader) { _onTextChange = onTextChange; _onTextLoaderChange = onTextLoaderChange; @@ -1037,7 +1033,7 @@ public override DocumentState WithText(SourceText sourceText, VersionStamp textV return base.WithText(sourceText, textVersion); } - public override DocumentState WithTextLoader(Func> loader) + public override DocumentState WithTextLoader(TextLoader loader) { _onTextLoaderChange?.Invoke(); return base.WithTextLoader(loader); diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Tooltip/ClassifiedTagHelperTooltipFactoryTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Tooltip/ClassifiedTagHelperTooltipFactoryTest.cs new file mode 100644 index 00000000000..579e92d236f --- /dev/null +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Tooltip/ClassifiedTagHelperTooltipFactoryTest.cs @@ -0,0 +1,651 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +#nullable disable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.Classification; +using Microsoft.CodeAnalysis.Razor.Workspaces.Test; +using Microsoft.VisualStudio.Text.Adornments; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.Razor.Tooltip; + +public class ClassifiedTagHelperTooltipFactoryTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput) +{ + [Fact] + public void CleanAndClassifySummaryContent_ClassifiedTextElement_ReplacesSeeCrefs() + { + // Arrange + var runs = new List(); + var summary = "Accepts s"; + + // Act + ClassifiedTagHelperTooltipFactory.CleanAndClassifySummaryContent(runs, summary); + + // Assert + + // Expected output: + // Accepts Lists + Assert.Collection(runs, + run => run.AssertExpectedClassification("Accepts ", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification("List", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification("<", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("string", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification(">", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("s", ClassificationTypeNames.Text)); + } + + [Fact] + public void CleanSummaryContent_ClassifiedTextElement_ReplacesSeeAlsoCrefs() + { + // Arrange + var runs = new List(); + var summary = "Accepts s"; + + // Act + ClassifiedTagHelperTooltipFactory.CleanAndClassifySummaryContent(runs, summary); + + // Assert + + // Expected output: + // Accepts Lists + Assert.Collection(runs, + run => run.AssertExpectedClassification("Accepts ", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification("List", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification("<", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("string", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification(">", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("s", ClassificationTypeNames.Text)); + } + + [Fact] + public void CleanSummaryContent_ClassifiedTextElement_TrimsSurroundingWhitespace() + { + // Arrange + var runs = new List(); + var summary = @" + Hello + + World + +"; + + // Act + ClassifiedTagHelperTooltipFactory.CleanAndClassifySummaryContent(runs, summary); + + // Assert + + // Expected output: + // Hello + // + // World + Assert.Collection(runs, run => run.AssertExpectedClassification( + """ + Hello + + World + """, ClassificationTypeNames.Text)); + } + + [Fact] + public void CleanSummaryContent_ClassifiedTextElement_ClassifiesCodeBlocks() + { + // Arrange + var runs = new List(); + var summary = @"code: This is code and This is some other code."; + + // Act + ClassifiedTagHelperTooltipFactory.CleanAndClassifySummaryContent(runs, summary); + + // Assert + + // Expected output: + // code: This is code and This is some other code. + Assert.Collection(runs, + run => run.AssertExpectedClassification("code: ", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification("This is code", ClassificationTypeNames.Text, ClassifiedTextRunStyle.UseClassificationFont), + run => run.AssertExpectedClassification(" and ", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification("This is some other code", ClassificationTypeNames.Text, ClassifiedTextRunStyle.UseClassificationFont), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Text)); + } + + [Fact] + public void CleanSummaryContent_ClassifiedTextElement_ClassifiesCBlocks() + { + // Arrange + var runs = new List(); + var summary = @"code: This is code and This is some other code."; + + // Act + ClassifiedTagHelperTooltipFactory.CleanAndClassifySummaryContent(runs, summary); + + // Assert + + // Expected output: + // code: This is code and This is some other code. + Assert.Collection(runs, + run => run.AssertExpectedClassification("code: ", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification("This is code", ClassificationTypeNames.Text, ClassifiedTextRunStyle.UseClassificationFont), + run => run.AssertExpectedClassification(" and ", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification("This is some other code", ClassificationTypeNames.Text, ClassifiedTextRunStyle.UseClassificationFont), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Text)); + } + + [Fact] + public void CleanSummaryContent_ClassifiedTextElement_ParasCreateNewLines() + { + // Arrange + var runs = new List(); + var summary = @"Summary description: +Paragraph text. +End summary description."; + + // Act + ClassifiedTagHelperTooltipFactory.CleanAndClassifySummaryContent(runs, summary); + + // Assert + + // Expected output: + // code: This is code and This is some other code. + Assert.Collection(runs, run => run.AssertExpectedClassification( + """ + Summary description: + + Paragraph text. + + End summary description. + """, + ClassificationTypeNames.Text)); + } + + [Fact] + public async Task TryCreateTooltip_ClassifiedTextElement_NoAssociatedTagHelperDescriptions_ReturnsFalse() + { + // Arrange + var projectManager = CreateProjectSnapshotManager(); + var elementDescription = AggregateBoundElementDescription.Empty; + + // Act + var classifiedTextElement = await ClassifiedTagHelperTooltipFactory.TryCreateTooltipAsync("file.razor", elementDescription, projectManager.GetQueryOperations(), CancellationToken.None); + + // Assert + Assert.Null(classifiedTextElement); + } + + [Fact] + public async Task TryCreateTooltip_ClassifiedTextElement_Element_SingleAssociatedTagHelper_ReturnsTrue_NestedTypes() + { + // Arrange + var projectManager = CreateProjectSnapshotManager(); + var associatedTagHelperInfos = new[] + { + new BoundElementDescriptionInfo( + "Microsoft.AspNetCore.SomeTagHelper", + "Uses s"), + }; + var elementDescription = new AggregateBoundElementDescription(associatedTagHelperInfos.ToImmutableArray()); + + // Act + var classifiedTextElement = await ClassifiedTagHelperTooltipFactory.TryCreateTooltipAsync("file.razor", elementDescription, projectManager.GetQueryOperations(), CancellationToken.None); + + // Assert + Assert.NotNull(classifiedTextElement); + + // Expected output: + // Microsoft.AspNetCore.SomeTagHelper + // Uses List>s + Assert.Collection(classifiedTextElement.Runs, + run => run.AssertExpectedClassification("Microsoft", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("AspNetCore", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("SomeTagHelper", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification(Environment.NewLine, ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification("Uses ", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification("List", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification("<", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("List", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification("<", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("string", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification(">", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification(">", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("s", ClassificationTypeNames.Text)); + } + + [Fact] + public async Task TryCreateTooltip_ClassifiedTextElement_Element_NamespaceContainsTypeName_ReturnsTrue() + { + // Arrange + var projectManager = CreateProjectSnapshotManager(); + var associatedTagHelperInfos = new[] + { + new BoundElementDescriptionInfo( + "Microsoft.AspNetCore.SomeTagHelper.SomeTagHelper", + "Uses s"), + }; + var elementDescription = new AggregateBoundElementDescription(associatedTagHelperInfos.ToImmutableArray()); + + // Act + var classifiedTextElement = await ClassifiedTagHelperTooltipFactory.TryCreateTooltipAsync("file.razor", elementDescription, projectManager.GetQueryOperations(), CancellationToken.None); + + // Assert + Assert.NotNull(classifiedTextElement); + + // Expected output: + // Microsoft.AspNetCore.SomeTagHelper.SomeTagHelper + // Uses Cs + Assert.Collection(classifiedTextElement.Runs, + run => run.AssertExpectedClassification("Microsoft", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("AspNetCore", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("SomeTagHelper", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("SomeTagHelper", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification(Environment.NewLine, ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification("Uses ", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification("C", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification("<", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("B", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification(">", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("s", ClassificationTypeNames.Text)); + } + + [Fact] + public async Task TryCreateTooltip_ClassifiedTextElement_Element_MultipleAssociatedTagHelpers_ReturnsTrue() + { + // Arrange + var projectManager = CreateProjectSnapshotManager(); + var associatedTagHelperInfos = new[] + { + new BoundElementDescriptionInfo("Microsoft.AspNetCore.SomeTagHelper", "\nUses s\n"), + new BoundElementDescriptionInfo("Microsoft.AspNetCore.OtherTagHelper", "\nAlso uses s\n\r\n\r\r"), + }; + var elementDescription = new AggregateBoundElementDescription(associatedTagHelperInfos.ToImmutableArray()); + + // Act + var classifiedTextElement = await ClassifiedTagHelperTooltipFactory.TryCreateTooltipAsync("file.razor", elementDescription, projectManager.GetQueryOperations(), CancellationToken.None); + + // Assert + Assert.NotNull(classifiedTextElement); + + // Expected output: + // Microsoft.AspNetCore.SomeTagHelper + // Uses Lists + // + // Microsoft.AspNetCore.OtherTagHelper + // Also uses Lists + Assert.Collection(classifiedTextElement.Runs, + run => run.AssertExpectedClassification("Microsoft", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("AspNetCore", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("SomeTagHelper", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification(Environment.NewLine, ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification("Uses ", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification("List", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification("<", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("string", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification(">", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("s", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(Environment.NewLine, ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification(Environment.NewLine, ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification("Microsoft", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("AspNetCore", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("OtherTagHelper", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification(Environment.NewLine, ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification("Also uses ", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification("List", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification("<", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("string", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification(">", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("s", ClassificationTypeNames.Text)); + } + + [Fact] + public void TryCreateTooltip_ClassifiedTextElement_NoAssociatedAttributeDescriptions_ReturnsFalse() + { + // Arrange + var elementDescription = AggregateBoundAttributeDescription.Empty; + + // Act + Assert.False(ClassifiedTagHelperTooltipFactory.TryCreateTooltip(elementDescription, out ClassifiedTextElement _)); + } + + [Fact] + public void TryCreateTooltip_ClassifiedTextElement_Attribute_SingleAssociatedAttribute_ReturnsTrue_NestedTypes() + { + // Arrange + var associatedAttributeDescriptions = new[] + { + new BoundAttributeDescriptionInfo( + ReturnTypeName: "System.String", + TypeName: "Microsoft.AspNetCore.SomeTagHelpers.SomeTypeName", + PropertyName: "SomeProperty", + Documentation: "Uses s") + }; + var attributeDescription = new AggregateBoundAttributeDescription(associatedAttributeDescriptions.ToImmutableArray()); + + // Act + Assert.True(ClassifiedTagHelperTooltipFactory.TryCreateTooltip(attributeDescription, out ClassifiedTextElement classifiedTextElement)); + + // Assert + // Expected output: + // string Microsoft.AspNetCore.SomeTagHelpers.SomeTypeName.SomeProperty + // Uses List>s + Assert.Collection(classifiedTextElement.Runs, + run => run.AssertExpectedClassification("string", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification(" ", ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification("Microsoft", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("AspNetCore", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("SomeTagHelpers", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("SomeTypeName", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("SomeProperty", ClassificationTypeNames.Identifier), + run => run.AssertExpectedClassification(Environment.NewLine, ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification("Uses ", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification("List", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification("<", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("List", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification("<", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("string", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification(">", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification(">", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("s", ClassificationTypeNames.Text)); + } + + [Fact] + public void TryCreateTooltip_ClassifiedTextElement_Attribute_MultipleAssociatedAttributes_ReturnsTrue() + { + // Arrange + var associatedAttributeDescriptions = new[] + { + new BoundAttributeDescriptionInfo( + ReturnTypeName: "System.String", + TypeName: "Microsoft.AspNetCore.SomeTagHelpers.SomeTypeName", + PropertyName: "SomeProperty", + Documentation: "Uses s"), + new BoundAttributeDescriptionInfo( + PropertyName: "AnotherProperty", + TypeName: "Microsoft.AspNetCore.SomeTagHelpers.AnotherTypeName", + ReturnTypeName: "System.Boolean?", + Documentation: "\nUses s\n"), + }; + var attributeDescription = new AggregateBoundAttributeDescription(associatedAttributeDescriptions.ToImmutableArray()); + + // Act + Assert.True(ClassifiedTagHelperTooltipFactory.TryCreateTooltip(attributeDescription, out ClassifiedTextElement classifiedTextElement)); + + // Assert + + // Expected output: + // string Microsoft.AspNetCore.SomeTagHelpers.SomeTypeName.SomeProperty + // Uses Lists + // + // bool? Microsoft.AspNetCore.SomeTagHelpers.AnotherTypeName.AnotherProperty + // Uses Lists + Assert.Collection(classifiedTextElement.Runs, + run => run.AssertExpectedClassification("string", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification(" ", ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification("Microsoft", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("AspNetCore", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("SomeTagHelpers", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("SomeTypeName", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("SomeProperty", ClassificationTypeNames.Identifier), + run => run.AssertExpectedClassification(Environment.NewLine, ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification("Uses ", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification("List", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification("<", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("string", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification(">", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("s", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(Environment.NewLine, ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification(Environment.NewLine, ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification("bool", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification("?", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification(" ", ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification("Microsoft", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("AspNetCore", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("SomeTagHelpers", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("AnotherTypeName", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("AnotherProperty", ClassificationTypeNames.Identifier), + run => run.AssertExpectedClassification(Environment.NewLine, ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification("Uses ", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification("List", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification("<", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("string", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification(">", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("s", ClassificationTypeNames.Text)); + } + + [Fact] + public async Task TryCreateTooltip_ContainerElement_NoAssociatedTagHelperDescriptions_ReturnsFalse() + { + // Arrange + var projectManager = CreateProjectSnapshotManager(); + var elementDescription = AggregateBoundElementDescription.Empty; + + // Act + var containerElement = await ClassifiedTagHelperTooltipFactory.TryCreateTooltipContainerAsync("file.razor", elementDescription, projectManager.GetQueryOperations(), CancellationToken.None); + + // Assert + Assert.Null(containerElement); + } + + [Fact] + public async Task TryCreateTooltip_ContainerElement_Attribute_MultipleAssociatedTagHelpers_ReturnsTrue() + { + // Arrange + var projectManager = CreateProjectSnapshotManager(); + var associatedTagHelperInfos = new[] + { + new BoundElementDescriptionInfo("Microsoft.AspNetCore.SomeTagHelper", "\nUses s\n"), + new BoundElementDescriptionInfo("Microsoft.AspNetCore.OtherTagHelper", "\nAlso uses s\n\r\n\r\r"), + }; + var elementDescription = new AggregateBoundElementDescription(associatedTagHelperInfos.ToImmutableArray()); + + // Act + var container = await ClassifiedTagHelperTooltipFactory.TryCreateTooltipContainerAsync("file.razor", elementDescription, projectManager.GetQueryOperations(), CancellationToken.None); + + // Assert + Assert.NotNull(container); + var containerElements = container.Elements.ToList(); + + // Expected output: + // [Class Glyph] Microsoft.AspNetCore.SomeTagHelper + // Uses Lists + // + // [Class Glyph] Microsoft.AspNetCore.OtherTagHelper + // Also uses Lists + Assert.Equal(ContainerElementStyle.Stacked, container.Style); + Assert.Equal(5, containerElements.Count); + + // [Class Glyph] Microsoft.AspNetCore.SomeTagHelper + var innerContainer = ((ContainerElement)containerElements[0]).Elements.ToList(); + var classifiedTextElement = (ClassifiedTextElement)innerContainer[1]; + Assert.Equal(2, innerContainer.Count); + Assert.Equal(ClassifiedTagHelperTooltipFactory.ClassGlyph, innerContainer[0]); + Assert.Collection(classifiedTextElement.Runs, + run => run.AssertExpectedClassification("Microsoft", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("AspNetCore", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("SomeTagHelper", ClassifiedTagHelperTooltipFactory.TypeClassificationName)); + + // Uses Lists + innerContainer = ((ContainerElement)containerElements[1]).Elements.ToList(); + classifiedTextElement = (ClassifiedTextElement)innerContainer[0]; + Assert.Single(innerContainer); + Assert.Collection(classifiedTextElement.Runs, + run => run.AssertExpectedClassification("Uses ", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification("List", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification("<", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("string", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification(">", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("s", ClassificationTypeNames.Text)); + + // new line + innerContainer = ((ContainerElement)containerElements[2]).Elements.ToList(); + classifiedTextElement = (ClassifiedTextElement)innerContainer[0]; + Assert.Single(innerContainer); + Assert.Empty(classifiedTextElement.Runs); + + // [Class Glyph] Microsoft.AspNetCore.OtherTagHelper + innerContainer = ((ContainerElement)containerElements[3]).Elements.ToList(); + classifiedTextElement = (ClassifiedTextElement)innerContainer[1]; + Assert.Equal(2, innerContainer.Count); + Assert.Equal(ClassifiedTagHelperTooltipFactory.ClassGlyph, innerContainer[0]); + Assert.Collection(classifiedTextElement.Runs, + run => run.AssertExpectedClassification("Microsoft", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("AspNetCore", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("OtherTagHelper", ClassifiedTagHelperTooltipFactory.TypeClassificationName)); + + // Also uses Lists + innerContainer = ((ContainerElement)containerElements[4]).Elements.ToList(); + classifiedTextElement = (ClassifiedTextElement)innerContainer[0]; + Assert.Single(innerContainer); + Assert.Collection(classifiedTextElement.Runs, + run => run.AssertExpectedClassification("Also uses ", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification("List", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification("<", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("string", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification(">", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("s", ClassificationTypeNames.Text)); + } + + [Fact] + public void TryCreateTooltip_ContainerElement_NoAssociatedAttributeDescriptions_ReturnsFalse() + { + // Arrange + var elementDescription = AggregateBoundAttributeDescription.Empty; + + // Act + Assert.False(ClassifiedTagHelperTooltipFactory.TryCreateTooltip(elementDescription, out ContainerElement _)); + } + + [Fact] + public void TryCreateTooltip_ContainerElement_Attribute_MultipleAssociatedAttributes_ReturnsTrue() + { + // Arrange + var associatedAttributeDescriptions = new[] + { + new BoundAttributeDescriptionInfo( + ReturnTypeName: "System.String", + TypeName: "Microsoft.AspNetCore.SomeTagHelpers.SomeTypeName", + PropertyName: "SomeProperty", + Documentation: "Uses s"), + new BoundAttributeDescriptionInfo( + PropertyName: "AnotherProperty", + TypeName: "Microsoft.AspNetCore.SomeTagHelpers.AnotherTypeName", + ReturnTypeName: "System.Boolean?", + Documentation: "\nUses s\n"), + }; + var attributeDescription = new AggregateBoundAttributeDescription(associatedAttributeDescriptions.ToImmutableArray()); + + // Act + Assert.True(ClassifiedTagHelperTooltipFactory.TryCreateTooltip(attributeDescription, out ContainerElement container)); + + // Assert + var containerElements = container.Elements.ToList(); + + // Expected output: + // [Property Glyph] string Microsoft.AspNetCore.SomeTagHelpers.SomeTypeName.SomeProperty + // Uses Lists + // + // [Property Glyph] bool? Microsoft.AspNetCore.SomeTagHelpers.AnotherTypeName.AnotherProperty + // Uses Lists + Assert.Equal(ContainerElementStyle.Stacked, container.Style); + Assert.Equal(5, containerElements.Count); + + // [TagHelper Glyph] string Microsoft.AspNetCore.SomeTagHelpers.SomeTypeName.SomeProperty + var innerContainer = ((ContainerElement)containerElements[0]).Elements.ToList(); + var classifiedTextElement = (ClassifiedTextElement)innerContainer[1]; + Assert.Equal(2, innerContainer.Count); + Assert.Equal(ClassifiedTagHelperTooltipFactory.PropertyGlyph, innerContainer[0]); + Assert.Collection(classifiedTextElement.Runs, + run => run.AssertExpectedClassification("string", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification(" ", ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification("Microsoft", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("AspNetCore", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("SomeTagHelpers", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("SomeTypeName", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("SomeProperty", ClassificationTypeNames.Identifier)); + + // Uses Lists + innerContainer = ((ContainerElement)containerElements[1]).Elements.ToList(); + classifiedTextElement = (ClassifiedTextElement)innerContainer[0]; + Assert.Single(innerContainer); + Assert.Collection(classifiedTextElement.Runs, + run => run.AssertExpectedClassification("Uses ", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification("List", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification("<", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("string", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification(">", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("s", ClassificationTypeNames.Text)); + + // new line + innerContainer = ((ContainerElement)containerElements[2]).Elements.ToList(); + classifiedTextElement = (ClassifiedTextElement)innerContainer[0]; + Assert.Single(innerContainer); + Assert.Empty(classifiedTextElement.Runs); + + // [TagHelper Glyph] bool? Microsoft.AspNetCore.SomeTagHelpers.AnotherTypeName.AnotherProperty + innerContainer = ((ContainerElement)containerElements[3]).Elements.ToList(); + classifiedTextElement = (ClassifiedTextElement)innerContainer[1]; + Assert.Equal(2, innerContainer.Count); + Assert.Equal(ClassifiedTagHelperTooltipFactory.PropertyGlyph, innerContainer[0]); + Assert.Collection(classifiedTextElement.Runs, + run => run.AssertExpectedClassification("bool", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification("?", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification(" ", ClassificationTypeNames.WhiteSpace), + run => run.AssertExpectedClassification("Microsoft", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("AspNetCore", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("SomeTagHelpers", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("AnotherTypeName", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification(".", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("AnotherProperty", ClassificationTypeNames.Identifier)); + + // Uses Lists + innerContainer = ((ContainerElement)containerElements[4]).Elements.ToList(); + classifiedTextElement = (ClassifiedTextElement)innerContainer[0]; + Assert.Single(innerContainer); + Assert.Collection(classifiedTextElement.Runs, + run => run.AssertExpectedClassification("Uses ", ClassificationTypeNames.Text), + run => run.AssertExpectedClassification("List", ClassifiedTagHelperTooltipFactory.TypeClassificationName), + run => run.AssertExpectedClassification("<", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("string", ClassificationTypeNames.Keyword), + run => run.AssertExpectedClassification(">", ClassificationTypeNames.Punctuation), + run => run.AssertExpectedClassification("s", ClassificationTypeNames.Text)); + } +} diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Tooltip/DocCommentHelperTests.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Tooltip/DocCommentHelperTests.cs new file mode 100644 index 00000000000..a2d7adaea90 --- /dev/null +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Tooltip/DocCommentHelperTests.cs @@ -0,0 +1,325 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Razor.Test.Common; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.CodeAnalysis.Razor.Tooltip; + +public class DocCommentHelperTests(ITestOutputHelper testOutput) : ToolingTestBase(testOutput) +{ + [Fact] + public void ReduceTypeName_Plain() + { + // Arrange + var content = "Microsoft.AspNetCore.SomeTagHelpers.SomeTypeName"; + + // Act + var reduced = DocCommentHelpers.ReduceTypeName(content); + + // Assert + Assert.Equal("SomeTypeName", reduced); + } + + [Fact] + public void ReduceTypeName_Generics() + { + // Arrange + var content = "System.Collections.Generic.List"; + + // Act + var reduced = DocCommentHelpers.ReduceTypeName(content); + + // Assert + Assert.Equal("List", reduced); + } + + [Fact] + public void ReduceTypeName_CrefGenerics() + { + // Arrange + var content = "System.Collections.Generic.List{System.String}"; + + // Act + var reduced = DocCommentHelpers.ReduceTypeName(content); + + // Assert + Assert.Equal("List{System.String}", reduced); + } + + [Fact] + public void ReduceTypeName_NestedGenerics() + { + // Arrange + var content = "Microsoft.AspNetCore.SometTagHelpers.SomeType>"; + + // Act + var reduced = DocCommentHelpers.ReduceTypeName(content); + + // Assert + Assert.Equal("SomeType>", reduced); + } + + [Theory] + [InlineData("Microsoft.AspNetCore.SometTagHelpers.SomeType.Foo.Bar>")] + [InlineData("Microsoft.AspNetCore.SometTagHelpers.SomeType.Foo.Bar{Baz.Phi}}")] + public void ReduceTypeName_UnbalancedDocs_NotRecoverable_ReturnsOriginalContent(string content) + { + // Arrange + + // Act + var reduced = DocCommentHelpers.ReduceTypeName(content); + + // Assert + Assert.Equal(content, reduced); + } + + [Fact] + public void ReduceMemberName_Plain() + { + // Arrange + var content = "Microsoft.AspNetCore.SometTagHelpers.SomeType.SomeProperty"; + + // Act + var reduced = DocCommentHelpers.ReduceMemberName(content); + + // Assert + Assert.Equal("SomeType.SomeProperty", reduced); + } + + [Fact] + public void ReduceMemberName_Generics() + { + // Arrange + var content = "Microsoft.AspNetCore.SometTagHelpers.SomeType.SomeProperty"; + + // Act + var reduced = DocCommentHelpers.ReduceMemberName(content); + + // Assert + Assert.Equal("SomeType.SomeProperty", reduced); + } + + [Fact] + public void ReduceMemberName_CrefGenerics() + { + // Arrange + var content = "Microsoft.AspNetCore.SometTagHelpers.SomeType{Foo.Bar}.SomeProperty{Foo.Bar}"; + + // Act + var reduced = DocCommentHelpers.ReduceMemberName(content); + + // Assert + Assert.Equal("SomeType{Foo.Bar}.SomeProperty{Foo.Bar}", reduced); + } + + [Fact] + public void ReduceMemberName_NestedGenericsMethodsTypes() + { + // Arrange + var content = "Microsoft.AspNetCore.SometTagHelpers.SomeType>.SomeMethod(Foo.Bar,Baz.Fi)"; + + // Act + var reduced = DocCommentHelpers.ReduceMemberName(content); + + // Assert + Assert.Equal("SomeType>.SomeMethod(Foo.Bar,Baz.Fi)", reduced); + } + + [Theory] + [InlineData("Microsoft.AspNetCore.SometTagHelpers.SomeType.Foo.Bar>")] + [InlineData("Microsoft.AspNetCore.SometTagHelpers.SomeType.Foo.Bar{Baz.Phi}}")] + [InlineData("Microsoft.AspNetCore.SometTagHelpers.SomeType.Foo.Bar(Baz.Phi))")] + [InlineData("Microsoft.AspNetCore.SometTagHelpers.SomeType.Foo{.>")] + public void ReduceMemberName_UnbalancedDocs_NotRecoverable_ReturnsOriginalContent(string content) + { + // Arrange + + // Act + var reduced = DocCommentHelpers.ReduceMemberName(content); + + // Assert + Assert.Equal(content, reduced); + } + + [Fact] + public void ReduceCrefValue_InvalidShortValue_ReturnsEmptyString() + { + // Arrange + var content = "T:"; + + // Act + var value = DocCommentHelpers.ReduceCrefValue(content); + + // Assert + Assert.Equal(string.Empty, value); + } + + [Fact] + public void ReduceCrefValue_InvalidUnknownIdentifierValue_ReturnsEmptyString() + { + // Arrange + var content = "X:"; + + // Act + var value = DocCommentHelpers.ReduceCrefValue(content); + + // Assert + Assert.Equal(string.Empty, value); + } + + [Fact] + public void ReduceCrefValue_Type() + { + // Arrange + var content = "T:Microsoft.AspNetCore.SometTagHelpers.SomeType"; + + // Act + var value = DocCommentHelpers.ReduceCrefValue(content); + + // Assert + Assert.Equal("SomeType", value); + } + + [Fact] + public void ReduceCrefValue_Property() + { + // Arrange + var content = "P:Microsoft.AspNetCore.SometTagHelpers.SomeType.SomeProperty"; + + // Act + var value = DocCommentHelpers.ReduceCrefValue(content); + + // Assert + Assert.Equal("SomeType.SomeProperty", value); + } + + [Fact] + public void ReduceCrefValue_Member() + { + // Arrange + var content = "P:Microsoft.AspNetCore.SometTagHelpers.SomeType.SomeMember"; + + // Act + var value = DocCommentHelpers.ReduceCrefValue(content); + + // Assert + Assert.Equal("SomeType.SomeMember", value); + } + + [Fact] + public void TryExtractSummary_Null_ReturnsFalse() + { + // Arrange & Act + var result = DocCommentHelpers.TryExtractSummary(documentation: null, out var summary); + + // Assert + Assert.False(result); + Assert.Null(summary); + } + + [Fact] + public void TryExtractSummary_ExtractsSummary_ReturnsTrue() + { + // Arrange + var expectedSummary = " Hello World "; + var documentation = $@" +Prefixed invalid content + + +{expectedSummary} + +Suffixed invalid content"; + + // Act + var result = DocCommentHelpers.TryExtractSummary(documentation, out var summary); + + // Assert + Assert.True(result); + Assert.Equal(expectedSummary, summary); + } + + [Fact] + public void TryExtractSummary_NoStartSummary_ReturnsFalse() + { + // Arrange + var documentation = @" +Prefixed invalid content + + +
+ +Suffixed invalid content"; + + // Act + var result = DocCommentHelpers.TryExtractSummary(documentation, out var summary); + + // Assert + Assert.True(result); + Assert.Equal(@"Prefixed invalid content + + + + +Suffixed invalid content", summary); + } + + [Fact] + public void TryExtractSummary_NoEndSummary_ReturnsTrue() + { + // Arrange + var documentation = @" +Prefixed invalid content + + + + +Suffixed invalid content"; + + // Act + var result = DocCommentHelpers.TryExtractSummary(documentation, out var summary); + + // Assert + Assert.True(result); + Assert.Equal(@"Prefixed invalid content + + + + +Suffixed invalid content", summary); + } + + [Fact] + public void TryExtractSummary_XMLButNoSummary_ReturnsFalse() + { + // Arrange + var documentation = @" +param1 +Result +"; + + // Act + var result = DocCommentHelpers.TryExtractSummary(documentation, out var summary); + + // Assert + Assert.False(result); + Assert.Null(summary); + } + + [Fact] + public void TryExtractSummary_NoXml_ReturnsTrue() + { + // Arrange + var documentation = @" +There is no xml, but I got you this < and the >. +"; + + // Act + var result = DocCommentHelpers.TryExtractSummary(documentation, out var summary); + + // Assert + Assert.True(result); + Assert.Equal("There is no xml, but I got you this < and the >.", summary); + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/DefaultLSPTagHelperTooltipFactoryTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Tooltip/MarkupTagHelperTooltipFactoryTest.cs similarity index 75% rename from src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/DefaultLSPTagHelperTooltipFactoryTest.cs rename to src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Tooltip/MarkupTagHelperTooltipFactoryTest.cs index d1d4854cdae..e97c2eba582 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/DefaultLSPTagHelperTooltipFactoryTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Tooltip/MarkupTagHelperTooltipFactoryTest.cs @@ -6,15 +6,16 @@ using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.CodeAnalysis.Razor.Tooltip; +using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.Razor.Workspaces.Test; using Microsoft.VisualStudio.LanguageServer.Protocol; using Xunit; using Xunit.Abstractions; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Tooltip; +namespace Microsoft.CodeAnalysis.Razor.Tooltip; -public class DefaultLSPTagHelperTooltipFactoryTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) +public class MarkupTagHelperTooltipFactoryTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput) { [Fact] public void CleanSummaryContent_Markup_ReplacesSeeCrefs() @@ -23,7 +24,7 @@ public void CleanSummaryContent_Markup_ReplacesSeeCrefs() var summary = "Accepts s"; // Act - var cleanedSummary = DefaultLSPTagHelperTooltipFactory.CleanSummaryContent(summary); + var cleanedSummary = MarkupTagHelperTooltipFactory.CleanSummaryContent(summary); // Assert Assert.Equal("Accepts `List`s", cleanedSummary); @@ -36,7 +37,7 @@ public void CleanSummaryContent_Markup_ReplacesSeeAlsoCrefs() var summary = "Accepts s"; // Act - var cleanedSummary = DefaultLSPTagHelperTooltipFactory.CleanSummaryContent(summary); + var cleanedSummary = MarkupTagHelperTooltipFactory.CleanSummaryContent(summary); // Assert Assert.Equal("Accepts `List`s", cleanedSummary); @@ -54,7 +55,7 @@ public void CleanSummaryContent_Markup_TrimsSurroundingWhitespace() "; // Act - var cleanedSummary = DefaultLSPTagHelperTooltipFactory.CleanSummaryContent(summary); + var cleanedSummary = MarkupTagHelperTooltipFactory.CleanSummaryContent(summary); // Assert Assert.Equal(@"Hello @@ -67,11 +68,10 @@ public async Task TryCreateTooltip_Markup_NoAssociatedTagHelperDescriptions_Retu { // Arrange var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultLSPTagHelperTooltipFactory(projectManager); var elementDescription = AggregateBoundElementDescription.Empty; // Act - var markdown = await descriptionFactory.TryCreateTooltipAsync("file.razor", elementDescription, MarkupKind.Markdown, CancellationToken.None); + var markdown = await MarkupTagHelperTooltipFactory.TryCreateTooltipAsync("file.razor", elementDescription, projectManager.GetQueryOperations(), MarkupKind.Markdown, CancellationToken.None); // Assert Assert.Null(markdown); @@ -82,14 +82,13 @@ public async Task TryCreateTooltip_Markup_Element_SingleAssociatedTagHelper_Retu { // Arrange var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultLSPTagHelperTooltipFactory(projectManager); var associatedTagHelperInfos = new[] { new BoundElementDescriptionInfo("Microsoft.AspNetCore.SomeTagHelper", "Uses s"), }; var elementDescription = new AggregateBoundElementDescription(associatedTagHelperInfos.ToImmutableArray()); // Act - var markdown = await descriptionFactory.TryCreateTooltipAsync("file.razor", elementDescription, MarkupKind.Markdown, CancellationToken.None); + var markdown = await MarkupTagHelperTooltipFactory.TryCreateTooltipAsync("file.razor", elementDescription, projectManager.GetQueryOperations(), MarkupKind.Markdown, CancellationToken.None); // Assert Assert.NotNull(markdown); @@ -104,7 +103,6 @@ public async Task TryCreateTooltip_Markup_Element_PlainText_NoBold() { // Arrange var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultLSPTagHelperTooltipFactory(projectManager); var associatedTagHelperInfos = new[] { new BoundElementDescriptionInfo("Microsoft.AspNetCore.SomeTagHelper", "Uses s"), @@ -112,7 +110,7 @@ public async Task TryCreateTooltip_Markup_Element_PlainText_NoBold() var elementDescription = new AggregateBoundElementDescription(associatedTagHelperInfos.ToImmutableArray()); // Act - var markdown = await descriptionFactory.TryCreateTooltipAsync("file.razor", elementDescription, MarkupKind.PlainText, CancellationToken.None); + var markdown = await MarkupTagHelperTooltipFactory.TryCreateTooltipAsync("file.razor", elementDescription, projectManager.GetQueryOperations(), MarkupKind.PlainText, CancellationToken.None); // Assert Assert.NotNull(markdown); @@ -126,8 +124,6 @@ public async Task TryCreateTooltip_Markup_Element_PlainText_NoBold() public void TryCreateTooltip_Markup_Attribute_PlainText_NoBold() { // Arrange - var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultLSPTagHelperTooltipFactory(projectManager); var associatedAttributeDescriptions = new[] { new BoundAttributeDescriptionInfo( @@ -139,7 +135,7 @@ public void TryCreateTooltip_Markup_Attribute_PlainText_NoBold() var attributeDescription = new AggregateBoundAttributeDescription(associatedAttributeDescriptions.ToImmutableArray()); // Act - var result = descriptionFactory.TryCreateTooltip(attributeDescription, MarkupKind.PlainText, out var markdown); + var result = MarkupTagHelperTooltipFactory.TryCreateTooltip(attributeDescription, MarkupKind.PlainText, out var markdown); // Assert Assert.True(result); @@ -154,7 +150,6 @@ public async Task TryCreateTooltip_Markup_Element_MultipleAssociatedTagHelpers_R { // Arrange var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultLSPTagHelperTooltipFactory(projectManager); var associatedTagHelperInfos = new[] { new BoundElementDescriptionInfo("Microsoft.AspNetCore.SomeTagHelper", "\nUses s\n"), @@ -163,7 +158,7 @@ public async Task TryCreateTooltip_Markup_Element_MultipleAssociatedTagHelpers_R var elementDescription = new AggregateBoundElementDescription(associatedTagHelperInfos.ToImmutableArray()); // Act - var markdown = await descriptionFactory.TryCreateTooltipAsync("file.razor", elementDescription, MarkupKind.Markdown, CancellationToken.None); + var markdown = await MarkupTagHelperTooltipFactory.TryCreateTooltipAsync("file.razor", elementDescription, projectManager.GetQueryOperations(), MarkupKind.Markdown, CancellationToken.None); // Assert Assert.NotNull(markdown); @@ -181,8 +176,6 @@ public async Task TryCreateTooltip_Markup_Element_MultipleAssociatedTagHelpers_R public void TryCreateTooltip_Markup_Attribute_SingleAssociatedAttribute_ReturnsTrue() { // Arrange - var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultLSPTagHelperTooltipFactory(projectManager); var associatedAttributeDescriptions = new[] { new BoundAttributeDescriptionInfo( @@ -194,7 +187,7 @@ public void TryCreateTooltip_Markup_Attribute_SingleAssociatedAttribute_ReturnsT var attributeDescription = new AggregateBoundAttributeDescription(associatedAttributeDescriptions.ToImmutableArray()); // Act - var result = descriptionFactory.TryCreateTooltip(attributeDescription, MarkupKind.Markdown, out var markdown); + var result = MarkupTagHelperTooltipFactory.TryCreateTooltip(attributeDescription, MarkupKind.Markdown, out var markdown); // Assert Assert.True(result); @@ -208,8 +201,6 @@ public void TryCreateTooltip_Markup_Attribute_SingleAssociatedAttribute_ReturnsT public void TryCreateTooltip_Markup_Attribute_MultipleAssociatedAttributes_ReturnsTrue() { // Arrange - var projectManager = CreateProjectSnapshotManager(); - var descriptionFactory = new DefaultLSPTagHelperTooltipFactory(projectManager); var associatedAttributeDescriptions = new[] { new BoundAttributeDescriptionInfo( @@ -226,7 +217,7 @@ public void TryCreateTooltip_Markup_Attribute_MultipleAssociatedAttributes_Retur var attributeDescription = new AggregateBoundAttributeDescription(associatedAttributeDescriptions.ToImmutableArray()); // Act - var result = descriptionFactory.TryCreateTooltip(attributeDescription, MarkupKind.Markdown, out var markdown); + var result = MarkupTagHelperTooltipFactory.TryCreateTooltip(attributeDescription, MarkupKind.Markdown, out var markdown); // Assert Assert.True(result); diff --git a/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Tooltip/ProjectAvailabilityTests.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Tooltip/ProjectAvailabilityTests.cs new file mode 100644 index 00000000000..d19a69bedee --- /dev/null +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Tooltip/ProjectAvailabilityTests.cs @@ -0,0 +1,215 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Components; +using Microsoft.AspNetCore.Razor.ProjectSystem; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Workspaces.Test; +using Roslyn.Test.Utilities; +using Xunit; +using Xunit.Abstractions; +using static Microsoft.AspNetCore.Razor.Language.CommonMetadata; + +namespace Microsoft.CodeAnalysis.Razor.Tooltip; + +public class ProjectAvailabilityTests(ITestOutputHelper testOutput) : ToolingTestBase(testOutput) +{ + [Fact] + public async Task GetProjectAvailabilityText_NoProjects_ReturnsNull() + { + var projectManager = CreateProjectSnapshotManager(); + var solutionQueryOperations = projectManager.GetQueryOperations(); + + var availability = await solutionQueryOperations.GetProjectAvailabilityTextAsync("file.razor", "MyTagHelper", DisposalToken); + + Assert.Null(availability); + } + + [Fact] + public async Task GetProjectAvailabilityText_OneProject_ReturnsNull() + { + var builder = TagHelperDescriptorBuilder.Create(ComponentMetadata.Component.TagHelperKind, "TestTagHelper", "TestAssembly"); + builder.TagMatchingRule(rule => rule.TagName = "Test"); + var tagHelperTypeName = "TestNamespace.TestTagHelper"; + builder.Metadata(TypeName(tagHelperTypeName)); + var tagHelpers = ImmutableArray.Create(builder.Build()); + var projectWorkspaceState = ProjectWorkspaceState.Create(tagHelpers); + + var hostProject = new HostProject( + "C:/path/to/project.csproj", + "C:/path/to/obj/1", + RazorConfiguration.Default, + rootNamespace: null, + displayName: "project"); + + var hostDocument = new HostDocument( + "C:/path/to/file.razor", + "file.razor", + FileKinds.Component); + + var projectManager = CreateProjectSnapshotManager(); + + await projectManager.UpdateAsync(updater => + { + updater.ProjectAdded(hostProject); + updater.ProjectWorkspaceStateChanged(hostProject.Key, projectWorkspaceState); + updater.DocumentAdded(hostProject.Key, hostDocument, TestMocks.CreateTextLoader(hostDocument.FilePath, text: "")); + }); + + var solutionQueryOperations = projectManager.GetQueryOperations(); + + var availability = await solutionQueryOperations.GetProjectAvailabilityTextAsync(hostDocument.FilePath, tagHelperTypeName, DisposalToken); + + Assert.Null(availability); + } + + [Fact] + public async Task GetProjectAvailabilityText_AvailableInAllProjects_ReturnsNull() + { + var builder = TagHelperDescriptorBuilder.Create(ComponentMetadata.Component.TagHelperKind, "TestTagHelper", "TestAssembly"); + builder.TagMatchingRule(rule => rule.TagName = "Test"); + var tagHelperTypeName = "TestNamespace.TestTagHelper"; + builder.Metadata(TypeName(tagHelperTypeName)); + var tagHelpers = ImmutableArray.Create(builder.Build()); + var projectWorkspaceState = ProjectWorkspaceState.Create(tagHelpers); + + var hostProject1 = new HostProject( + "C:/path/to/project.csproj", + "C:/path/to/obj/1", + RazorConfiguration.Default, + rootNamespace: null, + displayName: "project1"); + + var hostProject2 = new HostProject( + "C:/path/to/project.csproj", + "C:/path/to/obj/2", + RazorConfiguration.Default, + rootNamespace: null, + displayName: "project2"); + + var hostDocument = new HostDocument( + "C:/path/to/file.razor", + "file.razor", + FileKinds.Component); + + var projectManager = CreateProjectSnapshotManager(); + + await projectManager.UpdateAsync(updater => + { + updater.ProjectAdded(hostProject1); + updater.ProjectWorkspaceStateChanged(hostProject1.Key, projectWorkspaceState); + updater.DocumentAdded(hostProject1.Key, hostDocument, TestMocks.CreateTextLoader(hostDocument.FilePath, text: "")); + + updater.ProjectAdded(hostProject2); + updater.ProjectWorkspaceStateChanged(hostProject2.Key, projectWorkspaceState); + updater.DocumentAdded(hostProject2.Key, hostDocument, TestMocks.CreateTextLoader(hostDocument.FilePath, text: "")); + }); + + var solutionQueryOperations = projectManager.GetQueryOperations(); + + var availability = await solutionQueryOperations.GetProjectAvailabilityTextAsync(hostDocument.FilePath, tagHelperTypeName, DisposalToken); + + Assert.Null(availability); + } + + [Fact] + public async Task GetProjectAvailabilityText_NotAvailableInAllProjects_ReturnsText() + { + var builder = TagHelperDescriptorBuilder.Create(ComponentMetadata.Component.TagHelperKind, "TestTagHelper", "TestAssembly"); + builder.TagMatchingRule(rule => rule.TagName = "Test"); + var tagHelperTypeName = "TestNamespace.TestTagHelper"; + builder.Metadata(TypeName(tagHelperTypeName)); + var tagHelpers = ImmutableArray.Create(builder.Build()); + var projectWorkspaceState = ProjectWorkspaceState.Create(tagHelpers); + + var hostProject1 = new HostProject( + "C:/path/to/project.csproj", + "C:/path/to/obj/1", + RazorConfiguration.Default, + rootNamespace: null, + displayName: "project1"); + + var hostProject2 = new HostProject( + "C:/path/to/project.csproj", + "C:/path/to/obj/2", + RazorConfiguration.Default, + rootNamespace: null, + displayName: "project2"); + + var hostDocument = new HostDocument( + "C:/path/to/file.razor", + "file.razor", + FileKinds.Component); + + var projectManager = CreateProjectSnapshotManager(); + + await projectManager.UpdateAsync(updater => + { + updater.ProjectAdded(hostProject1); + updater.ProjectWorkspaceStateChanged(hostProject1.Key, projectWorkspaceState); + updater.DocumentAdded(hostProject1.Key, hostDocument, TestMocks.CreateTextLoader(hostDocument.FilePath, text: "")); + + updater.ProjectAdded(hostProject2); + updater.DocumentAdded(hostProject2.Key, hostDocument, TestMocks.CreateTextLoader(hostDocument.FilePath, text: "")); + }); + + var solutionQueryOperations = projectManager.GetQueryOperations(); + + var availability = await solutionQueryOperations.GetProjectAvailabilityTextAsync(hostDocument.FilePath, tagHelperTypeName, DisposalToken); + + AssertEx.EqualOrDiff(""" + + ⚠️ Not available in: + project2 + """, availability); + } + + [Fact] + public async Task GetProjectAvailabilityText_NotAvailableInAnyProject_ReturnsText() + { + var hostProject1 = new HostProject( + "C:/path/to/project.csproj", + "C:/path/to/obj/1", + RazorConfiguration.Default, + rootNamespace: null, + displayName: "project1"); + + var hostProject2 = new HostProject( + "C:/path/to/project.csproj", + "C:/path/to/obj/2", + RazorConfiguration.Default, + rootNamespace: null, + displayName: "project2"); + + var hostDocument = new HostDocument( + "C:/path/to/file.razor", + "file.razor", + FileKinds.Component); + + var projectManager = CreateProjectSnapshotManager(); + + await projectManager.UpdateAsync(updater => + { + updater.ProjectAdded(hostProject1); + updater.DocumentAdded(hostProject1.Key, hostDocument, TestMocks.CreateTextLoader(hostDocument.FilePath, text: "")); + + updater.ProjectAdded(hostProject2); + updater.DocumentAdded(hostProject2.Key, hostDocument, TestMocks.CreateTextLoader(hostDocument.FilePath, text: "")); + }); + + var solutionQueryOperations = projectManager.GetQueryOperations(); + + var availability = await solutionQueryOperations.GetProjectAvailabilityTextAsync(hostDocument.FilePath, "MyTagHelper", DisposalToken); + + AssertEx.EqualOrDiff(""" + + ⚠️ Not available in: + project1 + project2 + """, availability); + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentFormattingEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentFormattingEndpointTest.cs index 3c8f9b268b0..7370b711fc1 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentFormattingEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentFormattingEndpointTest.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.AspNetCore.Razor.Test.Common.Mef; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor.Formatting; @@ -19,8 +18,9 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; -[UseExportProvider] -public class CohostDocumentFormattingEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) +[Collection(HtmlFormattingCollection.Name)] +public class CohostDocumentFormattingEndpointTest(HtmlFormattingFixture htmlFormattingFixture, ITestOutputHelper testOutputHelper) + : CohostEndpointTestBase(testOutputHelper) { // All of the formatting tests in the language server exercise the formatting engine and cover various edge cases // and provide regression prevention. The tests here are not exhaustive, but they validate the the cohost endpoints @@ -108,7 +108,7 @@ private void M(string thisIsMyString) private async Task VerifyDocumentFormattingAsync(string input, string expected) { - var document = CreateProjectAndRazorDocument(input); + var document = await CreateProjectAndRazorDocumentAsync(input); var inputText = await document.GetTextAsync(DisposalToken); var htmlDocumentPublisher = new HtmlDocumentPublisher(RemoteServiceInvoker, StrictMock.Of(), StrictMock.Of(), LoggerFactory); @@ -116,7 +116,7 @@ private async Task VerifyDocumentFormattingAsync(string input, string expected) Assert.NotNull(generatedHtml); var uri = new Uri(document.CreateUri(), $"{document.FilePath}{FeatureOptions.HtmlVirtualDocumentSuffix}"); - var htmlEdits = await HtmlFormatting.GetDocumentFormattingEditsAsync(LoggerFactory, uri, generatedHtml, insertSpaces: true, tabSize: 4); + var htmlEdits = await htmlFormattingFixture.Service.GetDocumentFormattingEditsAsync(LoggerFactory, uri, generatedHtml, insertSpaces: true, tabSize: 4); var requestInvoker = new TestLSPRequestInvoker([(Methods.TextDocumentFormattingName, htmlEdits)]); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentHighlightEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentHighlightEndpointTest.cs index ce044474c00..65de7ed10a9 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentHighlightEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentHighlightEndpointTest.cs @@ -147,7 +147,7 @@ @inject [|IDis$$posable|] Disposable private async Task VerifyDocumentHighlightsAsync(string input, DocumentHighlight[]? htmlResponse = null) { TestFileMarkupParser.GetPositionAndSpans(input, out var source, out int cursorPosition, out ImmutableArray spans); - var document = CreateProjectAndRazorDocument(source); + var document = await CreateProjectAndRazorDocumentAsync(source); var inputText = await document.GetTextAsync(DisposalToken); var position = inputText.GetPosition(cursorPosition); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentPullDiagnosticsTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentPullDiagnosticsTest.cs new file mode 100644 index 00000000000..1bc8fd332cc --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentPullDiagnosticsTest.cs @@ -0,0 +1,121 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Roslyn.Test.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +public class CohostDocumentPullDiagnosticsTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) +{ + [Fact] + public Task CSharp() + => VerifyDiagnosticsAsync(""" +
+ + @code + { + public void IJustMetYou() + { + {|CS0103:CallMeMaybe|}(); + } + } + """); + + [Fact] + public Task Razor() + => VerifyDiagnosticsAsync(""" +
+ + {|RZ10012:|} + +
+ """); + + [Fact] + public Task Html() + { + TestCode input = """ +
+ + {|HTM1337:|} + +
+ """; + + return VerifyDiagnosticsAsync(input, + htmlResponse: [new VSInternalDiagnosticReport + { + Diagnostics = + [ + new Diagnostic + { + Code = "HTM1337", + Range = SourceText.From(input.Text).GetRange(input.NamedSpans.First().Value.First()) + } + ] + }]); + } + + [Fact] + public Task CombinedAndNestedDiagnostics() + => VerifyDiagnosticsAsync(""" + @using System.Threading.Tasks; + +
+ + {|RZ10012:|} + + @code + { + public void IJustMetYou() + { + {|CS0103:CallMeMaybe|}(); + } + } + +
+ @{ + {|CS4033:await Task.{|CS1501:Delay|}()|}; + } + + {|RZ9980:

|} +

+ +
+ """); + + private async Task VerifyDiagnosticsAsync(TestCode input, VSInternalDiagnosticReport[]? htmlResponse = null) + { + var document = await CreateProjectAndRazorDocumentAsync(input.Text, createSeparateRemoteAndLocalWorkspaces: true); + var inputText = await document.GetTextAsync(DisposalToken); + + var requestInvoker = new TestLSPRequestInvoker([(VSInternalMethods.DocumentPullDiagnosticName, htmlResponse)]); + + var endpoint = new CohostDocumentPullDiagnosticsEndpoint(RemoteServiceInvoker, TestHtmlDocumentSynchronizer.Instance, requestInvoker, FilePathService, LoggerFactory); + + var result = await endpoint.GetTestAccessor().HandleRequestAsync(document, DisposalToken); + + var markers = result!.SelectMany(d => d.Diagnostics.AssumeNotNull()).SelectMany(d => + new[] { + (index: inputText.GetTextSpan(d.Range).Start, text: $"{{|{d.Code!.Value.Second}:"), + (index: inputText.GetTextSpan(d.Range).End, text:"|}") + }); + + var testOutput = input.Text; + foreach (var (index, text) in markers.OrderByDescending(i => i.index)) + { + testOutput = testOutput.Insert(index, text); + } + + AssertEx.EqualOrDiff(input.OriginalInput, testOutput); + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentSpellCheckEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentSpellCheckEndpointTest.cs index 6711a4bdc2b..51c5d3d28a9 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentSpellCheckEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentSpellCheckEndpointTest.cs @@ -57,7 +57,7 @@ Eat more chickin. private async Task VerifySemanticTokensAsync(TestCode input) { - var document = CreateProjectAndRazorDocument(input.Text); + var document = await CreateProjectAndRazorDocumentAsync(input.Text); var sourceText = await document.GetTextAsync(DisposalToken); var endpoint = new CohostDocumentSpellCheckEndpoint(RemoteServiceInvoker); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentSymbolEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentSymbolEndpointTest.cs index 0f008bc6ef2..2cc98c4c923 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentSymbolEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentSymbolEndpointTest.cs @@ -69,7 +69,7 @@ public Task DocumentSymbols_CSharpMethods(bool hierarchical) private async Task VerifyDocumentSymbolsAsync(string input, bool hierarchical = false) { TestFileMarkupParser.GetSpans(input, out input, out ImmutableDictionary> spansDict); - var document = CreateProjectAndRazorDocument(input); + var document = await CreateProjectAndRazorDocumentAsync(input); var endpoint = new CohostDocumentSymbolEndpoint(RemoteServiceInvoker); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs index 3573e1badf6..fac6c663afa 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs @@ -4,15 +4,19 @@ using System; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Basic.Reference.Assemblies; using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.AspNetCore.Razor.Test.Common.Mef; +using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.Remote; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Remote.Razor; +using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Composition; using Xunit.Abstractions; @@ -68,7 +72,11 @@ private protected void UpdateClientInitializationOptions(Func CreateProjectAndRazorDocumentAsync( + string contents, + string? fileKind = null, + (string fileName, string contents)[]? additionalFiles = null, + bool createSeparateRemoteAndLocalWorkspaces = false) { // Using IsLegacy means null == component, so easier for test authors var isComponent = !FileKinds.IsLegacy(fileKind); @@ -82,6 +90,70 @@ protected TextDocument CreateProjectAndRazorDocument(string contents, string? fi var projectId = ProjectId.CreateNewId(debugName: projectName); var documentId = DocumentId.CreateNewId(projectId, debugName: documentFilePath); + var remoteWorkspace = RemoteWorkspaceAccessor.GetWorkspace(); + var remoteDocument = CreateProjectAndRazorDocument(remoteWorkspace, projectId, projectName, documentId, documentFilePath, contents, additionalFiles); + + if (createSeparateRemoteAndLocalWorkspaces) + { + // Usually its fine to just use the remote workspace, but sometimes we need to also have things available in the + // "devenv" side of Roslyn, which is a different workspace with a different set of services. We don't have any + // actual solution syncing set up for testing, and don't really use a service broker, but since we also would + // expect to never make changes to a workspace, it should be fine to simply create duplicated solutions as part + // of test setup. + return CreateLocalProjectAndRazorDocumentAsync( + remoteDocument.Project.Solution, + projectId, + projectName, + documentId, + documentFilePath, + contents, + additionalFiles); + } + + // If we're just creating one workspace, then its the remote one and we just return the remote document + // and assume that the endpoint under test doesn't need to do anything on the devenv side. This makes it + // easier for tests to mutate solutions + return Task.FromResult(remoteDocument); + } + + private async Task CreateLocalProjectAndRazorDocumentAsync( + Solution remoteSolution, + ProjectId projectId, + string projectName, + DocumentId documentId, + string documentFilePath, + string contents, + (string fileName, string contents)[]? additionalFiles) + { + var exportProvider = TestComposition.Roslyn.ExportProviderFactory.CreateExportProvider(); + AddDisposable(exportProvider); + var workspace = TestWorkspace.CreateWithDiagnosticAnalyzers(exportProvider); + AddDisposable(workspace); + + var razorDocument = CreateProjectAndRazorDocument(workspace, projectId, projectName, documentId, documentFilePath, contents, additionalFiles); + + // Until the source generator is hooked up, the workspace representing "local" projects doesn't have anything + // to actually compile the Razor to C#, so we just do it now at creation + var solution = razorDocument.Project.Solution; + // We're cheating a bit here and using the remote export provider to get something to do the compilation + var snapshotManager = _exportProvider.AssumeNotNull().GetExportedValue(); + var snapshot = snapshotManager.GetSnapshot(razorDocument); + // Compile the Razor file + var codeDocument = await snapshot.GetGeneratedOutputAsync(forceDesignTimeGeneratedOutput: false, DisposalToken); + // Update the generated doc contents + var generatedDocumentIds = solution.GetDocumentIdsWithFilePath(documentFilePath + CSharpVirtualDocumentSuffix); + solution = solution.WithDocumentText(generatedDocumentIds, codeDocument.GetCSharpSourceText()); + razorDocument = solution.GetAdditionalDocument(documentId).AssumeNotNull(); + + // If we're creating remote and local workspaces, then we'll return the local document, and have to allow + // the remote service invoker to map from the local solution to the remote one. + RemoteServiceInvoker.MapSolutionIdToRemote(razorDocument.Project.Solution.Id, remoteSolution); + + return razorDocument; + } + + private static TextDocument CreateProjectAndRazorDocument(CodeAnalysis.Workspace workspace, ProjectId projectId, string projectName, DocumentId documentId, string documentFilePath, string contents, (string fileName, string contents)[]? additionalFiles) + { var projectInfo = ProjectInfo .Create( projectId, @@ -93,9 +165,6 @@ protected TextDocument CreateProjectAndRazorDocument(string contents, string? fi .WithDefaultNamespace(TestProjectData.SomeProject.RootNamespace) .WithMetadataReferences(AspNet80.ReferenceInfos.All.Select(r => r.Reference)); - // Importantly, we use Roslyn's remote workspace here so that when our OOP services call into Roslyn, their code - // will be able to access their services. - var workspace = RemoteWorkspaceAccessor.GetWorkspace(); var solution = workspace.CurrentSolution.AddProject(projectInfo); solution = solution diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostFoldingRangeEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostFoldingRangeEndpointTest.cs index 08ad6977496..1bfb8b299cd 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostFoldingRangeEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostFoldingRangeEndpointTest.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -218,7 +217,7 @@ public void M() {[| private async Task VerifyFoldingRangesAsync(string input, string? fileKind = null) { TestFileMarkupParser.GetSpans(input, out var source, out ImmutableDictionary> spans); - var document = CreateProjectAndRazorDocument(source, fileKind); + var document = await CreateProjectAndRazorDocumentAsync(source, fileKind); var inputText = await document.GetTextAsync(DisposalToken); var htmlSpans = spans.GetValueOrDefault("html").NullToEmpty(); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToDefinitionEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToDefinitionEndpointTest.cs index b97fe4ef07a..f5b8b6e77e3 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToDefinitionEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToDefinitionEndpointTest.cs @@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis.ExternalAccess.Razor; -using Microsoft.CodeAnalysis.Remote.Razor; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; using Xunit; @@ -313,7 +312,7 @@ public async Task Html() """; - var document = CreateProjectAndRazorDocument(input.Text); + var document = await CreateProjectAndRazorDocumentAsync(input.Text); var inputText = await document.GetTextAsync(DisposalToken); var htmlResponse = new SumType?(new Location[] @@ -333,7 +332,7 @@ private static string FileName(string projectRelativeFileName) private async Task VerifyGoToDefinitionAsync(TestCode input, string? fileKind = null, SumType? htmlResponse = null) { - var document = CreateProjectAndRazorDocument(input.Text, fileKind); + var document = await CreateProjectAndRazorDocumentAsync(input.Text, fileKind); var result = await GetGoToDefinitionResultAsync(document, input, htmlResponse); Assumes.NotNull(result); @@ -349,11 +348,11 @@ private async Task VerifyGoToDefinitionAsync(TestCode input, string? fileKind = Assert.Equal(document.CreateUri(), location.Uri); } - private Task?> GetGoToDefinitionResultAsync( + private async Task?> GetGoToDefinitionResultAsync( TestCode input, string? fileKind = null, params (string fileName, string contents)[]? additionalFiles) { - var document = CreateProjectAndRazorDocument(input.Text, fileKind, additionalFiles); - return GetGoToDefinitionResultAsync(document, input, htmlResponse: null); + var document = await CreateProjectAndRazorDocumentAsync(input.Text, fileKind, additionalFiles); + return await GetGoToDefinitionResultAsync(document, input, htmlResponse: null); } private async Task?> GetGoToDefinitionResultAsync( diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToImplementationEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToImplementationEndpointTest.cs index 4ee5dab2c8d..b6c91a29bea 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToImplementationEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostGoToImplementationEndpointTest.cs @@ -100,7 +100,7 @@ public async Task Html() """; - var document = CreateProjectAndRazorDocument(input.Text); + var document = await CreateProjectAndRazorDocumentAsync(input.Text); var inputText = await document.GetTextAsync(DisposalToken); var htmlResponse = new SumType?(new LspLocation[] @@ -119,7 +119,7 @@ public async Task Html() private async Task VerifyCSharpGoToImplementationAsync(TestCode input) { - var document = CreateProjectAndRazorDocument(input.Text); + var document = await CreateProjectAndRazorDocumentAsync(input.Text); var requestInvoker = new TestLSPRequestInvoker(); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostInlayHintEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostInlayHintEndpointTest.cs index f7a3df1d975..1b81b87ef39 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostInlayHintEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostInlayHintEndpointTest.cs @@ -128,10 +128,34 @@ public Task InlayHints_ComponentAttributes() """); + [Theory] + [InlineData(0, 0, 0, 20)] + [InlineData(0, 0, 2, 0)] + [InlineData(2, 0, 4, 0)] + public async Task InlayHints_InvalidRange(int startLine, int starChar, int endLine, int endChar) + { + var input = """ +
+ """; + var document = await CreateProjectAndRazorDocumentAsync(input); + var endpoint = new CohostInlayHintEndpoint(RemoteServiceInvoker); + + var request = new InlayHintParams() + { + TextDocument = new TextDocumentIdentifier() { Uri = document.CreateUri() }, + Range = RoslynLspFactory.CreateRange(startLine, starChar, endLine, endChar) + }; + + var hints = await endpoint.GetTestAccessor().HandleRequestAsync(request, document, displayAllOverride: false, DisposalToken); + + // Assert + Assert.Null(hints); + } + private async Task VerifyInlayHintsAsync(string input, Dictionary toolTipMap, string output, bool displayAllOverride = false) { TestFileMarkupParser.GetSpans(input, out input, out ImmutableDictionary> spansDict); - var document = CreateProjectAndRazorDocument(input); + var document = await CreateProjectAndRazorDocumentAsync(input); var inputText = await document.GetTextAsync(DisposalToken); var endpoint = new CohostInlayHintEndpoint(RemoteServiceInvoker); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostLinkedEditingRangeEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostLinkedEditingRangeEndpointTest.cs index 28c05827166..865f5101125 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostLinkedEditingRangeEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostLinkedEditingRangeEndpointTest.cs @@ -159,7 +159,7 @@ The end. private async Task VerifyLinkedEditingRangeAsync(string input) { TestFileMarkupParser.GetPositionAndSpans(input, out input, out int cursorPosition, out ImmutableArray spans); - var document = CreateProjectAndRazorDocument(input); + var document = await CreateProjectAndRazorDocumentAsync(input); var sourceText = await document.GetTextAsync(DisposalToken); var endpoint = new CohostLinkedEditingRangeEndpoint(RemoteServiceInvoker); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostOnAutoInsertEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostOnAutoInsertEndpointTest.cs index b7dbb70194a..74d5036d454 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostOnAutoInsertEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostOnAutoInsertEndpointTest.cs @@ -196,7 +196,7 @@ private async Task VerifyOnAutoInsertAsync( bool formatOnType = true, bool autoClosingTags = true) { - var document = CreateProjectAndRazorDocument(input.Text); + var document = await CreateProjectAndRazorDocumentAsync(input.Text); var sourceText = await document.GetTextAsync(DisposalToken); var clientSettingsManager = new ClientSettingsManager([], null, null); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostOnTypeFormattingEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostOnTypeFormattingEndpointTest.cs index 7a1e3d913f3..1919956dad4 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostOnTypeFormattingEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostOnTypeFormattingEndpointTest.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.AspNetCore.Razor.Test.Common.Mef; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor.Formatting; @@ -19,8 +18,9 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; -[UseExportProvider] -public class CohostOnTypeFormattingEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) +[Collection(HtmlFormattingCollection.Name)] +public class CohostOnTypeFormattingEndpointTest(HtmlFormattingFixture htmlFormattingFixture, ITestOutputHelper testOutputHelper) + : CohostEndpointTestBase(testOutputHelper) { [Fact] public async Task InvalidTrigger() @@ -103,7 +103,7 @@ await VerifyOnTypeFormattingAsync( private async Task VerifyOnTypeFormattingAsync(TestCode input, string expected, char triggerCharacter, bool html = false) { - var document = CreateProjectAndRazorDocument(input.Text); + var document = await CreateProjectAndRazorDocumentAsync(input.Text); var inputText = await document.GetTextAsync(DisposalToken); var position = inputText.GetPosition(input.Position); @@ -115,7 +115,7 @@ private async Task VerifyOnTypeFormattingAsync(TestCode input, string expected, Assert.NotNull(generatedHtml); var uri = new Uri(document.CreateUri(), $"{document.FilePath}{FeatureOptions.HtmlVirtualDocumentSuffix}"); - var htmlEdits = await HtmlFormatting.GetOnTypeFormattingEditsAsync(LoggerFactory, uri, generatedHtml, position, insertSpaces: true, tabSize: 4); + var htmlEdits = await htmlFormattingFixture.Service.GetOnTypeFormattingEditsAsync(LoggerFactory, uri, generatedHtml, position, insertSpaces: true, tabSize: 4); requestInvoker = new TestLSPRequestInvoker([(Methods.TextDocumentOnTypeFormattingName, htmlEdits)]); } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRangeFormattingEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRangeFormattingEndpointTest.cs index 32d03e28002..97b3db3fd79 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRangeFormattingEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRangeFormattingEndpointTest.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.AspNetCore.Razor.Test.Common.Mef; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.ExternalAccess.Razor; using Microsoft.CodeAnalysis.Razor.Formatting; @@ -19,8 +18,9 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; -[UseExportProvider] -public class CohostRangeFormattingEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) +[Collection(HtmlFormattingCollection.Name)] +public class CohostRangeFormattingEndpointTest(HtmlFormattingFixture htmlFormattingFixture, ITestOutputHelper testOutputHelper) + : CohostEndpointTestBase(testOutputHelper) { [Fact] public Task RangeFormatting() @@ -102,7 +102,7 @@ private void M(string thisIsMyString) private async Task VerifyRangeFormattingAsync(TestCode input, string expected) { - var document = CreateProjectAndRazorDocument(input.Text); + var document = await CreateProjectAndRazorDocumentAsync(input.Text); var inputText = await document.GetTextAsync(DisposalToken); var htmlDocumentPublisher = new HtmlDocumentPublisher(RemoteServiceInvoker, StrictMock.Of(), StrictMock.Of(), LoggerFactory); @@ -110,7 +110,7 @@ private async Task VerifyRangeFormattingAsync(TestCode input, string expected) Assert.NotNull(generatedHtml); var uri = new Uri(document.CreateUri(), $"{document.FilePath}{FeatureOptions.HtmlVirtualDocumentSuffix}"); - var htmlEdits = await HtmlFormatting.GetDocumentFormattingEditsAsync(LoggerFactory, uri, generatedHtml, insertSpaces: true, tabSize: 4); + var htmlEdits = await htmlFormattingFixture.Service.GetDocumentFormattingEditsAsync(LoggerFactory, uri, generatedHtml, insertSpaces: true, tabSize: 4); var requestInvoker = new TestLSPRequestInvoker([(Methods.TextDocumentFormattingName, htmlEdits)]); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs index 9328744f2ae..6e1af1d4f06 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostRenameEndpointTest.cs @@ -198,7 +198,7 @@ public class Component : Microsoft.AspNetCore.Components.ComponentBase private async Task VerifyRenamesAsync(string input, string newName, string expected, string? fileKind = null, (string fileName, string contents)[]? additionalFiles = null, (string oldName, string newName)[]? renames = null) { TestFileMarkupParser.GetPosition(input, out var source, out var cursorPosition); - var document = CreateProjectAndRazorDocument(source, fileKind, additionalFiles); + var document = await CreateProjectAndRazorDocumentAsync(source, fileKind, additionalFiles); var inputText = await document.GetTextAsync(DisposalToken); var position = inputText.GetPosition(cursorPosition); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostSemanticTokensRangeEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostSemanticTokensRangeEndpointTest.cs index 42b792b7ad5..794cfc67ff1 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostSemanticTokensRangeEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostSemanticTokensRangeEndpointTest.cs @@ -67,6 +67,7 @@ public async Task Legacy(bool colorBackground, bool precise) { var input = """ @page "/" + @model AppThing.Model @using System
This is some HTML
@@ -90,7 +91,7 @@ @section MySection { private async Task VerifySemanticTokensAsync(string input, bool colorBackground, bool precise, string? fileKind = null, [CallerMemberName] string? testName = null) { - var document = CreateProjectAndRazorDocument(input, fileKind); + var document = await CreateProjectAndRazorDocumentAsync(input, fileKind); var sourceText = await document.GetTextAsync(DisposalToken); var legend = TestRazorSemanticTokensLegendService.Instance; diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostSignatureHelpEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostSignatureHelpEndpointTest.cs index 7c599b0a3ed..8facef4127a 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostSignatureHelpEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostSignatureHelpEndpointTest.cs @@ -91,7 +91,7 @@ void Act() private async Task VerifySignatureHelpAsync(string input, string expected, bool autoListParams = true, SignatureHelpTriggerKind? triggerKind = null) { TestFileMarkupParser.GetPosition(input, out input, out var cursorPosition); - var document = CreateProjectAndRazorDocument(input); + var document = await CreateProjectAndRazorDocumentAsync(input); var sourceText = await document.GetTextAsync(DisposalToken); var clientSettingsManager = new ClientSettingsManager([], null, null); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTextPresentationEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTextPresentationEndpointTest.cs index f946a642581..f2a4acc5b67 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTextPresentationEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostTextPresentationEndpointTest.cs @@ -55,7 +55,7 @@ private static string File(string projectRelativeFileName) private async Task VerifyUriPresentationAsync(string input, string text, string? expected, WorkspaceEdit? htmlResponse = null) { TestFileMarkupParser.GetSpan(input, out input, out var span); - var document = CreateProjectAndRazorDocument(input); + var document = await CreateProjectAndRazorDocumentAsync(input); var sourceText = await document.GetTextAsync(DisposalToken); var requestInvoker = new TestLSPRequestInvoker([(VSInternalMethods.TextDocumentTextPresentationName, htmlResponse)]); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs index d1f965f0b76..68b3be9565a 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostUriPresentationEndpointTest.cs @@ -286,7 +286,7 @@ private static string File(string projectRelativeFileName) private async Task VerifyUriPresentationAsync(string input, Uri[] uris, string? expected, WorkspaceEdit? htmlResponse = null, (string fileName, string contents)[]? additionalFiles = null) { TestFileMarkupParser.GetSpan(input, out input, out var span); - var document = CreateProjectAndRazorDocument(input, additionalFiles: additionalFiles); + var document = await CreateProjectAndRazorDocumentAsync(input, additionalFiles: additionalFiles); var sourceText = await document.GetTextAsync(DisposalToken); var requestInvoker = new TestLSPRequestInvoker([(VSInternalMethods.TextDocumentUriPresentationName, htmlResponse)]); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/HtmlFormattingCollection.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/HtmlFormattingCollection.cs new file mode 100644 index 00000000000..0380b2d5675 --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/HtmlFormattingCollection.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using Microsoft.CodeAnalysis.Razor.Formatting; +using Xunit; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +[CollectionDefinition(Name)] +public class HtmlFormattingCollection : ICollectionFixture +{ + public const string Name = nameof(HtmlFormattingCollection); +} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/RazorComponentDefinitionServiceTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/RazorComponentDefinitionServiceTest.cs index 041b95c6eee..2f4dc0c7b95 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/RazorComponentDefinitionServiceTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/RazorComponentDefinitionServiceTest.cs @@ -76,17 +76,22 @@ await VerifyDefinitionAsync(input, surveyPrompt, (FileName("SurveyPrompt.razor") private async Task VerifyDefinitionAsync(TestCode input, TestCode expectedDocument, params (string fileName, string contents)[]? additionalFiles) { - var document = CreateProjectAndRazorDocument(input.Text, FileKinds.Component, additionalFiles); + var document = await CreateProjectAndRazorDocumentAsync(input.Text, FileKinds.Component, additionalFiles); var service = OOPExportProvider.GetExportedValue(); - var documentSnapshotFactory = OOPExportProvider.GetExportedValue(); + var snapshotManager = OOPExportProvider.GetExportedValue(); var documentMappingService = OOPExportProvider.GetExportedValue(); - var documentSnapshot = documentSnapshotFactory.GetOrCreate(document); - var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(); + var documentSnapshot = snapshotManager.GetSnapshot(document); + var codeDocument = await documentSnapshot.GetGeneratedOutputAsync(DisposalToken); var positionInfo = documentMappingService.GetPositionInfo(codeDocument, input.Position); - var location = await service.GetDefinitionAsync(documentSnapshot, positionInfo, ignoreAttributes: false, DisposalToken); + var location = await service.GetDefinitionAsync( + documentSnapshot, + positionInfo, + solutionQueryOperations: documentSnapshot.ProjectSnapshot.SolutionSnapshot, + ignoreAttributes: false, + DisposalToken); Assert.NotNull(location); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TestBrokeredServiceInterceptor.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TestBrokeredServiceInterceptor.cs index 7d0da662311..fffa4b394c6 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TestBrokeredServiceInterceptor.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TestBrokeredServiceInterceptor.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -14,6 +15,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; internal sealed class TestBrokeredServiceInterceptor : IRazorBrokeredServiceInterceptor { private readonly TestSolutionStore _solutionStore = new(); + private readonly Dictionary _localToRemoteSolutionMap = []; public Task GetSolutionInfoAsync(Solution solution, CancellationToken cancellationToken) => _solutionStore.AddAsync(solution, cancellationToken); @@ -32,6 +34,18 @@ public ValueTask RunServiceAsync( Assert.NotNull(solution); + // Rather than actually syncing assets, we just let the test author directly map from a local solution + // to a remote solution; + if (_localToRemoteSolutionMap.TryGetValue(solution.Id, out var remoteSolution)) + { + solution = remoteSolution; + } + return implementation(solution); } + + internal void MapSolutionIdToRemote(SolutionId localSolutionId, Solution remoteSolution) + { + _localToRemoteSolutionMap.Add(localSolutionId, remoteSolution); + } } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TestRemoteServiceInvoker.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TestRemoteServiceInvoker.cs index 9e5c8651aac..581d08f35a4 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TestRemoteServiceInvoker.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/TestRemoteServiceInvoker.cs @@ -57,6 +57,11 @@ private async Task GetOrCreateServiceAsync() return await invocation(service, solutionInfo, cancellationToken); } + public void MapSolutionIdToRemote(SolutionId localSolutionId, Solution remoteSolution) + { + _serviceInterceptor.MapSolutionIdToRemote(localSolutionId, remoteSolution); + } + public void Dispose() { _reentrantSemaphore.Dispose(); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DocumentGenerator/BackgroundDocumentGeneratorTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DocumentGenerator/BackgroundDocumentGeneratorTest.cs index 67a071ed8a7..b4d91bec7e6 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DocumentGenerator/BackgroundDocumentGeneratorTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/DocumentGenerator/BackgroundDocumentGeneratorTest.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.ProjectSystem; using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; using Microsoft.AspNetCore.Razor.Test.Common.VisualStudio; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; @@ -32,17 +33,8 @@ public class BackgroundDocumentGeneratorTest(ITestOutputHelper testOutput) : Vis { private static readonly HostDocument[] s_documents = [TestProjectData.SomeProjectFile1, TestProjectData.AnotherProjectFile1]; - private static readonly HostProject s_hostProject1 = new( - TestProjectData.SomeProject.FilePath, - TestProjectData.SomeProject.IntermediateOutputPath, - FallbackRazorConfiguration.MVC_1_0, - TestProjectData.SomeProject.RootNamespace); - - private static readonly HostProject s_hostProject2 = new( - TestProjectData.AnotherProject.FilePath, - TestProjectData.AnotherProject.IntermediateOutputPath, - FallbackRazorConfiguration.MVC_1_0, - TestProjectData.AnotherProject.RootNamespace); + private static readonly HostProject s_hostProject1 = TestProjectData.SomeProject with { Configuration = FallbackRazorConfiguration.MVC_1_0 }; + private static readonly HostProject s_hostProject2 = TestProjectData.AnotherProject with { Configuration = FallbackRazorConfiguration.MVC_1_0 }; private readonly TestDynamicFileInfoProvider _dynamicFileInfoProvider = new(); @@ -195,8 +187,8 @@ await projectManager.UpdateAsync(updater => { updater.ProjectAdded(s_hostProject1); updater.ProjectAdded(s_hostProject2); - updater.DocumentAdded(s_hostProject1.Key, s_documents[0], null!); - updater.DocumentAdded(s_hostProject1.Key, s_documents[1], null!); + updater.DocumentAdded(s_hostProject1.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); + updater.DocumentAdded(s_hostProject1.Key, s_documents[1], s_documents[1].CreateEmptyTextLoader()); }); var project = projectManager.GetLoadedProject(s_hostProject1.Key); @@ -234,8 +226,8 @@ await projectManager.UpdateAsync(updater => { updater.ProjectAdded(hostProject1); updater.ProjectAdded(hostProject2); - updater.DocumentAdded(hostProject1.Key, hostDocument1, null!); - updater.DocumentAdded(hostProject1.Key, hostDocument2, null!); + updater.DocumentAdded(hostProject1.Key, hostDocument1, hostDocument1.CreateEmptyTextLoader()); + updater.DocumentAdded(hostProject1.Key, hostDocument2, hostDocument2.CreateEmptyTextLoader()); }); var project = projectManager.GetLoadedProject(hostProject1.Key); @@ -283,7 +275,7 @@ await projectManager.UpdateAsync(updater => updater.ProjectAdded(s_hostProject1); for (var i = 0; i < documents.Length; i++) { - updater.DocumentAdded(s_hostProject1.Key, documents[i], null!); + updater.DocumentAdded(s_hostProject1.Key, documents[i], documents[i].CreateEmptyTextLoader()); } }); @@ -327,8 +319,8 @@ public async Task DocumentRemoved_ReparsesRelatedFiles() await projectManager.UpdateAsync(updater => { updater.ProjectAdded(s_hostProject1); - updater.DocumentAdded(s_hostProject1.Key, TestProjectData.SomeProjectComponentFile1, null!); - updater.DocumentAdded(s_hostProject1.Key, TestProjectData.SomeProjectImportFile, null!); + updater.DocumentAdded(s_hostProject1.Key, TestProjectData.SomeProjectComponentFile1, TestProjectData.SomeProjectComponentFile1.CreateEmptyTextLoader()); + updater.DocumentAdded(s_hostProject1.Key, TestProjectData.SomeProjectImportFile, TestProjectData.SomeProjectImportFile.CreateEmptyTextLoader()); }); using var generator = new TestBackgroundDocumentGenerator(projectManager, _dynamicFileInfoProvider, LoggerFactory) @@ -416,12 +408,12 @@ public override void Enqueue(IProjectSnapshot project, IDocumentSnapshot documen base.Enqueue(project, document); } - protected override Task ProcessDocumentAsync(IProjectSnapshot project, IDocumentSnapshot document) + protected override Task ProcessDocumentAsync(IProjectSnapshot project, IDocumentSnapshot document, CancellationToken cancellationToken) { var key = GetKey(project, document); PendingWork.Remove(key); - var task = base.ProcessDocumentAsync(project, document); + var task = base.ProcessDocumentAsync(project, document, cancellationToken); CompletedWork.Add(key); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Documents/EditorDocumentManagerListenerTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Documents/EditorDocumentManagerListenerTest.cs index 0cee794457d..03b9648cf21 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Documents/EditorDocumentManagerListenerTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Documents/EditorDocumentManagerListenerTest.cs @@ -23,7 +23,7 @@ namespace Microsoft.VisualStudio.Razor.Documents; public class EditorDocumentManagerListenerTest(ITestOutputHelper testOutput) : VisualStudioTestBase(testOutput) { private static readonly HostProject s_hostProject = new( - projectFilePath: "/path/to/project.csproj", + filePath: "/path/to/project.csproj", intermediateOutputPath: "/path/to/obj", RazorConfiguration.Default, rootNamespace: null); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/CSharpDocumentExcerptServiceTests.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/CSharpDocumentExcerptServiceTests.cs index 2b693754950..1570ae345e9 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/CSharpDocumentExcerptServiceTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/CSharpDocumentExcerptServiceTests.cs @@ -35,7 +35,7 @@ public async Task TryGetExcerptInternalAsync_SingleLine_CanClassifyCSharp() "; - var (generatedDocument, razorSourceText, primarySpan, generatedSpan) = await InitializeAsync(razorSource); + var (generatedDocument, razorSourceText, primarySpan, generatedSpan) = await InitializeAsync(razorSource, DisposalToken); #pragma warning disable CS0618 // Type or member is obsolete var excerptService = new CSharpDocumentExcerptService(); @@ -123,7 +123,7 @@ public async Task TryGetExcerptInternalAsync_SingleLine_CanClassifyCSharp_Implic "; - var (generatedDocument, razorSourceText, primarySpan, generatedSpan) = await InitializeAsync(razorSource); + var (generatedDocument, razorSourceText, primarySpan, generatedSpan) = await InitializeAsync(razorSource, DisposalToken); #pragma warning disable CS0618 // Type or member is obsolete var excerptService = new CSharpDocumentExcerptService(); @@ -169,7 +169,7 @@ public async Task TryGetExcerptInternalAsync_SingleLine_CanClassifyCSharp_Comple "; - var (generatedDocument, razorSourceText, primarySpan, generatedSpan) = await InitializeAsync(razorSource); + var (generatedDocument, razorSourceText, primarySpan, generatedSpan) = await InitializeAsync(razorSource, DisposalToken); #pragma warning disable CS0618 // Type or member is obsolete var excerptService = new CSharpDocumentExcerptService(); @@ -216,7 +216,7 @@ public async Task TryGetExcerptInternalAsync_MultiLine_CanClassifyCSharp() "; - var (generatedDocument, razorSourceText, primarySpan, generatedSpan) = await InitializeAsync(razorSource); + var (generatedDocument, razorSourceText, primarySpan, generatedSpan) = await InitializeAsync(razorSource, DisposalToken); #pragma warning disable CS0618 // Type or member is obsolete var excerptService = new CSharpDocumentExcerptService(); @@ -262,7 +262,7 @@ public async Task TryGetExcerptInternalAsync_MultiLine_Boundaries_CanClassifyCSh // Arrange var razorSource = @"@{ var [|foo|] = ""Hello, World!""; }"; - var (generatedDocument, razorSourceText, primarySpan, generatedSpan) = await InitializeAsync(razorSource); + var (generatedDocument, razorSourceText, primarySpan, generatedSpan) = await InitializeAsync(razorSource, DisposalToken); #pragma warning disable CS0618 // Type or member is obsolete var excerptService = new CSharpDocumentExcerptService(); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/CSharpVirtualDocumentFactoryTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/CSharpVirtualDocumentFactoryTest.cs index 62ae5109772..65ecaca31e1 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/CSharpVirtualDocumentFactoryTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/CSharpVirtualDocumentFactoryTest.cs @@ -3,14 +3,12 @@ using System; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; using Microsoft.AspNetCore.Razor.Test.Common.VisualStudio; using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Workspaces; -using Microsoft.VisualStudio.Editor.Razor; using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Utilities; @@ -133,10 +131,13 @@ public async Task TryCreateMultipleFor_RazorLSPBuffer_ReturnsCSharpVirtualDocume var projectManager = CreateProjectSnapshotManager(); + var hostProject = TestHostProject.Create(@"C:\path\to\project.csproj"); + var hostDocument = TestHostDocument.Create(hostProject, @"C:\path\to\file.razor"); + await projectManager.UpdateAsync(updater => { - var project = updater.CreateAndAddProject(@"C:\path\to\project.csproj"); - updater.CreateAndAddDocument(project, @"C:\path\to\file.razor"); + updater.ProjectAdded(hostProject); + updater.DocumentAdded(hostProject.Key, hostDocument, hostDocument.CreateEmptyTextLoader()); }); var factory = new CSharpVirtualDocumentFactory( @@ -171,25 +172,25 @@ public async Task TryCreateMultipleFor_RazorLSPBuffer_ReturnsMultipleCSharpVirtu var projectManager = CreateProjectSnapshotManager(); + var hostProject1 = TestHostProject.Create( + filePath: @"C:\path\to\project1.csproj", + intermediateOutputPath: @"C:\path\to\obj1"); + + var hostDocument1 = TestHostDocument.Create(hostProject1, @"C:\path\to\file.razor"); + + var hostProject2 = TestHostProject.Create( + filePath: @"C:\path\to\project2.csproj", + intermediateOutputPath: @"C:\path\to\obj2"); + + var hostDocument2 = TestHostDocument.Create(hostProject2, @"C:\path\to\file.razor"); + await projectManager.UpdateAsync(updater => { - var project1 = TestProjectSnapshot.Create( - @"C:\path\to\project1.csproj", - @"C:\path\to\obj1", - documentFilePaths: [], - RazorConfiguration.Default, - projectWorkspaceState: null); - updater.ProjectAdded(project1.HostProject); - updater.CreateAndAddDocument(project1, @"C:\path\to\file.razor"); - - var project2 = TestProjectSnapshot.Create( - @"C:\path\to\project2.csproj", - @"C:\path\to\obj2", - documentFilePaths: [], - RazorConfiguration.Default, - projectWorkspaceState: null); - updater.ProjectAdded(project2.HostProject); - updater.CreateAndAddDocument(project2, @"C:\path\to\file.razor"); + updater.ProjectAdded(hostProject1); + updater.DocumentAdded(hostProject1.Key, hostDocument1, hostDocument1.CreateEmptyTextLoader()); + + updater.ProjectAdded(hostProject2); + updater.DocumentAdded(hostProject2.Key, hostDocument2, hostDocument2.CreateEmptyTextLoader()); }); var languageServerFeatureOptions = new TestLanguageServerFeatureOptions(includeProjectKeyInGeneratedFilePath: true); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/DynamicFiles/RazorDocumentExcerptServiceTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/DynamicFiles/RazorDocumentExcerptServiceTest.cs index d9565002185..6a104f675a2 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/DynamicFiles/RazorDocumentExcerptServiceTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/DynamicFiles/RazorDocumentExcerptServiceTest.cs @@ -28,7 +28,7 @@ public async Task TryGetExcerptInternalAsync_SingleLine_CanClassifyCSharp() "; - var (primary, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource); + var (primary, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource, DisposalToken); var service = CreateExcerptService(primary); @@ -106,7 +106,7 @@ public async Task TryGetExcerptInternalAsync_SingleLine_CanClassifyCSharp_Implic "; - var (primary, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource); + var (primary, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource, DisposalToken); var service = CreateExcerptService(primary); @@ -159,7 +159,7 @@ public async Task TryGetExcerptInternalAsync_SingleLine_CanClassifyCSharp_Comple "; - var (primary, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource); + var (primary, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource, DisposalToken); var service = CreateExcerptService(primary); @@ -266,7 +266,7 @@ than that. """; - var (primary, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource); + var (primary, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource, DisposalToken); var service = CreateExcerptService(primary); @@ -370,7 +370,7 @@ This is a """; - var (primary, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource); + var (primary, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource, DisposalToken); var service = CreateExcerptService(primary); @@ -444,7 +444,7 @@ public async Task TryGetExcerptInternalAsync_MultiLine_CanClassifyCSharp() "; - var (primary, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource); + var (primary, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource, DisposalToken); var service = CreateExcerptService(primary); @@ -553,7 +553,7 @@ public async Task TryGetExcerptInternalAsync_MultiLine_Boundaries_CanClassifyCSh // Arrange var razorSource = @"@{ var [|foo|] = ""Hello, World!""; }"; - var (primary, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource); + var (primary, secondary, secondarySpan) = await InitializeWithSnapshotAsync(razorSource, DisposalToken); var service = CreateExcerptService(primary); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/DynamicFiles/RazorSpanMappingServiceTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/DynamicFiles/RazorSpanMappingServiceTest.cs index 40c162f169f..b793ecc1080 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/DynamicFiles/RazorSpanMappingServiceTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/DynamicFiles/RazorSpanMappingServiceTest.cs @@ -29,22 +29,21 @@ public async Task TryGetMappedSpans_SpanMatchesSourceMapping_ReturnsTrue() @SomeProperty "); - var project = new ProjectSnapshot( - ProjectState.Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) - .WithAddedHostDocument(_hostDocument, () => Task.FromResult(TextAndVersion.Create(sourceText, VersionStamp.Create())))); + var project = new ProjectSnapshot(ProjectState + .Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) + .WithAddedHostDocument(_hostDocument, TestMocks.CreateTextLoader(sourceText, VersionStamp.Create()))); var document = project.GetDocument(_hostDocument.FilePath); Assert.NotNull(document); - var service = new RazorSpanMappingService(document); - var output = await document.GetGeneratedOutputAsync(); + var output = await document.GetGeneratedOutputAsync(DisposalToken); var generated = output.GetCSharpDocument(); var symbol = "SomeProperty"; var span = new TextSpan(generated.GeneratedCode.IndexOf(symbol, StringComparison.Ordinal), symbol.Length); // Act - var result = RazorSpanMappingService.TryGetMappedSpans(span, await document.GetTextAsync(), generated, out var mappedLinePositionSpan, out var mappedSpan); + var result = RazorSpanMappingService.TryGetMappedSpans(span, await document.GetTextAsync(DisposalToken), generated, out var mappedLinePositionSpan, out var mappedSpan); // Assert Assert.True(result); @@ -62,15 +61,14 @@ public async Task TryGetMappedSpans_SpanMatchesSourceMappingAndPosition_ReturnsT @SomeProperty "); - var project = new ProjectSnapshot( - ProjectState.Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) - .WithAddedHostDocument(_hostDocument, () => Task.FromResult(TextAndVersion.Create(sourceText, VersionStamp.Create())))); + var project = new ProjectSnapshot(ProjectState + .Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) + .WithAddedHostDocument(_hostDocument, TestMocks.CreateTextLoader(sourceText, VersionStamp.Create()))); var document = project.GetDocument(_hostDocument.FilePath); Assert.NotNull(document); - var service = new RazorSpanMappingService(document); - var output = await document.GetGeneratedOutputAsync(); + var output = await document.GetGeneratedOutputAsync(DisposalToken); var generated = output.GetCSharpDocument(); var symbol = "SomeProperty"; @@ -78,7 +76,7 @@ public async Task TryGetMappedSpans_SpanMatchesSourceMappingAndPosition_ReturnsT var span = new TextSpan(generated.GeneratedCode.IndexOf(symbol, generated.GeneratedCode.IndexOf(symbol, StringComparison.Ordinal) + symbol.Length, StringComparison.Ordinal), symbol.Length); // Act - var result = RazorSpanMappingService.TryGetMappedSpans(span, await document.GetTextAsync(), generated, out var mappedLinePositionSpan, out var mappedSpan); + var result = RazorSpanMappingService.TryGetMappedSpans(span, await document.GetTextAsync(DisposalToken), generated, out var mappedLinePositionSpan, out var mappedSpan); // Assert Assert.True(result); @@ -96,22 +94,21 @@ public async Task TryGetMappedSpans_SpanWithinSourceMapping_ReturnsTrue() } "); - var project = new ProjectSnapshot( - ProjectState.Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) - .WithAddedHostDocument(_hostDocument, () => Task.FromResult(TextAndVersion.Create(sourceText, VersionStamp.Create())))); + var project = new ProjectSnapshot(ProjectState + .Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) + .WithAddedHostDocument(_hostDocument, TestMocks.CreateTextLoader(sourceText, VersionStamp.Create()))); var document = project.GetDocument(_hostDocument.FilePath); Assert.NotNull(document); - var service = new RazorSpanMappingService(document); - var output = await document.GetGeneratedOutputAsync(); + var output = await document.GetGeneratedOutputAsync(DisposalToken); var generated = output.GetCSharpDocument(); var symbol = "SomeProperty"; var span = new TextSpan(generated.GeneratedCode.IndexOf(symbol, StringComparison.Ordinal), symbol.Length); // Act - var result = RazorSpanMappingService.TryGetMappedSpans(span, await document.GetTextAsync(), generated, out var mappedLinePositionSpan, out var mappedSpan); + var result = RazorSpanMappingService.TryGetMappedSpans(span, await document.GetTextAsync(DisposalToken), generated, out var mappedLinePositionSpan, out var mappedSpan); // Assert Assert.True(result); @@ -129,22 +126,21 @@ public async Task TryGetMappedSpans_SpanOutsideSourceMapping_ReturnsFalse() } "); - var project = new ProjectSnapshot( - ProjectState.Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) - .WithAddedHostDocument(_hostDocument, () => Task.FromResult(TextAndVersion.Create(sourceText, VersionStamp.Create())))); + var project = new ProjectSnapshot(ProjectState + .Create(ProjectEngineFactoryProvider, _hostProject, ProjectWorkspaceState.Default) + .WithAddedHostDocument(_hostDocument, TestMocks.CreateTextLoader(sourceText, VersionStamp.Create()))); var document = project.GetDocument(_hostDocument.FilePath); Assert.NotNull(document); - var service = new RazorSpanMappingService(document); - var output = await document.GetGeneratedOutputAsync(); + var output = await document.GetGeneratedOutputAsync(DisposalToken); var generated = output.GetCSharpDocument(); var symbol = "ExecuteAsync"; var span = new TextSpan(generated.GeneratedCode.IndexOf(symbol, StringComparison.Ordinal), symbol.Length); // Act - var result = RazorSpanMappingService.TryGetMappedSpans(span, await document.GetTextAsync(), generated, out var mappedLinePositionSpan, out var mappedSpan); + var result = RazorSpanMappingService.TryGetMappedSpans(span, await document.GetTextAsync(DisposalToken), generated, out _, out _); // Assert Assert.False(result); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs index 1cf6ee0b788..083e53b2aff 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/ProjectSystem/RazorProjectInfoDriverTest.cs @@ -19,17 +19,17 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.ProjectSystem; public class RazorProjectInfoDriverTest(ITestOutputHelper testOutput) : LanguageServerTestBase(testOutput) { private static readonly HostProject s_hostProject1 = new( - projectFilePath: "C:/path/to/project1/project1.csproj", + filePath: "C:/path/to/project1/project1.csproj", intermediateOutputPath: "C:/path/to/project1/obj", - razorConfiguration: RazorConfiguration.Default, + configuration: RazorConfiguration.Default, rootNamespace: "TestNamespace"); private static readonly HostDocument s_hostDocument1 = new("C:/path/to/project1/file.razor", "file.razor"); private static readonly HostProject s_hostProject2 = new( - projectFilePath: "C:/path/to/project2/project2.csproj", + filePath: "C:/path/to/project2/project2.csproj", intermediateOutputPath: "C:/path/to/project2/obj", - razorConfiguration: RazorConfiguration.Default, + configuration: RazorConfiguration.Default, rootNamespace: "TestNamespace"); private static readonly HostDocument s_hostDocument2 = new("C:/path/to/project2/file.razor", "file.razor"); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/RazorDocumentOptionsServiceTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/RazorDocumentOptionsServiceTest.cs index 7962574d9e0..9e6c078dd94 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/RazorDocumentOptionsServiceTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/RazorDocumentOptionsServiceTest.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.ProjectSystem; +using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Formatting; @@ -95,11 +96,12 @@ private Document InitializeDocument(SourceText sourceText) var hostDocument = new HostDocument( Path.Combine(baseDirectory, "SomeProject", "File1.cshtml"), "File1.cshtml", FileKinds.Legacy); - var project = new ProjectSnapshot( - ProjectState.Create(ProjectEngineFactoryProvider, hostProject, ProjectWorkspaceState.Default) - .WithAddedHostDocument(hostDocument, () => Task.FromResult(TextAndVersion.Create(sourceText, VersionStamp.Create())))); + var project = new ProjectSnapshot(ProjectState + .Create(ProjectEngineFactoryProvider, hostProject, ProjectWorkspaceState.Default) + .WithAddedHostDocument(hostDocument, TestMocks.CreateTextLoader(sourceText, VersionStamp.Create()))); var documentSnapshot = project.GetDocument(hostDocument.FilePath); + Assert.NotNull(documentSnapshot); var solution = Workspace.CurrentSolution.AddProject(ProjectInfo.Create( ProjectId.CreateNewId(Path.GetFileNameWithoutExtension(hostDocument.FilePath)), diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/TestLSPRequestInvoker.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/TestLSPRequestInvoker.cs index 3bbf1c56ee0..c4d01769a9f 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/TestLSPRequestInvoker.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/TestLSPRequestInvoker.cs @@ -111,7 +111,8 @@ public async override Task> ReinvokeRequestOnServerAs return new ReinvocationResponse(languageClientName: RazorLSPConstants.RazorCSharpLanguageServerName, result); } - if (_htmlResponses.TryGetValue(method, out var response)) + if (_htmlResponses is not null && + _htmlResponses.TryGetValue(method, out var response)) { return new ReinvocationResponse(languageClientName: "html", (TOut)response); } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj index 266d9132319..7a7dbddfdb0 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Microsoft.VisualStudio.LanguageServices.Razor.Test.csproj @@ -30,4 +30,8 @@ + + + + diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackProjectManagerTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackProjectManagerTest.cs index 82845dea721..43c28f4e8d0 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackProjectManagerTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/FallbackProjectManagerTest.cs @@ -48,12 +48,12 @@ public FallbackProjectManagerTest(ITestOutputHelper testOutputHelper) [UIFact] public async Task DynamicFileAdded_KnownProject_DoesNothing() { - var hostProject = new HostProject( - SomeProject.FilePath, - SomeProject.IntermediateOutputPath, - RazorConfiguration.Default, - "RootNamespace", - "DisplayName"); + var hostProject = SomeProject with + { + Configuration = RazorConfiguration.Default, + RootNamespace = "RootNamespace", + DisplayName = "DisplayName" + }; await _projectManager.UpdateAsync(updater => { @@ -134,12 +134,12 @@ public async Task DynamicFileAdded_UnknownToKnownProject_NotFallbackHostProject( var project = Assert.Single(_projectManager.GetProjects()); Assert.IsType(((ProjectSnapshot)project).HostProject); - var hostProject = new HostProject( - SomeProject.FilePath, - SomeProject.IntermediateOutputPath, - RazorConfiguration.Default, - "RootNamespace", - "DisplayName"); + var hostProject = SomeProject with + { + Configuration = RazorConfiguration.Default, + RootNamespace = "RootNamespace", + DisplayName = "DisplayName" + }; await _projectManager.UpdateAsync(updater => { diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/ProjectSnapshotManagerTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/ProjectSnapshotManagerTest.cs index 62b66ef903e..6f127b659dc 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/ProjectSnapshotManagerTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/ProjectSystem/ProjectSnapshotManagerTest.cs @@ -36,17 +36,15 @@ public class ProjectSnapshotManagerTest : VisualStudioWorkspaceTestBase TestProjectData.SomeProjectComponentFile2, ]; - private static readonly HostProject s_hostProject = new( - TestProjectData.SomeProject.FilePath, - TestProjectData.SomeProject.IntermediateOutputPath, - FallbackRazorConfiguration.MVC_2_0, - TestProjectData.SomeProject.RootNamespace); - - private static readonly HostProject s_hostProjectWithConfigurationChange = new( - TestProjectData.SomeProject.FilePath, - TestProjectData.SomeProject.IntermediateOutputPath, - FallbackRazorConfiguration.MVC_1_0, - TestProjectData.SomeProject.RootNamespace); + private static readonly HostProject s_hostProject = TestProjectData.SomeProject with + { + Configuration = FallbackRazorConfiguration.MVC_2_0 + }; + + private static readonly HostProject s_hostProjectWithConfigurationChange = TestProjectData.SomeProject with + { + Configuration = FallbackRazorConfiguration.MVC_1_0 + }; private readonly ProjectWorkspaceState _projectWorkspaceStateWithTagHelpers; private readonly TestProjectSnapshotManager _projectManager; @@ -99,7 +97,7 @@ await _projectManager.UpdateAsync(updater => // Act await _projectManager.UpdateAsync(updater => { - updater.DocumentAdded(s_hostProject.Key, s_documents[0], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); }); // Assert @@ -125,7 +123,7 @@ await _projectManager.UpdateAsync(updater => // Act await _projectManager.UpdateAsync(updater => { - updater.DocumentAdded(s_hostProject.Key, s_documents[0], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); }); // Assert @@ -153,7 +151,7 @@ await _projectManager.UpdateAsync(updater => // Act await _projectManager.UpdateAsync(updater => { - updater.DocumentAdded(s_hostProject.Key, s_documents[3], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[3], s_documents[3].CreateEmptyTextLoader()); }); // Assert @@ -174,7 +172,7 @@ public async Task DocumentAdded_IgnoresDuplicate() await _projectManager.UpdateAsync(updater => { updater.ProjectAdded(s_hostProject); - updater.DocumentAdded(s_hostProject.Key, s_documents[0], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); }); using var listener = _projectManager.ListenToNotifications(); @@ -182,7 +180,7 @@ await _projectManager.UpdateAsync(updater => // Act await _projectManager.UpdateAsync(updater => { - updater.DocumentAdded(s_hostProject.Key, s_documents[0], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); }); // Assert @@ -212,7 +210,7 @@ await _projectManager.UpdateAsync(updater => } [UIFact] - public async Task DocumentAdded_NullLoader_HasEmptyText() + public async Task DocumentAdded_EmptyLoader_HasEmptyText() { // Arrange await _projectManager.UpdateAsync(updater => @@ -223,7 +221,7 @@ await _projectManager.UpdateAsync(updater => // Act await _projectManager.UpdateAsync(updater => { - updater.DocumentAdded(s_hostProject.Key, s_documents[0], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); }); // Assert @@ -232,12 +230,12 @@ await _projectManager.UpdateAsync(updater => var document = project.GetDocument(documentFilePath); Assert.NotNull(document); - var text = await document.GetTextAsync(); + var text = await document.GetTextAsync(DisposalToken); Assert.Equal(0, text.Length); } [UIFact] - public async Task DocumentAdded_WithLoader_LoadesText() + public async Task DocumentAdded_WithLoader_LoadsText() { // Arrange await _projectManager.UpdateAsync(updater => @@ -259,7 +257,7 @@ await _projectManager.UpdateAsync(updater => var document = project.GetDocument(documentFilePath); Assert.NotNull(document); - var actual = await document.GetTextAsync(); + var actual = await document.GetTextAsync(DisposalToken); Assert.Same(expected, actual); } @@ -278,7 +276,7 @@ await _projectManager.UpdateAsync(updater => // Act await _projectManager.UpdateAsync(updater => { - updater.DocumentAdded(s_hostProject.Key, s_documents[0], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); }); // Assert @@ -306,7 +304,7 @@ await _projectManager.UpdateAsync(updater => // Act await _projectManager.UpdateAsync(updater => { - updater.DocumentAdded(s_hostProject.Key, s_documents[0], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); }); // Assert @@ -321,9 +319,9 @@ public async Task DocumentRemoved_RemovesDocument() await _projectManager.UpdateAsync(updater => { updater.ProjectAdded(s_hostProject); - updater.DocumentAdded(s_hostProject.Key, s_documents[0], null!); - updater.DocumentAdded(s_hostProject.Key, s_documents[1], null!); - updater.DocumentAdded(s_hostProject.Key, s_documents[2], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); + updater.DocumentAdded(s_hostProject.Key, s_documents[1], s_documents[1].CreateEmptyTextLoader()); + updater.DocumentAdded(s_hostProject.Key, s_documents[2], s_documents[2].CreateEmptyTextLoader()); }); using var listener = _projectManager.ListenToNotifications(); @@ -393,9 +391,9 @@ await _projectManager.UpdateAsync(updater => { updater.ProjectAdded(s_hostProject); updater.ProjectWorkspaceStateChanged(s_hostProject.Key, _projectWorkspaceStateWithTagHelpers); - updater.DocumentAdded(s_hostProject.Key, s_documents[0], null!); - updater.DocumentAdded(s_hostProject.Key, s_documents[1], null!); - updater.DocumentAdded(s_hostProject.Key, s_documents[2], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); + updater.DocumentAdded(s_hostProject.Key, s_documents[1], s_documents[1].CreateEmptyTextLoader()); + updater.DocumentAdded(s_hostProject.Key, s_documents[2], s_documents[2].CreateEmptyTextLoader()); }); var originalTagHelpers = await _projectManager.GetLoadedProject(s_hostProject.Key).GetTagHelpersAsync(DisposalToken); @@ -423,9 +421,9 @@ public async Task DocumentRemoved_CachesProjectEngine() await _projectManager.UpdateAsync(updater => { updater.ProjectAdded(s_hostProject); - updater.DocumentAdded(s_hostProject.Key, s_documents[0], null!); - updater.DocumentAdded(s_hostProject.Key, s_documents[1], null!); - updater.DocumentAdded(s_hostProject.Key, s_documents[2], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); + updater.DocumentAdded(s_hostProject.Key, s_documents[1], s_documents[1].CreateEmptyTextLoader()); + updater.DocumentAdded(s_hostProject.Key, s_documents[2], s_documents[2].CreateEmptyTextLoader()); }); var project = _projectManager.GetLoadedProject(s_hostProject.Key); @@ -449,7 +447,7 @@ public async Task DocumentOpened_UpdatesDocument() await _projectManager.UpdateAsync(updater => { updater.ProjectAdded(s_hostProject); - updater.DocumentAdded(s_hostProject.Key, s_documents[0], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); }); using var listener = _projectManager.ListenToNotifications(); @@ -467,7 +465,7 @@ await _projectManager.UpdateAsync(updater => var project = _projectManager.GetLoadedProject(s_hostProject.Key); var document = project.GetDocument(s_documents[0].FilePath); Assert.NotNull(document); - var text = await document.GetTextAsync(); + var text = await document.GetTextAsync(DisposalToken); Assert.Same(_sourceText, text); Assert.True(_projectManager.IsDocumentOpen(s_documents[0].FilePath)); @@ -482,7 +480,7 @@ public async Task DocumentClosed_UpdatesDocument() await _projectManager.UpdateAsync(updater => { updater.ProjectAdded(s_hostProject); - updater.DocumentAdded(s_hostProject.Key, s_documents[0], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); updater.DocumentOpened(s_hostProject.Key, s_documents[0].FilePath, _sourceText); }); @@ -506,7 +504,7 @@ await _projectManager.UpdateAsync(updater => var project = _projectManager.GetLoadedProject(s_hostProject.Key); var document = project.GetDocument(s_documents[0].FilePath); Assert.NotNull(document); - var text = await document.GetTextAsync(); + var text = await document.GetTextAsync(DisposalToken); Assert.Same(expected, text); Assert.False(_projectManager.IsDocumentOpen(s_documents[0].FilePath)); Assert.Equal(3, document.Version); @@ -519,7 +517,7 @@ public async Task DocumentClosed_AcceptsChange() await _projectManager.UpdateAsync(updater => { updater.ProjectAdded(s_hostProject); - updater.DocumentAdded(s_hostProject.Key, s_documents[0], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); }); using var listener = _projectManager.ListenToNotifications(); @@ -540,7 +538,7 @@ await _projectManager.UpdateAsync(updater => var project = _projectManager.GetLoadedProject(s_hostProject.Key); var document = project.GetDocument(s_documents[0].FilePath); Assert.NotNull(document); - var text = await document.GetTextAsync(); + var text = await document.GetTextAsync(DisposalToken); Assert.Same(expected, text); } @@ -551,7 +549,7 @@ public async Task DocumentChanged_Snapshot_UpdatesDocument() await _projectManager.UpdateAsync(updater => { updater.ProjectAdded(s_hostProject); - updater.DocumentAdded(s_hostProject.Key, s_documents[0], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); updater.DocumentOpened(s_hostProject.Key, s_documents[0].FilePath, _sourceText); }); @@ -572,7 +570,7 @@ await _projectManager.UpdateAsync(updater => var project = _projectManager.GetLoadedProject(s_hostProject.Key); var document = project.GetDocument(s_documents[0].FilePath); Assert.NotNull(document); - var text = await document.GetTextAsync(); + var text = await document.GetTextAsync(DisposalToken); Assert.Same(expected, text); Assert.Equal(3, document.Version); } @@ -584,7 +582,7 @@ public async Task DocumentChanged_Loader_UpdatesDocument() await _projectManager.UpdateAsync(updater => { updater.ProjectAdded(s_hostProject); - updater.DocumentAdded(s_hostProject.Key, s_documents[0], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); updater.DocumentOpened(s_hostProject.Key, s_documents[0].FilePath, _sourceText); }); @@ -606,7 +604,7 @@ await _projectManager.UpdateAsync(updater => var project = _projectManager.GetLoadedProject(s_hostProject.Key); var document = project.GetDocument(s_documents[0].FilePath); Assert.NotNull(document); - var text = await document.GetTextAsync(); + var text = await document.GetTextAsync(DisposalToken); Assert.Same(expected, text); Assert.Equal(3, document.Version); } @@ -763,7 +761,7 @@ public async Task ProjectWorkspaceStateChanged_UpdateDocuments() await _projectManager.UpdateAsync(updater => { updater.ProjectAdded(s_hostProject); - updater.DocumentAdded(s_hostProject.Key, s_documents[0], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); }); // Act @@ -856,7 +854,7 @@ await _projectManager.UpdateAsync(updater => // Act await _projectManager.UpdateAsync(updater => { - updater.DocumentAdded(s_hostProject.Key, s_documents[0], null!); + updater.DocumentAdded(s_hostProject.Key, s_documents[0], s_documents[0].CreateEmptyTextLoader()); }); // Assert diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Remote/OutOfProcTagHelperResolverTest.TestResolver.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Remote/OutOfProcTagHelperResolverTest.TestResolver.cs index b4c20289aa6..2deede00dca 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Remote/OutOfProcTagHelperResolverTest.TestResolver.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Remote/OutOfProcTagHelperResolverTest.TestResolver.cs @@ -14,8 +14,6 @@ using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Remote; -#pragma warning disable VSTHRD110 // Observe result of async calls - namespace Microsoft.VisualStudio.Razor.Remote; public partial class OutOfProcTagHelperResolverTest @@ -30,22 +28,13 @@ private class TestResolver( public Func>>? OnResolveInProcess { get; init; } - protected override ValueTask> ResolveTagHelpersOutOfProcessAsync(Project workspaceProject, IProjectSnapshot projectSnapshot, CancellationToken cancellationToken) - { - return OnResolveOutOfProcess?.Invoke(projectSnapshot) - ?? new ValueTask>(default(ImmutableArray)); - } + protected override ValueTask> ResolveTagHelpersOutOfProcessAsync(Project project, IProjectSnapshot projectSnapshot, CancellationToken cancellationToken) + => OnResolveOutOfProcess?.Invoke(projectSnapshot) ?? default; protected override ValueTask> ResolveTagHelpersInProcessAsync(Project project, IProjectSnapshot projectSnapshot, CancellationToken cancellationToken) - { - return OnResolveInProcess?.Invoke(projectSnapshot) - ?? new ValueTask>(default(ImmutableArray)); - } + => OnResolveInProcess?.Invoke(projectSnapshot) ?? default; public ImmutableArray PublicProduceChecksumsFromDelta(ProjectId projectId, int lastResultId, TagHelperDeltaResult deltaResult) => ProduceChecksumsFromDelta(projectId, lastResultId, deltaResult); - - protected override ImmutableArray ProduceChecksumsFromDelta(ProjectId projectId, int lastResultId, TagHelperDeltaResult deltaResult) - => base.ProduceChecksumsFromDelta(projectId, lastResultId, deltaResult); } } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Remote/OutOfProcTagHelperResolverTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Remote/OutOfProcTagHelperResolverTest.cs index f05af0e1399..929f4541dd1 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Remote/OutOfProcTagHelperResolverTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Remote/OutOfProcTagHelperResolverTest.cs @@ -27,14 +27,14 @@ namespace Microsoft.VisualStudio.Razor.Remote; public partial class OutOfProcTagHelperResolverTest : VisualStudioTestBase { private static readonly HostProject s_hostProject_For_2_0 = new( - projectFilePath: "Test.csproj", + filePath: "Test.csproj", intermediateOutputPath: "/obj", - razorConfiguration: FallbackRazorConfiguration.MVC_2_0, + configuration: FallbackRazorConfiguration.MVC_2_0, rootNamespace: null); private static readonly HostProject s_hostProject_For_NonSerializableConfiguration = new( - projectFilePath: "Test.csproj", + filePath: "Test.csproj", intermediateOutputPath: "/obj", - razorConfiguration: new(RazorLanguageVersion.Version_2_1, "Random-0.1", Extensions: []), + configuration: new(RazorLanguageVersion.Version_2_1, "Random-0.1", Extensions: []), rootNamespace: null); private readonly Project _workspaceProject; diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Settings/ClientSettingsManagerTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Settings/ClientSettingsManagerTest.cs index 498fb92da12..0edfde8f751 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Settings/ClientSettingsManagerTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Settings/ClientSettingsManagerTest.cs @@ -82,7 +82,7 @@ public void Update_TriggersChangedIfAdvancedSettingsAreDifferent() var manager = new ClientSettingsManager(_clientSettingsChangeTriggers); var called = false; manager.ClientSettingsChanged += (caller, args) => called = true; - var settings = new ClientAdvancedSettings(FormatOnType: false, AutoClosingTags: true, AutoInsertAttributeQuotes: true, ColorBackground: true, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: false, SnippetSetting: default, LogLevel: default); + var settings = new ClientAdvancedSettings(FormatOnType: false, AutoClosingTags: true, AutoInsertAttributeQuotes: true, ColorBackground: true, CodeBlockBraceOnNextLine: false, CommitElementsWithSpace: false, SnippetSetting: default, LogLevel: default, FormatOnPaste: false); // Act manager.Update(settings); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Telemetry/TelemetryReporterTests.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Telemetry/TelemetryReporterTests.cs index 50fa4623eba..2eeab073a0e 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Telemetry/TelemetryReporterTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Telemetry/TelemetryReporterTests.cs @@ -2,11 +2,16 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Telemetry; using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.LanguageServer; using Microsoft.VisualStudio.Editor.Razor.Test.Shared; +using Microsoft.VisualStudio.LanguageServer.Protocol; using Microsoft.VisualStudio.Telemetry; +using Microsoft.VisualStudio.Telemetry.Metrics; using StreamJsonRpc; using Xunit; using Xunit.Abstractions; @@ -18,7 +23,7 @@ public class TelemetryReporterTests(ITestOutputHelper testOutput) : ToolingTestB [Fact] public void NoArgument() { - var reporter = new TestTelemetryReporter(LoggerFactory); + using var reporter = new TestTelemetryReporter(LoggerFactory); reporter.ReportEvent("EventName", Severity.Normal); Assert.Collection(reporter.Events, e1 => @@ -32,7 +37,7 @@ public void NoArgument() [Fact] public void OneArgument() { - var reporter = new TestTelemetryReporter(LoggerFactory); + using var reporter = new TestTelemetryReporter(LoggerFactory); reporter.ReportEvent("EventName", Severity.Normal, new Property("P1", false)); Assert.Collection(reporter.Events, e1 => @@ -49,7 +54,7 @@ public void OneArgument() [Fact] public void TwoArguments() { - var reporter = new TestTelemetryReporter(LoggerFactory); + using var reporter = new TestTelemetryReporter(LoggerFactory); reporter.ReportEvent("EventName", Severity.Normal, new("P1", false), new("P2", "test")); Assert.Collection(reporter.Events, e1 => @@ -66,7 +71,7 @@ public void TwoArguments() [Fact] public void ThreeArguments() { - var reporter = new TestTelemetryReporter(LoggerFactory); + using var reporter = new TestTelemetryReporter(LoggerFactory); var p3Value = Guid.NewGuid(); reporter.ReportEvent("EventName", Severity.Normal, @@ -93,7 +98,7 @@ public void ThreeArguments() [Fact] public void FourArguments() { - var reporter = new TestTelemetryReporter(LoggerFactory); + using var reporter = new TestTelemetryReporter(LoggerFactory); var p3Value = Guid.NewGuid(); reporter.ReportEvent("EventName", Severity.Normal, @@ -123,7 +128,7 @@ public void FourArguments() [Fact] public void Block_NoArguments() { - var reporter = new TestTelemetryReporter(LoggerFactory); + using var reporter = new TestTelemetryReporter(LoggerFactory); using (var scope = reporter.BeginBlock("EventName", Severity.Normal)) { } @@ -142,7 +147,7 @@ public void Block_NoArguments() [Fact] public void Block_OneArgument() { - var reporter = new TestTelemetryReporter(LoggerFactory); + using var reporter = new TestTelemetryReporter(LoggerFactory); using (reporter.BeginBlock("EventName", Severity.Normal, new Property("P1", false))) { } @@ -162,7 +167,7 @@ public void Block_OneArgument() [Fact] public void Block_TwoArguments() { - var reporter = new TestTelemetryReporter(LoggerFactory); + using var reporter = new TestTelemetryReporter(LoggerFactory); using (reporter.BeginBlock("EventName", Severity.Normal, new("P1", false), new("P2", "test"))) { } @@ -183,7 +188,7 @@ public void Block_TwoArguments() [Fact] public void Block_ThreeArguments() { - var reporter = new TestTelemetryReporter(LoggerFactory); + using var reporter = new TestTelemetryReporter(LoggerFactory); var p3Value = Guid.NewGuid(); using (reporter.BeginBlock("EventName", Severity.Normal, @@ -213,7 +218,7 @@ public void Block_ThreeArguments() [Fact] public void Block_FourArguments() { - var reporter = new TestTelemetryReporter(LoggerFactory); + using var reporter = new TestTelemetryReporter(LoggerFactory); var p3Value = Guid.NewGuid(); using (reporter.BeginBlock("EventName", Severity.Normal, @@ -246,7 +251,7 @@ public void Block_FourArguments() [Fact] public void HandleRIEWithInnerException() { - var reporter = new TestTelemetryReporter(LoggerFactory); + using var reporter = new TestTelemetryReporter(LoggerFactory); var ae = new ApplicationException("expectedText"); var rie = new RemoteInvocationException("a", 0, ae); @@ -268,7 +273,7 @@ public void HandleRIEWithInnerException() [Fact] public void HandleRIEWithNoInnerException() { - var reporter = new TestTelemetryReporter(LoggerFactory); + using var reporter = new TestTelemetryReporter(LoggerFactory); var rie = new RemoteInvocationException("a", 0, errorData: null); @@ -289,7 +294,7 @@ public void HandleRIEWithNoInnerException() [Fact] public void TrackLspRequest() { - var reporter = new TestTelemetryReporter(LoggerFactory); + using var reporter = new TestTelemetryReporter(LoggerFactory); var correlationId = Guid.NewGuid(); using (reporter.TrackLspRequest("MethodName", "ServerName", correlationId)) { @@ -329,7 +334,7 @@ public void TrackLspRequest() public void ReportFault_OperationCanceledExceptionWithoutInnerException_SkipsFaultReport() { // Arrange - var reporter = new TestTelemetryReporter(LoggerFactory); + using var reporter = new TestTelemetryReporter(LoggerFactory); var exception = new OperationCanceledException("OCE", innerException: null); // Act @@ -343,7 +348,7 @@ public void ReportFault_OperationCanceledExceptionWithoutInnerException_SkipsFau public void ReportFault_TaskCanceledExceptionWithoutInnerException_SkipsFaultReport() { // Arrange - var reporter = new TestTelemetryReporter(LoggerFactory); + using var reporter = new TestTelemetryReporter(LoggerFactory); var exception = new TaskCanceledException("TCE", innerException: null); // Act @@ -358,7 +363,7 @@ public void ReportFault_InnerExceptionOfOCEIsNotAnOCE_ReportsFault() { // Arrange var depth = 3; - var reporter = new TestTelemetryReporter(LoggerFactory); + using var reporter = new TestTelemetryReporter(LoggerFactory); var innerMostException = new Exception(); var exception = new OperationCanceledException("Test", innerMostException); for (var i = 0; i < depth; i++) @@ -378,7 +383,7 @@ public void ReportFault_InnerMostExceptionIsOperationCanceledException_SkipsFaul { // Arrange var depth = 3; - var reporter = new TestTelemetryReporter(LoggerFactory); + using var reporter = new TestTelemetryReporter(LoggerFactory); var innerMostException = new OperationCanceledException(); var exception = new OperationCanceledException("Test", innerMostException); for (var i = 0; i < depth; i++) @@ -392,4 +397,122 @@ public void ReportFault_InnerMostExceptionIsOperationCanceledException_SkipsFaul // Assert Assert.Empty(reporter.Events); } + + [Fact] + public void ReportHistogram() + { + // Arrange + var reporter = new TestTelemetryReporter(LoggerFactory); + + // Act + reporter.ReportRequestTiming( + Methods.TextDocumentCodeActionName, + WellKnownLspServerKinds.RazorLspServer.GetContractName(), + TimeSpan.FromMilliseconds(100), + TimeSpan.FromMilliseconds(100), + AspNetCore.Razor.Telemetry.TelemetryResult.Succeeded); + + reporter.ReportRequestTiming( + Methods.TextDocumentCodeActionName, + WellKnownLspServerKinds.RazorLspServer.GetContractName(), + TimeSpan.FromMilliseconds(200), + TimeSpan.FromMilliseconds(200), + AspNetCore.Razor.Telemetry.TelemetryResult.Cancelled); + + reporter.ReportRequestTiming( + Methods.TextDocumentCodeActionName, + WellKnownLspServerKinds.RazorLspServer.GetContractName(), + TimeSpan.FromMilliseconds(300), + TimeSpan.FromMilliseconds(300), + AspNetCore.Razor.Telemetry.TelemetryResult.Failed); + + reporter.Dispose(); + + // Assert + reporter.AssertMetrics( + static evt => + { + var histogram = Assert.IsAssignableFrom>(evt.Instrument); + Assert.Equal("TimeInQueue", histogram.Name); + + var telemetryEvent = evt.Event; + Assert.Equal("dotnet/razor/lsp_timeinqueue", telemetryEvent.Name); + Assert.Collection(telemetryEvent.Properties, + static prop => + { + Assert.Equal("dotnet.razor.method", prop.Key); + Assert.Equal(Methods.TextDocumentCodeActionName, prop.Value); + }); + }, + static evt => + { + var histogram = Assert.IsAssignableFrom>(evt.Instrument); + Assert.Equal("RequestDuration", histogram.Name); + + var telemetryEvent = evt.Event; + Assert.Equal("dotnet/razor/lsp_requestduration", telemetryEvent.Name); + Assert.Collection(telemetryEvent.Properties, + static prop => + { + Assert.Equal("dotnet.razor.method", prop.Key); + Assert.Equal(Methods.TextDocumentCodeActionName, prop.Value); + }); + }); + + Assert.Collection(reporter.Events, + static evt => + { + Assert.Equal("dotnet/razor/lsp_requestcounter", evt.Name); + Assert.Collection(evt.Properties, + static prop => + { + Assert.Equal("dotnet.razor.method", prop.Key); + Assert.Equal(Methods.TextDocumentCodeActionName, prop.Value); + }, + static prop => + { + Assert.Equal("dotnet.razor.successful", prop.Key); + Assert.Equal(1, prop.Value); + }, + static prop => + { + Assert.Equal("dotnet.razor.failed", prop.Key); + Assert.Equal(1, prop.Value); + }, + static prop => + { + Assert.Equal("dotnet.razor.cancelled", prop.Key); + Assert.Equal(1, prop.Value); + }); + }); + } + + [Theory, MemberData(nameof(s_throwFunctions))] + public void GetModifiedFaultParameters_FiltersCorrectly(Func throwAction) + { + try + { + throwAction(); + } + catch (Exception ex) + { + var (param1, param2) = TestTelemetryReporter.GetModifiedFaultParameters(ex); + + Assert.Equal("Microsoft.VisualStudio.LanguageServices.Razor.Test", param1); + Assert.NotNull(param2); + + // Depending on debug/release the stack can contain a constructor or + // a call to this method. We expect one or the other and both + // are valid +#if DEBUG + Assert.StartsWith("<.cctor>", param2); +#else + Assert.Equal("GetModifiedFaultParameters_FiltersCorrectly", param2); +#endif + } + } + + public static readonly IEnumerable s_throwFunctions = [ + [() => ((object?)null).AssumeNotNull()] + ]; } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/SemanticTokens/Legacy.txt b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/SemanticTokens/Legacy.txt index 99325b3275a..f1f0cdb3e74 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/SemanticTokens/Legacy.txt +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/SemanticTokens/Legacy.txt @@ -3,6 +3,11 @@ Line Δ, Char Δ, Length, Type, Modifier(s), Text 0 1 4 razorDirective [] [page] 0 5 3 string [] ["/"] 1 0 1 razorTransition [] [@] +0 1 5 razorDirective [] [model] +0 6 8 variable [] [AppThing] +0 8 1 operator [] [.] +0 1 5 variable [] [Model] +1 0 1 razorTransition [] [@] 0 1 5 keyword [] [using] 0 6 6 namespace name [] [System] 2 0 1 markupTagDelimiter [] [<] diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/SemanticTokens/Legacy_with_background.txt b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/SemanticTokens/Legacy_with_background.txt index c2a10eb327f..77227b0c3b6 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/SemanticTokens/Legacy_with_background.txt +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/TestFiles/SemanticTokens/Legacy_with_background.txt @@ -3,6 +3,11 @@ Line Δ, Char Δ, Length, Type, Modifier(s), Text 0 1 4 razorDirective [] [page] 0 5 3 string [razorCode] ["/"] 1 0 1 razorTransition [] [@] +0 1 5 razorDirective [] [model] +0 6 8 variable [razorCode] [AppThing] +0 8 1 operator [razorCode] [.] +0 1 5 variable [razorCode] [Model] +1 0 1 razorTransition [] [@] 0 1 5 keyword [razorCode] [using] 0 5 1 markupTextLiteral [razorCode] [ ] 0 1 6 namespace name [razorCode] [System] diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs index 7a65324419e..b9158404c92 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/VsSolutionUpdatesProjectSnapshotChangeTriggerTest.cs @@ -22,16 +22,15 @@ namespace Microsoft.VisualStudio.Razor; public class VsSolutionUpdatesProjectSnapshotChangeTriggerTest : VisualStudioTestBase { - private static readonly HostProject s_someProject = new( - TestProjectData.SomeProject.FilePath, - TestProjectData.SomeProject.IntermediateOutputPath, - FallbackRazorConfiguration.MVC_1_0, - TestProjectData.SomeProject.RootNamespace); - private static readonly HostProject s_someOtherProject = new( - TestProjectData.AnotherProject.FilePath, - TestProjectData.AnotherProject.IntermediateOutputPath, - FallbackRazorConfiguration.MVC_2_0, - TestProjectData.AnotherProject.RootNamespace); + private static readonly HostProject s_someProject = TestProjectData.SomeProject with + { + Configuration = FallbackRazorConfiguration.MVC_1_0 + }; + + private static readonly HostProject s_someOtherProject = TestProjectData.AnotherProject with + { + Configuration = FallbackRazorConfiguration.MVC_2_0 + }; private readonly Project _someWorkspaceProject; private readonly IWorkspaceProvider _workspaceProvider; diff --git a/src/Razor/test/Microsoft.VisualStudio.LegacyEditor.Razor.Test/VisualStudioDocumentTrackerTest.cs b/src/Razor/test/Microsoft.VisualStudio.LegacyEditor.Razor.Test/VisualStudioDocumentTrackerTest.cs index ea936cb99b2..adf75ef0fae 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LegacyEditor.Razor.Test/VisualStudioDocumentTrackerTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LegacyEditor.Razor.Test/VisualStudioDocumentTrackerTest.cs @@ -39,8 +39,6 @@ public VisualStudioDocumentTrackerTest(ITestOutputHelper testOutput) VsMocks.ContentTypes.Create(RazorConstants.LegacyCoreContentType, RazorLanguage.CoreContentType)); _filePath = TestProjectData.SomeProjectFile1.FilePath; - var projectPath = TestProjectData.SomeProject.FilePath; - var rootNamespace = TestProjectData.SomeProject.RootNamespace; var importDocumentManagerMock = StrictMock.Of(); Mock.Get(importDocumentManagerMock) @@ -54,14 +52,14 @@ public VisualStudioDocumentTrackerTest(ITestOutputHelper testOutput) _projectManager = CreateProjectSnapshotManager(); - _hostProject = new HostProject(projectPath, TestProjectData.SomeProject.IntermediateOutputPath, FallbackRazorConfiguration.MVC_2_1, rootNamespace); - _updatedHostProject = new HostProject(projectPath, TestProjectData.SomeProject.IntermediateOutputPath, FallbackRazorConfiguration.MVC_2_0, rootNamespace); - _otherHostProject = new HostProject(TestProjectData.AnotherProject.FilePath, TestProjectData.AnotherProject.IntermediateOutputPath, FallbackRazorConfiguration.MVC_2_0, TestProjectData.AnotherProject.RootNamespace); + _hostProject = TestProjectData.SomeProject with { Configuration = FallbackRazorConfiguration.MVC_2_1 }; + _updatedHostProject = TestProjectData.SomeProject with { Configuration = FallbackRazorConfiguration.MVC_2_0 }; + _otherHostProject = TestProjectData.AnotherProject with { Configuration = FallbackRazorConfiguration.MVC_2_0 }; _documentTracker = new VisualStudioDocumentTracker( JoinableTaskFactory.Context, _filePath, - projectPath, + _hostProject.FilePath, _projectManager, workspaceEditorSettings, ProjectEngineFactoryProvider, diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CSharpCodeActionsTests.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CSharpCodeActionsTests.cs index b29cc243aa1..950cc32f258 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CSharpCodeActionsTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CSharpCodeActionsTests.cs @@ -1,9 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Roslyn.Test.Utilities; +using Microsoft.VisualStudio.Language.Intellisense; using Xunit; using Xunit.Abstractions; @@ -23,8 +24,7 @@ public async Task CSharpCodeActionsTests_MakeExpressionBodiedMethod() var codeActions = await TestServices.Editor.InvokeCodeActionListAsync(ControlledHangMitigatingCancellationToken); // Assert - var codeActionSet = Assert.Single(codeActions); - var codeAction = Assert.Single(codeActionSet.Actions, a => a.DisplayText.Equals("Use expression body for method")); + var codeAction = VerifyAndGetFirst(codeActions, "Use expression body for method"); await TestServices.Editor.InvokeCodeActionAsync(codeAction, ControlledHangMitigatingCancellationToken); @@ -50,11 +50,9 @@ await TestServices.Editor.SetTextAsync(""" var codeActions = await TestServices.Editor.InvokeCodeActionListAsync(ControlledHangMitigatingCancellationToken); // Assert - Assert.Collection(codeActions, - a => AssertEx.EqualOrDiff("@using System.Data", a.Actions.Single().DisplayText), - a => AssertEx.EqualOrDiff("System.Data.ConflictOption", a.Actions.Single().DisplayText)); - - var codeAction = codeActions.ElementAt(1).Actions.First(); + var codeAction = VerifyAndGetFirst(codeActions, + "System.Data.ConflictOption", + "@using System.Data"); await TestServices.Editor.InvokeCodeActionAsync(codeAction, ControlledHangMitigatingCancellationToken); @@ -80,11 +78,9 @@ await TestServices.Editor.SetTextAsync(""" var codeActions = await TestServices.Editor.InvokeCodeActionListAsync(ControlledHangMitigatingCancellationToken); // Assert - Assert.Collection(codeActions, - a => AssertEx.EqualOrDiff("@using System.Data", a.Actions.Single().DisplayText), - a => AssertEx.EqualOrDiff("System.Data.ConflictOption", a.Actions.Single().DisplayText)); - - var codeAction = codeActions.First().Actions.First(); + var codeAction = VerifyAndGetFirst(codeActions, + "@using System.Data", + "System.Data.ConflictOption"); await TestServices.Editor.InvokeCodeActionAsync(codeAction, ControlledHangMitigatingCancellationToken); @@ -117,10 +113,8 @@ await TestServices.Editor.SetTextAsync(""" var codeActions = await TestServices.Editor.InvokeCodeActionListAsync(ControlledHangMitigatingCancellationToken); // Assert - Assert.Collection(codeActions, - a => Assert.Equal("ConflictOption - @using System.Data", a.Actions.Single().DisplayText)); - - var codeAction = codeActions.First().Actions.First(); + var codeAction = VerifyAndGetFirst(codeActions, + "ConflictOption - @using System.Data"); await TestServices.Editor.InvokeCodeActionAsync(codeAction, ControlledHangMitigatingCancellationToken); @@ -160,10 +154,7 @@ void M(string[] args) var codeActions = await TestServices.Editor.InvokeCodeActionListAsync(ControlledHangMitigatingCancellationToken); // Assert - var introduceLocal = codeActions.FirstOrDefault(a => a.Actions.Single().DisplayText.Equals("Introduce local")); - Assert.NotNull(introduceLocal); - - var codeAction = introduceLocal.Actions.First(); + var codeAction = VerifyAndGetFirst(codeActions, "Introduce local"); Assert.True(codeAction.HasActionSets); @@ -214,10 +205,7 @@ void M(string[] args) var codeActions = await TestServices.Editor.InvokeCodeActionListAsync(ControlledHangMitigatingCancellationToken); // Assert - var introduceLocal = codeActions.FirstOrDefault(a => a.Actions.Single().DisplayText.Equals("Introduce local")); - Assert.NotNull(introduceLocal); - - var codeAction = introduceLocal.Actions.First(); + var codeAction = VerifyAndGetFirst(codeActions, "Introduce local"); Assert.True(codeAction.HasActionSets); @@ -241,4 +229,14 @@ void M(string[] args) } """, ControlledHangMitigatingCancellationToken); } + + private ISuggestedAction VerifyAndGetFirst(IEnumerable codeActions, params string[] expected) + { + foreach (var title in expected) + { + Assert.Contains(codeActions, a => a.Actions.Single().DisplayText == title); + } + + return codeActions.First(a => a.Actions.Single().DisplayText == expected[0]).Actions.Single(); + } } diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CodeFoldingTests.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CodeFoldingTests.cs index 993f4ee213e..69aff9a6bbb 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CodeFoldingTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CodeFoldingTests.cs @@ -178,7 +178,7 @@ private void IncrementCount() """); } - [IdeFact] + [IdeFact(Skip = "https://github.com/dotnet/razor/issues/10860")] // FUSE changes whitespace on folding ranges public async Task CodeFolding_IfBlock() { await TestServices.SolutionExplorer.AddFileAsync( diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CompletionIntegrationTests.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CompletionIntegrationTests.cs index 8c8c8a12945..d6c92cee242 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CompletionIntegrationTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/CompletionIntegrationTests.cs @@ -2,8 +2,10 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using Microsoft.VisualStudio.Extensibility.Testing; using Roslyn.Test.Utilities; using Xunit; using Xunit.Abstractions; @@ -17,55 +19,272 @@ public class CompletionIntegrationTests(ITestOutputHelper testOutputHelper) : Ab [IdeFact] public async Task SnippetCompletion_Html() { - await TestServices.SolutionExplorer.AddFileAsync( - RazorProjectConstants.BlazorProjectName, - "Test.razor", - """ -@page "Test" + await VerifyTypeAndCommitCompletionAsync( + input: """ + @page "Test" -Test + Test + +

Test

+ + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + output: """ + @page "Test" -

Test

+ Test -@code { - private int currentCount = 0; +

Test

+
+
+
+
- private void IncrementCount() + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + search: "

Test

", + stringsToType: ["{ENTER}", "d", "d"]); + } + + [IdeFact, WorkItem("https://github.com/dotnet/razor/issues/10787")] + public async Task CompletionCommit_HtmlAttributeWithoutValue() { - currentCount++; + await VerifyTypeAndCommitCompletionAsync( + input: """ + @page "Test" + + Test + + + + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + output: """ + @page "Test" + + Test + + + + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + search: "Test", charsOffset: 1, ControlledHangMitigatingCancellationToken); - TestServices.Input.Send("{ENTER}"); - TestServices.Input.Send("d"); - TestServices.Input.Send("d"); + Test - await CommitCompletionAndVerifyAsync(""" -@page "Test" + + + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + output: """ + @page "Test" -Test + Test -

Test

-
-
-
-
+ -@code { - private int currentCount = 0; + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + search: "Test + + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + output: """ + @page "Test" + + Test + + ", + stringsToType: ["{ENTER}", "{ENTER}", "<", "s", "p", "a"], + expectedSelectedItemLabel: "span"); } -} -"""); + + [IdeFact] + public async Task CompletionCommit_WithAngleBracket_HtmlTag() + { + await VerifyTypeAndCommitCompletionAsync( + input: """ + @page "Test" + + Test + + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + output: """ + @page "Test" + + Test + + + + @code { + private int currentCount = 0; + + private void IncrementCount() + { + currentCount++; + } + } + """, + search: "", + stringsToType: ["{ENTER}", "{ENTER}", "<", "s", "p", "a"], + commitChar: '>', + "span"); + } + + [IdeFact] + public async Task CompletionCommit_CSharp() + { + await VerifyTypeAndCommitCompletionAsync( + input: """ + @page "Test" + + Test + + @code { + private int myCurrentCount = 0; + + private void IncrementCount() + { + myCurrentCount++; + } + } + """, + output: """ + @page "Test" + + Test + + @code { + private int myCurrentCount = 0; + + private void IncrementCount() + { + myCurrentCount++; + + myCurrentCount + } + } + """, + search: "myCurrentCount++;", + stringsToType: ["{ENTER}", "{ENTER}", "m", "y", "C", "u", "r"], + expectedSelectedItemLabel: "myCurrentCount"); + } + + private async Task VerifyTypeAndCommitCompletionAsync(string input, string output, string search, string[] stringsToType, char? commitChar = null, string? expectedSelectedItemLabel = null) + { + await TestServices.SolutionExplorer.AddFileAsync( + RazorProjectConstants.BlazorProjectName, + "Test.razor", + input, + open: true, + ControlledHangMitigatingCancellationToken); + + await TestServices.Editor.WaitForComponentClassificationAsync(ControlledHangMitigatingCancellationToken); + + await TestServices.Editor.PlaceCaretAsync(search, charsOffset: 1, ControlledHangMitigatingCancellationToken); + foreach (var stringToType in stringsToType) + { + TestServices.Input.Send(stringToType); + } + + if (expectedSelectedItemLabel is not null) + { + await CommitCompletionAndVerifyAsync(output, expectedSelectedItemLabel, commitChar); + } + else + { + await CommitCompletionAndVerifyAsync(output, commitChar); + } } [IdeFact] @@ -225,12 +444,25 @@ public enum MyEnum """); } - private async Task CommitCompletionAndVerifyAsync(string expected) + private async Task CommitCompletionAndVerifyAsync(string expected, char? commitChar = null) { var session = await TestServices.Editor.WaitForCompletionSessionAsync(HangMitigatingCancellationToken); Assert.NotNull(session); - Assert.True(session.CommitIfUnique(HangMitigatingCancellationToken)); + if (commitChar.HasValue) + { + // Commit using the specified commit character + session.Commit(commitChar.Value, HangMitigatingCancellationToken); + + // session.Commit call above commits as if the commit character was typed, + // but doesn't actually insert the character into the buffer. + // So we still need to insert the character into the buffer ourselves. + TestServices.Input.Send(commitChar.Value.ToString()); + } + else + { + Assert.True(session.CommitIfUnique(HangMitigatingCancellationToken)); + } var textView = await TestServices.Editor.GetActiveTextViewAsync(HangMitigatingCancellationToken); var text = textView.TextBuffer.CurrentSnapshot.GetText(); @@ -239,4 +471,41 @@ private async Task CommitCompletionAndVerifyAsync(string expected) // tests allow for it as long as the content is correct AssertEx.AssertEqualToleratingWhitespaceDifferences(expected, text); } + + private async Task CommitCompletionAndVerifyAsync(string expected, string expectedSelectedItemLabel, char? commitChar = null) + { + // Actually open completion UI and wait for it have selected item we are interested in + var session = await TestServices.Editor.OpenCompletionSessionAndWaitForItemAsync(TimeSpan.FromSeconds(10), expectedSelectedItemLabel, HangMitigatingCancellationToken); + + Assert.NotNull(session); + if (commitChar is char commitCharValue) + { + // Commit using the specified commit character + session.Commit(commitCharValue, HangMitigatingCancellationToken); + + // session.Commit call above commits as if the commit character was typed, + // but doesn't actually insert the character into the buffer. + // So we still need to insert the character into the buffer ourselves. + TestServices.Input.Send(commitCharValue.ToString()); + } + else + { + Assert.True(session.CommitIfUnique(HangMitigatingCancellationToken)); + } + + var textView = await TestServices.Editor.GetActiveTextViewAsync(HangMitigatingCancellationToken); + + var stopwatch = new Stopwatch(); + string text; + while ((text = textView.TextBuffer.CurrentSnapshot.GetText()) != expected && stopwatch.ElapsedMilliseconds < EditorInProcess.DefaultCompletionWaitTimeMilliseconds) + { + // Text might get updated *after* completion by something like auto-insert, so wait for the desired text + await Task.Delay(100, HangMitigatingCancellationToken); + } + + // Snippets may have slight whitespace differences due to line endings. These + // tests allow for it as long as the content is correct + Assert.Equal(expected, text); + } + } diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/Formatting/TestFiles/Expected/FormatCommentWithKeyword.cshtml b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/Formatting/TestFiles/Expected/FormatCommentWithKeyword.cshtml index 22a432e6b73..9e56c758b58 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/Formatting/TestFiles/Expected/FormatCommentWithKeyword.cshtml +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/Formatting/TestFiles/Expected/FormatCommentWithKeyword.cshtml @@ -1,7 +1,7 @@ 

@* - {{{{{{{ - *@ +{{{{{{{ +*@ @while (true) { } diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/Formatting/TestFiles/Expected/FormatDocument.cshtml b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/Formatting/TestFiles/Expected/FormatDocument.cshtml index 5e30f6590e2..29a95b3bf6d 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/Formatting/TestFiles/Expected/FormatDocument.cshtml +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/Formatting/TestFiles/Expected/FormatDocument.cshtml @@ -1,7 +1,7 @@ 

@* - test - *@ +test +*@ @while (true) { } diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/GoToDefinitionTests.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/GoToDefinitionTests.cs index 4fa2dbdf4fc..e735c892d97 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/GoToDefinitionTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/GoToDefinitionTests.cs @@ -367,8 +367,8 @@ public string? MyProperty { set { } } [IdeTheory] [InlineData("MyProperty:get")] - [InlineData("MyProperty:set")] - [InlineData("MyProperty:after")] + [InlineData("MyProperty:set", Skip = "https://github.com/dotnet/razor/issues/10966")] + [InlineData("MyProperty:after", Skip = "https://github.com/dotnet/razor/issues/10966")] public async Task GoToDefinition_ComponentAttribute_BindSet(string attribute) { // Create the file diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/HtmlCodeActionsTests.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/HtmlCodeActionsTests.cs index 204711c97f0..05f6b437c2a 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/HtmlCodeActionsTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/HtmlCodeActionsTests.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using System.Linq; using System.Threading.Tasks; using Xunit; using Xunit.Abstractions; @@ -21,8 +22,7 @@ public async Task HtmlCodeActionsTests_RemoveTag() var codeActions = await TestServices.Editor.InvokeCodeActionListAsync(ControlledHangMitigatingCancellationToken); // Assert - var codeActionSet = Assert.Single(codeActions); - var codeAction = Assert.Single(codeActionSet.Actions, a => a.DisplayText.Equals("Remove

tag and leave contents")); + var codeAction = Assert.Single(codeActions.SelectMany(s => s.Actions), a => a.DisplayText.Equals("Remove

tag and leave contents")); await TestServices.Editor.InvokeCodeActionAsync(codeAction, ControlledHangMitigatingCancellationToken); @@ -41,8 +41,7 @@ public async Task HtmlCodeActionsTests_RemoveTag_WithCSharpContent() var codeActions = await TestServices.Editor.InvokeCodeActionListAsync(ControlledHangMitigatingCancellationToken); // Assert - var codeActionSet = Assert.Single(codeActions); - var codeAction = Assert.Single(codeActionSet.Actions, a => a.DisplayText.Equals("Remove tag and leave contents")); + var codeAction = Assert.Single(codeActions.SelectMany(s => s.Actions), a => a.DisplayText.Equals("Remove tag and leave contents")); await TestServices.Editor.InvokeCodeActionAsync(codeAction, ControlledHangMitigatingCancellationToken); diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/EditorInProcess_Completion.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/EditorInProcess_Completion.cs index 838e2eb79e8..caf687e31ee 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/EditorInProcess_Completion.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/InProcess/EditorInProcess_Completion.cs @@ -12,6 +12,8 @@ namespace Microsoft.VisualStudio.Extensibility.Testing; internal partial class EditorInProcess { + public const int DefaultCompletionWaitTimeMilliseconds = 10000; + public async Task DismissCompletionSessionsAsync(CancellationToken cancellationToken) { await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); @@ -55,4 +57,43 @@ public async Task DismissCompletionSessionsAsync(CancellationToken cancellationT return session; } + + /// + /// Open completion pop-up window UI and wait for the specified item to be present selected + /// + /// + /// + /// + /// Completion session that has matching selected item, or null otherwise + public async Task OpenCompletionSessionAndWaitForItemAsync(TimeSpan timeOut, string selectedItemLabel, CancellationToken cancellationToken) + { + await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); + + // Returns completion session that might or might not be visible in the IDE + var session = await WaitForCompletionSessionAsync(timeOut, cancellationToken); + + if (session is null) + { + return null; + } + + var textView = await GetActiveTextViewAsync(cancellationToken); + var stopWatch = Stopwatch.StartNew(); + + // Actually open the completion pop-up window and force visible items to be computed or re-computed + session.OpenOrUpdate(new CompletionTrigger(CompletionTriggerReason.Insertion, textView.TextSnapshot), textView.Caret.Position.BufferPosition, cancellationToken); + while (session.GetComputedItems(cancellationToken).SelectedItem?.DisplayText != selectedItemLabel) + { + if (stopWatch.ElapsedMilliseconds >= timeOut.TotalMilliseconds) + { + return null; + } + + await Task.Delay(100, cancellationToken); + + session.OpenOrUpdate(new CompletionTrigger(CompletionTriggerReason.Insertion, textView.TextSnapshot), textView.Caret.Position.BufferPosition, cancellationToken); + } + + return session; + } } diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/Microsoft.VisualStudio.Razor.IntegrationTests.csproj b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/Microsoft.VisualStudio.Razor.IntegrationTests.csproj index 496b5256c8b..a86f4cba7b0 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/Microsoft.VisualStudio.Razor.IntegrationTests.csproj +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/Microsoft.VisualStudio.Razor.IntegrationTests.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/MultiTargetProjectTests.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/MultiTargetProjectTests.cs index 006bccb5f92..d44b45ca01f 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/MultiTargetProjectTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/MultiTargetProjectTests.cs @@ -64,7 +64,7 @@ public async Task OpenExistingProject_WithReopenedFile() // Open SurveyPrompt and make sure its all up and running await TestServices.SolutionExplorer.OpenFileAsync(RazorProjectConstants.BlazorProjectName, RazorProjectConstants.ErrorCshtmlFile, ControlledHangMitigatingCancellationToken); - await TestServices.Editor.WaitForSemanticClassificationAsync("class name", ControlledHangMitigatingCancellationToken, count: 1); + await TestServices.Editor.WaitForSemanticClassificationAsync("RazorTagHelperElement", ControlledHangMitigatingCancellationToken, count: 1); await TestServices.SolutionExplorer.CloseSolutionAsync(ControlledHangMitigatingCancellationToken); @@ -73,7 +73,7 @@ public async Task OpenExistingProject_WithReopenedFile() await TestServices.Workspace.WaitForProjectSystemAsync(ControlledHangMitigatingCancellationToken); - await TestServices.Editor.WaitForSemanticClassificationAsync("class name", ControlledHangMitigatingCancellationToken, count: 1); + await TestServices.Editor.WaitForSemanticClassificationAsync("RazorTagHelperElement", ControlledHangMitigatingCancellationToken, count: 1); TestServices.Input.Send("1"); diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/ProjectTests.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/ProjectTests.cs index ad744cb6260..8e4de9be7e5 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/ProjectTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/ProjectTests.cs @@ -95,7 +95,7 @@ public async Task OpenExistingProject_WithReopenedFile() // Open SurveyPrompt and make sure its all up and running await TestServices.SolutionExplorer.OpenFileAsync(RazorProjectConstants.BlazorProjectName, RazorProjectConstants.ErrorCshtmlFile, ControlledHangMitigatingCancellationToken); - await TestServices.Editor.WaitForSemanticClassificationAsync("class name", ControlledHangMitigatingCancellationToken, count: 1); + await TestServices.Editor.WaitForSemanticClassificationAsync("RazorTagHelperElement", ControlledHangMitigatingCancellationToken, count: 1); await TestServices.SolutionExplorer.CloseSolutionAsync(ControlledHangMitigatingCancellationToken); @@ -104,7 +104,7 @@ public async Task OpenExistingProject_WithReopenedFile() await TestServices.Workspace.WaitForProjectSystemAsync(ControlledHangMitigatingCancellationToken); - await TestServices.Editor.WaitForSemanticClassificationAsync("class name", ControlledHangMitigatingCancellationToken, count: 1); + await TestServices.Editor.WaitForSemanticClassificationAsync("RazorTagHelperElement", ControlledHangMitigatingCancellationToken, count: 1); TestServices.Input.Send("1"); diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/Semantic/RazorSemanticTokensTests.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/Semantic/RazorSemanticTokensTests.cs index 9b9d8dcf6dc..fcf08a26472 100644 --- a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/Semantic/RazorSemanticTokensTests.cs +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/Semantic/RazorSemanticTokensTests.cs @@ -54,7 +54,7 @@ @using Microsoft.AspNetCore.Components.Forms await TestServices.Editor.VerifyGetClassificationsAsync(expectedClassifications, ControlledHangMitigatingCancellationToken); } - [IdeFact] + [IdeFact(Skip = "https://github.com/dotnet/razor/issues/5595")] // Broken in FUSE due to @inherits bug public async Task Components_AreColored() { // Arrange diff --git a/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/UnifiedSettingsTest.cs b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/UnifiedSettingsTest.cs new file mode 100644 index 00000000000..17022ffac33 --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.Razor.IntegrationTests/UnifiedSettingsTest.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.IO; +using System.Reflection; +using System.Text.Json; +using Xunit; + +namespace Microsoft.VisualStudio.Razor.IntegrationTests; + +public class UnifiedSettingsTest +{ + [Fact] + public void TestJsonIsValid() + { + var assembly = Assembly.GetExecutingAssembly(); + var resourceName = "Microsoft.VisualStudio.Razor.IntegrationTests.razor.registration.json"; + + using var stream = assembly.GetManifestResourceStream(resourceName); + using var reader = new StreamReader(stream); + var json = reader.ReadToEnd(); + + Assert.False(string.IsNullOrEmpty(json)); + + var options = new JsonDocumentOptions + { + CommentHandling = JsonCommentHandling.Skip + }; + var document = JsonDocument.Parse(json, options); + Assert.NotNull(document); + } +} diff --git a/src/Shared/Directory.Build.props b/src/Shared/Directory.Build.props index 22e7f16332f..e269e4a7fe1 100644 --- a/src/Shared/Directory.Build.props +++ b/src/Shared/Directory.Build.props @@ -14,7 +14,7 @@ LatestMajor - + diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Serialization.Json/ObjectReaders.cs b/src/Shared/Microsoft.AspNetCore.Razor.Serialization.Json/ObjectReaders.cs index e1726562363..91ae5629e82 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Serialization.Json/ObjectReaders.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Serialization.Json/ObjectReaders.cs @@ -42,6 +42,8 @@ public static RazorConfiguration ReadConfigurationFromProperties(JsonDataReader { var configurationName = reader.ReadNonNullString(nameof(RazorConfiguration.ConfigurationName)); var languageVersionText = reader.ReadNonNullString(nameof(RazorConfiguration.LanguageVersion)); + var suppressAddComponentParameter = reader.ReadBooleanOrFalse(nameof(RazorConfiguration.SuppressAddComponentParameter)); + var useConsolidatedMvcViews = reader.ReadBooleanOrTrue(nameof(RazorConfiguration.UseConsolidatedMvcViews)); var extensions = reader.ReadImmutableArrayOrEmpty(nameof(RazorConfiguration.Extensions), static r => { @@ -53,7 +55,7 @@ public static RazorConfiguration ReadConfigurationFromProperties(JsonDataReader ? version : RazorLanguageVersion.Version_2_1; - return new(languageVersion, configurationName, extensions); + return new(languageVersion, configurationName, extensions, SuppressAddComponentParameter: suppressAddComponentParameter); } public static RazorDiagnostic ReadDiagnostic(JsonDataReader reader) diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Serialization.Json/ObjectWriters.cs b/src/Shared/Microsoft.AspNetCore.Razor.Serialization.Json/ObjectWriters.cs index c8998ca6fc9..aad91449d2b 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Serialization.Json/ObjectWriters.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Serialization.Json/ObjectWriters.cs @@ -35,6 +35,9 @@ public static void WriteProperties(JsonDataWriter writer, RazorConfiguration val writer.Write(nameof(value.LanguageVersion), languageVersionText); + writer.WriteIfNotFalse(nameof(value.SuppressAddComponentParameter), value.SuppressAddComponentParameter); + writer.WriteIfNotTrue(nameof(value.UseConsolidatedMvcViews), value.UseConsolidatedMvcViews); + writer.WriteArrayIfNotNullOrEmpty(nameof(value.Extensions), value.Extensions, static (w, v) => w.Write(v.ExtensionName)); } diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Serialization.Json/SerializationFormat.cs b/src/Shared/Microsoft.AspNetCore.Razor.Serialization.Json/SerializationFormat.cs index 1be32b7f927..c6a8db23f4a 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Serialization.Json/SerializationFormat.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Serialization.Json/SerializationFormat.cs @@ -9,5 +9,5 @@ internal static class SerializationFormat // or any of the types that compose it changes. This includes: RazorConfiguration, // ProjectWorkspaceState, TagHelperDescriptor, and DocumentSnapshotHandle. // NOTE: If this version is changed, a coordinated insertion is required between Roslyn and Razor for the C# extension. - public const int Version = 5; + public const int Version = 6; } diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeWriter.cs b/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeWriter.cs index 2c53092456b..a69b24b522b 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeWriter.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/IntermediateNodeWriter.cs @@ -40,12 +40,12 @@ public override void VisitDefault(IntermediateNode node) public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node) { var entries = new List() - { - string.Join(" ", node.Modifiers), - node.ClassName, - node.BaseType, - string.Join(", ", node.Interfaces ?? Array.Empty()) - }; + { + string.Join(" ", node.Modifiers), + node.ClassName, + node.BaseType is { } baseType ? $"{baseType.BaseType.Content}{baseType.GreaterThan?.Content}{baseType.ModelType?.Content}{baseType.LessThan?.Content}" : "", + string.Join(", ", node.Interfaces.Select(i => i.Content)) + }; // Avoid adding the type parameters to the baseline if they aren't present. if (node.TypeParameters != null && node.TypeParameters.Count > 0) diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs b/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs index b64b99f7a4f..8781a1ffafd 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/Language/IntegrationTests/RazorIntegrationTestBase.cs @@ -328,6 +328,11 @@ protected CompileToAssemblyResult CompileToAssembly(string cshtmlRelativePath, s } protected CompileToAssemblyResult CompileToAssembly(CompileToCSharpResult cSharpResult, params DiagnosticDescription[] expectedDiagnostics) + { + return CompileToAssembly(cSharpResult, diagnostics => diagnostics.Verify(expectedDiagnostics)); + } + + protected CompileToAssemblyResult CompileToAssembly(CompileToCSharpResult cSharpResult, Action> verifyDiagnostics) { var syntaxTrees = new[] { @@ -340,7 +345,7 @@ protected CompileToAssemblyResult CompileToAssembly(CompileToCSharpResult cSharp .GetDiagnostics() .Where(d => d.Severity != DiagnosticSeverity.Hidden); - diagnostics.Verify(expectedDiagnostics); + verifyDiagnostics(diagnostics); if (diagnostics.Any()) { diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/ReferenceUtil.cs b/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/ReferenceUtil.cs index c1329b54189..4e7429e9dbf 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/ReferenceUtil.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/ReferenceUtil.cs @@ -12,5 +12,7 @@ internal static class ReferenceUtil { public static ImmutableArray AspNetLatestAll { get; } = AspNetLatest.References.All; public static PortableExecutableReference AspNetLatestComponents { get; } = AspNetLatest.References.MicrosoftAspNetCoreComponents; + public static PortableExecutableReference AspNetLatestRazor { get; } = AspNetLatest.References.MicrosoftAspNetCoreRazor; public static ImmutableArray NetLatestAll { get; } = NetLatest.References.All; + public static PortableExecutableReference NetLatestSystemRuntime { get; } = NetLatest.References.SystemRuntime; } diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/TestCompilation.cs b/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/TestCompilation.cs index 137ad28eb93..76dee4cb168 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/TestCompilation.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Test.Common/TestCompilation.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Reflection; +using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis.CSharp; using Microsoft.Extensions.DependencyModel; using Xunit; @@ -58,6 +59,8 @@ private static IEnumerable ResolvePaths(CompilationLibrary library) public static string AssemblyName => "TestAssembly"; + // When we use Basic.Reference.Assemblies everywhere, this overload should be removed. + // See also https://github.com/dotnet/razor/issues/10343. public static CSharpCompilation Create(Assembly assembly, SyntaxTree syntaxTree = null) { IEnumerable syntaxTrees = null; @@ -80,6 +83,21 @@ public static CSharpCompilation Create(Assembly assembly, SyntaxTree syntaxTree return compilation; } + public static CSharpCompilation Create() => Create(syntaxTrees: [], references: []); + + public static CSharpCompilation Create(IEnumerable syntaxTrees, IEnumerable references) + { + var compilation = CSharpCompilation.Create( + AssemblyName, + syntaxTrees, + [..references, ..ReferenceUtil.AspNetLatestAll], + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + EnsureValidCompilation(compilation); + + return compilation; + } + private static void EnsureValidCompilation(CSharpCompilation compilation) { using (var stream = new MemoryStream()) diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/FileUtilities.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/FileUtilities.cs new file mode 100644 index 00000000000..add56128ea7 --- /dev/null +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/FileUtilities.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Globalization; +using System.IO; + +namespace Microsoft.AspNetCore.Razor.Utilities; + +internal static class FileUtilities +{ + /// + /// Generate a file path adjacent to the input path that has the + /// specified file extension, using numbers to differentiate for + /// any collisions. + /// + /// The input file path. + /// The input file extension with a prepended ".". + /// A non-existent file path with a name in the specified format and a corresponding extension. + public static string GenerateUniquePath(string path, string extension) + { + var directoryName = Path.GetDirectoryName(path).AssumeNotNull(); + var baseFileName = Path.GetFileNameWithoutExtension(path); + + var n = 0; + string uniquePath; + do + { + var identifier = n > 0 ? n.ToString(CultureInfo.InvariantCulture) : string.Empty; // Make it look nice + + uniquePath = Path.Combine(directoryName, $"{baseFileName}{identifier}{extension}"); + n++; + } + while (File.Exists(uniquePath)); + + return uniquePath; + } +} diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/Microsoft.AspNetCore.Razor.Utilities.Shared.csproj b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/Microsoft.AspNetCore.Razor.Utilities.Shared.csproj index 18d42c533aa..15d4b337497 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/Microsoft.AspNetCore.Razor.Utilities.Shared.csproj +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/Microsoft.AspNetCore.Razor.Utilities.Shared.csproj @@ -5,7 +5,7 @@ Razor is a markup syntax for adding server-side logic to web pages. This package contains the language server library assets. false false - false + false Microsoft.AspNetCore.Razor false true