diff --git a/Directory.Packages.props b/Directory.Packages.props index 02c9a790a75..fac9962f013 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,9 +13,9 @@ <_BasicReferenceAssembliesVersion>1.7.2 <_BenchmarkDotNetPackageVersion>0.13.5.2136 <_MicrosoftVisualStudioExtensibilityTestingVersion>0.1.187-beta - <_MicrosoftCodeAnalysisAnalyzersPackageVersion>3.11.0-beta1.24170.2 + <_MicrosoftCodeAnalysisAnalyzersPackageVersion>3.11.0 <_MicrosoftVisualStudioLanguageServicesPackageVersion>$(MicrosoftVisualStudioLanguageServicesPackageVersion) - <_XunitPackageVersion>2.6.3 + <_XunitPackageVersion>2.6.6 <_MicrosoftBuildPackageVersion>17.11.0-preview-24309-01 @@ -34,7 +34,7 @@ - + @@ -106,7 +106,7 @@ - + @@ -117,7 +117,7 @@ - + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 4da4e2b24b2..0b14684cccb 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -11,82 +11,82 @@ 76c417253f5b3890997a3ef4b0613c2eab73d156 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 - + https://github.com/dotnet/roslyn - 7b7951aa13c50ad768538e58ed3805898b058928 + e2d4e372f19c16f9b3dea06f7ca857ed5d42bc09 diff --git a/eng/Versions.props b/eng/Versions.props index 94e345e7cba..9802c793093 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -53,25 +53,25 @@ 9.0.0-beta.24509.3 1.0.0-beta.23475.1 1.0.0-beta.23475.1 - 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 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 + 4.13.0-1.24505.1 $(MicrosoftNetCompilersToolsetPackageVersion) - 2.6.3 + 2.6.6 1.7.0 diff --git a/eng/targets/Services.props b/eng/targets/Services.props index 0501df0e116..f18168d032b 100644 --- a/eng/targets/Services.props +++ b/eng/targets/Services.props @@ -33,5 +33,6 @@ + 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 7fc0caba7bc..9c718b6fbb8 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCompletionBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/RazorCompletionBenchmark.cs @@ -2,7 +2,6 @@ // 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.Text; using System.Threading; @@ -13,6 +12,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; @@ -37,13 +37,12 @@ public async Task SetupAsync() { var razorCompletionListProvider = RazorLanguageServerHost.GetRequiredService(); var lspServices = RazorLanguageServerHost.GetRequiredService(); - var responseRewriters = lspServices.GetRequiredServices(); var documentMappingService = lspServices.GetRequiredService(); var clientConnection = lspServices.GetRequiredService(); var completionListCache = lspServices.GetRequiredService(); var loggerFactory = lspServices.GetRequiredService(); - var delegatedCompletionListProvider = new TestDelegatedCompletionListProvider(responseRewriters, documentMappingService, clientConnection, completionListCache); + var delegatedCompletionListProvider = new TestDelegatedCompletionListProvider(documentMappingService, clientConnection, completionListCache); var completionListProvider = new CompletionListProvider(razorCompletionListProvider, delegatedCompletionListProvider); var configurationService = new DefaultRazorConfigurationService(clientConnection, loggerFactory); var optionsMonitor = new RazorLSPOptionsMonitor(configurationService, RazorLSPOptions.Default); @@ -141,12 +140,19 @@ public async Task RazorCompletionAsync() private class TestDelegatedCompletionListProvider : DelegatedCompletionListProvider { - public TestDelegatedCompletionListProvider(IEnumerable responseRewriters, IDocumentMappingService documentMappingService, IClientConnection clientConnection, CompletionListCache completionListCache) - : base(responseRewriters, documentMappingService, clientConnection, completionListCache) + public TestDelegatedCompletionListProvider(IDocumentMappingService documentMappingService, IClientConnection clientConnection, CompletionListCache completionListCache) + : base(documentMappingService, clientConnection, completionListCache) { } - public override Task GetCompletionListAsync(int absoluteIndex, VSInternalCompletionContext completionContext, DocumentContext documentContext, VSInternalClientCapabilities clientCapabilities, Guid correlationId, CancellationToken cancellationToken) + public override Task GetCompletionListAsync( + int absoluteIndex, + VSInternalCompletionContext completionContext, + DocumentContext documentContext, + VSInternalClientCapabilities clientCapabilities, + RazorCompletionOptions completionOptions, + Guid correlationId, + CancellationToken cancellationToken) { return Task.FromResult( new VSInternalCompletionList diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/TagHelperCompletionBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/TagHelperCompletionBenchmark.cs index 391bf424470..f96af4a0ed4 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/TagHelperCompletionBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/LanguageServer/TagHelperCompletionBenchmark.cs @@ -29,7 +29,7 @@ public class TagHelperCompletionBenchmark [Benchmark] public object GetAttributeCompletions() { - var tagHelperCompletionService = new LspTagHelperCompletionService(); + var tagHelperCompletionService = new TagHelperCompletionService(); var context = new AttributeCompletionContext( TagHelperDocumentContext.Create(prefix: null, CommonResources.TelerikTagHelpers), existingCompletions: [], @@ -46,7 +46,7 @@ public object GetAttributeCompletions() [Benchmark] public object GetElementCompletions() { - var tagHelperCompletionService = new LspTagHelperCompletionService(); + var tagHelperCompletionService = new TagHelperCompletionService(); var context = new ElementCompletionContext( TagHelperDocumentContext.Create(prefix: null, CommonResources.TelerikTagHelpers), existingCompletions: s_existingElementCompletions, diff --git a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Serialization/CompletionListSerializationBenchmark.cs b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Serialization/CompletionListSerializationBenchmark.cs index 9e98822d88d..f8aa0692962 100644 --- a/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Serialization/CompletionListSerializationBenchmark.cs +++ b/src/Razor/benchmarks/Microsoft.AspNetCore.Razor.Microbenchmarks/Serialization/CompletionListSerializationBenchmark.cs @@ -22,10 +22,8 @@ public class CompletionListSerializationBenchmark public CompletionListSerializationBenchmark() { - var completionService = new LspTagHelperCompletionService(); - var configurationService = new BenchmarkConfigurationSyncService(); - var optionsMonitor = new RazorLSPOptionsMonitor(configurationService, RazorLSPOptions.Default); - var tagHelperCompletionProvider = new TagHelperCompletionProvider(completionService, optionsMonitor); + var completionService = new TagHelperCompletionService(); + var tagHelperCompletionProvider = new TagHelperCompletionProvider(completionService); var documentContent = "<"; var queryIndex = 1; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/CompletionListProvider.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/CompletionListProvider.cs index 9caeb7cc5de..910bba295c3 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/CompletionListProvider.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/CompletionListProvider.cs @@ -2,12 +2,14 @@ // 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.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; +using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -22,25 +24,27 @@ public CompletionListProvider(RazorCompletionListProvider razorCompletionListPro { _razorCompletionListProvider = razorCompletionListProvider; _delegatedCompletionListProvider = delegatedCompletionListProvider; - - var allTriggerCharacters = razorCompletionListProvider.TriggerCharacters.Concat(delegatedCompletionListProvider.TriggerCharacters); - var distinctTriggerCharacters = new HashSet(allTriggerCharacters); - AggregateTriggerCharacters = distinctTriggerCharacters.ToImmutableHashSet(); } - public ImmutableHashSet AggregateTriggerCharacters { get; } - public async Task GetCompletionListAsync( int absoluteIndex, VSInternalCompletionContext completionContext, DocumentContext documentContext, VSInternalClientCapabilities clientCapabilities, + RazorCompletionOptions razorCompletionOptions, Guid correlationId, CancellationToken cancellationToken) { // First we delegate to get completion items from the individual language server - var delegatedCompletionList = IsValidTrigger(_delegatedCompletionListProvider.TriggerCharacters, completionContext) - ? await _delegatedCompletionListProvider.GetCompletionListAsync(absoluteIndex, completionContext, documentContext, clientCapabilities, correlationId, cancellationToken).ConfigureAwait(false) + var delegatedCompletionList = CompletionTriggerCharacters.IsValidTrigger(_delegatedCompletionListProvider.TriggerCharacters, completionContext) + ? await _delegatedCompletionListProvider.GetCompletionListAsync( + absoluteIndex, + completionContext, + documentContext, + clientCapabilities, + razorCompletionOptions, + correlationId, + cancellationToken).ConfigureAwait(false) : null; // Extract the items we got back from the delegated server, to inform tag helper completion @@ -49,17 +53,19 @@ public CompletionListProvider(RazorCompletionListProvider razorCompletionListPro : null; // Now we get the Razor completion list, using information from the actual language server if necessary - var razorCompletionList = IsValidTrigger(_razorCompletionListProvider.TriggerCharacters, completionContext) - ? await _razorCompletionListProvider.GetCompletionListAsync(absoluteIndex, completionContext, documentContext, clientCapabilities, existingItems, cancellationToken).ConfigureAwait(false) + var razorCompletionList = CompletionTriggerCharacters.IsValidTrigger(_razorCompletionListProvider.TriggerCharacters, completionContext) + ? await _razorCompletionListProvider.GetCompletionListAsync( + absoluteIndex, + completionContext, + documentContext, + clientCapabilities, + existingItems, + razorCompletionOptions, + cancellationToken).ConfigureAwait(false) : null; var finalCompletionList = CompletionListMerger.Merge(razorCompletionList, delegatedCompletionList); return finalCompletionList; } - - private bool IsValidTrigger(ImmutableHashSet triggerCharacters, VSInternalCompletionContext completionContext) - => completionContext.TriggerKind != CompletionTriggerKind.TriggerCharacter || - completionContext.TriggerCharacter is null || - triggerCharacters.Contains(completionContext.TriggerCharacter); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionItemResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionItemResolver.cs index 81d2a97b3e6..bacfb7e3969 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionItemResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionItemResolver.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; 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 900b1fcc39b..e7e0e259ca1 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 @@ -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.Linq; @@ -11,42 +12,34 @@ using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Razor.Completion; +using Microsoft.CodeAnalysis.Razor.Completion.Delegation; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Protocol.Completion; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; internal class DelegatedCompletionListProvider { - private static readonly ImmutableHashSet s_razorTriggerCharacters = new[] { "@" }.ToImmutableHashSet(); - private static readonly ImmutableHashSet s_csharpTriggerCharacters = new[] { " ", "(", "=", "#", ".", "<", "[", "{", "\"", "/", ":", "~" }.ToImmutableHashSet(); - private static readonly ImmutableHashSet s_htmlTriggerCharacters = new[] { ":", "@", "#", ".", "!", "*", ",", "(", "[", "-", "<", "&", "\\", "/", "'", "\"", "=", ":", " ", "`" }.ToImmutableHashSet(); - private static readonly ImmutableHashSet s_allTriggerCharacters = - s_csharpTriggerCharacters - .Union(s_htmlTriggerCharacters) - .Union(s_razorTriggerCharacters); - - private readonly ImmutableArray _responseRewriters; private readonly IDocumentMappingService _documentMappingService; private readonly IClientConnection _clientConnection; private readonly CompletionListCache _completionListCache; public DelegatedCompletionListProvider( - IEnumerable responseRewriters, IDocumentMappingService documentMappingService, IClientConnection clientConnection, CompletionListCache completionListCache) { - _responseRewriters = responseRewriters.OrderByAsArray(static x => x.Order); _documentMappingService = documentMappingService; _clientConnection = clientConnection; _completionListCache = completionListCache; } // virtual for tests - public virtual ImmutableHashSet TriggerCharacters => s_allTriggerCharacters; + public virtual FrozenSet TriggerCharacters => CompletionTriggerCharacters.AllDelegationTriggerCharacters; // virtual for tests public virtual async Task GetCompletionListAsync( @@ -54,6 +47,7 @@ public DelegatedCompletionListProvider( VSInternalCompletionContext completionContext, DocumentContext documentContext, VSInternalClientCapabilities clientCapabilities, + RazorCompletionOptions razorCompletionOptions, Guid correlationId, CancellationToken cancellationToken) { @@ -67,17 +61,27 @@ public DelegatedCompletionListProvider( return null; } - var provisionalCompletion = await TryGetProvisionalCompletionInfoAsync(documentContext, completionContext, positionInfo, cancellationToken).ConfigureAwait(false); + var provisionalCompletion = await DelegatedCompletionHelper.TryGetProvisionalCompletionInfoAsync( + documentContext, + completionContext, + positionInfo, + _documentMappingService, + cancellationToken).ConfigureAwait(false); TextEdit? provisionalTextEdit = null; - if (provisionalCompletion is not null) + if (provisionalCompletion is { } provisionalCompletionValue) { - provisionalTextEdit = provisionalCompletion.ProvisionalTextEdit; - positionInfo = provisionalCompletion.ProvisionalPositionInfo; + provisionalTextEdit = provisionalCompletionValue.ProvisionalTextEdit; + positionInfo = provisionalCompletionValue.DocumentPositionInfo; } - completionContext = RewriteContext(completionContext, positionInfo.LanguageKind); + completionContext = DelegatedCompletionHelper.RewriteContext(completionContext, positionInfo.LanguageKind); - var shouldIncludeSnippets = await ShouldIncludeSnippetsAsync(documentContext, absoluteIndex, cancellationToken).ConfigureAwait(false); + var razorCodeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + // It's a bit confusing, but we have two different "add snippets" options - one is a part of + // RazorCompletionOptions and becomes a part of RazorCompletionContext and is used by + // RazorCompletionFactsService, and the second one below that's used for delegated completion + // Their values are not related in any way. + var shouldIncludeDelegationSnippets = DelegatedCompletionHelper.ShouldIncludeSnippets(razorCodeDocument, absoluteIndex); var delegatedParams = new DelegatedCompletionParams( documentContext.GetTextDocumentIdentifierAndVersion(), @@ -85,7 +89,7 @@ public DelegatedCompletionListProvider( positionInfo.LanguageKind, completionContext, provisionalTextEdit, - shouldIncludeSnippets, + shouldIncludeDelegationSnippets, correlationId); var delegatedResponse = await _clientConnection.SendRequestAsync( @@ -93,26 +97,16 @@ public DelegatedCompletionListProvider( delegatedParams, cancellationToken).ConfigureAwait(false); - if (delegatedResponse is null) - { - // If we don't get a response from the delegated server, we have to make sure to return an incomplete completion - // list. When a user is typing quickly, the delegated request from the first keystroke will fail to synchronize, - // so if we return a "complete" list then the query won't re-query us for completion once the typing stops/slows - // so we'd only ever return Razor completion items. - return new VSInternalCompletionList() { IsIncomplete = true, Items = [] }; - } - - var rewrittenResponse = delegatedResponse; - - foreach (var rewriter in _responseRewriters) - { - rewrittenResponse = await rewriter.RewriteAsync( - rewrittenResponse, + var rewrittenResponse = delegatedParams.ProjectedKind == RazorLanguageKind.CSharp + ? await DelegatedCompletionHelper.RewriteCSharpResponseAsync( + delegatedResponse, absoluteIndex, documentContext, - delegatedParams, - cancellationToken).ConfigureAwait(false); - } + delegatedParams.ProjectedPosition, + razorCompletionOptions, + cancellationToken) + .ConfigureAwait(false) + : DelegatedCompletionHelper.RewriteHtmlResponse(delegatedResponse, razorCompletionOptions); var completionCapability = clientCapabilities?.TextDocument?.Completion as VSInternalCompletionSetting; var resolutionContext = new DelegatedCompletionResolutionContext(delegatedParams, rewrittenResponse.Data); @@ -121,113 +115,4 @@ public DelegatedCompletionListProvider( return rewrittenResponse; } - - private async Task ShouldIncludeSnippetsAsync(DocumentContext documentContext, int absoluteIndex, CancellationToken cancellationToken) - { - var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - var tree = codeDocument.GetSyntaxTree(); - - var token = tree.Root.FindToken(absoluteIndex, includeWhitespace: false); - var node = token.Parent; - var startOrEndTag = node?.FirstAncestorOrSelf(n => RazorSyntaxFacts.IsAnyStartTag(n) || RazorSyntaxFacts.IsAnyEndTag(n)); - - if (startOrEndTag is null) - { - return token.Kind is not (SyntaxKind.OpenAngle or SyntaxKind.CloseAngle); - } - - if (startOrEndTag.Span.Start == absoluteIndex) - { - // We're at the start of the tag, we should include snippets. This is the case for things like $$
or
$$
, since the - // index is right associative to the token when using FindToken. - return true; - } - - return !startOrEndTag.Span.Contains(absoluteIndex); - } - - private static VSInternalCompletionContext RewriteContext(VSInternalCompletionContext context, RazorLanguageKind languageKind) - { - if (context.TriggerKind != CompletionTriggerKind.TriggerCharacter || - context.TriggerCharacter is not { } triggerCharacter) - { - // Non-triggered based completion, the existing context is valid. - return context; - } - - if (languageKind == RazorLanguageKind.CSharp && s_csharpTriggerCharacters.Contains(triggerCharacter)) - { - // C# trigger character for C# content - return context; - } - - if (languageKind == RazorLanguageKind.Html && s_htmlTriggerCharacters.Contains(triggerCharacter)) - { - // HTML trigger character for HTML content - return context; - } - - // Trigger character not associated with the current language. Transform the context into an invoked context. - var rewrittenContext = new VSInternalCompletionContext() - { - InvokeKind = context.InvokeKind, - TriggerKind = CompletionTriggerKind.Invoked, - }; - - if (languageKind == RazorLanguageKind.CSharp && s_razorTriggerCharacters.Contains(triggerCharacter)) - { - // The C# language server will not return any completions for the '@' character unless we - // send the completion request explicitly. - rewrittenContext.InvokeKind = VSInternalCompletionInvokeKind.Explicit; - } - - return rewrittenContext; - } - - private async Task TryGetProvisionalCompletionInfoAsync( - DocumentContext documentContext, - VSInternalCompletionContext completionContext, - DocumentPositionInfo positionInfo, - CancellationToken cancellationToken) - { - if (positionInfo.LanguageKind != RazorLanguageKind.Html || - completionContext.TriggerKind != CompletionTriggerKind.TriggerCharacter || - completionContext.TriggerCharacter != ".") - { - // Invalid provisional completion context - return null; - } - - if (positionInfo.Position.Character == 0) - { - // We're at the start of line. Can't have provisional completions here. - return null; - } - - var previousCharacterPositionInfo = await _documentMappingService - .GetPositionInfoAsync(documentContext, positionInfo.HostDocumentIndex - 1, cancellationToken) - .ConfigureAwait(false); - - if (previousCharacterPositionInfo.LanguageKind != RazorLanguageKind.CSharp) - { - return null; - } - - var previousPosition = previousCharacterPositionInfo.Position; - - // Edit the CSharp projected document to contain a '.'. This allows C# completion to provide valid - // completion items for moments when a user has typed a '.' that's typically interpreted as Html. - var addProvisionalDot = VsLspFactory.CreateTextEdit(previousPosition, "."); - - var provisionalPositionInfo = new DocumentPositionInfo( - RazorLanguageKind.CSharp, - VsLspFactory.CreatePosition( - previousPosition.Line, - previousPosition.Character + 1), - previousCharacterPositionInfo.HostDocumentIndex + 1); - - return new ProvisionalCompletionInfo(addProvisionalDot, provisionalPositionInfo); - } - - private record class ProvisionalCompletionInfo(TextEdit ProvisionalTextEdit, DocumentPositionInfo ProvisionalPositionInfo); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionResponseRewriter.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionResponseRewriter.cs deleted file mode 100644 index 68c04994f45..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DelegatedCompletionResponseRewriter.cs +++ /dev/null @@ -1,39 +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.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Protocol; -using Microsoft.VisualStudio.LanguageServer.Protocol; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; - -internal abstract class DelegatedCompletionResponseRewriter -{ - /// - /// Defines the order in which the rewriter will run. Implementors of should utilize - /// the type to determine order. - /// - /// is only called once to determine order (needs to represent a static order). - /// - public abstract int Order { get; } - - public abstract Task RewriteAsync( - VSInternalCompletionList completionList, - int hostDocumentIndex, - DocumentContext hostDocumentContext, - DelegatedCompletionParams delegatedParameters, - CancellationToken cancellationToken); - - protected static class ExecutionBehaviorOrder - { - public static readonly int FiltersCompletionItems = -20; - - public static readonly int AddsCompletionItems = -10; - - public static readonly int Default = 0; - - public static readonly int ChangesCompletionItems = 10; - } -} 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 298e83b4bb7..3990add43e4 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionEndpoint.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; using Microsoft.AspNetCore.Razor.Telemetry; +using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -35,7 +36,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V serverCapabilities.CompletionProvider = new CompletionOptions() { ResolveProvider = true, - TriggerCharacters = _completionListProvider.AggregateTriggerCharacters.ToArray(), + TriggerCharacters = CompletionTriggerCharacters.AllTriggerCharacters, // 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 @@ -78,11 +79,16 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(CompletionParams request var correlationId = Guid.NewGuid(); using var _ = _telemetryReporter?.TrackLspRequest(Methods.TextDocumentCompletionName, LanguageServerConstants.RazorLanguageServerName, correlationId); + var razorCompletionOptions = new RazorCompletionOptions( + SnippetsSupported: true, + AutoInsertAttributeQuotes: _optionsMonitor.CurrentValue.AutoInsertAttributeQuotes, + CommitElementsWithSpace: _optionsMonitor.CurrentValue.CommitElementsWithSpace); var completionList = await _completionListProvider.GetCompletionListAsync( hostDocumentIndex, completionContext, documentContext, _clientCapabilities!, + razorCompletionOptions, correlationId, cancellationToken).ConfigureAwait(false); return completionList; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionItemExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionItemExtensions.cs deleted file mode 100644 index 98f60f32985..00000000000 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionItemExtensions.cs +++ /dev/null @@ -1,51 +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.Razor.Completion; -using Microsoft.CodeAnalysis.Razor.Tooltip; - -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; - -internal static class RazorCompletionItemExtensions -{ - private readonly static string s_markupTransitionDescriptionKey = "Razor.MarkupTransitionDescription"; - private readonly static string s_tagHelperElementCompletionDescriptionKey = "Razor.TagHelperElementDescription"; - - public static void SetMarkupTransitionCompletionDescription(this RazorCompletionItem completionItem, MarkupTransitionCompletionDescription markupTransitionCompletionDescription) - { - if (completionItem is null) - { - throw new ArgumentNullException(nameof(completionItem)); - } - - completionItem.Items[s_markupTransitionDescriptionKey] = markupTransitionCompletionDescription; - } - - public static MarkupTransitionCompletionDescription? GetMarkupTransitionCompletionDescription(this RazorCompletionItem completionItem) - { - if (completionItem is null) - { - throw new ArgumentNullException(nameof(completionItem)); - } - - var markupTransitionCompletionDescription = completionItem.Items[s_markupTransitionDescriptionKey] as MarkupTransitionCompletionDescription; - return markupTransitionCompletionDescription; - } - - public static void SetTagHelperElementDescriptionInfo(this RazorCompletionItem completionItem, AggregateBoundElementDescription elementDescriptionInfo) - { - completionItem.Items[s_tagHelperElementCompletionDescriptionKey] = elementDescriptionInfo; - } - - public static AggregateBoundElementDescription? GetTagHelperElementDescriptionInfo(this RazorCompletionItem completionItem) - { - if (completionItem is null) - { - throw new ArgumentNullException(nameof(completionItem)); - } - - var description = completionItem.Items[s_tagHelperElementCompletionDescriptionKey] as AggregateBoundElementDescription; - return description; - } -} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionResolveEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionResolveEndpoint.cs index 5c22672d50f..41274aae245 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionResolveEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionResolveEndpoint.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; +using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; 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 985d4395b83..429ca440bb8 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -21,6 +21,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.SpellCheck; using Microsoft.AspNetCore.Razor.ProjectEngineHost; using Microsoft.CodeAnalysis.Razor.Completion; +using Microsoft.CodeAnalysis.Razor.Completion.Delegation; using Microsoft.CodeAnalysis.Razor.Diagnostics; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.Formatting; @@ -81,15 +82,11 @@ public static void AddCompletionServices(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index 780d31c5e56..3ec3a12e926 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -130,7 +130,6 @@ protected override ILspServices ConstructLspServices() services.AddSemanticTokensServices(featureOptions); services.AddDocumentManagementServices(featureOptions); - services.AddCompletionServices(); services.AddFormattingServices(featureOptions); services.AddCodeActionsServices(); services.AddOptionsServices(_lspOptions); @@ -142,6 +141,9 @@ protected override ILspServices ConstructLspServices() // Diagnostics services.AddDiagnosticServices(); + // Completion + services.AddCompletionServices(); + // Auto insert services.AddSingleton(); services.AddSingleton(); 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 f652571c68b..c783544e730 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/SR.resx +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Resources/SR.resx @@ -117,9 +117,6 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Blazor directive attributes - Changes: @@ -141,18 +138,12 @@ Generate Event Handler '{0}' - - "Re-trigger completions..." - Unknown ProjectChangeKind {0} Provided version should not be null. - - statement - Extract element to new component 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 993c3f2b9d1..b630702a1b3 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 @@ -2,11 +2,6 @@ - - Blazor directive attributes - Atributy direktivy Blazor - - Changes: Změny: @@ -47,16 +42,6 @@ Generovat obslužnou rutinu události {0} - - "Re-trigger completions..." - Aktivovat znovu dokončení… - - - - statement - příkaz - - 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 6e155851810..74b87e6f100 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 @@ -2,11 +2,6 @@ - - Blazor directive attributes - Attribute für Blazor-Direktiven - - Changes: Änderungen: @@ -47,16 +42,6 @@ Ereignishandler "{0}" generieren - - "Re-trigger completions..." - "Abschlüsse erneut auslösen..." - - - - statement - Anweisung - - 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 786cbbdc094..6b83a1f78bf 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 @@ -2,11 +2,6 @@ - - Blazor directive attributes - Atributos de directiva de Blazor - - Changes: Cambios: @@ -47,16 +42,6 @@ Generar controlador de eventos ''{0}'' - - "Re-trigger completions..." - "Volver a desencadenar las finalizaciones..." - - - - statement - instrucción - - 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 500f1d42e0e..8fb0151b276 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 @@ -2,11 +2,6 @@ - - Blazor directive attributes - Attributs de directive Blazor - - Changes: Modifications : @@ -47,16 +42,6 @@ Générer le gestionnaire d’événements '{0}' - - "Re-trigger completions..." - « Déclencher à nouveau les complétions... » - - - - statement - déclaration - - 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 9c54cc89004..066658b5ce9 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 @@ -2,11 +2,6 @@ - - Blazor directive attributes - Attributo per la direttiva Blazor - - Changes: Modifiche: @@ -47,16 +42,6 @@ Genera gestore dell'evento '{0}' - - "Re-trigger completions..." - "Riattiva i completamenti..." - - - - statement - istruzione - - 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 d37fd154637..3e5dd36870f 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 @@ -2,11 +2,6 @@ - - Blazor directive attributes - Blazor ディレクティブ属性 - - Changes: 変更: @@ -47,16 +42,6 @@ イベント ハンドラー '{0}' の生成 - - "Re-trigger completions..." - "再トリガーの完了..." - - - - statement - ステートメント - - 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 ff072a41e06..38e3e292241 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 @@ -2,11 +2,6 @@ - - Blazor directive attributes - Blazor 지시문 특성 - - Changes: 변경 내용: @@ -47,16 +42,6 @@ 이벤트 처리기 '{0}' 생성 - - "Re-trigger completions..." - "완료된 항목 다시 트리거" - - - - statement - - - 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 098d35923f5..87cef9f2e44 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 @@ -2,11 +2,6 @@ - - Blazor directive attributes - Atrybut dyrektywy Blazor - - Changes: Zmiany: @@ -47,16 +42,6 @@ Generuj obsługę zdarzeń „{0}” - - "Re-trigger completions..." - "Ponownie wyzwalaj uzupełniania..." - - - - statement - instrukcja - - 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 38674a41ddc..1546d112925 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 @@ -2,11 +2,6 @@ - - Blazor directive attributes - Atributos da diretiva Blazor - - Changes: Alterações: @@ -47,16 +42,6 @@ Gerar manipulador de eventos '{0}' - - "Re-trigger completions..." - "Disparar conclusões novamente..." - - - - statement - instrução - - 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 87fd1dc65e1..8e012c1442e 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 @@ -2,11 +2,6 @@ - - Blazor directive attributes - Атрибуты директивы Blazor - - Changes: Изменения: @@ -47,16 +42,6 @@ Создать обработчик событий "{0}" - - "Re-trigger completions..." - "Повторный запуск завершений..." - - - - statement - инструкция - - 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 db1e0aba01d..5d3e4817090 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 @@ -2,11 +2,6 @@ - - Blazor directive attributes - Blazor yönergesi öznitelikleri - - Changes: Değişiklikler: @@ -47,16 +42,6 @@ '{0}' Olay İşleyicisi Oluştur - - "Re-trigger completions..." - "Tamamlamaları yeniden tetikleyin..." - - - - statement - deyim - - 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 951409f4398..50adf39a2ba 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 @@ -2,11 +2,6 @@ - - Blazor directive attributes - Blazor 指令特性 - - Changes: 更改: @@ -47,16 +42,6 @@ 生成事件处理程序 "{0}" - - "Re-trigger completions..." - “重新触发完成…” - - - - statement - 语句 - - 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 c61189487d1..a34fc90c751 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 @@ -2,11 +2,6 @@ - - Blazor directive attributes - Blazor 指示詞屬性 - - Changes: 變更: @@ -47,16 +42,6 @@ 產生事件處理常式 '{0}' - - "Re-trigger completions..." - "重新觸發完成..." - - - - statement - 陳述式 - - Unknown ProjectChangeKind {0} 未知的 ProjectChangeKind {0} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/CompletionListCache.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/CompletionListCache.cs similarity index 96% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/CompletionListCache.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/CompletionListCache.cs index ee163ec8b1a..149dd360c14 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/CompletionListCache.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/CompletionListCache.cs @@ -4,9 +4,9 @@ using System; using Microsoft.VisualStudio.LanguageServer.Protocol; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; -internal sealed class CompletionListCache +internal class CompletionListCache { private record struct Slot( int Id, diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/CompletionListMerger.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/CompletionListMerger.cs similarity index 98% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/CompletionListMerger.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/CompletionListMerger.cs index cf1344a1936..a6c511d1911 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/CompletionListMerger.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/CompletionListMerger.cs @@ -4,17 +4,13 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol; using Newtonsoft.Json.Linq; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; internal static class CompletionListMerger { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/CompletionListOptimizer.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/CompletionListOptimizer.cs similarity index 98% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/CompletionListOptimizer.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/CompletionListOptimizer.cs index 0fa09dcc0b2..cdf9a3766fe 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/CompletionListOptimizer.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/CompletionListOptimizer.cs @@ -6,7 +6,7 @@ using Microsoft.VisualStudio.LanguageServer.Protocol; using AliasedVSCommitCharacters = Microsoft.VisualStudio.LanguageServer.Protocol.SumType; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; internal static class CompletionListOptimizer { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/CompletionTriggerCharacters.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/CompletionTriggerCharacters.cs new file mode 100644 index 00000000000..b8603e2d1f5 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/CompletionTriggerCharacters.cs @@ -0,0 +1,23 @@ +// 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.Frozen; +using System.Linq; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Razor.Completion; + +internal static class CompletionTriggerCharacters +{ + public static FrozenSet RazorTriggerCharacters { get; } = new[] { "@", "<", ":", " " }.ToFrozenSet(); + public static FrozenSet RazorDelegationTriggerCharacters { get; } = new[] { "@" }.ToFrozenSet(); + public static FrozenSet CSharpTriggerCharacters { get; } = new[] { " ", "(", "=", "#", ".", "<", "[", "{", "\"", "/", ":", "~" }.ToFrozenSet(); + public static FrozenSet HtmlTriggerCharacters { get; } = new[] { ":", "@", "#", ".", "!", "*", ",", "(", "[", "-", "<", "&", "\\", "/", "'", "\"", "=", ":", " ", "`" }.ToFrozenSet(); + public static FrozenSet AllDelegationTriggerCharacters { get; } = RazorDelegationTriggerCharacters.Union(CSharpTriggerCharacters).Union(HtmlTriggerCharacters).ToFrozenSet(); + public static string[] AllTriggerCharacters { get; } = RazorTriggerCharacters.Union(AllDelegationTriggerCharacters).ToArray(); + + public static bool IsValidTrigger(FrozenSet triggerCharacters, CompletionContext completionContext) + => completionContext.TriggerKind != CompletionTriggerKind.TriggerCharacter || + completionContext.TriggerCharacter is null || + triggerCharacters.Contains(completionContext.TriggerCharacter); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs new file mode 100644 index 00000000000..9429a1e7709 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DelegatedCompletionHelper.cs @@ -0,0 +1,238 @@ +// 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.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Protocol.Completion; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Razor.Completion.Delegation; + +using SyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; + +/// +/// Helper methods for C# and HTML completion ("delegated" completion) that are used both in LSP and cohosting +/// completion handler code. +/// +internal static class DelegatedCompletionHelper +{ + // Ordering should be: + // 1. Changes items + // 2. Adds items + // 3. Filters items + private static readonly ImmutableArray s_delegatedCSharpCompletionResponseRewriters = + [new SnippetResponseRewriter(), new TextEditResponseRewriter(), new DesignTimeHelperResponseRewriter()]; + + // Currently we only have one HTML response re-writer. Should we ever need more, we can create a common base and a collection + private static readonly HtmlCommitCharacterResponseRewriter s_delegatedHtmlCompletionResponseRewriter = new HtmlCommitCharacterResponseRewriter(); + + /// + /// Modifies completion context if needed so that it's acceptable to the delegated language. + /// + /// Original completion context passed to the completion handler + /// Language of the completion position + /// Possibly modified completion context + /// For example, if we invoke C# completion in Razor via @ character, we will not + /// want C# to see @ as the trigger character and instead will transform completion context + /// into "invoked" and "explicit" rather than "typing", without a trigger character + public static VSInternalCompletionContext RewriteContext(VSInternalCompletionContext context, RazorLanguageKind languageKind) + { + Debug.Assert(languageKind != RazorLanguageKind.Razor, + $"{nameof(RewriteContext)} should be called for delegated completion only"); + + if (context.TriggerKind != CompletionTriggerKind.TriggerCharacter || + context.TriggerCharacter is not { } triggerCharacter) + { + // Non-triggered based completion, the existing context is valid. + return context; + } + + if (languageKind == RazorLanguageKind.CSharp + && CompletionTriggerCharacters.CSharpTriggerCharacters.Contains(triggerCharacter)) + { + // C# trigger character for C# content + return context; + } + + if (languageKind == RazorLanguageKind.Html + && CompletionTriggerCharacters.HtmlTriggerCharacters.Contains(triggerCharacter)) + { + // HTML trigger character for HTML content + return context; + } + + // Trigger character not associated with the current language. Transform the context into an invoked context. + var rewrittenContext = new VSInternalCompletionContext() + { + InvokeKind = context.InvokeKind, + TriggerKind = CompletionTriggerKind.Invoked, + }; + + if (languageKind == RazorLanguageKind.CSharp + && CompletionTriggerCharacters.RazorDelegationTriggerCharacters.Contains(triggerCharacter)) + { + // The C# language server will not return any completions for the '@' character unless we + // send the completion request explicitly. + rewrittenContext.InvokeKind = VSInternalCompletionInvokeKind.Explicit; + } + + return rewrittenContext; + } + + /// + /// Modifies C# completion response to be usable by Razor. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async ValueTask RewriteCSharpResponseAsync( + VSInternalCompletionList? delegatedResponse, + int absoluteIndex, + DocumentContext documentContext, + Position projectedPosition, + RazorCompletionOptions completionOptions, + CancellationToken cancellationToken) + { + if (delegatedResponse?.Items is null) + { + // If we don't get a response from the delegated server, we have to make sure to return an incomplete completion + // list. When a user is typing quickly, the delegated request from the first keystroke will fail to synchronize, + // so if we return a "complete" list then the query won't re-query us for completion once the typing stops/slows + // so we'd only ever return Razor completion items. + return new VSInternalCompletionList() { IsIncomplete = true, Items = [] }; + } + + var rewrittenResponse = delegatedResponse; + + foreach (var rewriter in s_delegatedCSharpCompletionResponseRewriters) + { + rewrittenResponse = await rewriter.RewriteAsync( + rewrittenResponse, + absoluteIndex, + documentContext, + projectedPosition, + completionOptions, + cancellationToken).ConfigureAwait(false); + } + + return rewrittenResponse; + } + + public static VSInternalCompletionList RewriteHtmlResponse( + VSInternalCompletionList? delegatedResponse, + RazorCompletionOptions completionOptions) + { + if (delegatedResponse?.Items is null) + { + // If we don't get a response from the delegated server, we have to make sure to return an incomplete completion + // list. When a user is typing quickly, the delegated request from the first keystroke will fail to synchronize, + // so if we return a "complete" list then the query won't re-query us for completion once the typing stops/slows + // so we'd only ever return Razor completion items. + return new VSInternalCompletionList() { IsIncomplete = true, Items = [] }; + } + + var rewrittenResponse = s_delegatedHtmlCompletionResponseRewriter.Rewrite( + delegatedResponse, + completionOptions); + + return rewrittenResponse; + } + + /// + /// Returns possibly update document position info and provisional edit (if any) + /// + /// + /// + /// Original position info + /// + /// + /// + /// + /// Provisional completion happens when typing something like @DateTime. in a document. + /// In this case the '.' initially is parsed as belonging to HTML. However, we want to + /// show C# member completion in this case, so we want to make a temporary change to the + /// generated C# code so that '.' ends up in C#. This method will check for such case, + /// and provisional completion case is detected, will update position language from HTML + /// to C# and will return a temporary edit that should be made to the generated document + /// in order to add the '.' to the generated C# contents. + /// + public static async Task TryGetProvisionalCompletionInfoAsync( + DocumentContext documentContext, + VSInternalCompletionContext completionContext, + DocumentPositionInfo positionInfo, + IDocumentMappingService documentMappingService, + CancellationToken cancellationToken) + { + if (positionInfo.LanguageKind != RazorLanguageKind.Html || + completionContext.TriggerKind != CompletionTriggerKind.TriggerCharacter || + completionContext.TriggerCharacter != ".") + { + // Invalid provisional completion context + return null; + } + + if (positionInfo.Position.Character == 0) + { + // We're at the start of line. Can't have provisional completions here. + return null; + } + + var previousCharacterPositionInfo = await documentMappingService + .GetPositionInfoAsync(documentContext, positionInfo.HostDocumentIndex - 1, cancellationToken) + .ConfigureAwait(false); + + if (previousCharacterPositionInfo.LanguageKind != RazorLanguageKind.CSharp) + { + return null; + } + + var previousPosition = previousCharacterPositionInfo.Position; + + // Edit the CSharp projected document to contain a '.'. This allows C# completion to provide valid + // completion items for moments when a user has typed a '.' that's typically interpreted as Html. + var addProvisionalDot = VsLspFactory.CreateTextEdit(previousPosition, "."); + + var provisionalPositionInfo = new DocumentPositionInfo( + RazorLanguageKind.CSharp, + VsLspFactory.CreatePosition( + previousPosition.Line, + previousPosition.Character + 1), + previousCharacterPositionInfo.HostDocumentIndex + 1); + + return new CompletionPositionInfo(addProvisionalDot, provisionalPositionInfo, ShouldIncludeDelegationSnippets: false); + } + + public static bool ShouldIncludeSnippets(RazorCodeDocument razorCodeDocument, int absoluteIndex) + { + var tree = razorCodeDocument.GetSyntaxTree(); + + var token = tree.Root.FindToken(absoluteIndex, includeWhitespace: false); + var node = token.Parent; + var startOrEndTag = node?.FirstAncestorOrSelf(n => RazorSyntaxFacts.IsAnyStartTag(n) || RazorSyntaxFacts.IsAnyEndTag(n)); + + if (startOrEndTag is null) + { + return token.Kind is not (SyntaxKind.OpenAngle or SyntaxKind.CloseAngle); + } + + if (startOrEndTag.Span.Start == absoluteIndex) + { + // We're at the start of the tag, we should include snippets. This is the case for things like $$
or
$$
, since the + // index is right associative to the token when using FindToken. + return true; + } + + return !startOrEndTag.Span.Contains(absoluteIndex); + } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DesignTimeHelperResponseRewriter.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DesignTimeHelperResponseRewriter.cs similarity index 86% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DesignTimeHelperResponseRewriter.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DesignTimeHelperResponseRewriter.cs index 675b2d27148..1b57f4909c9 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/DesignTimeHelperResponseRewriter.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/DesignTimeHelperResponseRewriter.cs @@ -5,20 +5,20 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; +namespace Microsoft.CodeAnalysis.Razor.Completion.Delegation; /// /// Removes Razor design-time helpers from a C# completion list. /// -internal class DesignTimeHelperResponseRewriter : DelegatedCompletionResponseRewriter +internal class DesignTimeHelperResponseRewriter : IDelegatedCSharpCompletionResponseRewriter { private static readonly ImmutableHashSet s_designTimeHelpers = new[] { @@ -32,20 +32,14 @@ internal class DesignTimeHelperResponseRewriter : DelegatedCompletionResponseRew "BuildRenderTree" }.ToImmutableHashSet(); - public override int Order => ExecutionBehaviorOrder.FiltersCompletionItems; - - public override async Task RewriteAsync( + public async Task RewriteAsync( VSInternalCompletionList completionList, int hostDocumentIndex, DocumentContext hostDocumentContext, - DelegatedCompletionParams delegatedParameters, + Position projectedPosition, + RazorCompletionOptions completionOptions, CancellationToken cancellationToken) { - if (delegatedParameters.ProjectedKind != RazorLanguageKind.CSharp) - { - return completionList; - } - var syntaxTree = await hostDocumentContext.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); var owner = syntaxTree.Root.FindInnermostNode(hostDocumentIndex); if (owner is null) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/HtmlCommitCharacterResponseRewriter.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/HtmlCommitCharacterResponseRewriter.cs similarity index 65% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/HtmlCommitCharacterResponseRewriter.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/HtmlCommitCharacterResponseRewriter.cs index 1bd58636764..5e34b3dd8f7 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/HtmlCommitCharacterResponseRewriter.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/HtmlCommitCharacterResponseRewriter.cs @@ -3,31 +3,19 @@ using System.Collections.Generic; using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Protocol; -using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.VisualStudio.LanguageServer.Protocol; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; +namespace Microsoft.CodeAnalysis.Razor.Completion.Delegation; -internal class HtmlCommitCharacterResponseRewriter(RazorLSPOptionsMonitor razorLSPOptionsMonitor) : DelegatedCompletionResponseRewriter +internal class HtmlCommitCharacterResponseRewriter { - private readonly RazorLSPOptionsMonitor _razorLSPOptionsMonitor = razorLSPOptionsMonitor; - - public override int Order => ExecutionBehaviorOrder.ChangesCompletionItems; - - public override Task RewriteAsync(VSInternalCompletionList completionList, int hostDocumentIndex, DocumentContext hostDocumentContext, DelegatedCompletionParams delegatedParameters, CancellationToken cancellationToken) + public VSInternalCompletionList Rewrite( + VSInternalCompletionList completionList, + RazorCompletionOptions completionOptions) { - if (delegatedParameters.ProjectedKind != RazorLanguageKind.Html) - { - return Task.FromResult(completionList); - } - - if (_razorLSPOptionsMonitor.CurrentValue.CommitElementsWithSpace) + if (completionOptions.CommitElementsWithSpace) { - return Task.FromResult(completionList); + return completionList; } string[]? itemCommitChars = null; @@ -75,6 +63,6 @@ public override Task RewriteAsync(VSInternalCompletion } } - return Task.FromResult(completionList); + return completionList; } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/IDelegatedCSharpCompletionResponseRewriter.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/IDelegatedCSharpCompletionResponseRewriter.cs new file mode 100644 index 00000000000..fa460cbde47 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/IDelegatedCSharpCompletionResponseRewriter.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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Razor.Completion.Delegation; + +internal interface IDelegatedCSharpCompletionResponseRewriter +{ + public Task RewriteAsync( + VSInternalCompletionList completionList, + int hostDocumentIndex, + DocumentContext hostDocumentContext, + Position projectedPosition, + RazorCompletionOptions completionOptions, + CancellationToken cancellationToken); +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/SnippetResponseRewriter.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/SnippetResponseRewriter.cs similarity index 77% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/SnippetResponseRewriter.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/SnippetResponseRewriter.cs index 85ceaa682a6..a000ff4f0a6 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/SnippetResponseRewriter.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/SnippetResponseRewriter.cs @@ -6,10 +6,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; +namespace Microsoft.CodeAnalysis.Razor.Completion.Delegation; /// /// Modifies delegated snippet completion items @@ -18,7 +17,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; /// At the moment primarily used to modify C# "using" snippet to "using statement" snippet /// in order to disambiguate it from Razor "using directive" snippet /// -internal class SnippetResponseRewriter : DelegatedCompletionResponseRewriter +internal class SnippetResponseRewriter : IDelegatedCSharpCompletionResponseRewriter { private static readonly FrozenDictionary s_snippetToCompletionData = new Dictionary() { @@ -29,9 +28,13 @@ internal class SnippetResponseRewriter : DelegatedCompletionResponseRewriter } .ToFrozenDictionary(); - public override int Order => ExecutionBehaviorOrder.ChangesCompletionItems; - - public override Task RewriteAsync(VSInternalCompletionList completionList, int hostDocumentIndex, DocumentContext hostDocumentContext, DelegatedCompletionParams delegatedParameters, CancellationToken cancellationToken) + public Task RewriteAsync( + VSInternalCompletionList completionList, + int hostDocumentIndex, + DocumentContext hostDocumentContext, + Position projectedPosition, + RazorCompletionOptions completionOptions, + CancellationToken cancellationToken) { foreach (var item in completionList.Items) { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/TextEditResponseRewriter.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/TextEditResponseRewriter.cs similarity index 83% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/TextEditResponseRewriter.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/TextEditResponseRewriter.cs index fff2e226c9b..120c4793638 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/Delegation/TextEditResponseRewriter.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/Delegation/TextEditResponseRewriter.cs @@ -4,42 +4,35 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; +namespace Microsoft.CodeAnalysis.Razor.Completion.Delegation; -internal class TextEditResponseRewriter : DelegatedCompletionResponseRewriter +internal class TextEditResponseRewriter : IDelegatedCSharpCompletionResponseRewriter { - public override int Order => ExecutionBehaviorOrder.ChangesCompletionItems; - - public override async Task RewriteAsync( + public async Task RewriteAsync( VSInternalCompletionList completionList, int hostDocumentIndex, DocumentContext hostDocumentContext, - DelegatedCompletionParams delegatedParameters, + Position projectedPosition, + RazorCompletionOptions completionOptions, CancellationToken cancellationToken) { - if (delegatedParameters.ProjectedKind != RazorLanguageKind.CSharp) - { - return completionList; - } - var sourceText = await hostDocumentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false); var hostDocumentPosition = sourceText.GetPosition(hostDocumentIndex); - completionList = TranslateTextEdits(hostDocumentPosition, delegatedParameters.ProjectedPosition, completionList); + completionList = TranslateTextEdits(hostDocumentPosition, projectedPosition, completionList); if (completionList.ItemDefaults?.EditRange is { } editRange) { if (editRange.TryGetFirst(out var range)) { - completionList.ItemDefaults.EditRange = TranslateRange(hostDocumentPosition, delegatedParameters.ProjectedPosition, range); + completionList.ItemDefaults.EditRange = TranslateRange(hostDocumentPosition, projectedPosition, range); } else if (editRange.TryGetSecond(out var insertReplaceRange)) { - insertReplaceRange.Insert = TranslateRange(hostDocumentPosition, delegatedParameters.ProjectedPosition, insertReplaceRange.Insert); - insertReplaceRange.Replace = TranslateRange(hostDocumentPosition, delegatedParameters.ProjectedPosition, insertReplaceRange.Replace); + insertReplaceRange.Insert = TranslateRange(hostDocumentPosition, projectedPosition, insertReplaceRange.Insert); + insertReplaceRange.Replace = TranslateRange(hostDocumentPosition, projectedPosition, insertReplaceRange.Replace); } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/DirectiveAttributeTransitionCompletionItemProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/DirectiveAttributeTransitionCompletionItemProvider.cs similarity index 97% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/DirectiveAttributeTransitionCompletionItemProvider.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/DirectiveAttributeTransitionCompletionItemProvider.cs index d3a0831a7bb..164c849d249 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/DirectiveAttributeTransitionCompletionItemProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/DirectiveAttributeTransitionCompletionItemProvider.cs @@ -5,10 +5,9 @@ using System.Collections.Immutable; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Syntax; -using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Text; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; internal class DirectiveAttributeTransitionCompletionItemProvider : DirectiveAttributeCompletionItemProviderBase { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/DirectiveCompletionItemProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/DirectiveCompletionItemProvider.cs index 8c83d70f305..9ceab30b29c 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/DirectiveCompletionItemProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/DirectiveCompletionItemProvider.cs @@ -31,7 +31,6 @@ internal class DirectiveCompletionItemProvider : IRazorCompletionItemProvider CSharpCodeParser.UsingDirectiveDescriptor }; - // Test accessor internal static IEnumerable MvcDefaultDirectives => s_mvcDefaultDirectives; internal static IEnumerable ComponentDefaultDirectives => s_componentDefaultDirectives; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/LspRazorCompletionFactsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/LspRazorCompletionFactsService.cs similarity index 78% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/LspRazorCompletionFactsService.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/LspRazorCompletionFactsService.cs index 165084b628c..9822140d6eb 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/LspRazorCompletionFactsService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/LspRazorCompletionFactsService.cs @@ -3,9 +3,8 @@ using System.Collections.Generic; using System.Collections.Immutable; -using Microsoft.CodeAnalysis.Razor.Completion; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; internal sealed class LspRazorCompletionFactsService(IEnumerable providers) : AbstractRazorCompletionFactsService(providers.ToImmutableArray()) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/MarkupTransitionCompletionDescription.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/MarkupTransitionCompletionDescription.cs similarity index 82% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/MarkupTransitionCompletionDescription.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/MarkupTransitionCompletionDescription.cs index 6fd0a551757..b85653659a7 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/MarkupTransitionCompletionDescription.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/MarkupTransitionCompletionDescription.cs @@ -2,9 +2,8 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; -using Microsoft.CodeAnalysis.Razor.Completion; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; internal class MarkupTransitionCompletionDescription : CompletionDescription { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/MarkupTransitionCompletionItemProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/MarkupTransitionCompletionItemProvider.cs similarity index 96% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/MarkupTransitionCompletionItemProvider.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/MarkupTransitionCompletionItemProvider.cs index 12b105acbce..b63d6545034 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/MarkupTransitionCompletionItemProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/MarkupTransitionCompletionItemProvider.cs @@ -6,12 +6,10 @@ using System.Diagnostics; using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Syntax; -using Microsoft.CodeAnalysis.Razor; -using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.VisualStudio.Editor.Razor; using RazorSyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; internal class MarkupTransitionCompletionItemProvider : IRazorCompletionItemProvider { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionItemExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionItemExtensions.cs index bf3d8f5d7c5..f83b8e411d3 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionItemExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionItemExtensions.cs @@ -11,45 +11,27 @@ internal static class RazorCompletionItemExtensions { private readonly static string s_attributeCompletionDescriptionKey = "Razor.AttributeDescription"; private readonly static string s_directiveCompletionDescriptionKey = "Razor.DirectiveDescription"; + private readonly static string s_markupTransitionDescriptionKey = "Razor.MarkupTransitionDescription"; + private readonly static string s_tagHelperElementCompletionDescriptionKey = "Razor.TagHelperElementDescription"; public static void SetAttributeCompletionDescription(this RazorCompletionItem completionItem, AggregateBoundAttributeDescription attributeCompletionDescription) { - if (completionItem is null) - { - throw new ArgumentNullException(nameof(completionItem)); - } - completionItem.Items[s_attributeCompletionDescriptionKey] = attributeCompletionDescription; } public static AggregateBoundAttributeDescription? GetAttributeCompletionDescription(this RazorCompletionItem completionItem) { - if (completionItem is null) - { - throw new ArgumentNullException(nameof(completionItem)); - } - var attributeCompletionDescription = completionItem.Items[s_attributeCompletionDescriptionKey] as AggregateBoundAttributeDescription; return attributeCompletionDescription; } public static void SetDirectiveCompletionDescription(this RazorCompletionItem completionItem, DirectiveCompletionDescription attributeCompletionDescription) { - if (completionItem is null) - { - throw new ArgumentNullException(nameof(completionItem)); - } - completionItem.Items[s_directiveCompletionDescriptionKey] = attributeCompletionDescription; } public static DirectiveCompletionDescription? GetDirectiveCompletionDescription(this RazorCompletionItem completionItem) { - if (completionItem is null) - { - throw new ArgumentNullException(nameof(completionItem)); - } - var attributeCompletionDescription = completionItem.Items[s_directiveCompletionDescriptionKey] as DirectiveCompletionDescription; return attributeCompletionDescription; } @@ -68,4 +50,26 @@ public static IEnumerable GetAttributeCompletionTypes(this RazorCompleti yield return descriptionInfo.ReturnTypeName; } } + + public static void SetMarkupTransitionCompletionDescription(this RazorCompletionItem completionItem, MarkupTransitionCompletionDescription markupTransitionCompletionDescription) + { + completionItem.Items[s_markupTransitionDescriptionKey] = markupTransitionCompletionDescription; + } + + public static MarkupTransitionCompletionDescription? GetMarkupTransitionCompletionDescription(this RazorCompletionItem completionItem) + { + var markupTransitionCompletionDescription = completionItem.Items[s_markupTransitionDescriptionKey] as MarkupTransitionCompletionDescription; + return markupTransitionCompletionDescription; + } + + public static void SetTagHelperElementDescriptionInfo(this RazorCompletionItem completionItem, AggregateBoundElementDescription elementDescriptionInfo) + { + completionItem.Items[s_tagHelperElementCompletionDescriptionKey] = elementDescriptionInfo; + } + + public static AggregateBoundElementDescription? GetTagHelperElementDescriptionInfo(this RazorCompletionItem completionItem) + { + var description = completionItem.Items[s_tagHelperElementCompletionDescriptionKey] as AggregateBoundElementDescription; + return description; + } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionListProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionListProvider.cs similarity index 97% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionListProvider.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionListProvider.cs index ea0b2a76a02..1585b98a91d 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionListProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionListProvider.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; @@ -11,12 +12,11 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.LanguageServer.Protocol; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; internal class RazorCompletionListProvider( IRazorCompletionFactsService completionFactsService, @@ -33,7 +33,7 @@ internal class RazorCompletionListProvider( }; // virtual for tests - public virtual ImmutableHashSet TriggerCharacters => new[] { "@", "<", ":", " " }.ToImmutableHashSet(); + public virtual FrozenSet TriggerCharacters => CompletionTriggerCharacters.RazorTriggerCharacters; // virtual for tests public virtual async Task GetCompletionListAsync( @@ -42,6 +42,7 @@ internal class RazorCompletionListProvider( DocumentContext documentContext, VSInternalClientCapabilities clientCapabilities, HashSet? existingCompletions, + RazorCompletionOptions completionOptions, CancellationToken cancellationToken) { if (!IsApplicableTriggerContext(completionContext)) @@ -57,7 +58,6 @@ internal class RazorCompletionListProvider( _ => CompletionReason.Typing, }; - var completionOptions = new RazorCompletionOptions(SnippetsSupported: true); var syntaxTree = await documentContext.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); var tagHelperContext = await documentContext.GetTagHelperContextAsync(cancellationToken).ConfigureAwait(false); var owner = syntaxTree.Root.FindInnermostNode(absoluteIndex, includeWhitespace: true, walkMarkersBack: true); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionOptions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionOptions.cs index de2d65c2406..0980d4b7f49 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionOptions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionOptions.cs @@ -3,4 +3,7 @@ namespace Microsoft.CodeAnalysis.Razor.Completion; -internal record struct RazorCompletionOptions(bool SnippetsSupported); +internal record struct RazorCompletionOptions( + bool SnippetsSupported, + bool AutoInsertAttributeQuotes, + bool CommitElementsWithSpace); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionResolveContext.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionResolveContext.cs similarity index 73% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionResolveContext.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionResolveContext.cs index 937b1a42709..11c10755ef7 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/RazorCompletionResolveContext.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/RazorCompletionResolveContext.cs @@ -2,8 +2,7 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System.Collections.Immutable; -using Microsoft.CodeAnalysis.Razor.Completion; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; internal record RazorCompletionResolveContext(string FilePath, ImmutableArray CompletionItems); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/TagHelperCompletionProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/TagHelperCompletionProvider.cs similarity index 96% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/TagHelperCompletionProvider.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/TagHelperCompletionProvider.cs index ba07f35576c..00c4fad030d 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/TagHelperCompletionProvider.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/TagHelperCompletionProvider.cs @@ -10,11 +10,12 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Razor.Tooltip; using Microsoft.VisualStudio.Editor.Razor; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; + +using SyntaxNode = AspNetCore.Razor.Language.Syntax.SyntaxNode; internal class TagHelperCompletionProvider : IRazorCompletionItemProvider { @@ -27,14 +28,11 @@ internal class TagHelperCompletionProvider : IRazorCompletionItemProvider private static readonly ImmutableArray s_elementCommitCharacters_WithoutSpace = RazorCommitCharacter.CreateArray([">"]); private readonly ITagHelperCompletionService _tagHelperCompletionService; - private readonly RazorLSPOptionsMonitor _optionsMonitor; public TagHelperCompletionProvider( - ITagHelperCompletionService tagHelperCompletionService, - RazorLSPOptionsMonitor optionsMonitor) + ITagHelperCompletionService tagHelperCompletionService) { _tagHelperCompletionService = tagHelperCompletionService; - _optionsMonitor = optionsMonitor; } public ImmutableArray GetCompletionItems(RazorCompletionContext context) @@ -172,7 +170,7 @@ private ImmutableArray GetAttributeCompletions( // Do not turn attributes into snippets if we are in an already written full attribute (https://github.com/dotnet/razor-tooling/issues/6724) if (containingAttribute is not (MarkupTagHelperAttributeSyntax or MarkupAttributeBlockSyntax) && - TryResolveInsertText(insertText, attributeContext, out var snippetText)) + TryResolveInsertText(insertText, attributeContext, options.AutoInsertAttributeQuotes, out var snippetText)) { isSnippet = true; insertText = snippetText; @@ -209,11 +207,11 @@ private ImmutableArray GetAttributeCompletions( return completionItems.DrainToImmutable(); } - private bool TryResolveInsertText(string baseInsertText, AttributeContext context, [NotNullWhen(true)] out string? snippetText) + private bool TryResolveInsertText(string baseInsertText, AttributeContext context, bool autoInsertAttributeQuotes, [NotNullWhen(true)] out string? snippetText) { if (context == AttributeContext.FullSnippet) { - snippetText = _optionsMonitor.CurrentValue.AutoInsertAttributeQuotes + snippetText = autoInsertAttributeQuotes ? $"{baseInsertText}=\"$0\"" : $"{baseInsertText}=$0"; @@ -244,7 +242,7 @@ private ImmutableArray GetElementCompletions( var completionResult = _tagHelperCompletionService.GetElementCompletions(elementCompletionContext); using var completionItems = new PooledArrayBuilder(); - var commitChars = _optionsMonitor.CurrentValue.CommitElementsWithSpace + var commitChars = context.Options.CommitElementsWithSpace ? s_elementCommitCharacters : s_elementCommitCharacters_WithoutSpace; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/LspTagHelperCompletionService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/TagHelperCompletionService.cs similarity index 98% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/LspTagHelperCompletionService.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/TagHelperCompletionService.cs index a286267c233..e4bcd48662c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/LspTagHelperCompletionService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/TagHelperCompletionService.cs @@ -8,13 +8,12 @@ using System.Linq; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.VisualStudio.Editor.Razor; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; -internal class LspTagHelperCompletionService : ITagHelperCompletionService +internal class TagHelperCompletionService : ITagHelperCompletionService { private static readonly HashSet s_emptyHashSet = new(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/VSInternalCompletionItemExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/VSInternalCompletionItemExtensions.cs similarity index 95% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/VSInternalCompletionItemExtensions.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/VSInternalCompletionItemExtensions.cs index c52fe46804c..e4f4330726c 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/VSInternalCompletionItemExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/VSInternalCompletionItemExtensions.cs @@ -4,12 +4,10 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.VisualStudio.LanguageServer.Protocol; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; internal static class VSInternalCompletionItemExtensions { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/VSInternalCompletionListExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/VSInternalCompletionListExtensions.cs similarity index 95% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/VSInternalCompletionListExtensions.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/VSInternalCompletionListExtensions.cs index 94a502eb190..6e4b52b9cb3 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Completion/VSInternalCompletionListExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Completion/VSInternalCompletionListExtensions.cs @@ -1,13 +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.Linq; using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.VisualStudio.LanguageServer.Protocol; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; internal static class VSInternalCompletionListExtensions { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/DocumentPositionInfo.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/DocumentPositionInfo.cs index bbb745aea2c..07524302032 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/DocumentPositionInfo.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/DocumentMapping/DocumentPositionInfo.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.Text.Json.Serialization; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -10,4 +11,11 @@ namespace Microsoft.CodeAnalysis.Razor.DocumentMapping; /// Represents a position in a document. If is Razor then the position will be /// in the host document, otherwise it will be in the corresponding generated document. /// -internal readonly record struct DocumentPositionInfo(RazorLanguageKind LanguageKind, Position Position, int HostDocumentIndex); +internal record struct DocumentPositionInfo( + + [property:JsonPropertyName("languageKind")] RazorLanguageKind LanguageKind, + + [property: JsonPropertyName("position")] Position Position, + + [property:JsonPropertyName("hostDocumentIndex")] int HostDocumentIndex); + diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/Competion/CompletionPositionInfo.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/Competion/CompletionPositionInfo.cs new file mode 100644 index 00000000000..159bc27b2af --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/Competion/CompletionPositionInfo.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.Text.Json.Serialization; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Razor.Protocol.Completion; + +/// +/// Completion-related information about a position. +/// +/// Text edit that should be applied to generated C# document prior to invoking completion +/// +/// Provisional completion happens when the user just type "." in something like @DateTime. +/// and the dot is initially in HTML rather than C#. Since we don't want HTML completions +/// in that case, we cheat and modify C# buffer immediately but temporarily, not waiting for +/// reparse/regen, before showing completion. +/// +/// Document position mapping data for language mappings +/// Indicates that snippets should be added to delegated completion list (currently for HTML only) +internal record struct CompletionPositionInfo( + [property:JsonPropertyName("provisionalTextEdit")] TextEdit? ProvisionalTextEdit, + [property:JsonPropertyName("documentPositionInfo")] DocumentPositionInfo DocumentPositionInfo, + [property:JsonPropertyName("shouldIncludeDelegationSnippets")] bool ShouldIncludeDelegationSnippets); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteCompletionService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteCompletionService.cs new file mode 100644 index 00000000000..d57cb2f4c00 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteCompletionService.cs @@ -0,0 +1,33 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor.Completion; +using Microsoft.CodeAnalysis.Razor.Protocol.Completion; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse; + +namespace Microsoft.CodeAnalysis.Razor.Remote; + +internal interface IRemoteCompletionService : IRemoteJsonService +{ + ValueTask GetPositionInfoAsync( + JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, + JsonSerializableDocumentId documentId, + VSInternalCompletionContext completionContext, + Position position, + CancellationToken cancellationToken); + + ValueTask GetCompletionAsync( + JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, + JsonSerializableDocumentId documentId, + CompletionPositionInfo positionInfo, + VSInternalCompletionContext completionContext, + VSInternalClientCapabilities clientCapabilities, + RazorCompletionOptions razorCompletionOptions, + HashSet existingHtmlCompletions, + 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 2e4257cdb3c..8ced461079b 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs @@ -36,6 +36,7 @@ internal static class RazorServices (typeof(IRemoteRenameService), null), (typeof(IRemoteGoToImplementationService), null), (typeof(IRemoteDiagnosticsService), null), + (typeof(IRemoteCompletionService), null), ]; private const string ComponentName = "Razor"; 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 e0a0efc1cdd..3169d37cb1f 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/SR.resx +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/SR.resx @@ -172,4 +172,13 @@ Razor TagHelper Element Glyph + + "Re-trigger completions..." + + + Blazor directive attributes + + + statement + \ 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 31f86168480..a525119117e 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 @@ -7,6 +7,11 @@ Hodnota nesmí být null ani prázdný řetězec.
+ + Blazor directive attributes + Blazor directive attributes + + Diagnostics after: Diagnostika po: @@ -74,6 +79,11 @@ Proběhl dotaz na řádek {0} mimo rozsah {1} {2}. Dokument nemusí být aktuální. + + statement + statement + + Razor TagHelper Attribute Glyph Piktogram atributu Razor TagHelper @@ -84,6 +94,11 @@ Piktogram elementu Razor TagHelper + + "Re-trigger completions..." + "Re-trigger completions..." + + 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 1726a2f73d0..60480c673e9 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 @@ -7,6 +7,11 @@ Der Wert darf nicht NULL oder eine leere Zeichenfolge sein. + + Blazor directive attributes + Blazor directive attributes + + Diagnostics after: Diagnose nach: @@ -74,6 +79,11 @@ Die Zeile "{0}" außerhalb des {1} Bereichs von "{2}" wurde abgefragt. Das Dokument ist möglicherweise nicht auf dem neuesten Stand. + + statement + statement + + Razor TagHelper Attribute Glyph Razor TagHelper-Attributsymbol @@ -84,6 +94,11 @@ Razor TagHelper-Elementsymbol + + "Re-trigger completions..." + "Re-trigger completions..." + + 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 af8cec0ae9d..9505ac9e5b1 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 @@ -7,6 +7,11 @@ El valor no puede ser nulo ni una cadena vacía. + + Blazor directive attributes + Blazor directive attributes + + Diagnostics after: Diagnósticos después de: @@ -74,6 +79,11 @@ La línea '{0}' se consultó fuera del {1} rango de '{2}'. Es posible que el documento no esté actualizado. + + statement + statement + + Razor TagHelper Attribute Glyph Glifo del atributo TagHelper de Razor @@ -84,6 +94,11 @@ Glifo del elemento TagHelper de Razor + + "Re-trigger completions..." + "Re-trigger completions..." + + 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 83af245e366..46b4b0f6049 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 @@ -7,6 +7,11 @@ La valeur ne peut pas être Null ni être une chaîne vide. + + Blazor directive attributes + Blazor directive attributes + + Diagnostics after: Diagnostics après : @@ -74,6 +79,11 @@ La ligne «{0}» en dehors de la plage{1} de «{2}» a été interrogée. Le document n’est peut-être pas à jour. + + statement + statement + + Razor TagHelper Attribute Glyph Glyphe d’attribut Razor TagHelper @@ -84,6 +94,11 @@ Glyphe de l’élément Razor TagHelper + + "Re-trigger completions..." + "Re-trigger completions..." + + 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 5317948e62d..b531d08d38e 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 @@ -7,6 +7,11 @@ Il valore non può essere null o una stringa vuota. + + Blazor directive attributes + Blazor directive attributes + + Diagnostics after: Diagnostica dopo: @@ -74,6 +79,11 @@ È stata eseguita una query sulla riga '{0}' non compresa nell'intervallo {1} di '{2}'. Il documento potrebbe non essere aggiornato. + + statement + statement + + Razor TagHelper Attribute Glyph Glifo attributo TagHelper Razor @@ -84,6 +94,11 @@ Glifo elemento TagHelper Razor + + "Re-trigger completions..." + "Re-trigger completions..." + + 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 55bbf78dcd9..fe38fd32116 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 @@ -7,6 +7,11 @@ 値を null または空の文字列にすることはできません。 + + Blazor directive attributes + Blazor directive attributes + + Diagnostics after: 次の時間が経過した後の診断: @@ -74,6 +79,11 @@ '{2}' の {1} 範囲外の行 '{0}' がクエリされました。ドキュメントが最新ではない可能性があります。 + + statement + statement + + Razor TagHelper Attribute Glyph Razor TagHelper 属性のグリフ @@ -84,6 +94,11 @@ Razor TagHelper 要素のグリフ + + "Re-trigger completions..." + "Re-trigger completions..." + + 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 2931b15ec1e..3b5af63e4f9 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 @@ -7,6 +7,11 @@ 값은 null이거나 빈 문자열일 수 없습니다. + + Blazor directive attributes + Blazor directive attributes + + Diagnostics after: 다음 이후 진단: @@ -74,6 +79,11 @@ '{2}'의 {1} 범위를 벗어나는 줄 '{0}'을(를) 쿼리했습니다. 문서가 최신이 아닐 수 있습니다. + + statement + statement + + Razor TagHelper Attribute Glyph Razor TagHelper 특성 문자 모양 @@ -84,6 +94,11 @@ Razor TagHelper 요소 문자 모양 + + "Re-trigger completions..." + "Re-trigger completions..." + + 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 60f05b66521..c32348f165b 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 @@ -7,6 +7,11 @@ Wartość nie może być wartością null ani pustym ciągiem. + + Blazor directive attributes + Blazor directive attributes + + Diagnostics after: Diagnostyka po: @@ -74,6 +79,11 @@ Wykonano zapytanie wiersza "{0}" poza zakresem {1} "{2}". Dokument może być nieaktualny. + + statement + statement + + Razor TagHelper Attribute Glyph Symbol atrybutu pomocnika tagów składni Razor @@ -84,6 +94,11 @@ Symbol elementu pomocnika tagów składni Razor + + "Re-trigger completions..." + "Re-trigger completions..." + + 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 e834585d7ed..c5489e591f9 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 @@ -7,6 +7,11 @@ O valor não pode ser nulo ou uma cadeia de caracteres vazia. + + Blazor directive attributes + Blazor directive attributes + + Diagnostics after: Diagnóstico após: @@ -74,6 +79,11 @@ A linha '{0}' fora do intervalo {1} de '{2}' foi consultada. O documento pode não estar atualizado. + + statement + statement + + Razor TagHelper Attribute Glyph Atributo Glyph Razor TagHelper @@ -84,6 +94,11 @@ Elemento Glyph Razor TagHelper + + "Re-trigger completions..." + "Re-trigger completions..." + + 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 b4577c8ec99..a147db65cf6 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 @@ -7,6 +7,11 @@ Значение не может быть NULL или пустой строкой. + + Blazor directive attributes + Blazor directive attributes + + Diagnostics after: Диагностика после: @@ -74,6 +79,11 @@ Запрошена строка "{0}" за пределами диапазона {1} "{2}". Возможно, документ не обновлен. + + statement + statement + + Razor TagHelper Attribute Glyph Глиф атрибута TagHelper Razor @@ -84,6 +94,11 @@ Глиф элемента TagHelper Razor + + "Re-trigger completions..." + "Re-trigger completions..." + + 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 0faa1367043..57ed7cbb401 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 @@ -7,6 +7,11 @@ Değer null veya boş bir dize olamaz. + + Blazor directive attributes + Blazor directive attributes + + Diagnostics after: Şundan sonraki tanılama: @@ -74,6 +79,11 @@ {1} / '{2}' aralığının dışındaki '{0}' satırı sorgulandı. Belge güncel olmayabilir. + + statement + statement + + Razor TagHelper Attribute Glyph Razor TagHelper Öznitelik Karakteri @@ -84,6 +94,11 @@ Razor TagHelper Element Karakteri + + "Re-trigger completions..." + "Re-trigger completions..." + + 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 51764818206..e2ca9b6f9c4 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 @@ -7,6 +7,11 @@ 值不能为 null 或空字符串。 + + Blazor directive attributes + Blazor directive attributes + + Diagnostics after: 诊断后: @@ -74,6 +79,11 @@ 查询了 "{0}" 的 {1} 范围外的行 "{2}"。文档可能不是最新的。 + + statement + statement + + Razor TagHelper Attribute Glyph Razor TagHelper 特性字形 @@ -84,6 +94,11 @@ Razor TagHelper 元素字形 + + "Re-trigger completions..." + "Re-trigger completions..." + + 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 74d00b2d74b..a7e3766365a 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 @@ -7,6 +7,11 @@ 值不能為 Null 或空字串。 + + Blazor directive attributes + Blazor directive attributes + + Diagnostics after: 在以下事項之後的診斷: @@ -74,6 +79,11 @@ 已查詢 '{2}' 之 {1} 範圍以外的行 '{0}'。文件可能不是最新狀態。 + + statement + statement + + Razor TagHelper Attribute Glyph Razor TagHelper 屬性字元 @@ -84,6 +94,11 @@ Razor TagHelper 元素字符 + + "Re-trigger completions..." + "Re-trigger completions..." + + Attempted to visit a RazorMetaCode other than '{' or '}'. 嘗試瀏覽 '{' or '}' 除外的 RazorMetaCode。 diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/OOPCompletionListCache.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/OOPCompletionListCache.cs new file mode 100644 index 00000000000..6f8e4af1298 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/OOPCompletionListCache.cs @@ -0,0 +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.Composition; +using Microsoft.CodeAnalysis.Razor.Completion; + +namespace Microsoft.CodeAnalysis.Remote.Razor.Completion; + +[Export(typeof(CompletionListCache)), Shared] +internal sealed class OOPCompletionListCache : CompletionListCache +{ +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/OOPRazorCompletionFactsService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/OOPRazorCompletionFactsService.cs new file mode 100644 index 00000000000..411e1679943 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/OOPRazorCompletionFactsService.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.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis.Razor.Completion; + +namespace Microsoft.CodeAnalysis.Remote.Razor.Completion; + +[Export(typeof(IRazorCompletionFactsService)), Shared] +[method: ImportingConstructor] +internal sealed class OOPRazorCompletionFactsService([ImportMany] IEnumerable providers) + : AbstractRazorCompletionFactsService(providers.ToImmutableArray()) +{ +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/OOPRazorCompletionItemProviders.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/OOPRazorCompletionItemProviders.cs new file mode 100644 index 00000000000..438e73b4d81 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/OOPRazorCompletionItemProviders.cs @@ -0,0 +1,27 @@ +// 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.Completion; + +namespace Microsoft.CodeAnalysis.Remote.Razor.Completion; + +[Export(typeof(IRazorCompletionItemProvider)), Shared] +internal sealed class OOPDirectiveCompletionItemProvider : DirectiveCompletionItemProvider; + +[Export(typeof(IRazorCompletionItemProvider)), Shared] +internal sealed class OOPDirectiveAttributeCompletionItemProvider : DirectiveAttributeCompletionItemProvider; + +[Export(typeof(IRazorCompletionItemProvider)), Shared] +internal sealed class OOPDirectiveAttributeParameterCompletionItemProvider : DirectiveAttributeParameterCompletionItemProvider; + +[Export(typeof(IRazorCompletionItemProvider)), Shared] +internal sealed class OOPDirectiveAttributeTransitionCompletionItemProvider : DirectiveAttributeTransitionCompletionItemProvider; + +[Export(typeof(IRazorCompletionItemProvider)), Shared] +internal sealed class OOPMarkupTransitionCompletionItemProvider : MarkupTransitionCompletionItemProvider; + +[Export(typeof(IRazorCompletionItemProvider)), Shared] +[method: ImportingConstructor] +internal sealed class OOPTagHelperCompletionProvider(ITagHelperCompletionService tagHelperCompletionService) + : TagHelperCompletionProvider(tagHelperCompletionService); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/OOPRazorCompletionListProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/OOPRazorCompletionListProvider.cs new file mode 100644 index 00000000000..4d6776cc816 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/OOPRazorCompletionListProvider.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.Composition; +using Microsoft.CodeAnalysis.Razor.Completion; +using Microsoft.CodeAnalysis.Razor.Logging; + +namespace Microsoft.CodeAnalysis.Remote.Razor.Completion; + +[Export(typeof(RazorCompletionListProvider)), Shared] +[method: ImportingConstructor] +internal sealed class OOPRazorCompletionListProvider( + IRazorCompletionFactsService completionFactsService, + CompletionListCache completionListCache, + ILoggerFactory loggerFactory) +: RazorCompletionListProvider(completionFactsService, completionListCache, loggerFactory) +{ +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/OOPTagHelperCompletionService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/OOPTagHelperCompletionService.cs new file mode 100644 index 00000000000..adc9c10fa69 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/OOPTagHelperCompletionService.cs @@ -0,0 +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.Composition; +using Microsoft.CodeAnalysis.Razor.Completion; + +namespace Microsoft.CodeAnalysis.Remote.Razor.Completion; + +[Export(typeof(ITagHelperCompletionService)), Shared] +internal sealed class OOPTagHelperCompletionService : TagHelperCompletionService +{ +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs new file mode 100644 index 00000000000..37a6e737c59 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/Completion/RemoteCompletionService.cs @@ -0,0 +1,228 @@ +// 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.Diagnostics; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor.Completion; +using Microsoft.CodeAnalysis.Razor.Completion.Delegation; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Protocol.Completion; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse; +using RoslynCompletionContext = Roslyn.LanguageServer.Protocol.CompletionContext; +using RoslynCompletionSetting = Roslyn.LanguageServer.Protocol.CompletionSetting; + +namespace Microsoft.CodeAnalysis.Remote.Razor; + +internal sealed class RemoteCompletionService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteCompletionService +{ + internal sealed class Factory : FactoryBase + { + protected override IRemoteCompletionService CreateService(in ServiceArgs args) + => new RemoteCompletionService(in args); + } + + private readonly RazorCompletionListProvider _razorCompletionListProvider = args.ExportProvider.GetExportedValue(); + + public ValueTask GetPositionInfoAsync( + JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, + JsonSerializableDocumentId documentId, + VSInternalCompletionContext completionContext, + Position position, + CancellationToken cancellationToken) + => RunServiceAsync( + solutionInfo, + documentId, + context => GetPositionInfoAsync(context, completionContext, position, cancellationToken), + cancellationToken); + + private async ValueTask GetPositionInfoAsync( + RemoteDocumentContext remoteDocumentContext, + VSInternalCompletionContext completionContext, + Position position, + CancellationToken cancellationToken) + { + var sourceText = await remoteDocumentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false); + if (!sourceText.TryGetAbsoluteIndex(position, out var index)) + { + return null; + } + + var codeDocument = await remoteDocumentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + + var positionInfo = GetPositionInfo(codeDocument, index); + + if(positionInfo.LanguageKind != RazorLanguageKind.Razor + && await DelegatedCompletionHelper.TryGetProvisionalCompletionInfoAsync( + remoteDocumentContext, + completionContext, + positionInfo, + DocumentMappingService, + cancellationToken) is { } provisionalCompletionInfo) + { + return new CompletionPositionInfo( + provisionalCompletionInfo.ProvisionalTextEdit, + provisionalCompletionInfo.DocumentPositionInfo, + ShouldIncludeDelegationSnippets: false); + } + + var shouldIncludeSnippets = positionInfo.LanguageKind == RazorLanguageKind.Html + && DelegatedCompletionHelper.ShouldIncludeSnippets(codeDocument, index); + + return new CompletionPositionInfo(ProvisionalTextEdit: null, positionInfo, shouldIncludeSnippets); + } + + public ValueTask GetCompletionAsync( + JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, + JsonSerializableDocumentId documentId, + CompletionPositionInfo positionInfo, + VSInternalCompletionContext completionContext, + VSInternalClientCapabilities clientCapabilities, + RazorCompletionOptions razorCompletionOptions, + HashSet existingHtmlCompletions, + CancellationToken cancellationToken) + => RunServiceAsync( + solutionInfo, + documentId, + context => GetCompletionAsync( + context, + positionInfo, + completionContext, + clientCapabilities, + razorCompletionOptions, + existingHtmlCompletions, + cancellationToken), + cancellationToken); + + private async ValueTask GetCompletionAsync( + RemoteDocumentContext remoteDocumentContext, + CompletionPositionInfo positionInfo, + VSInternalCompletionContext completionContext, + VSInternalClientCapabilities clientCapabilities, + RazorCompletionOptions razorCompletionOptions, + HashSet existingDelegatedCompletions, + CancellationToken cancellationToken) + { + VSInternalCompletionList? csharpCompletionList = null; + var documentPositionInfo = positionInfo.DocumentPositionInfo; + if (documentPositionInfo.LanguageKind == RazorLanguageKind.CSharp && + CompletionTriggerCharacters.IsValidTrigger(CompletionTriggerCharacters.CSharpTriggerCharacters, completionContext)) + { + var mappedPosition = documentPositionInfo.Position; + csharpCompletionList = await GetCSharpCompletionAsync( + remoteDocumentContext, + documentPositionInfo.HostDocumentIndex, + mappedPosition, + positionInfo.ProvisionalTextEdit, + completionContext, + clientCapabilities, + razorCompletionOptions, + cancellationToken); + + if (csharpCompletionList is not null) + { + Debug.Assert(existingDelegatedCompletions.Count == 0, "Delegated completion should be either C# or HTML, not both"); + existingDelegatedCompletions.UnionWith(csharpCompletionList.Items.Select((item) => item.Label)); + } + } + + var razorCompletionList = CompletionTriggerCharacters.IsValidTrigger(CompletionTriggerCharacters.RazorTriggerCharacters, completionContext) + ? await _razorCompletionListProvider.GetCompletionListAsync( + documentPositionInfo.HostDocumentIndex, + completionContext, + remoteDocumentContext, + clientCapabilities, + existingCompletions: existingDelegatedCompletions, + razorCompletionOptions, + cancellationToken) + : null; + + if (CompletionListMerger.Merge(razorCompletionList, csharpCompletionList) is not { } mergedCompletionList) + { + return Response.CallHtml; + } + + return Response.Results(mergedCompletionList); + } + + private async ValueTask GetCSharpCompletionAsync( + RemoteDocumentContext remoteDocumentContext, + int documentIndex, + Position mappedPosition, + TextEdit? provisionalTextEdit, + CompletionContext completionContext, + VSInternalClientCapabilities clientCapabilities, + RazorCompletionOptions razorCompletionOptions, + CancellationToken cancellationToken) + { + var generatedDocument = await remoteDocumentContext.Snapshot + .GetGeneratedDocumentAsync(cancellationToken).ConfigureAwait(false); + if (provisionalTextEdit is not null) + { + var generatedText = await generatedDocument.GetTextAsync(cancellationToken); + var startIndex = generatedText.GetPosition(provisionalTextEdit.Range.Start); + var endIndex = generatedText.GetPosition(provisionalTextEdit.Range.End); + var generatedTextWithEdit = generatedText.Replace(startIndex, length: endIndex - startIndex, newText: provisionalTextEdit.NewText); + generatedDocument = generatedDocument.WithText(generatedTextWithEdit); + } + + // This is, to say the least, not ideal. In future we're going to normalize on to Roslyn LSP types, and this can go. + var options = new JsonSerializerOptions(); + foreach (var converter in RazorServiceDescriptorsWrapper.GetLspConverters()) + { + options.Converters.Add(converter); + } + + if (JsonSerializer.Deserialize(JsonSerializer.SerializeToDocument(completionContext), options) is not { } roslynCompletionContext) + { + return null; + } + + if (JsonSerializer.Deserialize(JsonSerializer.SerializeToDocument(clientCapabilities.TextDocument?.Completion), options) is not { } roslynCompletionSetting) + { + return null; + } + + var mappedLinePosition = mappedPosition.ToLinePosition(); + var roslynCompletionList = await ExternalAccess.Razor.Cohost.Handlers.Completion.GetCompletionListAsync( + generatedDocument, + mappedLinePosition, + roslynCompletionContext, + clientCapabilities.SupportsVisualStudioExtensions, + roslynCompletionSetting, + cancellationToken); + + if (roslynCompletionList is null) + { + // If we don't get a response from the delegated server, we have to make sure to return an incomplete completion + // list. When a user is typing quickly, the delegated request from the first keystroke will fail to synchronize, + // so if we return a "complete" list then the query won't re-query us for completion once the typing stops/slows + // so we'd only ever return Razor completion items. + return new VSInternalCompletionList() + { + Items = [], + IsIncomplete = true + }; + } + + var vsPlatformCompletionList = JsonSerializer.Deserialize(JsonSerializer.SerializeToDocument(roslynCompletionList), options); + + var rewrittenResponse = await DelegatedCompletionHelper.RewriteCSharpResponseAsync( + vsPlatformCompletionList, + documentIndex, + remoteDocumentContext, + mappedPosition, + razorCompletionOptions, + cancellationToken); + + return rewrittenResponse; + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionEndpoint.cs new file mode 100644 index 00000000000..4dbea57e652 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentCompletionEndpoint.cs @@ -0,0 +1,295 @@ +// 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.Composition; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.CodeAnalysis.Razor.Completion; +using Microsoft.CodeAnalysis.Razor.Completion.Delegation; +using Microsoft.CodeAnalysis.Razor.Logging; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.Protocol.Completion; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.Razor.Settings; +using Microsoft.VisualStudio.Razor.Snippets; +using Response = Microsoft.CodeAnalysis.Razor.Remote.RemoteResponse; +using RoslynCompletionParams = Roslyn.LanguageServer.Protocol.CompletionParams; +using RoslynLspExtensions = Roslyn.LanguageServer.Protocol.RoslynLspExtensions; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +#pragma warning disable RS0030 // Do not use banned APIs +[Shared] +[CohostEndpoint(Methods.TextDocumentCompletionName)] +[Export(typeof(IDynamicRegistrationProvider))] +[ExportCohostStatelessLspService(typeof(CohostDocumentCompletionEndpoint))] +[method: ImportingConstructor] +#pragma warning restore RS0030 // Do not use banned APIs +internal class CohostDocumentCompletionEndpoint( + IRemoteServiceInvoker remoteServiceInvoker, + IClientSettingsManager clientSettingsManager, + IHtmlDocumentSynchronizer htmlDocumentSynchronizer, + SnippetCompletionItemProvider snippetCompletionItemProvider, + LSPRequestInvoker requestInvoker, + ILoggerFactory loggerFactory) + : AbstractRazorCohostDocumentRequestHandler, IDynamicRegistrationProvider +{ + private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker; + private readonly IClientSettingsManager _clientSettingsManager = clientSettingsManager; + private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer = htmlDocumentSynchronizer; + private readonly SnippetCompletionItemProvider _snippetCompletionItemProvider = snippetCompletionItemProvider; + private readonly LSPRequestInvoker _requestInvoker = requestInvoker; + private readonly ILogger _logger = loggerFactory.GetOrCreateLogger(); + + private VSInternalClientCapabilities? _clientCapabilities; + + protected override bool MutatesSolutionState => false; + + protected override bool RequiresLSPSolution => true; + + public ImmutableArray GetRegistrations(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + { + _clientCapabilities = clientCapabilities; + + if (clientCapabilities.TextDocument?.Completion?.DynamicRegistration is true) + { + return [new Registration() + { + Method = Methods.TextDocumentCompletionName, + RegisterOptions = new CompletionRegistrationOptions() + { + ResolveProvider = true, + TriggerCharacters = CompletionTriggerCharacters.AllTriggerCharacters, + DocumentSelector = filter, + AllCommitCharacters = [" ", ">", ";", "="] + } + }]; + } + + return []; + } + + protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(RoslynCompletionParams request) + => request.TextDocument is null ? null : RoslynLspExtensions.ToRazorTextDocumentIdentifier(request.TextDocument); + + protected override Task HandleRequestAsync(RoslynCompletionParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + => HandleRequestAsync(request, context.TextDocument.AssumeNotNull(), cancellationToken); + + private async Task HandleRequestAsync(RoslynCompletionParams request, TextDocument razorDocument, CancellationToken cancellationToken) + { + if (request.Context is null || ToVsLSP(request.Context) is not VSInternalCompletionContext completionContext) + { + _logger.LogError("Completion request context is null"); + return null; + } + + // Return immediately if this is auto-shown completion but auto-shown completion is disallowed in settings + var clientSettings = _clientSettingsManager.GetClientSettings(); + var autoShownCompletion = completionContext.TriggerKind != CompletionTriggerKind.Invoked; + if (autoShownCompletion && !clientSettings.ClientCompletionSettings.AutoShowCompletion) + { + return null; + } + + _logger.LogDebug($"Invoking completion for {razorDocument.FilePath}"); + + if (await _remoteServiceInvoker.TryInvokeAsync( + razorDocument.Project.Solution, + (service, solutionInfo, cancellationToken) + => service.GetPositionInfoAsync( + solutionInfo, + razorDocument.Id, + completionContext, + ToVsLSP(request.Position).AssumeNotNull(), + cancellationToken), + cancellationToken).ConfigureAwait(false) is not { } completionPositionInfo) + { + // If we can't figure out position info for request position we can't return completions + return null; + } + + var documentPositionInfo = completionPositionInfo.DocumentPositionInfo; + if (documentPositionInfo.LanguageKind != RazorLanguageKind.Razor) + { + completionContext = DelegatedCompletionHelper.RewriteContext(completionContext, documentPositionInfo.LanguageKind); + } + + // First of all, see if we in HTML and get HTML completions before calling OOP to get Razor completions. + // Razor completion provider needs a set of existing HTML item labels. + + VSInternalCompletionList? htmlCompletionList = null; + var razorCompletionOptions = new RazorCompletionOptions( + SnippetsSupported: true, // always true in non-legacy Razor + AutoInsertAttributeQuotes: clientSettings.AdvancedSettings.AutoInsertAttributeQuotes, + CommitElementsWithSpace: clientSettings.AdvancedSettings.CommitElementsWithSpace); + using var _ = HashSetPool.GetPooledObject(out var existingHtmlCompletions); + + if (CompletionTriggerCharacters.IsValidTrigger(CompletionTriggerCharacters.HtmlTriggerCharacters, completionContext)) + { + // We can just blindly call HTML LSP because if we are in C#, generated HTML seen by HTML LSP may return + // results we don't want to show. So we want to call HTML LSP only if we know we are in HTML content. + if (documentPositionInfo.LanguageKind == RazorLanguageKind.Html) + { + htmlCompletionList = await GetHtmlCompletionListAsync(request, razorDocument, razorCompletionOptions, cancellationToken); + if (htmlCompletionList is not null) + { + existingHtmlCompletions.UnionWith(htmlCompletionList.Items.Select(i => i.Label)); + } + } + } + + _logger.LogDebug($"Calling OOP to get completion items at {request.Position} invoked by typing '{request.Context?.TriggerCharacter}'"); + + var data = await _remoteServiceInvoker.TryInvokeAsync( + razorDocument.Project.Solution, + (service, solutionInfo, cancellationToken) + => service.GetCompletionAsync( + solutionInfo, + razorDocument.Id, + completionPositionInfo, + completionContext, + _clientCapabilities.AssumeNotNull(), + razorCompletionOptions, + existingHtmlCompletions, + cancellationToken), + cancellationToken).ConfigureAwait(false); + + if (data.StopHandling) + { + return null; + } + + VSInternalCompletionList? combinedCompletionList = null; + if (data.Result is { } oopCompletionList) + { + combinedCompletionList = htmlCompletionList?.Items is not null && htmlCompletionList.Items.Length > 0 + // If we have HTML completions, that means OOP completion list is really just Razor completion list + ? CompletionListMerger.Merge(oopCompletionList, htmlCompletionList) + : oopCompletionList; + } + else + { + // Didn't get anything from OOP, so just return HTML completion list or null + combinedCompletionList = htmlCompletionList; + } + + if (completionPositionInfo.ShouldIncludeDelegationSnippets) + { + combinedCompletionList = AddSnippets( + combinedCompletionList, + documentPositionInfo.LanguageKind, + completionContext.InvokeKind, + completionContext.TriggerCharacter); + } + + return combinedCompletionList; + } + + private async Task GetHtmlCompletionListAsync( + RoslynCompletionParams request, + TextDocument razorDocument, + RazorCompletionOptions razorCompletionOptions, + CancellationToken cancellationToken) + { + var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false); + if (htmlDocument is null) + { + return null; + } + + request.TextDocument = RoslynLspExtensions.WithUri(request.TextDocument, htmlDocument.Uri); + + _logger.LogDebug($"Resolving auto-insertion edit for {htmlDocument.Uri}"); + + var result = await _requestInvoker.ReinvokeRequestOnServerAsync( + htmlDocument.Buffer, + Methods.TextDocumentCompletionName, + RazorLSPConstants.HtmlLanguageServerName, + request, + cancellationToken).ConfigureAwait(false); + + var rewrittenResponse = DelegatedCompletionHelper.RewriteHtmlResponse(result?.Response, razorCompletionOptions); + + return rewrittenResponse; + } + + private static T? ToVsLSP(object source) where T : class + { + // This is, to say the least, not ideal. In future we're going to normalize on to Roslyn LSP types, and this can go. + var options = new JsonSerializerOptions(); + foreach (var converter in RazorServiceDescriptorsWrapper.GetLspConverters()) + { + options.Converters.Add(converter); + } + + if (JsonSerializer.Deserialize(JsonSerializer.SerializeToDocument(source), options) is not { } target) + { + return null; + } + + return target; + } + + private VSInternalCompletionList? AddSnippets( + VSInternalCompletionList? completionList, + RazorLanguageKind languageKind, + VSInternalCompletionInvokeKind invokeKind, + string? triggerCharacter) + { + using var builder = new PooledArrayBuilder(); + _snippetCompletionItemProvider.AddSnippetCompletions( + languageKind, + invokeKind, + triggerCharacter, + ref builder.AsRef()); + + // If there were no snippets, just return the original list + if (builder.Count == 0) + { + return completionList; + } + + // If there was a list with items, add them to snippets + if (completionList?.Items is { } combinedItems) + { + builder.AddRange(combinedItems); + } + + // Create or update final completion list + if (completionList is null) + { + completionList = new VSInternalCompletionList { IsIncomplete = true, Items = builder.ToArray() }; + } + else + { + completionList.Items = builder.ToArray(); + } + + return completionList; + } + + internal TestAccessor GetTestAccessor() => new(this); + + internal readonly struct TestAccessor(CohostDocumentCompletionEndpoint instance) + { + public Task HandleRequestAsync( + RoslynCompletionParams request, + TextDocument razorDocument, + CancellationToken cancellationToken) + => instance.HandleRequestAsync(request, razorDocument, cancellationToken); + public void SetClientCapabilities(VSInternalClientCapabilities clientCapabilities) + { + instance._clientCapabilities = clientCapabilities; + } + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Endpoints/RazorCustomMessageTarget.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Endpoints/RazorCustomMessageTarget.cs index 406fbc3df4e..c5b0175df84 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Endpoints/RazorCustomMessageTarget.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Endpoints/RazorCustomMessageTarget.cs @@ -34,7 +34,7 @@ internal partial class RazorCustomMessageTarget private readonly ITelemetryReporter _telemetryReporter; private readonly LanguageServerFeatureOptions _languageServerFeatureOptions; private readonly IProjectSnapshotManager _projectManager; - private readonly SnippetCache _snippetCache; + private readonly SnippetCompletionItemProvider _snippetCompletionItemProvider; private readonly IWorkspaceProvider _workspaceProvider; private readonly IHtmlDocumentSynchronizer _htmlDocumentSynchronizer; private readonly FormattingOptionsProvider _formattingOptionsProvider; @@ -55,7 +55,7 @@ public RazorCustomMessageTarget( ITelemetryReporter telemetryReporter, LanguageServerFeatureOptions languageServerFeatureOptions, IProjectSnapshotManager projectManager, - SnippetCache snippetCache, + SnippetCompletionItemProvider snippetCompletionItemProvider, IWorkspaceProvider workspaceProvider, IHtmlDocumentSynchronizer htmlDocumentSynchronizer, ILoggerFactory loggerFactory) @@ -86,7 +86,7 @@ public RazorCustomMessageTarget( _telemetryReporter = telemetryReporter ?? throw new ArgumentNullException(nameof(telemetryReporter)); _languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions)); _projectManager = projectManager ?? throw new ArgumentNullException(nameof(projectManager)); - _snippetCache = snippetCache ?? throw new ArgumentNullException(nameof(snippetCache)); + _snippetCompletionItemProvider = snippetCompletionItemProvider ?? throw new ArgumentNullException(nameof(snippetCompletionItemProvider)); _workspaceProvider = workspaceProvider; _htmlDocumentSynchronizer = htmlDocumentSynchronizer; _logger = loggerFactory.GetOrCreateLogger(); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Endpoints/RazorCustomMessageTarget_Completion.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Endpoints/RazorCustomMessageTarget_Completion.cs index 8a85d553cdf..6575ce4e232 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Endpoints/RazorCustomMessageTarget_Completion.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Endpoints/RazorCustomMessageTarget_Completion.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.PooledObjects; @@ -150,7 +149,7 @@ internal partial class RazorCustomMessageTarget }; } - AddSnippetCompletions(request, ref builder.AsRef()); + _snippetCompletionItemProvider.AddSnippetCompletions(request.ProjectedKind, request.Context.InvokeKind, request.Context.TriggerCharacter, ref builder.AsRef()); completionList.Items = builder.ToArray(); completionList.Data = JsonHelpers.TryConvertFromJObject(completionList.Data); @@ -238,7 +237,7 @@ private void UpdateVirtualDocument( { // Check if we're completing a snippet item that we provided if (SnippetCompletionData.TryParse(request.CompletionItem.Data, out var snippetCompletionData) && - _snippetCache.TryResolveSnippetString(snippetCompletionData) is { } snippetInsertText) + _snippetCompletionItemProvider.SnippetCache.TryResolveSnippetString(snippetCompletionData) is { } snippetInsertText) { request.CompletionItem.InsertText = snippetInsertText; return request.CompletionItem; @@ -314,60 +313,4 @@ private void UpdateVirtualDocument( var formattingOptions = _formattingOptionsProvider.GetOptions(document.TextDocumentIdentifier.Uri); return Task.FromResult(formattingOptions); } - - private void AddSnippetCompletions(DelegatedCompletionParams request, ref PooledArrayBuilder builder) - { - if (!request.ShouldIncludeSnippets) - { - return; - } - - // Temporary fix: snippets are broken in CSharp. We're investigating - // but this is very disruptive. This quick fix unblocks things. - // TODO: Add an option to enable this. - if (request.ProjectedKind != RazorLanguageKind.Html) - { - return; - } - - // Don't add snippets for deletion of a character - if (request.Context.InvokeKind == VSInternalCompletionInvokeKind.Deletion) - { - return; - } - - // Don't add snippets if the trigger characters contain whitespace - if (request.Context.TriggerCharacter is not null - && request.Context.TriggerCharacter.Contains(' ')) - { - return; - } - - var snippets = _snippetCache.GetSnippets(ConvertLanguageKind(request.ProjectedKind)); - if (snippets.IsDefaultOrEmpty) - { - return; - } - - builder.AddRange(snippets - .Select(s => new CompletionItem() - { - Label = s.Shortcut, - Detail = s.Description, - InsertTextFormat = InsertTextFormat.Snippet, - InsertText = s.Shortcut, - Data = s.CompletionData, - Kind = CompletionItemKind.Snippet, - CommitCharacters = [] - })); - } - - private static SnippetLanguage ConvertLanguageKind(RazorLanguageKind languageKind) - => languageKind switch - { - RazorLanguageKind.CSharp => SnippetLanguage.CSharp, - RazorLanguageKind.Html => SnippetLanguage.Html, - RazorLanguageKind.Razor => SnippetLanguage.Razor, - _ => throw new InvalidOperationException($"Unexpected value {languageKind}") - }; } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Snippets/SnippetCompletionItemProvider.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Snippets/SnippetCompletionItemProvider.cs new file mode 100644 index 00000000000..8a99acf51cb --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/Snippets/SnippetCompletionItemProvider.cs @@ -0,0 +1,77 @@ +// 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.ComponentModel.Composition; +using System.Linq; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.VisualStudio.Razor.Snippets; + +[Export(typeof(SnippetCompletionItemProvider))] +internal sealed class SnippetCompletionItemProvider +{ + [ImportingConstructor] + public SnippetCompletionItemProvider(SnippetCache snippetCache) + { + SnippetCache = snippetCache; + } + + public SnippetCache SnippetCache { get; } + + public void AddSnippetCompletions( + RazorLanguageKind projectedKind, + VSInternalCompletionInvokeKind invokeKind, + string? triggerCharacter, + ref PooledArrayBuilder builder) + { + // Temporary fix: snippets are broken in CSharp. We're investigating + // but this is very disruptive. This quick fix unblocks things. + // TODO: Add an option to enable this. + if (projectedKind != RazorLanguageKind.Html) + { + return; + } + + // Don't add snippets for deletion of a character + if (invokeKind == VSInternalCompletionInvokeKind.Deletion) + { + return; + } + + // Don't add snippets if the trigger characters contain whitespace + if (triggerCharacter is not null && triggerCharacter.Contains(' ')) + { + return; + } + + var snippets = SnippetCache.GetSnippets(ConvertLanguageKind(projectedKind)); + if (snippets.IsDefaultOrEmpty) + { + return; + } + + builder.AddRange(snippets + .Select(s => new CompletionItem() + { + Label = s.Shortcut, + Detail = s.Description, + InsertTextFormat = InsertTextFormat.Snippet, + InsertText = s.Shortcut, + Data = s.CompletionData, + Kind = CompletionItemKind.Snippet, + CommitCharacters = [] + })); + } + + private static SnippetLanguage ConvertLanguageKind(RazorLanguageKind languageKind) + => languageKind switch + { + RazorLanguageKind.CSharp => SnippetLanguage.CSharp, + RazorLanguageKind.Html => SnippetLanguage.Html, + RazorLanguageKind.Razor => SnippetLanguage.Razor, + _ => throw new InvalidOperationException($"Unexpected value {languageKind}") + }; +} 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 c44f32afc7a..659a8014854 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 @@ -4,12 +4,13 @@ #nullable disable using System; +using System.Collections.Frozen; using System.Collections.Generic; -using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; +using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -29,6 +30,7 @@ public class CompletionListProviderTest : LanguageServerTestBase private readonly VSInternalCompletionContext _completionContext; private readonly DocumentContext _documentContext; private readonly VSInternalClientCapabilities _clientCapabilities; + private readonly RazorCompletionOptions _razorCompletionOptions; public CompletionListProviderTest(ITestOutputHelper testOutput) : base(testOutput) @@ -40,6 +42,7 @@ public CompletionListProviderTest(ITestOutputHelper testOutput) _completionContext = new VSInternalCompletionContext(); _documentContext = TestDocumentContext.Create("C:/path/to/file.cshtml"); _clientCapabilities = new VSInternalClientCapabilities(); + _razorCompletionOptions = new RazorCompletionOptions(SnippetsSupported: true, AutoInsertAttributeQuotes: true, CommitElementsWithSpace: true); } [Fact] @@ -50,7 +53,7 @@ public async Task MultipleCompletionLists_Merges() // Act var completionList = await provider.GetCompletionListAsync( - absoluteIndex: 0, _completionContext, _documentContext, _clientCapabilities, correlationId: Guid.Empty, cancellationToken: DisposalToken); + absoluteIndex: 0, _completionContext, _documentContext, _clientCapabilities, _razorCompletionOptions, correlationId: Guid.Empty, cancellationToken: DisposalToken); // Assert Assert.NotSame(_completionList1, completionList); @@ -67,7 +70,7 @@ public async Task MultipleCompletionLists_DifferentCommitCharacters_OnlyCallsApp // Act var completionList = await provider.GetCompletionListAsync( - absoluteIndex: 0, _completionContext, _documentContext, _clientCapabilities, correlationId: Guid.Empty, cancellationToken: DisposalToken); + absoluteIndex: 0, _completionContext, _documentContext, _clientCapabilities, _razorCompletionOptions, correlationId: Guid.Empty, cancellationToken: DisposalToken); // Assert Assert.Same(_completionList2, completionList); @@ -78,19 +81,20 @@ private class TestDelegatedCompletionListProvider : DelegatedCompletionListProvi private readonly VSInternalCompletionList _completionList; public TestDelegatedCompletionListProvider(VSInternalCompletionList completionList, IEnumerable triggerCharacters) - : base(Array.Empty(), null, null, null) + : base(null, null, null) { _completionList = completionList; - TriggerCharacters = triggerCharacters.ToImmutableHashSet(); + TriggerCharacters = triggerCharacters.ToFrozenSet(); } - public override ImmutableHashSet TriggerCharacters { get; } + public override FrozenSet TriggerCharacters { get; } public override Task GetCompletionListAsync( int absoluteIndex, VSInternalCompletionContext completionContext, DocumentContext documentContext, VSInternalClientCapabilities clientCapabilities, + RazorCompletionOptions completionOptions, Guid correlationId, CancellationToken cancellationToken) { @@ -109,10 +113,10 @@ public TestRazorCompletionListProvider( : base(completionFactsService: null, completionListCache: null, loggerFactory) { _completionList = completionList; - TriggerCharacters = triggerCharacters.ToImmutableHashSet(); + TriggerCharacters = triggerCharacters.ToFrozenSet(); } - public override ImmutableHashSet TriggerCharacters { get; } + public override FrozenSet TriggerCharacters { get; } public override Task GetCompletionListAsync( int absoluteIndex, @@ -120,6 +124,7 @@ public override Task GetCompletionListAsync( DocumentContext documentContext, VSInternalClientCapabilities clientCapabilities, HashSet existingCompletions, + RazorCompletionOptions razorCompletionOptions, CancellationToken cancellationToken) { return Task.FromResult(_completionList); 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 4bad7b08bcd..37a38438c4b 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,6 +12,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Formatting; using Microsoft.AspNetCore.Razor.LanguageServer.Test; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; +using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Razor.Formatting; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; @@ -32,6 +33,7 @@ public class DelegatedCompletionItemResolverTest : LanguageServerTestBase private readonly DelegatedCompletionParams _htmlCompletionParams; private readonly IDocumentContextFactory _documentContextFactory; private readonly AsyncLazy _formattingService; + private readonly RazorCompletionOptions _defaultRazorCompletionOptions; public DelegatedCompletionItemResolverTest(ITestOutputHelper testOutput) : base(testOutput) @@ -71,6 +73,10 @@ public DelegatedCompletionItemResolverTest(ITestOutputHelper testOutput) _documentContextFactory = new TestDocumentContextFactory(); _formattingService = new AsyncLazy(() => TestRazorFormattingService.CreateWithFullSupportAsync(LoggerFactory)); + _defaultRazorCompletionOptions = new RazorCompletionOptions( + SnippetsSupported: true, + AutoInsertAttributeQuotes: true, + CommitElementsWithSpace: true); } [Fact] @@ -277,7 +283,13 @@ private async Task CreateCSharpServerAsync(RazorCodeDocumen var provider = TestDelegatedCompletionListProvider.Create(csharpServer, LoggerFactory, DisposalToken); var completionList = await provider.GetCompletionListAsync( - cursorPosition, completionContext, documentContext, _clientCapabilities, correlationId: Guid.Empty, cancellationToken: DisposalToken); + cursorPosition, + completionContext, + documentContext, + _clientCapabilities, + _defaultRazorCompletionOptions, + correlationId: Guid.Empty, + cancellationToken: DisposalToken); return (completionList, provider.DelegatedParams); } 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 a36c6a557fb..f97b4f2ca53 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 @@ -4,15 +4,14 @@ #nullable disable using System; -using System.Linq; using System.Threading; 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.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Razor.DocumentMapping; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; @@ -27,33 +26,17 @@ public class DelegatedCompletionListProviderTest : LanguageServerTestBase { private readonly TestDelegatedCompletionListProvider _provider; private readonly VSInternalClientCapabilities _clientCapabilities; + private readonly RazorCompletionOptions _defaultRazorCompletionOptions; public DelegatedCompletionListProviderTest(ITestOutputHelper testOutput) : base(testOutput) { _provider = TestDelegatedCompletionListProvider.Create(LoggerFactory); _clientCapabilities = new VSInternalClientCapabilities(); - } - - [Fact] - public async Task ResponseRewritersGetExecutedInOrder() - { - // Arrange - var completionContext = new VSInternalCompletionContext(); - var codeDocument = CreateCodeDocument("<"); - 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); - - // Act - var completionList = await provider.GetCompletionListAsync( - absoluteIndex: 1, completionContext, documentContext, _clientCapabilities, correlationId: Guid.Empty, cancellationToken: DisposalToken); - - // Assert - Assert.Collection(completionList.Items, - item => Assert.Equal("20", item.Label), - item => Assert.Equal("100", item.Label)); + _defaultRazorCompletionOptions = new RazorCompletionOptions( + SnippetsSupported: true, + AutoInsertAttributeQuotes: true, + CommitElementsWithSpace: true); } [Fact] @@ -66,7 +49,13 @@ public async Task HtmlDelegation_Invoked() // Act await _provider.GetCompletionListAsync( - absoluteIndex: 1, completionContext, documentContext, _clientCapabilities, correlationId: Guid.Empty, cancellationToken: DisposalToken); + absoluteIndex: 1, + completionContext, + documentContext, + _clientCapabilities, + _defaultRazorCompletionOptions, + correlationId: Guid.Empty, + cancellationToken: DisposalToken); // Assert var delegatedParameters = _provider.DelegatedParams; @@ -93,7 +82,13 @@ public async Task HtmlDelegation_TriggerCharacter() // Act await _provider.GetCompletionListAsync( - absoluteIndex: 1, completionContext, documentContext, _clientCapabilities, correlationId: Guid.Empty, cancellationToken: DisposalToken); + absoluteIndex: 1, + completionContext, + documentContext, + _clientCapabilities, + _defaultRazorCompletionOptions, + correlationId: Guid.Empty, + cancellationToken: DisposalToken); // Assert var delegatedParameters = _provider.DelegatedParams; @@ -121,7 +116,13 @@ public async Task HtmlDelegation_UnsupportedTriggerCharacter_TranslatesToInvoked // Act await _provider.GetCompletionListAsync( - absoluteIndex: 1, completionContext, documentContext, _clientCapabilities, correlationId: Guid.Empty, cancellationToken: DisposalToken); + absoluteIndex: 1, + completionContext, + documentContext, + _clientCapabilities, + _defaultRazorCompletionOptions, + correlationId: Guid.Empty, + cancellationToken: DisposalToken); // Assert var delegatedParameters = _provider.DelegatedParams; @@ -150,7 +151,13 @@ public async Task Delegation_NullResult_ToIncompleteResult() // Act var delegatedCompletionList = await provider.GetCompletionListAsync( - absoluteIndex: 1, completionContext, documentContext, _clientCapabilities, correlationId: Guid.Empty, cancellationToken: DisposalToken); + absoluteIndex: 1, + completionContext, + documentContext, + _clientCapabilities, + _defaultRazorCompletionOptions, + correlationId: Guid.Empty, + cancellationToken: DisposalToken); // Assert Assert.NotNull(delegatedCompletionList); @@ -197,7 +204,13 @@ public async Task RazorDelegation_Noop() // Act var completionList = await _provider.GetCompletionListAsync( - absoluteIndex: 11, completionContext, documentContext, _clientCapabilities, correlationId: Guid.Empty, cancellationToken: DisposalToken); + absoluteIndex: 11, + completionContext, + documentContext, + _clientCapabilities, + _defaultRazorCompletionOptions, + correlationId: Guid.Empty, + cancellationToken: DisposalToken); // Assert Assert.Null(completionList); @@ -220,7 +233,13 @@ public async Task ProvisionalCompletion_TranslatesToCSharpWithProvisionalTextEdi // Act await _provider.GetCompletionListAsync( - absoluteIndex: 10, completionContext, documentContext, _clientCapabilities, correlationId: Guid.Empty, cancellationToken: DisposalToken); + absoluteIndex: 10, + completionContext, + documentContext, + _clientCapabilities, + _defaultRazorCompletionOptions, + correlationId: Guid.Empty, + cancellationToken: DisposalToken); // Assert var delegatedParameters = _provider.DelegatedParams; @@ -250,7 +269,13 @@ public async Task DotTriggerInMiddleOfCSharpImplicitExpressionNotTreatedAsProvis // Act await _provider.GetCompletionListAsync( - absoluteIndex: 10, completionContext, documentContext, _clientCapabilities, correlationId: Guid.Empty, cancellationToken: DisposalToken); + absoluteIndex: 10, + completionContext, + documentContext, + _clientCapabilities, + _defaultRazorCompletionOptions, + correlationId: Guid.Empty, + cancellationToken: DisposalToken); // Assert var delegatedParameters = _provider.DelegatedParams; @@ -295,7 +320,6 @@ public async Task ShouldIncludeSnippets(string input, bool shouldIncludeSnippets .Returns(true); var completionProvider = new DelegatedCompletionListProvider( - responseRewriters: [], documentMappingServiceMock.Object, clientConnection, new CompletionListCache()); @@ -316,34 +340,18 @@ public async Task ShouldIncludeSnippets(string input, bool shouldIncludeSnippets TriggerCharacter = ".", }; - await completionProvider.GetCompletionListAsync(cursorPosition, completionContext, documentContext, _clientCapabilities, correlationId: Guid.Empty, DisposalToken); + await completionProvider.GetCompletionListAsync( + cursorPosition, + completionContext, + documentContext, + _clientCapabilities, + _defaultRazorCompletionOptions, + correlationId: Guid.Empty, + DisposalToken); Assert.True(requestSent); } - private class TestResponseRewriter : DelegatedCompletionResponseRewriter - { - private readonly int _order; - - public TestResponseRewriter(int order) - { - _order = order; - } - - public override int Order => _order; - - public override Task RewriteAsync(VSInternalCompletionList completionList, int hostDocumentIndex, DocumentContext hostDocumentContext, DelegatedCompletionParams delegatedParameters, CancellationToken cancellationToken) - { - var completionItem = new VSInternalCompletionItem() - { - Label = Order.ToString(), - }; - completionList.Items = completionList.Items.Concat(new[] { completionItem }).ToArray(); - - return Task.FromResult(completionList); - } - } - private async Task GetCompletionListAsync(string content, CompletionTriggerKind triggerKind) { TestFileMarkupParser.GetPosition(content, out var output, out var cursorPosition); @@ -376,7 +384,13 @@ private async Task GetCompletionListAsync(string conte var provider = TestDelegatedCompletionListProvider.Create(csharpServer, LoggerFactory, DisposalToken); var completionList = await provider.GetCompletionListAsync( - absoluteIndex: cursorPosition, completionContext, documentContext, _clientCapabilities, correlationId: Guid.Empty, cancellationToken: DisposalToken); + absoluteIndex: cursorPosition, + completionContext, + documentContext, + _clientCapabilities, + _defaultRazorCompletionOptions, + correlationId: Guid.Empty, + cancellationToken: DisposalToken); return completionList; } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DesignTimeHelperResponseRewriterTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DesignTimeHelperResponseRewriterTest.cs index 9a1ff7556f7..e22d97af9de 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DesignTimeHelperResponseRewriterTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/DesignTimeHelperResponseRewriterTest.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; public class DesignTimeHelperResponseRewriterTest(ITestOutputHelper testOutput) - : ResponseRewriterTestBase(new DesignTimeHelperResponseRewriter(), testOutput) + : ResponseRewriterTestBase(testOutput) { [Fact] public async Task RewriteAsync_NotCSharp_Noops() diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/HtmlCommitCharacterResponseRewriterTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/HtmlCommitCharacterResponseRewriterTest.cs index 098d1a3f090..1d3d246f754 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/HtmlCommitCharacterResponseRewriterTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/HtmlCommitCharacterResponseRewriterTest.cs @@ -4,8 +4,9 @@ #nullable disable using System.Linq; -using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.Completion; +using Microsoft.CodeAnalysis.Razor.Completion.Delegation; using Microsoft.CodeAnalysis.Testing; using Microsoft.VisualStudio.LanguageServer.Protocol; using Xunit; @@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; public class HtmlCommitCharacterResponseRewriterTest(ITestOutputHelper testOutput) - : ResponseRewriterTestBase(new HtmlCommitCharacterResponseRewriter(TestRazorLSPOptionsMonitor.Create()), testOutput) + : ResponseRewriterTestBase(testOutput) { [Theory] [CombinatorialData] @@ -68,12 +69,14 @@ public async Task RewriteAsync_DefaultCommitCharacters_RemovesSpace(bool useVSTy TestFileMarkupParser.GetPosition(input, out var documentContent, out var cursorPosition); var delegatedCompletionList = GenerateCompletionList(useDefaultCommitCharacters: true, useVSTypes, "Element1", "Element2"); - var options = TestRazorLSPOptionsMonitor.Create(); - await options.UpdateAsync(options.CurrentValue with { CommitElementsWithSpace = false }, CancellationToken.None); - var rewriter = new HtmlCommitCharacterResponseRewriter(options); + var razorCompletionOptions = new RazorCompletionOptions( + SnippetsSupported: true, + AutoInsertAttributeQuotes: true, + CommitElementsWithSpace: false); // Act - var rewrittenCompletionList = await GetRewrittenCompletionListAsync(cursorPosition, documentContent, delegatedCompletionList, rewriter); + var rewrittenCompletionList = await GetRewrittenCompletionListAsync( + cursorPosition, documentContent, delegatedCompletionList, razorCompletionOptions); // Assert if (useVSTypes) @@ -111,12 +114,18 @@ public async Task RewriteAsync_ItemCommitCharacters_RemovesSpace(bool useVSTypes TestFileMarkupParser.GetPosition(input, out var documentContent, out var cursorPosition); var delegatedCompletionList = GenerateCompletionList(useDefaultCommitCharacters: false, useVSTypes, "Element1", "Element2"); - var options = TestRazorLSPOptionsMonitor.Create(); - await options.UpdateAsync(options.CurrentValue with { CommitElementsWithSpace = false }, CancellationToken.None); - var rewriter = new HtmlCommitCharacterResponseRewriter(options); + var razorCompletionOptions = new RazorCompletionOptions( + SnippetsSupported: true, + AutoInsertAttributeQuotes: true, + CommitElementsWithSpace: false); + var rewriter = new HtmlCommitCharacterResponseRewriter(); // Act - var rewrittenCompletionList = await GetRewrittenCompletionListAsync(cursorPosition, documentContent, delegatedCompletionList, rewriter); + var rewrittenCompletionList = await GetRewrittenCompletionListAsync( + cursorPosition, + documentContent, + delegatedCompletionList, + razorCompletionOptions); // Assert Assert.Null(rewrittenCompletionList.CommitCharacters); 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 db8ab5ed69a..68a0c64e40d 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 @@ -6,6 +6,7 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; +using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.VisualStudio.LanguageServer.Protocol; using Xunit.Abstractions; @@ -13,24 +14,44 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; public abstract class ResponseRewriterTestBase : LanguageServerTestBase { - private protected DelegatedCompletionResponseRewriter Rewriter { get; } - private protected ResponseRewriterTestBase( - DelegatedCompletionResponseRewriter rewriter, ITestOutputHelper testOutput) : base(testOutput) { - Rewriter = rewriter; } - private protected async Task GetRewrittenCompletionListAsync(int absoluteIndex, string documentContent, VSInternalCompletionList initialCompletionList, DelegatedCompletionResponseRewriter rewriter = null) + private protected Task GetRewrittenCompletionListAsync( + int absoluteIndex, + string documentContent, + VSInternalCompletionList initialCompletionList) + { + var razorCompletionOptions = new RazorCompletionOptions( + SnippetsSupported: true, + AutoInsertAttributeQuotes: true, + CommitElementsWithSpace: true); + + return GetRewrittenCompletionListAsync(absoluteIndex, documentContent, initialCompletionList, razorCompletionOptions); + } + + private protected async Task GetRewrittenCompletionListAsync( + int absoluteIndex, + string documentContent, + VSInternalCompletionList initialCompletionList, + RazorCompletionOptions razorCompletionOptions) { var completionContext = new VSInternalCompletionContext(); var codeDocument = CreateCodeDocument(documentContent); var documentContext = TestDocumentContext.Create("C:/path/to/file.cshtml", codeDocument); - var provider = TestDelegatedCompletionListProvider.Create(initialCompletionList, LoggerFactory, rewriter ?? Rewriter); + var provider = TestDelegatedCompletionListProvider.Create(initialCompletionList, LoggerFactory); var clientCapabilities = new VSInternalClientCapabilities(); - var completionList = await provider.GetCompletionListAsync(absoluteIndex, completionContext, documentContext, clientCapabilities, correlationId: Guid.Empty, cancellationToken: DisposalToken); + var completionList = await provider.GetCompletionListAsync( + absoluteIndex, + completionContext, + documentContext, + clientCapabilities, + razorCompletionOptions, + correlationId: Guid.Empty, + cancellationToken: DisposalToken); return completionList; } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/SnippetResponseRewriterTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/SnippetResponseRewriterTest.cs index 78f23228bcc..7de0121b894 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/SnippetResponseRewriterTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/SnippetResponseRewriterTest.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; +using Microsoft.CodeAnalysis.Razor.Completion.Delegation; using Microsoft.CodeAnalysis.Testing; using Microsoft.VisualStudio.LanguageServer.Protocol; using Xunit; @@ -13,7 +14,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.Completion.Delegation; public class SnippetResponseRewriterTest(ITestOutputHelper testOutput) - : ResponseRewriterTestBase(new SnippetResponseRewriter(), testOutput) + : ResponseRewriterTestBase(testOutput) { [Fact] public async Task RewriteAsync_ChangesUsingSnippetLabel() @@ -29,7 +30,7 @@ public async Task RewriteAsync_ChangesUsingSnippetLabel() // Act var rewrittenCompletionList = await GetRewrittenCompletionListAsync( - cursorPosition, documentContent, delegatedCompletionList, rewriter); + cursorPosition, documentContent, delegatedCompletionList); // Assert Assert.Null(rewrittenCompletionList.CommitCharacters); @@ -62,7 +63,7 @@ public async Task RewriteAsync_DoesNotChangeUsingKeywordLabel() // Act var rewrittenCompletionList = await GetRewrittenCompletionListAsync( - cursorPosition, documentContent, delegatedCompletionList, rewriter); + cursorPosition, documentContent, delegatedCompletionList); // Assert Assert.Null(rewrittenCompletionList.CommitCharacters); @@ -95,7 +96,7 @@ public async Task RewriteAsync_DoesNotChangeIfSnippetLabel() // Act var rewrittenCompletionList = await GetRewrittenCompletionListAsync( - cursorPosition, documentContent, delegatedCompletionList, rewriter); + cursorPosition, documentContent, delegatedCompletionList); // Assert Assert.Null(rewrittenCompletionList.CommitCharacters); @@ -114,39 +115,6 @@ public async Task RewriteAsync_DoesNotChangeIfSnippetLabel() ); } - [Fact] - public async Task RewriteAsync_HandlesNullLabels() - { - // Arrange - var documentContent = "@$$"; - TestFileMarkupParser.GetPosition(documentContent, out documentContent, out var cursorPosition); - var delegatedCompletionList = GenerateCompletionList( - (null, CompletionItemKind.Keyword), - ("using", CompletionItemKind.Snippet) - ); - var rewriter = new SnippetResponseRewriter(); - - // Act - var rewrittenCompletionList = await GetRewrittenCompletionListAsync( - cursorPosition, documentContent, delegatedCompletionList, rewriter); - - // Assert - Assert.Null(rewrittenCompletionList.CommitCharacters); - Assert.Collection( - rewrittenCompletionList.Items, - completion => - { - Assert.Null(completion.Label); - Assert.Null(completion.SortText); - }, - completion => - { - Assert.Equal("using statement", completion.Label); - Assert.Equal("using ", completion.SortText); - } - ); - } - private static VSInternalCompletionList GenerateCompletionList(params (string? Label, CompletionItemKind Kind)[] itemsData) { var items = itemsData.Select(itemData => new VSInternalCompletionItem() diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/TestDelegatedCompletionListProvider.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/TestDelegatedCompletionListProvider.cs index 746ba8cc729..9cd45150fc6 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/TestDelegatedCompletionListProvider.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/TestDelegatedCompletionListProvider.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Test; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.AspNetCore.Razor.Test.Common.Workspaces; +using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Razor.Logging; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -21,11 +22,9 @@ internal class TestDelegatedCompletionListProvider : DelegatedCompletionListProv private readonly CompletionRequestResponseFactory _completionFactory; private TestDelegatedCompletionListProvider( - DelegatedCompletionResponseRewriter[] responseRewriters, CompletionRequestResponseFactory completionFactory, ILoggerFactory loggerFactory) : base( - responseRewriters, new LspDocumentMappingService(new LSPFilePathService(TestLanguageServerFeatureOptions.Instance), new TestDocumentContextFactory(), loggerFactory), new TestLanguageServer(new Dictionary>>() { @@ -37,41 +36,37 @@ private TestDelegatedCompletionListProvider( } public static TestDelegatedCompletionListProvider Create( - ILoggerFactory loggerFactory, - params DelegatedCompletionResponseRewriter[] responseRewriters) => - Create(delegatedCompletionList: null, loggerFactory, responseRewriters: responseRewriters); + ILoggerFactory loggerFactory) => + Create(delegatedCompletionList: null, loggerFactory); public static TestDelegatedCompletionListProvider Create( CSharpTestLspServer csharpServer, ILoggerFactory loggerFactory, - CancellationToken cancellationToken, - params DelegatedCompletionResponseRewriter[] responseRewriters) + CancellationToken cancellationToken) { var requestResponseFactory = new DelegatedCSharpCompletionRequestResponseFactory(csharpServer, cancellationToken); - var provider = new TestDelegatedCompletionListProvider(responseRewriters, requestResponseFactory, loggerFactory); + var provider = new TestDelegatedCompletionListProvider(requestResponseFactory, loggerFactory); return provider; } public static TestDelegatedCompletionListProvider Create( VSInternalCompletionList delegatedCompletionList, - ILoggerFactory loggerFactory, - params DelegatedCompletionResponseRewriter[] responseRewriters) + ILoggerFactory loggerFactory) { delegatedCompletionList ??= new VSInternalCompletionList() { Items = Array.Empty(), }; var requestResponseFactory = new StaticCompletionRequestResponseFactory(delegatedCompletionList); - var provider = new TestDelegatedCompletionListProvider(responseRewriters, requestResponseFactory, loggerFactory); + var provider = new TestDelegatedCompletionListProvider(requestResponseFactory, loggerFactory); return provider; } public static TestDelegatedCompletionListProvider CreateWithNullResponse( - ILoggerFactory loggerFactory, - params DelegatedCompletionResponseRewriter[] responseRewriters) + ILoggerFactory loggerFactory) { var requestResponseFactory = new StaticCompletionRequestResponseFactory(null); - var provider = new TestDelegatedCompletionListProvider(responseRewriters, requestResponseFactory, loggerFactory); + var provider = new TestDelegatedCompletionListProvider(requestResponseFactory, loggerFactory); return provider; } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/TextEditResponseRewriterTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/TextEditResponseRewriterTest.cs index 38115319260..61c04e3c98e 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/TextEditResponseRewriterTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/Delegation/TextEditResponseRewriterTest.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion.Delegation; public class TextEditResponseRewriterTest(ITestOutputHelper testOutput) - : ResponseRewriterTestBase(new TextEditResponseRewriter(), testOutput) + : ResponseRewriterTestBase(testOutput) { [Fact] public async Task RewriteAsync_NotCSharp_Noops() @@ -79,6 +79,7 @@ private static VSInternalCompletionList GenerateCompletionList(Range textEditRan Items = [ new VSInternalCompletionItem() { + Label = string.Empty, // label string is non-nullable TextEdit = VsLspFactory.CreateTextEdit(textEditRange, "Hello") } ] diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionResolveEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionResolveEndpointTest.cs index 8586d5b9635..f39565e99ad 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionResolveEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionResolveEndpointTest.cs @@ -4,12 +4,12 @@ #nullable disable using System; -using System.Text.Json; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.Test; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; +using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.VisualStudio.LanguageServer.Protocol; using Xunit; using Xunit.Abstractions; 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 d2034729071..a044d20d8ae 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 @@ -3,10 +3,10 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.LanguageServer.Completion; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Razor.GoToDefinition; using Microsoft.CodeAnalysis.Testing; using Microsoft.VisualStudio.LanguageServer.Protocol; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DirectoryHelperTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DirectoryHelperTest.cs index 705c30e1483..0eb14d8e33c 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DirectoryHelperTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/DirectoryHelperTest.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using Microsoft.AspNetCore.Razor.LanguageServer.Completion; +using Microsoft.CodeAnalysis.Razor.Completion; using Xunit; using Xunit.Abstractions; using static Microsoft.AspNetCore.Razor.LanguageServer.DirectoryHelper; 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 6dc5f7eda36..759022fb19b 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 @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.LanguageServer.Completion; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.AspNetCore.Razor.LanguageServer.Hover; using Microsoft.AspNetCore.Razor.ProjectSystem; @@ -14,6 +13,7 @@ using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.AspNetCore.Razor.Test.Common.ProjectSystem; using Microsoft.CodeAnalysis.Classification; +using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Razor.DocumentMapping; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/LinkedEditingRange/LinkedEditingRangeEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/LinkedEditingRange/LinkedEditingRangeEndpointTest.cs index 4afda8d32d0..8b5f4f31106 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/LinkedEditingRange/LinkedEditingRangeEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/LinkedEditingRange/LinkedEditingRangeEndpointTest.cs @@ -6,7 +6,7 @@ using System; using System.Text.RegularExpressions; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.LanguageServer.Completion; +using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Razor.LinkedEditingRange; using Microsoft.VisualStudio.LanguageServer.Protocol; using Xunit; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Microsoft.AspNetCore.Razor.LanguageServer.Test.csproj b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Microsoft.AspNetCore.Razor.LanguageServer.Test.csproj index 685173189f4..ddcf41ab63a 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Microsoft.AspNetCore.Razor.LanguageServer.Test.csproj +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Microsoft.AspNetCore.Razor.LanguageServer.Test.csproj @@ -10,12 +10,12 @@ - + 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 6ef88b7406d..194d78f6ee5 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 @@ -14,12 +14,12 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.LanguageServer.Completion; using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.SemanticTokens; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TagHelperFactsServiceTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TagHelperFactsServiceTest.cs index 4e4bba21832..f4976ce41f7 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TagHelperFactsServiceTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/TagHelperFactsServiceTest.cs @@ -7,7 +7,7 @@ using System.Collections.Immutable; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Syntax; -using Microsoft.AspNetCore.Razor.LanguageServer.Completion; +using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.VisualStudio.Editor.Razor; using Xunit; using Xunit.Abstractions; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/CompletionListCacheTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/CompletionListCacheTest.cs similarity index 98% rename from src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/CompletionListCacheTest.cs rename to src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/CompletionListCacheTest.cs index e8e4168611b..09d4ddcd119 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/CompletionListCacheTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/CompletionListCacheTest.cs @@ -6,7 +6,7 @@ using Xunit; using Xunit.Abstractions; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; public class CompletionListCacheTest : ToolingTestBase { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/CompletionListMergerTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/CompletionListMergerTest.cs similarity index 98% rename from src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/CompletionListMergerTest.cs rename to src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/CompletionListMergerTest.cs index ee70ac5c7b0..2b9eacc282c 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/CompletionListMergerTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/CompletionListMergerTest.cs @@ -9,7 +9,7 @@ using Xunit; using Xunit.Abstractions; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; public class CompletionListMergerTest : ToolingTestBase { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/CompletionListOptimizerTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/CompletionListOptimizerTest.cs similarity index 97% rename from src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/CompletionListOptimizerTest.cs rename to src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/CompletionListOptimizerTest.cs index 9d37f1f9655..c4023265695 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/CompletionListOptimizerTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/CompletionListOptimizerTest.cs @@ -8,7 +8,7 @@ using Xunit; using Xunit.Abstractions; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; public class CompletionListOptimizerTest : ToolingTestBase { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/DirectiveAttributeTransitionCompletionItemProviderTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeTransitionCompletionItemProviderTest.cs similarity index 98% rename from src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/DirectiveAttributeTransitionCompletionItemProviderTest.cs rename to src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeTransitionCompletionItemProviderTest.cs index 62edca4716a..f8c52ceb2d2 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/DirectiveAttributeTransitionCompletionItemProviderTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveAttributeTransitionCompletionItemProviderTest.cs @@ -7,12 +7,11 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Text; using Xunit; using Xunit.Abstractions; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; public class DirectiveAttributeTransitionCompletionItemProviderTest : ToolingTestBase { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/DirectiveVerifier.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveVerifier.cs similarity index 93% rename from src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/DirectiveVerifier.cs rename to src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveVerifier.cs index 586a8d25dd7..513bc307235 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/DirectiveVerifier.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/DirectiveVerifier.cs @@ -4,11 +4,10 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.VisualStudio.LanguageServer.Protocol; using Xunit; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; internal static class DirectiveVerifier { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/LanguageServerTagHelperCompletionServiceTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/LanguageServerTagHelperCompletionServiceTest.cs similarity index 99% rename from src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/LanguageServerTagHelperCompletionServiceTest.cs rename to src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/LanguageServerTagHelperCompletionServiceTest.cs index 8243e3c05a8..7d78b3e3a93 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/LanguageServerTagHelperCompletionServiceTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/LanguageServerTagHelperCompletionServiceTest.cs @@ -9,12 +9,11 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Components; using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.CodeAnalysis.Razor.Completion; using Xunit; using Xunit.Abstractions; using static Microsoft.AspNetCore.Razor.Language.CommonMetadata; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; public class LanguageServerTagHelperCompletionServiceTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput) { @@ -1459,7 +1458,7 @@ public void GetElementCompletions_MustSatisfyAttributeRules_NoAttributes_Allowed AssertCompletionsAreEquivalent(expectedCompletions, completions); } - private static LspTagHelperCompletionService CreateTagHelperCompletionFactsService() => new(); + private static TagHelperCompletionService CreateTagHelperCompletionFactsService() => new(); private static void AssertCompletionsAreEquivalent(ElementCompletionResult expected, ElementCompletionResult actual) { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/MarkupTransitionCompletionItemProviderTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/MarkupTransitionCompletionItemProviderTest.cs similarity index 98% rename from src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/MarkupTransitionCompletionItemProviderTest.cs rename to src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/MarkupTransitionCompletionItemProviderTest.cs index 02c27f18395..5a79ab6f76b 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/MarkupTransitionCompletionItemProviderTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/MarkupTransitionCompletionItemProviderTest.cs @@ -9,12 +9,10 @@ using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.CodeAnalysis.Razor; -using Microsoft.CodeAnalysis.Razor.Completion; using Xunit; using Xunit.Abstractions; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; public class MarkupTransitionCompletionItemProviderTest(ITestOutputHelper testOutput) : ToolingTestBase(testOutput) { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionListProviderTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/RazorCompletionListProviderTest.cs similarity index 95% rename from src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionListProviderTest.cs rename to src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/RazorCompletionListProviderTest.cs index e941770fc8f..e7a654019be 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/RazorCompletionListProviderTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/RazorCompletionListProviderTest.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Razor.Language.Components; using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; -using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Razor.Tooltip; using Microsoft.CodeAnalysis.Testing; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -20,7 +19,7 @@ using Xunit.Abstractions; using static Microsoft.AspNetCore.Razor.Language.CommonMetadata; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; public class RazorCompletionListProviderTest : LanguageServerTestBase { @@ -28,6 +27,7 @@ public class RazorCompletionListProviderTest : LanguageServerTestBase private readonly CompletionListCache _completionListCache; private readonly VSInternalClientCapabilities _clientCapabilities; private readonly VSInternalCompletionContext _defaultCompletionContext; + private readonly RazorCompletionOptions _razorCompletionOptions; public RazorCompletionListProviderTest(ITestOutputHelper testOutput) : base(testOutput) @@ -54,21 +54,20 @@ public RazorCompletionListProviderTest(ITestOutputHelper testOutput) }; _defaultCompletionContext = new VSInternalCompletionContext(); + _razorCompletionOptions = new RazorCompletionOptions(SnippetsSupported: true, AutoInsertAttributeQuotes: true, CommitElementsWithSpace: true); } - private static IEnumerable GetCompletionProviders(RazorLSPOptionsMonitor optionsMonitor = null) + private static IEnumerable GetCompletionProviders() { // Working around strong naming restriction. - var tagHelperCompletionService = new LspTagHelperCompletionService(); - - optionsMonitor ??= TestRazorLSPOptionsMonitor.Create(); + var tagHelperCompletionService = new TagHelperCompletionService(); var completionProviders = new IRazorCompletionItemProvider[] { new DirectiveCompletionItemProvider(), new DirectiveAttributeCompletionItemProvider(), new DirectiveAttributeParameterCompletionItemProvider(), - new TagHelperCompletionProvider(tagHelperCompletionService, optionsMonitor) + new TagHelperCompletionProvider(tagHelperCompletionService) }; return completionProviders; @@ -369,7 +368,7 @@ public async Task GetCompletionListAsync_ProvidesDirectiveCompletionItems(string // Act var completionList = await provider.GetCompletionListAsync( - absoluteIndex: cursorPosition, _defaultCompletionContext, documentContext, _clientCapabilities, existingCompletions: null, DisposalToken); + absoluteIndex: cursorPosition, _defaultCompletionContext, documentContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions, DisposalToken); // Assert @@ -395,7 +394,7 @@ public async Task GetCompletionListAsync_ProvidesDirectiveCompletions_Incomplete // Act var completionList = await provider.GetCompletionListAsync( - absoluteIndex: 1, completionContext, documentContext, _clientCapabilities, existingCompletions: null, DisposalToken); + absoluteIndex: 1, completionContext, documentContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions, DisposalToken); // Assert @@ -427,7 +426,7 @@ public async Task GetCompletionListAsync_ProvidesInjectOnIncomplete_KeywordIn() // Act var completionList = await provider.GetCompletionListAsync( - absoluteIndex: 1, completionContext, documentContext, _clientCapabilities, existingCompletions: null, DisposalToken); + absoluteIndex: 1, completionContext, documentContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions, DisposalToken); // Assert Assert.Collection(completionList.Items, @@ -456,7 +455,7 @@ public async Task GetCompletionListAsync_DoesNotProvideInjectOnInvoked() // Act var completionList = await provider.GetCompletionListAsync( - absoluteIndex: 1, completionContext, documentContext, _clientCapabilities, existingCompletions: null, DisposalToken); + absoluteIndex: 1, completionContext, documentContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions, DisposalToken); // Assert Assert.Empty(completionList.Items); @@ -484,7 +483,7 @@ public async Task GetCompletionListAsync_ProvidesInjectOnIncomplete() // Act var completionList = await provider.GetCompletionListAsync( - absoluteIndex: 1, completionContext, documentContext, _clientCapabilities, existingCompletions: null, DisposalToken); + absoluteIndex: 1, completionContext, documentContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions, DisposalToken); // Assert Assert.Collection(completionList.Items, @@ -510,7 +509,7 @@ public async Task GetCompletionListAsync_ProvidesTagHelperElementCompletionItems // Act var completionList = await provider.GetCompletionListAsync( - absoluteIndex: 1, _defaultCompletionContext, documentContext, _clientCapabilities, existingCompletions: null, DisposalToken); + absoluteIndex: 1, _defaultCompletionContext, documentContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions, DisposalToken); // Assert Assert.Contains(completionList.Items, item => item.InsertText == "Test"); @@ -540,7 +539,7 @@ public async Task GetCompletionListAsync_ProvidesTagHelperAttributeItems() // Act var completionList = await provider.GetCompletionListAsync( - absoluteIndex: 6, _defaultCompletionContext, documentContext, _clientCapabilities, existingCompletions: null, DisposalToken); + absoluteIndex: 6, _defaultCompletionContext, documentContext, _clientCapabilities, existingCompletions: null, _razorCompletionOptions, DisposalToken); // Assert Assert.Contains(completionList.Items, item => item.InsertText == "testAttribute=\"$0\""); @@ -566,16 +565,15 @@ public async Task GetCompletionListAsync_ProvidesTagHelperAttributeItems_Attribu codeDocument.SetTagHelperContext(tagHelperContext); var documentContext = TestDocumentContext.Create(documentPath, codeDocument); - // Set up a custom options monitor with desired options - var optionsMonitor = TestRazorLSPOptionsMonitor.Create(); - await optionsMonitor.UpdateAsync(optionsMonitor.CurrentValue with { AutoInsertAttributeQuotes = false }, DisposalToken); + // Set up desired options + var razorCompletionOptions = new RazorCompletionOptions(SnippetsSupported: true, AutoInsertAttributeQuotes: false, CommitElementsWithSpace: true); - var completionFactsService = new LspRazorCompletionFactsService(GetCompletionProviders(optionsMonitor)); + var completionFactsService = new LspRazorCompletionFactsService(GetCompletionProviders()); var provider = new RazorCompletionListProvider(completionFactsService, _completionListCache, LoggerFactory); // Act var completionList = await provider.GetCompletionListAsync( - absoluteIndex: 6, _defaultCompletionContext, documentContext, _clientCapabilities, existingCompletions: null, DisposalToken); + absoluteIndex: 6, _defaultCompletionContext, documentContext, _clientCapabilities, existingCompletions: null, razorCompletionOptions, DisposalToken); // Assert Assert.Contains(completionList.Items, item => item.InsertText == "testAttribute=$0"); diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperCompletionProviderTest.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/TagHelperCompletionProviderTest.cs similarity index 95% rename from src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperCompletionProviderTest.cs rename to src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/TagHelperCompletionProviderTest.cs index 174c2a11c51..909a87a8f7b 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperCompletionProviderTest.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/TagHelperCompletionProviderTest.cs @@ -5,24 +5,21 @@ using System.Collections.Generic; using System.Collections.Immutable; -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.Test.Common; -using Microsoft.CodeAnalysis.Razor.Completion; using Microsoft.CodeAnalysis.Testing; using Microsoft.VisualStudio.Editor.Razor; using Xunit; using Xunit.Abstractions; using static Microsoft.AspNetCore.Razor.Language.CommonMetadata; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; public class TagHelperCompletionProviderTest(ITestOutputHelper testOutput) : TagHelperServiceTestBase(testOutput) { private TagHelperCompletionProvider CreateTagHelperCompletionProvider() - => new(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + => new(RazorTagHelperCompletionService); [Fact] public void GetNearestAncestorTagInfo_MarkupElement() @@ -69,7 +66,7 @@ public void GetNearestAncestorTagInfo_TagHelperElement() public void GetCompletionAt_AtEmptyTagName_ReturnsCompletions() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -92,7 +89,7 @@ public void GetCompletionAt_AtEmptyTagName_ReturnsCompletions() public void GetCompletionAt_InEmptyDocument_ReturnsEmptyCompletionArray() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService); var context = CreateRazorCompletionContext( "$$", isRazorFile: true, @@ -109,7 +106,7 @@ public void GetCompletionAt_InEmptyDocument_ReturnsEmptyCompletionArray() public void GetCompletionAt_OutsideOfTagName_DoesNotReturnCompletions() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -129,7 +126,7 @@ public void GetCompletionAt_OutsideOfTagName_DoesNotReturnCompletions() public void GetCompletionAt_OutsideOfTagName_InsideCSharp_DoesNotReturnCompletions() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -153,7 +150,7 @@ public void GetCompletionAt_OutsideOfTagName_InsideCSharp_DoesNotReturnCompletio public void GetCompletionAt_SelfClosingTag_NotAtEndOfName_DoesNotReturnCompletions() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -173,7 +170,7 @@ public void GetCompletionAt_SelfClosingTag_NotAtEndOfName_DoesNotReturnCompletio public void GetCompletionAt_SelfClosingTag_ReturnsCompletions() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -193,7 +190,7 @@ public void GetCompletionAt_SelfClosingTag_ReturnsCompletions() public void GetCompletionAt_SelfClosingTag_InsideCSharp_ReturnsCompletions() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -217,7 +214,7 @@ public void GetCompletionAt_SelfClosingTag_InsideCSharp_ReturnsCompletions() public void GetCompletionAt_MalformedElement() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -238,7 +235,7 @@ public void GetCompletionAt_MalformedElement() public void GetCompletionAt_AtHtmlElementNameEdge_ReturnsCompletions() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -260,7 +257,7 @@ public void GetCompletionAt_AtHtmlElementNameEdge_ReturnsCompletions() public void GetCompletionAt_AtTagHelperElementNameEdge_ReturnsCompletions() { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -317,7 +314,7 @@ public void GetCompletionAt_AtTagHelperElementNameEdge_ReturnsCompletions() public void GetCompletionAt_AtAttributeEdge_BothAttribute_ReturnsCompletions(string documentText) { // Arrange - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, TestRazorLSPOptionsMonitor.Create()); + var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService); var context = CreateRazorCompletionContext( documentText, isRazorFile: false, @@ -348,7 +345,7 @@ public void GetCompletionAt_AtAttributeEdge_IntAttribute_Snippets_ReturnsComplet { // Arrange var service = CreateTagHelperCompletionProvider(); - var options = new RazorCompletionOptions(SnippetsSupported: true); + var options = new RazorCompletionOptions(SnippetsSupported: true, AutoInsertAttributeQuotes: true, CommitElementsWithSpace: true); var context = CreateRazorCompletionContext( """ @addTagHelper *, TestAssembly @@ -439,12 +436,11 @@ public void GetCompletionAt_InBody_ReturnsCompletions() } [Fact] - public async Task GetCompletionAt_InBody_WithoutSpace_ReturnsCompletions() + public void GetCompletionAt_InBody_WithoutSpace_ReturnsCompletions() { // Arrange - var options = TestRazorLSPOptionsMonitor.Create(); - await options.UpdateAsync(options.CurrentValue with { CommitElementsWithSpace = false }, CancellationToken.None); - var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService, options); + var razorCompletionOptions = new RazorCompletionOptions(SnippetsSupported: true, AutoInsertAttributeQuotes: true, CommitElementsWithSpace: false); + var service = new TagHelperCompletionProvider(RazorTagHelperCompletionService); var context = CreateRazorCompletionContext( """ @@ -454,7 +450,8 @@ public async Task GetCompletionAt_InBody_WithoutSpace_ReturnsCompletions() """, isRazorFile: false, - tagHelpers: DefaultTagHelpers); + tagHelpers: DefaultTagHelpers, + options: razorCompletionOptions); // Act var completions = service.GetCompletionItems(context); @@ -840,7 +837,7 @@ public void GetCompletionsAt_MiddleOfFullAttribute_ReturnsCompletions_NoSnippetB """, isRazorFile: false, - options: new(SnippetsSupported: true), + options: new(SnippetsSupported: true, AutoInsertAttributeQuotes: true, CommitElementsWithSpace: true), tagHelpers: DefaultTagHelpers); // Act diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperServiceTestBase.cs b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/TagHelperServiceTestBase.cs similarity index 94% rename from src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperServiceTestBase.cs rename to src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/TagHelperServiceTestBase.cs index de6cbf14e92..61cc1d0e561 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/Completion/TagHelperServiceTestBase.cs +++ b/src/Razor/test/Microsoft.CodeAnalysis.Razor.Workspaces.Test/Completion/TagHelperServiceTestBase.cs @@ -8,11 +8,10 @@ using Microsoft.AspNetCore.Razor.Language.Components; using Microsoft.AspNetCore.Razor.Test.Common.LanguageServer; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Razor.Completion; using Xunit.Abstractions; using static Microsoft.AspNetCore.Razor.Language.CommonMetadata; -namespace Microsoft.AspNetCore.Razor.LanguageServer.Completion; +namespace Microsoft.CodeAnalysis.Razor.Completion; public abstract class TagHelperServiceTestBase : LanguageServerTestBase { @@ -246,23 +245,23 @@ public TagHelperServiceTestBase(ITestOutputHelper testOutput) directiveAttribute3.Build(), htmlTagMutator.Build()); - RazorTagHelperCompletionService = new LspTagHelperCompletionService(); + RazorTagHelperCompletionService = new TagHelperCompletionService(); } protected static string GetFileName(bool isRazorFile) => isRazorFile ? RazorFile : CSHtmlFile; - internal static RazorCodeDocument CreateCodeDocument(string text, bool isRazorFile, ImmutableArray tagHelpers) + protected internal static RazorCodeDocument CreateCodeDocument(string text, bool isRazorFile, ImmutableArray tagHelpers) { return CreateCodeDocument(text, GetFileName(isRazorFile), tagHelpers); } - internal static RazorCodeDocument CreateCodeDocument(string text, bool isRazorFile, params TagHelperDescriptor[] tagHelpers) + protected internal static RazorCodeDocument CreateCodeDocument(string text, bool isRazorFile, params TagHelperDescriptor[] tagHelpers) { return CreateCodeDocument(text, GetFileName(isRazorFile), tagHelpers); } - internal static RazorCodeDocument CreateCodeDocument(string text, string filePath, ImmutableArray tagHelpers) + protected internal static RazorCodeDocument CreateCodeDocument(string text, string filePath, ImmutableArray tagHelpers) { tagHelpers = tagHelpers.NullToEmpty(); @@ -274,7 +273,7 @@ internal static RazorCodeDocument CreateCodeDocument(string text, string filePat return codeDocument; } - internal static RazorCodeDocument CreateCodeDocument(string text, string filePath, params TagHelperDescriptor[] tagHelpers) + protected internal static RazorCodeDocument CreateCodeDocument(string text, string filePath, params TagHelperDescriptor[] tagHelpers) { tagHelpers ??= Array.Empty(); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentCompletionEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentCompletionEndpointTest.cs new file mode 100644 index 00000000000..4dea627af65 --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentCompletionEndpointTest.cs @@ -0,0 +1,508 @@ +// 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.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor.Settings; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.Razor.Settings; +using Microsoft.VisualStudio.Razor.Snippets; +using Xunit; +using Xunit.Abstractions; +using RoslynCompletionParams = Roslyn.LanguageServer.Protocol.CompletionParams; +using RoslynCompletionTriggerKind = Roslyn.LanguageServer.Protocol.CompletionTriggerKind; +using RoslynLspExtensions = Roslyn.LanguageServer.Protocol.RoslynLspExtensions; +using RoslynTextDocumentIdentifier = Roslyn.LanguageServer.Protocol.TextDocumentIdentifier; +using RoslynVSInternalCompletionContext = Roslyn.LanguageServer.Protocol.VSInternalCompletionContext; +using RoslynVSInternalCompletionInvokeKind = Roslyn.LanguageServer.Protocol.VSInternalCompletionInvokeKind; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +public class CohostDocumentCompletionEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) +{ + [Fact] + public async Task CSharpClassesAtTransition() + { + await VerifyCompletionListAsync( + input: $""" + This is a Razor document. + +
@$$
+ + The end. + """, + completionContext: new RoslynVSInternalCompletionContext() + { + InvokeKind = RoslynVSInternalCompletionInvokeKind.Typing, + TriggerCharacter = "@", + TriggerKind = RoslynCompletionTriggerKind.TriggerCharacter + }, + expectedItemLabels: ["char", "DateTime", "Exception"], + expectedItemCount: 996); + } + + [Fact] + public async Task CSharpClassMembersAtProvisionalCompletion() + { + await VerifyCompletionListAsync( + input: $""" + This is a Razor document. + +
@DateTime.$$
+ + The end. + """, + completionContext: new RoslynVSInternalCompletionContext() + { + InvokeKind = RoslynVSInternalCompletionInvokeKind.Typing, + TriggerCharacter = ".", + TriggerKind = RoslynCompletionTriggerKind.TriggerCharacter + }, + expectedItemLabels: ["DaysInMonth", "IsLeapYear", "Now"], + expectedItemCount: 20); + } + + [Fact] + public async Task CSharpClassesInCodeBlock() + { + await VerifyCompletionListAsync( + input: $$""" + This is a Razor document. + +
+ + @code{ $$ } + + The end. + """, + completionContext: new RoslynVSInternalCompletionContext() + { + InvokeKind = RoslynVSInternalCompletionInvokeKind.Explicit, + TriggerCharacter = null, + TriggerKind = RoslynCompletionTriggerKind.Invoked + }, + expectedItemLabels: ["char", "DateTime", "Exception"], + expectedItemCount: 1000); + } + + [Fact] + public async Task CSharpClassMembersInCodeBlock() + { + await VerifyCompletionListAsync( + input: $$""" + This is a Razor document. + +
+ + @code{ + void foo() + { + DateTime.$$ + } + } + + The end. + """, + completionContext: new RoslynVSInternalCompletionContext() + { + InvokeKind = RoslynVSInternalCompletionInvokeKind.Typing, + TriggerCharacter = ".", + TriggerKind = RoslynCompletionTriggerKind.TriggerCharacter + }, + expectedItemLabels: ["DaysInMonth", "IsLeapYear", "Now"], + expectedItemCount: 20); + } + + // Tests MarkupTransitionCompletionItemProvider + [Fact] + public async Task CSharpMarkupTransitionAndTagHelpersInCodeBlock() + { + await VerifyCompletionListAsync( + input: $$""" + This is a Razor document. + +
+ + @code{ + void foo() + { + <$$ + } + } + + The end. + """, + completionContext: new RoslynVSInternalCompletionContext() + { + InvokeKind = RoslynVSInternalCompletionInvokeKind.Typing, + TriggerCharacter = "<", + TriggerKind = RoslynCompletionTriggerKind.TriggerCharacter + }, + expectedItemLabels: ["text", "EditForm", "InputDate"], + expectedItemCount: 34); + } + + [Fact] + public async Task RazorDirectives() + { + await VerifyCompletionListAsync( + input: $$""" + @$$ + This is a Razor document. + +
+ + @code{ + void foo() + { + + } + } + + The end. + """, + completionContext: new RoslynVSInternalCompletionContext() + { + InvokeKind = RoslynVSInternalCompletionInvokeKind.Typing, + TriggerCharacter = "@", + TriggerKind = RoslynCompletionTriggerKind.TriggerCharacter + }, + expectedItemLabels: ["using", "using directive ...", "page", "page directive ..."], + expectedItemCount: 538); + } + + [Fact] + public async Task ElementNameTagHelpersCompletion() + { + await VerifyCompletionListAsync( + input: $""" + This is a Razor document. + + <$$ + + The end. + """, + completionContext: new RoslynVSInternalCompletionContext() + { + InvokeKind = RoslynVSInternalCompletionInvokeKind.Typing, + TriggerCharacter = "<", + TriggerKind = RoslynCompletionTriggerKind.TriggerCharacter + }, + expectedItemLabels: ["LayoutView", "EditForm", "ValidationMessage"], + expectedItemCount: 33); + } + + [Fact] + public async Task HtmlElementNamesAndTagHelpersCompletion() + { + await VerifyCompletionListAsync( + input: $""" + This is a Razor document. + + <$$ + + The end. + """, + completionContext: new RoslynVSInternalCompletionContext() + { + InvokeKind = RoslynVSInternalCompletionInvokeKind.Typing, + TriggerCharacter = "<", + TriggerKind = RoslynCompletionTriggerKind.TriggerCharacter + }, + expectedItemLabels: ["div", "h1", "LayoutView", "EditForm", "ValidationMessage"], + expectedItemCount: 35, + delegatedItemLabels: ["div", "h1"]); + } + + [Fact] + public async Task HtmlElementDoNotCommitWithSpace() + { + await VerifyCompletionListAsync( + input: $""" + This is a Razor document. + + <$$ + + The end. + """, + completionContext: new RoslynVSInternalCompletionContext() + { + InvokeKind = RoslynVSInternalCompletionInvokeKind.Typing, + TriggerCharacter = "<", + TriggerKind = RoslynCompletionTriggerKind.TriggerCharacter + }, + expectedItemLabels: ["div", "h1", "LayoutView", "EditForm", "ValidationMessage"], + expectedItemCount: 35, + delegatedItemLabels: ["div", "h1"], + delegatedItemCommitCharacters: [" ", ">"], + commitElementsWithSpace: false); + } + + [Fact] + public async Task HtmlSnippetsCompletion() + { + await VerifyCompletionListAsync( + input: $""" + This is a Razor document. + + $$ + + The end. + """, + completionContext: new RoslynVSInternalCompletionContext() + { + InvokeKind = RoslynVSInternalCompletionInvokeKind.Explicit, + TriggerCharacter = null, + TriggerKind = RoslynCompletionTriggerKind.Invoked + }, + expectedItemLabels: ["snippet1", "snippet2"], + expectedItemCount: 2, + snippetLabels: ["snippet1", "snippet2"]); + } + + // Tests HTML attributes and DirectiveAttributeTransitionCompletionItemProvider + [Fact] + public async Task HtmlAndDirectiveAttributeTransitionNamesCompletion() + { + await VerifyCompletionListAsync( + input: $""" + This is a Razor document. + +
+ + The end. + """, + completionContext: new RoslynVSInternalCompletionContext() + { + InvokeKind = RoslynVSInternalCompletionInvokeKind.Typing, + TriggerCharacter = " ", + TriggerKind = RoslynCompletionTriggerKind.TriggerCharacter + }, + expectedItemLabels: ["style", "dir", "@..."], + expectedItemCount: 3, + delegatedItemLabels: ["style", "dir"]); + } + + // Tests HTML attributes and DirectiveAttributeCompletionItemProvider + [Fact] + public async Task HtmlAndDirectiveAttributeNamesCompletion() + { + await VerifyCompletionListAsync( + input: $""" + This is a Razor document. + +
+ + The end. + """, + completionContext: new RoslynVSInternalCompletionContext() + { + InvokeKind = RoslynVSInternalCompletionInvokeKind.Typing, + TriggerCharacter = "@", + TriggerKind = RoslynCompletionTriggerKind.TriggerCharacter + }, + expectedItemLabels: ["style", "dir", "@rendermode", "@bind-..."], + expectedItemCount: 104, + delegatedItemLabels: ["style", "dir"]); + } + + // Tests HTML attributes and DirectiveAttributeParameterCompletionItemProvider + [Fact] + public async Task HtmlAndDirectiveAttributeParameterNamesCompletion() + { + await VerifyCompletionListAsync( + input: $""" + This is a Razor document. + + + + The end. + """, + completionContext: new RoslynVSInternalCompletionContext() + { + InvokeKind = RoslynVSInternalCompletionInvokeKind.Typing, + TriggerCharacter = "f", + TriggerKind = RoslynCompletionTriggerKind.TriggerCharacter + }, + expectedItemLabels: ["style", "dir", "culture", "event", "format", "get", "set", "after"], + expectedItemCount: 8, + delegatedItemLabels: ["style", "dir"]); + } + + [Fact] + public async Task HtmlAttributeNamesAndTagHelpersCompletion() + { + await VerifyCompletionListAsync( + input: $""" + This is a Razor document. + + + + The end. + """, + completionContext: new RoslynVSInternalCompletionContext() + { + InvokeKind = RoslynVSInternalCompletionInvokeKind.Typing, + TriggerCharacter = " ", + TriggerKind = RoslynCompletionTriggerKind.TriggerCharacter + }, + expectedItemLabels: ["style", "dir", "FormName", "OnValidSubmit", "@..."], + expectedItemCount: 13, + delegatedItemLabels: ["style", "dir"]); + } + + [Fact] + public async Task TagHelperAttributes_NoAutoInsertQuotes_Completion() + { + await VerifyCompletionListAsync( + input: $""" + This is a Razor document. + + + + The end. + """, + completionContext: new RoslynVSInternalCompletionContext() + { + InvokeKind = RoslynVSInternalCompletionInvokeKind.Typing, + TriggerCharacter = " ", + TriggerKind = RoslynCompletionTriggerKind.TriggerCharacter + }, + expectedItemLabels: ["FormName", "OnValidSubmit", "@..."], + expectedItemCount: 11, + autoInsertAttributeQuotes: false); + } + + private async Task VerifyCompletionListAsync( + TestCode input, + RoslynVSInternalCompletionContext completionContext, + string[] expectedItemLabels, + int expectedItemCount, + string[]? delegatedItemLabels = null, + string[]? delegatedItemCommitCharacters = null, + string[]? snippetLabels = null, + bool autoInsertAttributeQuotes = true, + bool commitElementsWithSpace = true) + { + var document = await CreateProjectAndRazorDocumentAsync(input.Text); + var sourceText = await document.GetTextAsync(DisposalToken); + + var clientSettingsManager = new ClientSettingsManager([], null, null); + clientSettingsManager.Update(ClientAdvancedSettings.Default with { AutoInsertAttributeQuotes = autoInsertAttributeQuotes, CommitElementsWithSpace = commitElementsWithSpace }); + + VSInternalCompletionList? response = null; + if (delegatedItemLabels is not null) + { + response = new VSInternalCompletionList() + { + Items = delegatedItemLabels.Select((label) => new VSInternalCompletionItem() + { + Label = label, + CommitCharacters = delegatedItemCommitCharacters, + // If test specifies not to commit with space, set kind to element since we remove space + // commit from elements only. Otherwise test doesn't care, so set to None + Kind = !commitElementsWithSpace ? CompletionItemKind.Element : CompletionItemKind.None, + }).ToArray(), + IsIncomplete = true + }; + } + + var requestInvoker = new TestLSPRequestInvoker([(Methods.TextDocumentCompletionName, response)]); + + var snippetCompletionItemProvider = new SnippetCompletionItemProvider(new SnippetCache()); + if (snippetLabels is not null) + { + var snippetInfos = snippetLabels.Select(label => new SnippetInfo(label, label, label, string.Empty, SnippetLanguage.Html)).ToImmutableArray(); + snippetCompletionItemProvider.SnippetCache.Update(SnippetLanguage.Html, snippetInfos); + } + + var completionSetting = new CompletionSetting + { + CompletionItem = new CompletionItemSetting(), + CompletionItemKind = new CompletionItemKindSetting() + { + ValueSet = (CompletionItemKind[])Enum.GetValues(typeof(CompletionItemKind)), + }, + CompletionListSetting = new CompletionListSetting() + { + ItemDefaults = ["commitCharacters", "editRange", "insertTextFormat"] + }, + ContextSupport = false, + InsertTextMode = InsertTextMode.AsIs, + }; + var clientCapabilities = new VSInternalClientCapabilities + { + TextDocument = new TextDocumentClientCapabilities + { + Completion = completionSetting + } + }; + var endpoint = new CohostDocumentCompletionEndpoint( + RemoteServiceInvoker, + clientSettingsManager, + TestHtmlDocumentSynchronizer.Instance, + snippetCompletionItemProvider, + requestInvoker, + LoggerFactory); + endpoint.GetTestAccessor().SetClientCapabilities(clientCapabilities); + + var request = new RoslynCompletionParams() + { + TextDocument = new RoslynTextDocumentIdentifier() + { + Uri = document.CreateUri() + }, + Position = RoslynLspExtensions.GetPosition(sourceText, input.Position), + Context = completionContext + }; + + // Roslyn doesn't always return all items right away, so using retry logic + VSInternalCompletionList? result = null; + var resultCount = 0; + const int maxResultCount = 100; + do + { + if (resultCount > 0) + { + await Task.Delay(100); + } + + result = await endpoint.GetTestAccessor().HandleRequestAsync(request, document, DisposalToken); + } + while (result is not null + && result.IsIncomplete + && result.Items.Length < expectedItemCount + && resultCount++ < maxResultCount); + + Assert.NotNull(result); + Assert.Equal(expectedItemCount, result.Items.Length); + + using var _ = HashSetPool.GetPooledObject(out var labelSet); + labelSet.AddRange(result.Items.Select((item) => item.Label)); + foreach (var expectedItemLabel in expectedItemLabels) + { + Assert.Contains(expectedItemLabel, labelSet); + } + + if (!commitElementsWithSpace) + { + Assert.False(result.Items.Any(item => item.CommitCharacters?.First().Contains(" ") ?? false)); + } + + if (!autoInsertAttributeQuotes) + { + // Tag helper attributes create InsertText that looks something like + // "OnValidSubmit=\"$0\"" (for OnValidSubmit attribute). Make sure the value + // placeholder $0 is not surrounded with quotes if we set AutoInsertAttributeQuotes + // to false + Assert.False(result.Items.Any(item => item.InsertText?.Contains("\"$0\"") ?? false)); + } + } +} diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/RazorCustomMessageTargetTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/RazorCustomMessageTargetTest.cs index 99132c4935c..69b3f7ed42a 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/RazorCustomMessageTargetTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/LanguageClient/RazorCustomMessageTargetTest.cs @@ -64,7 +64,7 @@ public async Task UpdateCSharpBuffer_CannotLookupDocument_NoopsGracefully() Mock.Of(MockBehavior.Strict), TestLanguageServerFeatureOptions.Instance, StrictMock.Of(), - new SnippetCache(), + new SnippetCompletionItemProvider(new SnippetCache()), StrictMock.Of(), StrictMock.Of(), LoggerFactory); @@ -106,7 +106,7 @@ public async Task UpdateCSharpBuffer_UpdatesDocument() Mock.Of(MockBehavior.Strict), TestLanguageServerFeatureOptions.Instance, StrictMock.Of(), - new SnippetCache(), + new SnippetCompletionItemProvider(new SnippetCache()), StrictMock.Of(), StrictMock.Of(), LoggerFactory); @@ -159,7 +159,7 @@ public async Task UpdateCSharpBuffer_UpdatesCorrectDocument() Mock.Of(MockBehavior.Strict), new TestLanguageServerFeatureOptions(includeProjectKeyInGeneratedFilePath: true), StrictMock.Of(), - new SnippetCache(), + new SnippetCompletionItemProvider(new SnippetCache()), StrictMock.Of(), StrictMock.Of(), LoggerFactory); @@ -200,7 +200,7 @@ public async Task ProvideCodeActionsAsync_CannotLookupDocument_ReturnsNullAsync( Mock.Of(MockBehavior.Strict), TestLanguageServerFeatureOptions.Instance, StrictMock.Of(), - new SnippetCache(), + new SnippetCompletionItemProvider(new SnippetCache()), StrictMock.Of(), StrictMock.Of(), LoggerFactory); @@ -280,7 +280,7 @@ async IAsyncEnumerable> telemetryReporter.Object, TestLanguageServerFeatureOptions.Instance, StrictMock.Of(), - new SnippetCache(), + new SnippetCompletionItemProvider(new SnippetCache()), StrictMock.Of(), StrictMock.Of(), LoggerFactory); @@ -367,7 +367,7 @@ async IAsyncEnumerable> GetExpectedRe telemetryReporter.Object, TestLanguageServerFeatureOptions.Instance, StrictMock.Of(), - new SnippetCache(), + new SnippetCompletionItemProvider(new SnippetCache()), StrictMock.Of(), StrictMock.Of(), LoggerFactory); @@ -409,7 +409,7 @@ public async Task ProvideSemanticTokensAsync_CannotLookupDocument_ReturnsNullAsy Mock.Of(MockBehavior.Strict), TestLanguageServerFeatureOptions.Instance, StrictMock.Of(), - new SnippetCache(), + new SnippetCompletionItemProvider(new SnippetCache()), StrictMock.Of(), StrictMock.Of(), LoggerFactory); @@ -458,7 +458,7 @@ public async Task ProvideSemanticTokensAsync_CannotLookupVirtualDocument_Returns Mock.Of(MockBehavior.Strict), TestLanguageServerFeatureOptions.Instance, StrictMock.Of(), - new SnippetCache(), + new SnippetCompletionItemProvider(new SnippetCache()), StrictMock.Of(), StrictMock.Of(), LoggerFactory); @@ -535,7 +535,7 @@ public async Task ProvideSemanticTokensAsync_ContainsRange_ReturnsSemanticTokens telemetryReporter.Object, TestLanguageServerFeatureOptions.Instance, StrictMock.Of(), - new SnippetCache(), + new SnippetCompletionItemProvider(new SnippetCache()), StrictMock.Of(), StrictMock.Of(), LoggerFactory); @@ -613,7 +613,7 @@ public async Task ProvideSemanticTokensAsync_EmptyRange_ReturnsNoSemanticTokens( telemetryReporter.Object, TestLanguageServerFeatureOptions.Instance, StrictMock.Of(), - new SnippetCache(), + new SnippetCompletionItemProvider(new SnippetCache()), StrictMock.Of(), StrictMock.Of(), LoggerFactory);