diff --git a/eng/Versions.props b/eng/Versions.props index 0b69a55c261..e00b6a4aac4 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -93,7 +93,7 @@ 17.0.31723.112 17.0.487 4.1.0-1.21471.13 - 17.1.2 + 17.1.8 5.0.0-preview.4.20205.1 diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Common/LanguageServerConstants.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Common/LanguageServerConstants.cs index 1291baeccc4..5bbf232d5f9 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Common/LanguageServerConstants.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Common/LanguageServerConstants.cs @@ -50,6 +50,8 @@ public static class LanguageServerConstants public const string RazorServerReadyEndpoint = "razor/serverReady"; + public const string RazorInlineCompletionEndpoint = "razor/inlineCompletion"; + // This needs to be the same as in Web Tools, that is used by the HTML editor, because // we actually respond to the Web Tools "Wrap With Div" command handler, which sends this message // to all servers. We then take the message, get the HTML virtual document, and send it diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/IInlineCompletionHandler.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/IInlineCompletionHandler.cs new file mode 100644 index 00000000000..bfa6bd39337 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/IInlineCompletionHandler.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using OmniSharp.Extensions.JsonRpc; + +namespace Microsoft.AspNetCore.Razor.LanguageServer; + +[Parallel, Method("textDocument/_vs_inlineCompletion")] +internal interface IInlineCompletionHandler : IJsonRpcRequestHandler, IRegistrationExtension +{ +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionContext.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionContext.cs new file mode 100644 index 00000000000..75f282c22b2 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionContext.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. + +#nullable disable + +using System.Runtime.Serialization; +using Newtonsoft.Json; + +namespace Microsoft.AspNetCore.Razor.LanguageServer; + +/// +/// Corresponds to https://devdiv.visualstudio.com/DevDiv/_git/VSLanguageServerClient?path=/src/product/Protocol/LanguageServer.Protocol.Internal/VSInternalInlineCompletionContext.cs +/// +internal class InlineCompletionContext +{ + [DataMember(Name = "_vs_triggerKind")] + [JsonProperty(Required = Required.Always)] + public InlineCompletionTriggerKind TriggerKind { get; set; } = InlineCompletionTriggerKind.Explicit; + + [DataMember(Name = "_vs_selectedCompletionInfo")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public SelectedCompletionInfo SelectedCompletionInfo { get; set; } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs new file mode 100644 index 00000000000..0f432c690b8 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs @@ -0,0 +1,254 @@ +// 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.Composition; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.LanguageServer.Common; +using Microsoft.AspNetCore.Razor.LanguageServer.Common.Extensions; +using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; +using Microsoft.AspNetCore.Razor.LanguageServer.Formatting; +using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; +using Microsoft.CodeAnalysis.Razor; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.AspNetCore.Razor.LanguageServer; + +internal class InlineCompletionEndpoint : IInlineCompletionHandler +{ + // Usually when we need to format code, we utilize the formatting options provided + // by the platform. Similar to DefaultCSharpCodeActionResolver we do not have any, so use defaults. + private static readonly FormattingOptions s_defaultFormattingOptions = new FormattingOptions() + { + TabSize = 4, + InsertSpaces = true, + TrimTrailingWhitespace = true, + InsertFinalNewline = true, + TrimFinalNewlines = true + }; + + private static readonly ImmutableHashSet s_cSharpKeywords = ImmutableHashSet.Create( + "~", "Attribute", "checked", "class", "ctor", "cw", "do", "else", "enum", "equals", "Exception", "for", "foreach", "forr", + "if", "indexer", "interface", "invoke", "iterator", "iterindex", "lock", "mbox", "namespace", "#if", "#region", "prop", + "propfull", "propg", "sim", "struct", "svm", "switch", "try", "tryf", "unchecked", "unsafe", "using", "while"); + + private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher; + private readonly DocumentResolver _documentResolver; + private readonly RazorDocumentMappingService _documentMappingService; + private readonly ClientNotifierServiceBase _languageServer; + private readonly AdhocWorkspaceFactory _adhocWorkspaceFactory; + private readonly ILogger _logger; + + [ImportingConstructor] + public InlineCompletionEndpoint( + ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher, + DocumentResolver documentResolver, + RazorDocumentMappingService documentMappingService, + ClientNotifierServiceBase languageServer, + AdhocWorkspaceFactory adhocWorkspaceFactory, + ILoggerFactory loggerFactory) + { + if (projectSnapshotManagerDispatcher is null) + { + throw new ArgumentNullException(nameof(projectSnapshotManagerDispatcher)); + } + + if (documentResolver is null) + { + throw new ArgumentNullException(nameof(documentResolver)); + } + + if (documentMappingService is null) + { + throw new ArgumentNullException(nameof(documentMappingService)); + } + + if (languageServer is null) + { + throw new ArgumentNullException(nameof(languageServer)); + } + + if (adhocWorkspaceFactory is null) + { + throw new ArgumentNullException(nameof(adhocWorkspaceFactory)); + } + + if (loggerFactory is null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + _projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher; + _documentResolver = documentResolver; + _documentMappingService = documentMappingService; + _languageServer = languageServer; + _adhocWorkspaceFactory = adhocWorkspaceFactory; + _logger = loggerFactory.CreateLogger(); + } + + public RegistrationExtensionResult GetRegistration() + { + const string AssociatedServerCapability = "_vs_inlineCompletionOptions"; + + var registrationOptions = new InlineCompletionOptions() + { + DocumentSelector = RazorDefaults.Selector, + Pattern = string.Join("|", s_cSharpKeywords) + }; + + return new RegistrationExtensionResult(AssociatedServerCapability, registrationOptions); + } + + public async Task Handle(InlineCompletionRequest request, CancellationToken cancellationToken) + { + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + _logger.LogInformation($"Starting request for {request.TextDocument.Uri} at {request.Position}."); + + var document = await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(() => + { + _documentResolver.TryResolveDocument(request.TextDocument.Uri.GetAbsoluteOrUNCPath(), out var documentSnapshot); + + return documentSnapshot; + }, cancellationToken).ConfigureAwait(false); + + if (document is null) + { + return null; + } + + var codeDocument = await document.GetGeneratedOutputAsync(); + if (codeDocument.IsUnsupported()) + { + return null; + } + + var sourceText = await document.GetTextAsync(); + var linePosition = new LinePosition(request.Position.Line, request.Position.Character); + var hostDocumentIndex = sourceText.Lines.GetPosition(linePosition); + + var languageKind = _documentMappingService.GetLanguageKind(codeDocument, hostDocumentIndex); + + // Map to the location in the C# document. + if (languageKind != RazorLanguageKind.CSharp || + !_documentMappingService.TryMapToProjectedDocumentPosition(codeDocument, hostDocumentIndex, out var projectedPosition, out _)) + { + _logger.LogInformation($"Unsupported location for {request.TextDocument.Uri}."); + return null; + } + + var razorRequest = new RazorInlineCompletionRequest + { + TextDocument = request.TextDocument, + Context = request.Context, + Position = projectedPosition, + Kind = languageKind, + }; + + request.Position = projectedPosition; + var response = await _languageServer.SendRequestAsync(LanguageServerConstants.RazorInlineCompletionEndpoint, razorRequest).ConfigureAwait(false); + var list = await response.Returning(cancellationToken).ConfigureAwait(false); + if (list == null || !list.Items.Any()) + { + _logger.LogInformation($"Did not get any inline completions from delegation."); + return null; + } + + var items = new List(); + var csharpDocOptions = codeDocument.GetCSharpDocument(); + foreach (var item in list.Items) + { + var containsSnippet = item.TextFormat == InsertTextFormat.Snippet; + var range = item.Range ?? new Range { Start = projectedPosition, End = projectedPosition }; + + if (!_documentMappingService.TryMapFromProjectedDocumentRange(codeDocument, range, out var rangeInRazorDoc)) + { + _logger.LogWarning($"Could not remap projected range {range} to razor document"); + continue; + } + + using var formattingContext = FormattingContext.Create(request.TextDocument.Uri, document, codeDocument, s_defaultFormattingOptions, _adhocWorkspaceFactory, isFormatOnType: true, automaticallyAddUsings: false); + if (!TryGetSnippetWithAdjustedIndentation(formattingContext, item.Text, hostDocumentIndex, out var newSnippetText)) + { + continue; + } + + var remappedItem = new InlineCompletionItem + { + Command = item.Command, + Range = rangeInRazorDoc, + Text = newSnippetText.ToString(), + TextFormat = item.TextFormat, + }; + items.Add(remappedItem); + } + + if (items.Count == 0) + { + _logger.LogInformation($"Could not format / map the items from delegation."); + return null; + } + + _logger.LogInformation($"Returning {items.Count} items."); + return new InlineCompletionList + { + Items = items.ToArray() + }; + } + + private static bool TryGetSnippetWithAdjustedIndentation(FormattingContext formattingContext, string snippetText, int hostDocumentIndex, [NotNullWhen(true)] out string? newSnippetText) + { + newSnippetText = null; + if (!formattingContext.TryGetFormattingSpan(hostDocumentIndex, out var formattingSpan)) + { + return false; + } + + // Take the amount of indentation razor and html are adding, then remove the amount of C# indentation that is 'hidden'. + // This should give us the desired base indentation that must be applied to each line. + var razorAndHtmlContributionsToIndentation = formattingSpan.RazorIndentationLevel + formattingSpan.HtmlIndentationLevel; + var amountToAddToCSharpIndentation = razorAndHtmlContributionsToIndentation - formattingSpan.MinCSharpIndentLevel; + + var snippetSourceText = SourceText.From(snippetText); + List indentationChanges = new(); + // Adjust each line, skipping the first since it must start at the snippet keyword. + foreach (var line in snippetSourceText.Lines.Skip(1)) + { + var lineText = snippetSourceText.GetSubText(line.Span); + if (lineText.Length == 0) + { + // We just have an empty line, nothing to do. + continue; + } + + // Get the indentation of the line in the C# document based on what options the C# document was generated with. + var csharpLineIndentationSize = line.GetIndentationSize(formattingContext.Options.TabSize); + var csharpIndentationLevel = csharpLineIndentationSize / formattingContext.Options.TabSize; + + // Get the new indentation level based on the context in the razor document. + var newIndentationLevel = csharpIndentationLevel + amountToAddToCSharpIndentation; + var newIndentationString = formattingContext.GetIndentationLevelString(newIndentationLevel); + + // Replace the current indentation with the new indentation. + var spanToReplace = new TextSpan(line.Start, line.GetFirstNonWhitespaceOffset() ?? line.Span.End); + var textChange = new TextChange(spanToReplace, newIndentationString); + indentationChanges.Add(textChange); + } + + var newSnippetSourceText = snippetSourceText.WithChanges(indentationChanges); + newSnippetText = newSnippetSourceText.ToString(); + return true; + } +} + diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionItem.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionItem.cs new file mode 100644 index 00000000000..146032cba3e --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionItem.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +#nullable disable + +using System.Runtime.Serialization; +using Newtonsoft.Json; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.AspNetCore.Razor.LanguageServer; + +/// +/// Corresponds to https://devdiv.visualstudio.com/DevDiv/_git/VSLanguageServerClient?path=/src/product/Protocol/LanguageServer.Protocol.Internal/VSInternalInlineCompletionItem.cs +/// +internal class InlineCompletionItem +{ + [DataMember(Name = "_vs_text")] + [JsonProperty(Required = Required.Always)] + public string Text { get; set; } + + [DataMember(Name = "_vs_range")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public Range Range { get; set; } + + [DataMember(Name = "_vs_command")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public Command Command { get; set; } + + [DataMember(Name = "_vs_insertTextFormat")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public InsertTextFormat? TextFormat { get; set; } = InsertTextFormat.PlainText; +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionList.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionList.cs new file mode 100644 index 00000000000..e28dd815e12 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionList.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +#nullable disable + +using System.Runtime.Serialization; +using Newtonsoft.Json; + +namespace Microsoft.AspNetCore.Razor.LanguageServer; + +internal class InlineCompletionList +{ + /// + /// Gets or sets the inline completion items. + /// + [DataMember(Name = "_vs_items")] + [JsonProperty(Required = Required.Always)] + public InlineCompletionItem[] Items { get; set; } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionOptions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionOptions.cs new file mode 100644 index 00000000000..88f2d7e3f44 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionOptions.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +#nullable disable + +using System.Runtime.Serialization; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.AspNetCore.Razor.LanguageServer; + +internal class InlineCompletionOptions : ITextDocumentRegistrationOptions +{ + /// + /// Gets or sets a regex used by the client to determine when to ask the server for snippets. + /// + [DataMember(Name = "_vs_pattern")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string Pattern { get; set; } + + public DocumentSelector DocumentSelector { get; set; } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionRequest.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionRequest.cs new file mode 100644 index 00000000000..80b6d24555b --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionRequest.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +#nullable disable + +using System.Runtime.Serialization; +using MediatR; +using Newtonsoft.Json; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.AspNetCore.Razor.LanguageServer; + +/// +/// Corresponds to https://devdiv.visualstudio.com/DevDiv/_git/VSLanguageServerClient?path=/src/product/Protocol/LanguageServer.Protocol.Internal/VSInternalInlineCompletionRequest.cs +/// +internal class InlineCompletionRequest : ITextDocumentIdentifierParams, IRequest, IBaseRequest +{ + [DataMember(Name = "_vs_textDocument")] + [JsonProperty(Required = Required.Always)] + public TextDocumentIdentifier TextDocument { get; set; } + + [DataMember(Name = "_vs_position")] + [JsonProperty(Required = Required.Always)] + public Position Position { get; set; } + + [DataMember(Name = "_vs_context")] + [JsonProperty(Required = Required.Always)] + public InlineCompletionContext Context { get; set; } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionTriggerKind.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionTriggerKind.cs new file mode 100644 index 00000000000..f32bd4b1641 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionTriggerKind.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +#nullable disable + +using System.Runtime.Serialization; + +namespace Microsoft.AspNetCore.Razor.LanguageServer; + +/// +/// Corresponds to https://devdiv.visualstudio.com/DevDiv/_git/VSLanguageServerClient?path=/src/product/Protocol/LanguageServer.Protocol.Internal/VSInternalInlineCompletionTriggerKind.cs +/// +[DataContract] +internal enum InlineCompletionTriggerKind +{ + Automatic = 0, + + Explicit = 1, +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/RazorInlineCompletionRequest.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/RazorInlineCompletionRequest.cs new file mode 100644 index 00000000000..cdca37f5711 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/RazorInlineCompletionRequest.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. + +#nullable disable + +using System.Runtime.Serialization; +using Newtonsoft.Json; + +namespace Microsoft.AspNetCore.Razor.LanguageServer; + +internal class RazorInlineCompletionRequest : InlineCompletionRequest +{ + [DataMember(Name = "razorLanguageKind")] + [JsonProperty(Required = Required.Always)] + public RazorLanguageKind Kind { get; set; } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/SelectedCompletionInfo.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/SelectedCompletionInfo.cs new file mode 100644 index 00000000000..08913cf69d3 --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/SelectedCompletionInfo.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +#nullable disable + +using System.Runtime.Serialization; +using Newtonsoft.Json; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; + +namespace Microsoft.AspNetCore.Razor.LanguageServer; + +/// +/// Corresponds to https://devdiv.visualstudio.com/DevDiv/_git/VSLanguageServerClient?path=/src/product/Protocol/LanguageServer.Protocol.Internal/VSInternalSelectedCompletionInfo.cs +/// +internal class SelectedCompletionInfo +{ + [DataMember(Name = "_vs_range")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public Range Range { get; set; } + + [DataMember(Name = "_vs_text")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public string Text { get; set; } + + [DataMember(Name = "_vs_completionKind")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public CompletionItemKind CompletionKind { get; set; } + + [DataMember(Name = "_vs_isSnippetText")] + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool IsSnippetText { get; set; } +} diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index 06a0817da18..85f1da51d6a 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -135,6 +135,7 @@ public static Task CreateAsync(Stream input, Stream output, .WithHandler() .WithHandler() .WithHandler() + .WithHandler() .WithHandler() .WithServices(services => { diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/DefaultRazorLanguageServerCustomMessageTarget.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/DefaultRazorLanguageServerCustomMessageTarget.cs index 79789934888..291dde802dd 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/DefaultRazorLanguageServerCustomMessageTarget.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/DefaultRazorLanguageServerCustomMessageTarget.cs @@ -9,6 +9,7 @@ using System.Net; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.LanguageServer; using Microsoft.AspNetCore.Razor.LanguageServer.Semantic; using Microsoft.AspNetCore.Razor.LanguageServer.Semantic.Models; using Microsoft.CodeAnalysis.ExternalAccess.Razor; @@ -20,6 +21,7 @@ using Microsoft.VisualStudio.LanguageServerClient.Razor.WrapWithTag; using Microsoft.VisualStudio.Threading; using Newtonsoft.Json.Linq; +using OmniSharp.Extensions.LanguageServer.Protocol; using OmniSharpConfigurationParams = OmniSharp.Extensions.LanguageServer.Protocol.Models.ConfigurationParams; using SemanticTokensRangeParams = OmniSharp.Extensions.LanguageServer.Protocol.Models.SemanticTokensRangeParams; using Task = System.Threading.Tasks.Task; @@ -523,5 +525,41 @@ public override async Task RazorWrapWithTagAsync( return response; } + + public override async Task ProvideInlineCompletionAsync(RazorInlineCompletionRequest inlineCompletionParams, CancellationToken cancellationToken) + { + if (inlineCompletionParams is null) + { + throw new ArgumentNullException(nameof(inlineCompletionParams)); + } + + var hostDocumentUri = inlineCompletionParams.TextDocument.Uri.ToUri(); + if (!_documentManager.TryGetDocument(hostDocumentUri, out var documentSnapshot)) + { + return null; + } + + if (!documentSnapshot.TryGetVirtualDocument(out var csharpDoc)) + { + return null; + } + + var csharpRequest = new InlineCompletionRequest + { + Context = inlineCompletionParams.Context, + Position = inlineCompletionParams.Position, + TextDocument = DocumentUri.From(csharpDoc.Uri), + }; + + var textBuffer = csharpDoc.Snapshot.TextBuffer; + var request = await _requestInvoker.ReinvokeRequestOnServerAsync( + textBuffer, + VSInternalMethods.TextDocumentInlineCompletionName, + RazorLSPConstants.RazorCSharpLanguageServerName, + csharpRequest, + cancellationToken).ConfigureAwait(false); + + return request?.Response; + } } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/CompletionHandler.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/CompletionHandler.cs index 91f1229b0ef..383424ca955 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/CompletionHandler.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/CompletionHandler.cs @@ -32,11 +32,6 @@ internal class CompletionHandler : IRequestHandler s_keywords = new string[] { - "for", "foreach", "while", "switch", "lock", - "case", "if", "try", "do", "using" - }; - private static readonly IReadOnlyCollection s_designTimeHelpers = new string[] { "__builder", @@ -49,7 +44,6 @@ internal class CompletionHandler : IRequestHandler s_keywordCompletionItems = GenerateCompletionItems(s_keywords); private static readonly IReadOnlyCollection s_designTimeHelpersCompletionItems = GenerateCompletionItems(s_designTimeHelpers); private readonly JoinableTaskFactory _joinableTaskFactory; @@ -159,17 +153,6 @@ public CompletionHandler( cancellationToken).ConfigureAwait(false); if (projectionResult is null) { - if (IsRazorCompilerBugWithCSharpKeywords(request, wordExtent.Value)) - { - var csharpPolyfilledCompletionList = new CompletionList() - { - Items = Array.Empty(), - IsIncomplete = true, - }; - csharpPolyfilledCompletionList = IncludeCSharpKeywords(csharpPolyfilledCompletionList); - return csharpPolyfilledCompletionList; - } - return null; } @@ -285,62 +268,6 @@ static bool TryConvertToCompletionList(SumType } } - // Internal for testing - internal static bool IsRazorCompilerBugWithCSharpKeywords(CompletionParams request, TextExtent wordExtent) - { - // This was originally found when users would attempt to type out `@using` in an _Imports.razor file and get 0 completion items at the `g` of `using`. - // After lots of investigation it turns out that the Razor compiler will generate 0 C# source for an incomplete using directive. This in turn results - // in 0 C# information at `@using|`. This is tracked here: https://github.com/dotnet/aspnetcore/issues/37568 - // - // The entire purpose of this method is to encapsulate this compiler bug and try and make users experiences a little better in a low-risk fashion. - return request.Context!.TriggerKind == CompletionTriggerKind.TriggerForIncompleteCompletions && - WordSpanMatchesCSharpPolyfills(wordExtent); - } - - private static bool WordSpanMatchesCSharpPolyfills(TextExtent? wordExtent) - { - if (wordExtent is null || !wordExtent.Value.IsSignificant) - { - return false; - } - - var wordSpan = wordExtent.Value.Span; - - foreach (var keyword in s_keywords) - { - if (wordSpan.Length != keyword.Length) - { - // Word can't match, different length - continue; - } - - var allCharactersMatch = true; - for (var j = 0; j < keyword.Length; j++) - { - var wordSpanIndex = wordSpan.Start.Position + j; - if (wordSpanIndex >= wordSpan.Snapshot.Length) - { - // Don't think this is technically possible but being extra cautious to stay low-risk. - break; - } - - var wordCharacter = wordSpan.Snapshot[wordSpanIndex]; - if (keyword[j] != wordCharacter) - { - allCharactersMatch = false; - break; - } - } - - if (allCharactersMatch) - { - return true; - } - } - - return false; - } - private bool TryGetWordExtent(CompletionParams request, LSPDocumentSnapshot documentSnapshot, [NotNullWhen(true)] out TextExtent? wordExtent) { var wordCharacterPosition = request.Position.Character; @@ -390,7 +317,6 @@ private CompletionList PostProcessCSharpCompletionList( if (IsSimpleImplicitExpression(request, documentSnapshot, wordExtent)) { completionList = RemovePreselection(completionList); - completionList = IncludeCSharpKeywords(completionList); // -1 is to account for the transition so base indentation is "|@if" instead of "@|if" var baseIndentation = Math.Max(GetBaseIndentation(wordExtent, formattingOptions) - 1, 0); @@ -736,17 +662,6 @@ private static CompletionList RemovePreselection(CompletionList completionList) return completionList; } - // C# keywords were previously provided by snippets, but as of now C# LSP doesn't provide snippets. - // We're providing these for now to improve the user experience (not having to ESC out of completions to finish), - // but once C# starts providing them their completion will be offered instead, at which point we should be able to remove this step. - private static CompletionList IncludeCSharpKeywords(CompletionList completionList) - { - var newList = completionList.Items.Union(s_keywordCompletionItems, CompletionItemComparer.Instance); - completionList.Items = newList.ToArray(); - - return completionList; - } - // The TextEdit positions returned to us from the C#/HTML language servers are positions correlating to the virtual document. // We need to translate these positions to apply to the Razor document instead. Performance is a big concern here, so we want to // make the logic as simple as possible, i.e. no asynchronous calls. diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/RazorLanguageServerCustomMessageTarget.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/RazorLanguageServerCustomMessageTarget.cs index 7e4fb7bf1ec..32a7ee15646 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/RazorLanguageServerCustomMessageTarget.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/RazorLanguageServerCustomMessageTarget.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.LanguageServer; using Microsoft.AspNetCore.Razor.LanguageServer.Common; using Microsoft.AspNetCore.Razor.LanguageServer.Semantic; using Microsoft.AspNetCore.Razor.LanguageServer.Semantic.Models; @@ -57,5 +58,9 @@ internal abstract class RazorLanguageServerCustomMessageTarget // Called by Visual Studio to wrap the current selection with a tag [JsonRpcMethod(LanguageServerConstants.RazorWrapWithTagEndpoint, UseSingleObjectParameterDeserialization = true)] public abstract Task RazorWrapWithTagAsync(VSInternalWrapWithTagParams wrapWithParams, CancellationToken cancellationToken); + + // Called by the Razor Language Server to provide inline completions from the platform. + [JsonRpcMethod(LanguageServerConstants.RazorInlineCompletionEndpoint, UseSingleObjectParameterDeserialization = true)] + public abstract Task ProvideInlineCompletionAsync(RazorInlineCompletionRequest inlineCompletionParams, CancellationToken cancellationToken); } } diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServerClient.Razor.Test/HtmlCSharp/CompletionHandlerTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServerClient.Razor.Test/HtmlCSharp/CompletionHandlerTest.cs index cf0f43969de..7f522223a06 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServerClient.Razor.Test/HtmlCSharp/CompletionHandlerTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServerClient.Razor.Test/HtmlCSharp/CompletionHandlerTest.cs @@ -58,94 +58,6 @@ public CompletionHandlerTest() private readonly string _languageClient = "languageClient"; - [Theory] - [WorkItem("https://github.com/dotnet/razor-tooling/issues/5606")] - [WorkItem("https://github.com/dotnet/aspnetcore/issues/37568")] - [InlineData("if")] - [InlineData("using")] - [InlineData("foreach")] - [InlineData("try")] - public void IsRazorCompilerBugWithCSharpKeywords_ReturnsTrue(string keyword) - { - // Arrange & Act - var result = RunTest_IsRazorCompilerBugWithCSharpKeywords($"@|{keyword}|"); - - // Assert - Assert.True(result); - } - - [Fact] - [WorkItem("https://github.com/dotnet/razor-tooling/issues/5606")] - [WorkItem("https://github.com/dotnet/aspnetcore/issues/37568")] - public void IsRazorCompilerBugWithCSharpKeywords_Invoked_ReturnsFalse() - { - // Arrange & Act - var result = RunTest_IsRazorCompilerBugWithCSharpKeywords("@|using| SomeNamespace.Foo", CompletionTriggerKind.Invoked); - - // Assert - Assert.False(result); - } - - [Fact] - [WorkItem("https://github.com/dotnet/razor-tooling/issues/5606")] - [WorkItem("https://github.com/dotnet/aspnetcore/issues/37568")] - public void IsRazorCompilerBugWithCSharpKeywords_MoreThanKeyword_ReturnsFalse() - { - // Arrange & Act - var result = RunTest_IsRazorCompilerBugWithCSharpKeywords("@|usingMore|"); - - // Assert - Assert.False(result); - } - - [Fact] - [WorkItem("https://github.com/dotnet/razor-tooling/issues/5606")] - [WorkItem("https://github.com/dotnet/aspnetcore/issues/37568")] - public void IsRazorCompilerBugWithCSharpKeywords_Other_ReturnsFalse() - { - // Arrange & Act - var result = RunTest_IsRazorCompilerBugWithCSharpKeywords("@|unrelated|"); - - // Assert - Assert.False(result); - } - - [Fact] - [WorkItem("https://github.com/dotnet/razor-tooling/issues/5606")] - [WorkItem("https://github.com/dotnet/aspnetcore/issues/37568")] - public void IsRazorCompilerBugWithCSharpKeywords_NonSignificant_ReturnsFalse() - { - // Arrange & Act - var result = RunTest_IsRazorCompilerBugWithCSharpKeywords("@using | |"); - - // Assert - Assert.False(result); - } - - [Fact] - [WorkItem("https://github.com/dotnet/razor-tooling/issues/5606")] - [WorkItem("https://github.com/dotnet/aspnetcore/issues/37568")] - public void IsRazorCompilerBugWithCSharpKeywords_Empty_ReturnsFalse() - { - // Arrange & Act - var result = RunTest_IsRazorCompilerBugWithCSharpKeywords("@usin||g"); - - // Assert - Assert.False(result); - } - - [Fact] - [WorkItem("https://github.com/dotnet/razor-tooling/issues/5606")] - [WorkItem("https://github.com/dotnet/aspnetcore/issues/37568")] - public void IsRazorCompilerBugWithCSharpKeywords_Subset_ReturnsFalse() - { - // Arrange & Act - var result = RunTest_IsRazorCompilerBugWithCSharpKeywords("@u|sin|g"); - - // Assert - Assert.False(result); - } - [Fact] public async Task HandleRequestAsync_DocumentNotFound_ReturnsNull() { @@ -193,48 +105,6 @@ public async Task HandleRequestAsync_ProjectionNotFound_ReturnsNull() Assert.Null(result); } - [Fact] - [WorkItem("https://github.com/dotnet/razor-tooling/issues/5606")] - [WorkItem("https://github.com/dotnet/aspnetcore/issues/37568")] - public async Task HandleRequestAsync_ProjectionNotFound_IsCompilerBug_ReturnsPolyfills() - { - // Arrange - var documentManager = new TestDocumentManager(); - var snapshot = new TestLSPDocumentSnapshot(new Uri("C:/path/file.razor"), version: 0, "@using"); - documentManager.AddDocument(Uri, snapshot); - var wordExtent = GetWordExtent("@|using|"); - var navigator = BuildNavigatorSelector(wordExtent); - var requestInvoker = Mock.Of(MockBehavior.Strict); - var projectionProvider = new Mock(MockBehavior.Strict).Object; - Mock.Get(projectionProvider).Setup(projectionProvider => projectionProvider.GetProjectionForCompletionAsync(It.IsAny(), It.IsAny(), CancellationToken.None)) - .Returns(Task.FromResult(null)); - var completionHandler = new CompletionHandler(JoinableTaskContext, requestInvoker, documentManager, projectionProvider, navigator, CompletionRequestContextCache, FormattingOptionsProvider, LoggerProvider); - var completionRequest = new CompletionParams() - { - TextDocument = new TextDocumentIdentifier() { Uri = Uri }, - Context = new CompletionContext() { TriggerKind = CompletionTriggerKind.TriggerForIncompleteCompletions }, - Position = new Position(0, 6) - }; - - // Act - var result = await completionHandler.HandleRequestAsync(completionRequest, new ClientCapabilities(), CancellationToken.None).ConfigureAwait(false); - - // Assert - var completionList = result.Value.Second; - Assert.True(completionList.IsIncomplete); - Assert.Collection(completionList.Items, - item => Assert.Equal("for", item.Label), - item => Assert.Equal("foreach", item.Label), - item => Assert.Equal("while", item.Label), - item => Assert.Equal("switch", item.Label), - item => Assert.Equal("lock", item.Label), - item => Assert.Equal("case", item.Label), - item => Assert.Equal("if", item.Label), - item => Assert.Equal("try", item.Label), - item => Assert.Equal("do", item.Label), - item => Assert.Equal("using", item.Label)); - } - [Fact] public async Task HandleRequestAsync_HtmlProjection_InvokesHtmlLanguageServer() { @@ -615,234 +485,6 @@ public async Task HandleRequestAsync_CSharpProjection_ReturnsKeywordsFromRazor() }); } - [Fact] - public async Task HandleRequestAsync_CSharpProjection_ReturnsKeywordsFromCSharp_Triggered() - { - // Arrange - var called = false; - var expectedItems = new CompletionItem[] { - new CompletionItem() { InsertText = "DateTime", Label = "DateTime" }, - new CompletionItem() { InsertText = "FROMCSHARP", Label = "for" }, - - }; - - var completionRequest = new CompletionParams() - { - TextDocument = new TextDocumentIdentifier() { Uri = Uri }, - Context = new CompletionContext() { TriggerKind = CompletionTriggerKind.TriggerCharacter, TriggerCharacter = "@" }, - Position = new Position(0, 1) - }; - - var documentManager = new TestDocumentManager(); - documentManager.AddDocument(Uri, new TestLSPDocumentSnapshot(new Uri("C:/path/file.razor"), 0, CSharpVirtualDocumentSnapshot)); - - var requestInvoker = new Mock(MockBehavior.Strict); - requestInvoker - .Setup(r => r.ReinvokeRequestOnServerAsync?>(TextBuffer, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((textBuffer, method, clientName, completionParams, ct) => - { - Assert.Equal(Methods.TextDocumentCompletionName, method); - Assert.Equal(RazorLSPConstants.RazorCSharpLanguageServerName, clientName); - called = true; - }) - .Returns(Task.FromResult(new ReinvocationResponse?>(_languageClient, expectedItems))); - - var projectionResult = new ProjectionResult() - { - LanguageKind = RazorLanguageKind.CSharp, - }; - var projectionProvider = new Mock(MockBehavior.Strict); - projectionProvider.Setup(p => p.GetProjectionForCompletionAsync(It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(projectionResult)); - - var completionHandler = new CompletionHandler(JoinableTaskContext, requestInvoker.Object, documentManager, projectionProvider.Object, TextStructureNavigatorSelectorService, CompletionRequestContextCache, FormattingOptionsProvider, LoggerProvider); - - // Act - var result = await completionHandler.HandleRequestAsync(completionRequest, new ClientCapabilities(), CancellationToken.None).ConfigureAwait(false); - - // Assert - Assert.True(called); - Assert.True(result.HasValue); - var _ = result.Value.Match>( - array => throw new NotImplementedException(), - list => - { - Assert.Collection(list.Items, - item => Assert.Equal("DateTime", item.InsertText), - item => - { - Assert.Equal("for", item.Label); - Assert.Equal("FROMCSHARP", item.InsertText); - }, - item => Assert.Equal("foreach", item.Label), - item => Assert.Equal("while", item.Label), - item => Assert.Equal("switch", item.Label), - item => Assert.Equal("lock", item.Label), - item => Assert.Equal("case", item.Label), - item => Assert.Equal("if", item.Label), - item => Assert.Equal("try", item.Label), - item => Assert.Equal("do", item.Label), - item => Assert.Equal("using", item.Label), - item => Assert.Equal("for (...)", item.Label), - item => Assert.Equal("foreach (...)", item.Label), - item => Assert.Equal("if (...)", item.Label), - item => Assert.Equal("prop", item.Label) - ); - - return list; - }); - } - - [Fact] - public async Task HandleRequestAsync_CSharpProjection_ReturnsKeywordsFromCSharp_Reinvoked() - { - // Arrange - var called = false; - var expectedItems = new CompletionItem[] { - new CompletionItem() { InsertText = "DateTime", Label = "DateTime" }, - new CompletionItem() { InsertText = "FROMCSHARP", Label = "for" }, - }; - - var completionRequest = new CompletionParams() - { - TextDocument = new TextDocumentIdentifier() { Uri = Uri }, - Context = new CompletionContext() { TriggerKind = CompletionTriggerKind.Invoked }, - Position = new Position(0, 1) - }; - - var documentSnapshot = new TestLSPDocumentSnapshot(new Uri("C:/path/file.razor"), 0, snapshotContent: "@Da", CSharpVirtualDocumentSnapshot); - var documentManager = new TestDocumentManager(); - documentManager.AddDocument(Uri, documentSnapshot); - - var wordSnapshotSpan = new SnapshotSpan(documentSnapshot.Snapshot, new Span(1, 2)); - var wordRange = new TextExtent(wordSnapshotSpan, isSignificant: true); - var navigatorSelector = BuildNavigatorSelector(wordRange); - var requestInvoker = new Mock(MockBehavior.Strict); - requestInvoker - .Setup(r => r.ReinvokeRequestOnServerAsync?>(TextBuffer, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((textBuffer, method, clientName, completionParams, ct) => - { - Assert.Equal(Methods.TextDocumentCompletionName, method); - Assert.Equal(RazorLSPConstants.RazorCSharpLanguageServerName, clientName); - called = true; - }) - .Returns(Task.FromResult(new ReinvocationResponse?>(_languageClient, expectedItems))); - - var projectionResult = new ProjectionResult() - { - LanguageKind = RazorLanguageKind.CSharp, - }; - var projectionProvider = new Mock(MockBehavior.Strict); - projectionProvider.Setup(p => p.GetProjectionForCompletionAsync(It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(projectionResult)); - - var completionHandler = new CompletionHandler(JoinableTaskContext, requestInvoker.Object, documentManager, projectionProvider.Object, navigatorSelector, CompletionRequestContextCache, FormattingOptionsProvider, LoggerProvider); - - // Act - var result = await completionHandler.HandleRequestAsync(completionRequest, new ClientCapabilities(), CancellationToken.None).ConfigureAwait(false); - - // Assert - Assert.True(called); - Assert.True(result.HasValue); - var _ = result.Value.Match>( - array => throw new NotImplementedException(), - list => - { - Assert.Collection(list.Items, - item => Assert.Equal("DateTime", item.InsertText), - item => - { - Assert.Equal("for", item.Label); - Assert.Equal("FROMCSHARP", item.InsertText); - }, - item => Assert.Equal("foreach", item.Label), - item => Assert.Equal("while", item.Label), - item => Assert.Equal("switch", item.Label), - item => Assert.Equal("lock", item.Label), - item => Assert.Equal("case", item.Label), - item => Assert.Equal("if", item.Label), - item => Assert.Equal("try", item.Label), - item => Assert.Equal("do", item.Label), - item => Assert.Equal("using", item.Label), - item => Assert.Equal("for (...)", item.Label), - item => Assert.Equal("foreach (...)", item.Label), - item => Assert.Equal("if (...)", item.Label), - item => Assert.Equal("prop", item.Label) - ); ; - - return list; - }); - } - - [Fact] - public async Task HandleRequestAsync_CSharpProjection_ReturnsKeywordsFromCSharp_Reinvoked_UsingStatement() - { - // Arrange - var called = false; - - var completionRequest = new CompletionParams() - { - TextDocument = new TextDocumentIdentifier() { Uri = Uri }, - Context = new CompletionContext() { TriggerKind = CompletionTriggerKind.Invoked }, - Position = new Position(0, 3) - }; - - var documentSnapshot = new TestLSPDocumentSnapshot(new Uri("C:/path/file.razor"), 0, snapshotContent: "@us", CSharpVirtualDocumentSnapshot); - var documentManager = new TestDocumentManager(); - documentManager.AddDocument(Uri, documentSnapshot); - - var wordSnapshotSpan = new SnapshotSpan(documentSnapshot.Snapshot, new Span(1, 2)); - var wordRange = new TextExtent(wordSnapshotSpan, isSignificant: true); - var navigatorSelector = BuildNavigatorSelector(wordRange); - var requestInvoker = new Mock(MockBehavior.Strict); - requestInvoker - .Setup(r => r.ReinvokeRequestOnServerAsync?>(TextBuffer, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((textBuffer, method, clientName, completionParams, ct) => - { - Assert.Equal(Methods.TextDocumentCompletionName, method); - Assert.Equal(RazorLSPConstants.RazorCSharpLanguageServerName, clientName); - called = true; - }) - .Returns(Task.FromResult(new ReinvocationResponse?>(_languageClient, Array.Empty()))); - - var projectionResult = new ProjectionResult() - { - LanguageKind = RazorLanguageKind.CSharp, - }; - var projectionProvider = new Mock(MockBehavior.Strict); - projectionProvider.Setup(p => p.GetProjectionForCompletionAsync(It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(projectionResult)); - - var completionHandler = new CompletionHandler(JoinableTaskContext, requestInvoker.Object, documentManager, projectionProvider.Object, navigatorSelector, CompletionRequestContextCache, FormattingOptionsProvider, LoggerProvider); - - // Act - var result = await completionHandler.HandleRequestAsync(completionRequest, new ClientCapabilities(), CancellationToken.None).ConfigureAwait(false); - - // Assert - Assert.True(called); - Assert.True(result.HasValue); - _ = result.Value.Match>( - array => throw new NotImplementedException(), - list => - { - Assert.Collection(list.Items, - item => Assert.Equal("for", item.Label), - item => Assert.Equal("foreach", item.Label), - item => Assert.Equal("while", item.Label), - item => Assert.Equal("switch", item.Label), - item => Assert.Equal("lock", item.Label), - item => Assert.Equal("case", item.Label), - item => Assert.Equal("if", item.Label), - item => Assert.Equal("try", item.Label), - item => Assert.Equal("do", item.Label), - item => Assert.Equal("using", item.Label), - item => Assert.Equal("for (...)", item.Label), - item => Assert.Equal("foreach (...)", item.Label), - item => Assert.Equal("if (...)", item.Label), - item => Assert.Equal("prop", item.Label) - ); - - return list; - }); - } - [Fact] public async Task HandleRequestAsync_HtmlProjection_IncompatibleTriggerCharacter_ReturnsNull() { @@ -1122,16 +764,6 @@ public async Task HandleRequestAsync_CSharpProjection_RemoveAllDesignTimeHelpers { Assert.Collection(list.Items, item => Assert.Equal("DateTime", item.InsertText), - item => Assert.Equal("for", item.Label), - item => Assert.Equal("foreach", item.Label), - item => Assert.Equal("while", item.Label), - item => Assert.Equal("switch", item.Label), - item => Assert.Equal("lock", item.Label), - item => Assert.Equal("case", item.Label), - item => Assert.Equal("if", item.Label), - item => Assert.Equal("try", item.Label), - item => Assert.Equal("do", item.Label), - item => Assert.Equal("using", item.Label), item => Assert.Equal("for (...)", item.Label), item => Assert.Equal("foreach (...)", item.Label), item => Assert.Equal("if (...)", item.Label), @@ -1944,20 +1576,6 @@ private static ITextStructureNavigatorSelectorService BuildNavigatorSelector(Tex return navigatorSelector.Object; } - private static bool RunTest_IsRazorCompilerBugWithCSharpKeywords(string input, CompletionTriggerKind? triggerKind = null) - { - var request = new CompletionParams() - { - Context = new CompletionContext() - { - TriggerKind = triggerKind ?? CompletionTriggerKind.TriggerForIncompleteCompletions, - } - }; - var wordExtent = GetWordExtent(input); - var result = CompletionHandler.IsRazorCompilerBugWithCSharpKeywords(request, wordExtent); - return result; - } - private static TextExtent GetWordExtent(string input) { var wordStart = input.IndexOf("|");