From 86459fd20c21d065ebe6f1f13594e6adc2cb4fab Mon Sep 17 00:00:00 2001 From: David Barbet Date: Tue, 25 Jan 2022 20:44:15 -0800 Subject: [PATCH 01/10] Add support for invoking C# inline completions --- eng/Versions.props | 2 +- .../HtmlCSharp/InitializeHandler.cs | 12 ++ .../HtmlCSharp/InlineCompletionHandler.cs | 176 ++++++++++++++++++ .../RazorHtmlCSharpLanguageServer.cs | 11 ++ 4 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InlineCompletionHandler.cs diff --git a/eng/Versions.props b/eng/Versions.props index 8d4b16d82ad..f9d5a90aba3 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -92,7 +92,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.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InitializeHandler.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InitializeHandler.cs index 4fc2cf1b9f0..9e6cd698f4d 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InitializeHandler.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InitializeHandler.cs @@ -6,6 +6,7 @@ using System.Composition; using System.Diagnostics; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -47,6 +48,10 @@ internal class InitializeHandler : IRequestHandler s.Capabilities.RenameProvider?.Value is bool isRenameSupported && isRenameSupported); } + private VSInternalInlineCompletionOptions GetMergedInlineCompletionProvider() + { + var regexes = _serverCapabilities.Where(s => s.Capabilities.InlineCompletionOptions != null).Select(s => s.Capabilities.InlineCompletionOptions.Pattern.ToString()); + return new VSInternalInlineCompletionOptions { Pattern = new Regex(string.Join("|", regexes)) }; + } + private async Task VerifyMergedOnAutoInsertAsync(VSInternalServerCapabilities mergedCapabilities) { var triggerCharEnumeration = mergedCapabilities.OnAutoInsertProvider?.TriggerCharacters ?? Enumerable.Empty(); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InlineCompletionHandler.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InlineCompletionHandler.cs new file mode 100644 index 00000000000..6fa58f2db56 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InlineCompletionHandler.cs @@ -0,0 +1,176 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; +using Microsoft.VisualStudio.LanguageServer.Protocol; +using Microsoft.VisualStudio.LanguageServerClient.Razor.Extensions; +using Microsoft.VisualStudio.LanguageServerClient.Razor.Logging; + +namespace Microsoft.VisualStudio.LanguageServerClient.Razor.HtmlCSharp +{ + [Shared] + [ExportLspMethod(VSInternalMethods.TextDocumentInlineCompletionName)] + internal class InlineCompletionHandler : IRequestHandler + { + internal static readonly ImmutableHashSet 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 LSPDocumentManager _documentManager; + private readonly LSPRequestInvoker _requestInvoker; + private readonly LSPProjectionProvider _projectionProvider; + private readonly LSPDocumentMappingProvider _documentMappingProvider; + private readonly FormattingOptionsProvider _formattingOptionsProvider; + private readonly ILogger _logger; + + [ImportingConstructor] + public InlineCompletionHandler( + LSPDocumentManager documentManager, + LSPRequestInvoker requestInvoker, + LSPProjectionProvider projectionProvider, + LSPDocumentMappingProvider documentMappingProvider, + HTMLCSharpLanguageServerLogHubLoggerProvider loggerProvider, + FormattingOptionsProvider formattingOptionsProvider) + { + if (documentManager is null) + { + throw new ArgumentNullException(nameof(documentManager)); + } + + if (requestInvoker is null) + { + throw new ArgumentNullException(nameof(requestInvoker)); + } + + if (projectionProvider is null) + { + throw new ArgumentNullException(nameof(projectionProvider)); + } + + if (documentMappingProvider is null) + { + throw new ArgumentNullException(nameof(documentMappingProvider)); + } + + if (loggerProvider is null) + { + throw new ArgumentNullException(nameof(loggerProvider)); + } + + if (formattingOptionsProvider is null) + { + throw new ArgumentNullException(nameof(formattingOptionsProvider)); + } + + _documentManager = documentManager; + _requestInvoker = requestInvoker; + _projectionProvider = projectionProvider; + _documentMappingProvider = documentMappingProvider; + _formattingOptionsProvider = formattingOptionsProvider; + + _logger = loggerProvider.CreateLogger(nameof(InlineCompletionHandler)); + } + + public async Task HandleRequestAsync(VSInternalInlineCompletionRequest request, ClientCapabilities clientCapabilities, CancellationToken cancellationToken) + { + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + _logger.LogInformation($"Starting request for {request.TextDocument.Uri} at {request.Position}."); + + if (!_documentManager.TryGetDocument(request.TextDocument.Uri, out var documentSnapshot)) + { + return null; + } + + var projectionResult = await _projectionProvider.GetProjectionAsync(documentSnapshot, request.Position, cancellationToken).ConfigureAwait(false); + if (projectionResult is null) + { + _logger.LogWarning($"Failed to find document {request.TextDocument.Uri}."); + return null; + } + + if (projectionResult.LanguageKind != RazorLanguageKind.CSharp && projectionResult.LanguageKind != RazorLanguageKind.Html) + { + _logger.LogInformation($"Inline completions not supported for {projectionResult.LanguageKind}"); + return null; + } + + var inlineCompletionParams = new VSInternalInlineCompletionRequest + { + Context = request.Context, + TextDocument = new TextDocumentIdentifier { Uri = projectionResult.Uri }, + Position = projectionResult.Position, + }; + + _logger.LogInformation($"Requesting inline completions for {projectionResult.Uri}."); + + var serverKind = projectionResult.LanguageKind.ToLanguageServerKind(); + var textBuffer = serverKind.GetTextBuffer(documentSnapshot); + var languageServerName = serverKind.ToLanguageServerName(); + var response = await _requestInvoker.ReinvokeRequestOnServerAsync( + textBuffer, + VSInternalMethods.TextDocumentInlineCompletionName, + languageServerName, + inlineCompletionParams, + cancellationToken).ConfigureAwait(false); + + if (!ReinvocationResponseHelper.TryExtractResultOrLog(response, _logger, languageServerName, out var result)) + { + return null; + } + + _logger.LogInformation("Received result, remapping."); + + if (response?.Response == null) + { + return null; + } + + var formattingOptions = _formattingOptionsProvider.GetOptions(documentSnapshot); + + var items = new List(); + foreach (var item in response.Response.Items) + { + var containsSnippet = item.TextFormat == InsertTextFormat.Snippet; + var range = item.Range ?? new Range { Start = projectionResult.Position, End = projectionResult.Position }; + + var textEdit = new TextEdit { NewText = item.Text, Range = range }; + var remappedEdit = await _documentMappingProvider.RemapFormattedTextEditsAsync(projectionResult.Uri, new[] { textEdit }, formattingOptions, containsSnippet, cancellationToken).ConfigureAwait(false); + + if (!remappedEdit.Any()) + { + _logger.LogInformation("Discarding inline completion item after remapping"); + continue; + } + + var remappedItem = new VSInternalInlineCompletionItem + { + Command = item.Command, + Range = remappedEdit.Single().Range, + Text = remappedEdit.Single().NewText, + TextFormat = item.TextFormat, + }; + items.Add(remappedItem); + } + + _logger.LogInformation($"Returning items."); + return new VSInternalInlineCompletionList + { + Items = items.ToArray() + }; + } + } +} + diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/RazorHtmlCSharpLanguageServer.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/RazorHtmlCSharpLanguageServer.cs index b962f0f1877..d1945b38026 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/RazorHtmlCSharpLanguageServer.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/RazorHtmlCSharpLanguageServer.cs @@ -304,6 +304,17 @@ public Task ExitAsync(CancellationToken _) return Task.FromResult(null); } + [JsonRpcMethod(VSInternalMethods.TextDocumentInlineCompletionName, UseSingleObjectParameterDeserialization = true)] + public Task InlineCompletionAsync(VSInternalInlineCompletionRequest inlineCompletionRequest, CancellationToken cancellationToken) + { + if (inlineCompletionRequest is null) + { + throw new ArgumentNullException(nameof(inlineCompletionRequest)); + } + + return ExecuteRequestAsync(VSInternalMethods.TextDocumentInlineCompletionName, inlineCompletionRequest, ClientCapabilities, cancellationToken); + } + // Internal for testing internal Task ExecuteRequestAsync( string methodName, From 059ad970950a2b0bb95d63fa49e183f6f5830540 Mon Sep 17 00:00:00 2001 From: David Barbet Date: Wed, 26 Jan 2022 13:09:37 -0800 Subject: [PATCH 02/10] small feedback --- .../HtmlCSharp/InitializeHandler.cs | 6 +++--- .../HtmlCSharp/InlineCompletionHandler.cs | 7 ++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InitializeHandler.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InitializeHandler.cs index 9e6cd698f4d..6e517770bce 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InitializeHandler.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InitializeHandler.cs @@ -50,7 +50,7 @@ internal class InitializeHandler : IRequestHandler s.Capabilities.InlineCompletionOptions != null).Select(s => s.Capabilities.InlineCompletionOptions.Pattern.ToString()); - return new VSInternalInlineCompletionOptions { Pattern = new Regex(string.Join("|", regexes)) }; + var regexes = _serverCapabilities.Where(s => !string.IsNullOrEmpty(s.Capabilities.InlineCompletionOptions?.Pattern?.ToString())).Select(s => s.Capabilities.InlineCompletionOptions.Pattern.ToString()); + return new VSInternalInlineCompletionOptions { Pattern = new Regex(string.Join("|", regexes), RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)) }; } private async Task VerifyMergedOnAutoInsertAsync(VSInternalServerCapabilities mergedCapabilities) diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InlineCompletionHandler.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InlineCompletionHandler.cs index 6fa58f2db56..e02e552cc85 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InlineCompletionHandler.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InlineCompletionHandler.cs @@ -101,7 +101,7 @@ public InlineCompletionHandler( return null; } - if (projectionResult.LanguageKind != RazorLanguageKind.CSharp && projectionResult.LanguageKind != RazorLanguageKind.Html) + if (projectionResult.LanguageKind != RazorLanguageKind.CSharp) { _logger.LogInformation($"Inline completions not supported for {projectionResult.LanguageKind}"); return null; @@ -131,13 +131,14 @@ public InlineCompletionHandler( return null; } - _logger.LogInformation("Received result, remapping."); - if (response?.Response == null) { + _logger.LogInformation($"Did not get any items for {projectionResult.LanguageKind}"); return null; } + _logger.LogInformation("Received result, remapping."); + var formattingOptions = _formattingOptionsProvider.GetOptions(documentSnapshot); var items = new List(); From 12e617b59c36e81b3cac7e925d1e90d2e5f568fe Mon Sep 17 00:00:00 2001 From: David Barbet Date: Wed, 26 Jan 2022 20:22:58 -0800 Subject: [PATCH 03/10] move impl to razor ls --- .../LanguageServerConstants.cs | 2 + .../DefaultRazorFormattingService.cs | 43 +++- .../IInlineCompletionHandler.cs | 11 + .../InlineCompletionContext.cs | 23 ++ .../InlineCompletionEndPoint.cs | 199 ++++++++++++++++++ .../InlineCompletion/InlineCompletionItem.cs | 32 +++ .../InlineCompletion/InlineCompletionList.cs | 19 ++ .../InlineCompletionOptions.cs | 24 +++ .../InlineCompletionRequest.cs | 29 +++ .../InlineCompletionTriggerKind.cs | 19 ++ .../SelectedCompletionInfo.cs | 32 +++ .../RazorLanguageEndpoint.cs | 35 --- .../RazorLanguageServer.cs | 1 + ...tRazorLanguageServerCustomMessageTarget.cs | 30 +++ .../HtmlCSharp/CompletionHandler.cs | 6 +- .../HtmlCSharp/InitializeHandler.cs | 11 - .../HtmlCSharp/InlineCompletionHandler.cs | 177 ---------------- .../RazorLanguageServerCustomMessageTarget.cs | 4 + 18 files changed, 469 insertions(+), 228 deletions(-) create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/IInlineCompletionHandler.cs create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionContext.cs create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionItem.cs create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionList.cs create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionOptions.cs create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionRequest.cs create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionTriggerKind.cs create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/SelectedCompletionInfo.cs delete mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InlineCompletionHandler.cs 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 51490c20c73..55f83e9c4a0 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Common/LanguageServerConstants.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer.Common/LanguageServerConstants.cs @@ -48,6 +48,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/Formatting/DefaultRazorFormattingService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DefaultRazorFormattingService.cs index f6d2c435a49..327d10480d1 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DefaultRazorFormattingService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DefaultRazorFormattingService.cs @@ -94,8 +94,47 @@ public override Task FormatOnTypeAsync(DocumentUri uri, DocumentSnap public override Task FormatCodeActionAsync(DocumentUri uri, DocumentSnapshot documentSnapshot, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) => ApplyFormattedEditsAsync(uri, documentSnapshot, kind, formattedEdits, options, bypassValidationPasses: true, collapseEdits: false, automaticallyAddUsings: true, cancellationToken: cancellationToken); - public override Task FormatSnippetAsync(DocumentUri uri, DocumentSnapshot documentSnapshot, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) - => ApplyFormattedEditsAsync(uri, documentSnapshot, kind, formattedEdits, options, bypassValidationPasses: true, collapseEdits: true, automaticallyAddUsings: false, cancellationToken: cancellationToken); + public override async Task FormatSnippetAsync(DocumentUri uri, DocumentSnapshot documentSnapshot, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) + { + if (kind == RazorLanguageKind.CSharp) + { + WrapCSharpSnippets(formattedEdits); + } + + var edits = await ApplyFormattedEditsAsync(uri, documentSnapshot, kind, formattedEdits, options, bypassValidationPasses: true, collapseEdits: true, automaticallyAddUsings: false, cancellationToken: cancellationToken); + + if (kind == RazorLanguageKind.CSharp) + { + UnwrapCSharpSnippets(edits); + } + + return edits; + + static void WrapCSharpSnippets(TextEdit[] snippetEdits) + { + for (var i = 0; i < snippetEdits.Length; i++) + { + var snippetEdit = snippetEdits[i]; + + // Formatting doesn't work with syntax errors caused by the cursor marker ($0). + // So, let's avoid the error by wrapping the cursor marker in a comment. + var wrappedText = snippetEdit.NewText.Replace("$0", "/*$0*/"); + snippetEdits[i] = snippetEdit with { NewText = wrappedText }; + } + } + + static void UnwrapCSharpSnippets(TextEdit[] snippetEdits) + { + for (var i = 0; i < snippetEdits.Length; i++) + { + var snippetEdit = snippetEdits[i]; + + // Unwrap the cursor marker. + var unwrappedText = snippetEdit.NewText.Replace("/*$0*/", "$0"); + snippetEdits[i] = snippetEdit with { NewText = unwrappedText }; + } + } + } private async Task ApplyFormattedEditsAsync( DocumentUri uri, 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..6ffbe6aca26 --- /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 +/// +public 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..377909fc31f --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs @@ -0,0 +1,199 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +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 RazorFormattingService _formattingService; + private readonly ILogger _logger; + + [ImportingConstructor] + public InlineCompletionEndpoint( + ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher, + DocumentResolver documentResolver, + RazorDocumentMappingService documentMappingService, + ClientNotifierServiceBase languageServer, + RazorFormattingService razorFormattingService, + 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 (razorFormattingService is null) + { + throw new ArgumentNullException(nameof(razorFormattingService)); + } + + if (loggerFactory is null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + _projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher; + _documentResolver = documentResolver; + _documentMappingService = documentMappingService; + _languageServer = languageServer; + _formattingService = razorFormattingService; + _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; + } + + request.Position = projectedPosition; + var response = await _languageServer.SendRequestAsync(LanguageServerConstants.RazorInlineCompletionEndpoint, request).ConfigureAwait(false); + var list = await response.Returning(cancellationToken); + if (list == null) + { + _logger.LogInformation($"Did not get any inline completions from delegation."); + return null; + } + + var items = new List(); + foreach (var item in list.Items) + { + var containsSnippet = item.TextFormat == InsertTextFormat.Snippet; + var range = item.Range ?? new Range { Start = projectedPosition, End = projectedPosition }; + + var textEdit = new TextEdit { NewText = item.Text, Range = range }; + + // Remaps the text edits from the generated C# to the razor file, + // as well as applying appropriate formatting. + var formattedEdits = await _formattingService.FormatSnippetAsync( + request.TextDocument.Uri, + document, + RazorLanguageKind.CSharp, + new[] { textEdit }, + s_defaultFormattingOptions, + cancellationToken); + + if (!formattedEdits.Any()) + { + _logger.LogInformation("Discarding inline completion item after remapping"); + continue; + } + + var remappedItem = new InlineCompletionItem + { + Command = item.Command, + Range = formattedEdits.Single().Range, + Text = formattedEdits.Single().NewText, + TextFormat = item.TextFormat, + }; + items.Add(remappedItem); + } + + _logger.LogInformation($"Returning items."); + return new InlineCompletionList + { + Items = items.ToArray() + }; + } +} + 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..0ab2e067676 --- /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 +/// +public 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..20342526533 --- /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; + +public 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..25fe027c708 --- /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; + +public 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..5ec54500d95 --- /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 +/// +public 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..2546ec16cb9 --- /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] +public enum InlineCompletionTriggerKind +{ + Automatic = 0, + + Explicit = 1, +} 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..84079837184 --- /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 +/// +public 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/RazorLanguageEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageEndpoint.cs index 952d079fc91..e8ef96df9a5 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageEndpoint.cs @@ -246,18 +246,8 @@ public async Task Handle(RazorMapToDocumentEdit } else if (request.TextEditKind == TextEditKind.Snippet) { - if (request.Kind == RazorLanguageKind.CSharp) - { - WrapCSharpSnippets(request.ProjectedTextEdits); - } - var mappedEdits = await _razorFormattingService.FormatSnippetAsync(request.RazorDocumentUri, documentSnapshot, request.Kind, request.ProjectedTextEdits, request.FormattingOptions, cancellationToken); - if (request.Kind == RazorLanguageKind.CSharp) - { - UnwrapCSharpSnippets(mappedEdits); - } - return new RazorMapToDocumentEditsResponse() { TextEdits = mappedEdits, @@ -299,31 +289,6 @@ public async Task Handle(RazorMapToDocumentEdit TextEdits = edits.ToArray(), HostDocumentVersion = documentVersion, }; - - static void WrapCSharpSnippets(TextEdit[] snippetEdits) - { - for (var i = 0; i < snippetEdits.Length; i++) - { - var snippetEdit = snippetEdits[i]; - - // Formatting doesn't work with syntax errors caused by the cursor marker ($0). - // So, let's avoid the error by wrapping the cursor marker in a comment. - var wrappedText = snippetEdit.NewText.Replace("$0", "/*$0*/"); - snippetEdits[i] = snippetEdit with { NewText = wrappedText }; - } - } - - static void UnwrapCSharpSnippets(TextEdit[] snippetEdits) - { - for (var i = 0; i < snippetEdits.Length; i++) - { - var snippetEdit = snippetEdits[i]; - - // Unwrap the cursor marker. - var unwrappedText = snippetEdit.NewText.Replace("/*$0*/", "$0"); - snippetEdits[i] = snippetEdit with { NewText = unwrappedText }; - } - } } } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs index 67af47cb346..1ee16ba7997 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs @@ -134,6 +134,7 @@ public static Task CreateAsync(Stream input, Stream output, .WithHandler() .WithHandler() .WithHandler() + .WithHandler() .WithServices(services => { services.AddLogging(builder => builder diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/DefaultRazorLanguageServerCustomMessageTarget.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/DefaultRazorLanguageServerCustomMessageTarget.cs index 79789934888..0cbed4f8d11 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/DefaultRazorLanguageServerCustomMessageTarget.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/DefaultRazorLanguageServerCustomMessageTarget.cs @@ -523,5 +523,35 @@ public override async Task RazorWrapWithTagAsync( return response; } + + public override async Task ProvideInlineCompletionAsync(VSInternalInlineCompletionRequest inlineCompletionParams, CancellationToken cancellationToken) + { + if (inlineCompletionParams is null) + { + throw new ArgumentNullException(nameof(inlineCompletionParams)); + } + + if (!_documentManager.TryGetDocument(inlineCompletionParams.TextDocument.Uri, out var documentSnapshot)) + { + return null; + } + + if (!documentSnapshot.TryGetVirtualDocument(out var csharpDoc)) + { + return null; + } + + inlineCompletionParams.TextDocument.Uri = csharpDoc.Uri; + + var textBuffer = csharpDoc.Snapshot.TextBuffer; + var request = await _requestInvoker.ReinvokeRequestOnServerAsync( + textBuffer, + VSInternalMethods.TextDocumentInlineCompletionName, + RazorLSPConstants.RazorCSharpLanguageServerName, + inlineCompletionParams, + 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..f77f3cc956b 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/CompletionHandler.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/CompletionHandler.cs @@ -159,7 +159,7 @@ public CompletionHandler( cancellationToken).ConfigureAwait(false); if (projectionResult is null) { - if (IsRazorCompilerBugWithCSharpKeywords(request, wordExtent.Value)) + /*if (IsRazorCompilerBugWithCSharpKeywords(request, wordExtent.Value)) { var csharpPolyfilledCompletionList = new CompletionList() { @@ -168,7 +168,7 @@ public CompletionHandler( }; csharpPolyfilledCompletionList = IncludeCSharpKeywords(csharpPolyfilledCompletionList); return csharpPolyfilledCompletionList; - } + }*/ return null; } @@ -390,7 +390,7 @@ private CompletionList PostProcessCSharpCompletionList( if (IsSimpleImplicitExpression(request, documentSnapshot, wordExtent)) { completionList = RemovePreselection(completionList); - completionList = IncludeCSharpKeywords(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); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InitializeHandler.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InitializeHandler.cs index 6e517770bce..f4f099440f8 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InitializeHandler.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InitializeHandler.cs @@ -48,10 +48,6 @@ internal class InitializeHandler : IRequestHandler s.Capabilities.RenameProvider?.Value is bool isRenameSupported && isRenameSupported); } - private VSInternalInlineCompletionOptions GetMergedInlineCompletionProvider() - { - var regexes = _serverCapabilities.Where(s => !string.IsNullOrEmpty(s.Capabilities.InlineCompletionOptions?.Pattern?.ToString())).Select(s => s.Capabilities.InlineCompletionOptions.Pattern.ToString()); - return new VSInternalInlineCompletionOptions { Pattern = new Regex(string.Join("|", regexes), RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)) }; - } - private async Task VerifyMergedOnAutoInsertAsync(VSInternalServerCapabilities mergedCapabilities) { var triggerCharEnumeration = mergedCapabilities.OnAutoInsertProvider?.TriggerCharacters ?? Enumerable.Empty(); diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InlineCompletionHandler.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InlineCompletionHandler.cs deleted file mode 100644 index e02e552cc85..00000000000 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InlineCompletionHandler.cs +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT license. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Composition; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.VisualStudio.LanguageServer.ContainedLanguage; -using Microsoft.VisualStudio.LanguageServer.Protocol; -using Microsoft.VisualStudio.LanguageServerClient.Razor.Extensions; -using Microsoft.VisualStudio.LanguageServerClient.Razor.Logging; - -namespace Microsoft.VisualStudio.LanguageServerClient.Razor.HtmlCSharp -{ - [Shared] - [ExportLspMethod(VSInternalMethods.TextDocumentInlineCompletionName)] - internal class InlineCompletionHandler : IRequestHandler - { - internal static readonly ImmutableHashSet 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 LSPDocumentManager _documentManager; - private readonly LSPRequestInvoker _requestInvoker; - private readonly LSPProjectionProvider _projectionProvider; - private readonly LSPDocumentMappingProvider _documentMappingProvider; - private readonly FormattingOptionsProvider _formattingOptionsProvider; - private readonly ILogger _logger; - - [ImportingConstructor] - public InlineCompletionHandler( - LSPDocumentManager documentManager, - LSPRequestInvoker requestInvoker, - LSPProjectionProvider projectionProvider, - LSPDocumentMappingProvider documentMappingProvider, - HTMLCSharpLanguageServerLogHubLoggerProvider loggerProvider, - FormattingOptionsProvider formattingOptionsProvider) - { - if (documentManager is null) - { - throw new ArgumentNullException(nameof(documentManager)); - } - - if (requestInvoker is null) - { - throw new ArgumentNullException(nameof(requestInvoker)); - } - - if (projectionProvider is null) - { - throw new ArgumentNullException(nameof(projectionProvider)); - } - - if (documentMappingProvider is null) - { - throw new ArgumentNullException(nameof(documentMappingProvider)); - } - - if (loggerProvider is null) - { - throw new ArgumentNullException(nameof(loggerProvider)); - } - - if (formattingOptionsProvider is null) - { - throw new ArgumentNullException(nameof(formattingOptionsProvider)); - } - - _documentManager = documentManager; - _requestInvoker = requestInvoker; - _projectionProvider = projectionProvider; - _documentMappingProvider = documentMappingProvider; - _formattingOptionsProvider = formattingOptionsProvider; - - _logger = loggerProvider.CreateLogger(nameof(InlineCompletionHandler)); - } - - public async Task HandleRequestAsync(VSInternalInlineCompletionRequest request, ClientCapabilities clientCapabilities, CancellationToken cancellationToken) - { - if (request is null) - { - throw new ArgumentNullException(nameof(request)); - } - - _logger.LogInformation($"Starting request for {request.TextDocument.Uri} at {request.Position}."); - - if (!_documentManager.TryGetDocument(request.TextDocument.Uri, out var documentSnapshot)) - { - return null; - } - - var projectionResult = await _projectionProvider.GetProjectionAsync(documentSnapshot, request.Position, cancellationToken).ConfigureAwait(false); - if (projectionResult is null) - { - _logger.LogWarning($"Failed to find document {request.TextDocument.Uri}."); - return null; - } - - if (projectionResult.LanguageKind != RazorLanguageKind.CSharp) - { - _logger.LogInformation($"Inline completions not supported for {projectionResult.LanguageKind}"); - return null; - } - - var inlineCompletionParams = new VSInternalInlineCompletionRequest - { - Context = request.Context, - TextDocument = new TextDocumentIdentifier { Uri = projectionResult.Uri }, - Position = projectionResult.Position, - }; - - _logger.LogInformation($"Requesting inline completions for {projectionResult.Uri}."); - - var serverKind = projectionResult.LanguageKind.ToLanguageServerKind(); - var textBuffer = serverKind.GetTextBuffer(documentSnapshot); - var languageServerName = serverKind.ToLanguageServerName(); - var response = await _requestInvoker.ReinvokeRequestOnServerAsync( - textBuffer, - VSInternalMethods.TextDocumentInlineCompletionName, - languageServerName, - inlineCompletionParams, - cancellationToken).ConfigureAwait(false); - - if (!ReinvocationResponseHelper.TryExtractResultOrLog(response, _logger, languageServerName, out var result)) - { - return null; - } - - if (response?.Response == null) - { - _logger.LogInformation($"Did not get any items for {projectionResult.LanguageKind}"); - return null; - } - - _logger.LogInformation("Received result, remapping."); - - var formattingOptions = _formattingOptionsProvider.GetOptions(documentSnapshot); - - var items = new List(); - foreach (var item in response.Response.Items) - { - var containsSnippet = item.TextFormat == InsertTextFormat.Snippet; - var range = item.Range ?? new Range { Start = projectionResult.Position, End = projectionResult.Position }; - - var textEdit = new TextEdit { NewText = item.Text, Range = range }; - var remappedEdit = await _documentMappingProvider.RemapFormattedTextEditsAsync(projectionResult.Uri, new[] { textEdit }, formattingOptions, containsSnippet, cancellationToken).ConfigureAwait(false); - - if (!remappedEdit.Any()) - { - _logger.LogInformation("Discarding inline completion item after remapping"); - continue; - } - - var remappedItem = new VSInternalInlineCompletionItem - { - Command = item.Command, - Range = remappedEdit.Single().Range, - Text = remappedEdit.Single().NewText, - TextFormat = item.TextFormat, - }; - items.Add(remappedItem); - } - - _logger.LogInformation($"Returning items."); - return new VSInternalInlineCompletionList - { - Items = items.ToArray() - }; - } - } -} - diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/RazorLanguageServerCustomMessageTarget.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/RazorLanguageServerCustomMessageTarget.cs index 7e4fb7bf1ec..358060c4f1b 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/RazorLanguageServerCustomMessageTarget.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/RazorLanguageServerCustomMessageTarget.cs @@ -57,5 +57,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(VSInternalInlineCompletionRequest inlineCompletionParams, CancellationToken cancellationToken); } } From 87e5a914940be568b75223ee86848c67f6d22e26 Mon Sep 17 00:00:00 2001 From: David Barbet Date: Thu, 27 Jan 2022 02:24:25 -0800 Subject: [PATCH 04/10] Switch to alternate method of formatting to avoid issues with syntax errors in snippets --- .../DefaultRazorFormattingService.cs | 43 +--------- .../InlineCompletionEndPoint.cs | 81 ++++++++++++++----- .../RazorLanguageEndpoint.cs | 35 ++++++++ .../HtmlCSharp/CompletionHandler.cs | 23 ------ .../HtmlCSharp/InitializeHandler.cs | 1 - .../RazorHtmlCSharpLanguageServer.cs | 11 --- 6 files changed, 98 insertions(+), 96 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DefaultRazorFormattingService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DefaultRazorFormattingService.cs index 327d10480d1..f6d2c435a49 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DefaultRazorFormattingService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Formatting/DefaultRazorFormattingService.cs @@ -94,47 +94,8 @@ public override Task FormatOnTypeAsync(DocumentUri uri, DocumentSnap public override Task FormatCodeActionAsync(DocumentUri uri, DocumentSnapshot documentSnapshot, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) => ApplyFormattedEditsAsync(uri, documentSnapshot, kind, formattedEdits, options, bypassValidationPasses: true, collapseEdits: false, automaticallyAddUsings: true, cancellationToken: cancellationToken); - public override async Task FormatSnippetAsync(DocumentUri uri, DocumentSnapshot documentSnapshot, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) - { - if (kind == RazorLanguageKind.CSharp) - { - WrapCSharpSnippets(formattedEdits); - } - - var edits = await ApplyFormattedEditsAsync(uri, documentSnapshot, kind, formattedEdits, options, bypassValidationPasses: true, collapseEdits: true, automaticallyAddUsings: false, cancellationToken: cancellationToken); - - if (kind == RazorLanguageKind.CSharp) - { - UnwrapCSharpSnippets(edits); - } - - return edits; - - static void WrapCSharpSnippets(TextEdit[] snippetEdits) - { - for (var i = 0; i < snippetEdits.Length; i++) - { - var snippetEdit = snippetEdits[i]; - - // Formatting doesn't work with syntax errors caused by the cursor marker ($0). - // So, let's avoid the error by wrapping the cursor marker in a comment. - var wrappedText = snippetEdit.NewText.Replace("$0", "/*$0*/"); - snippetEdits[i] = snippetEdit with { NewText = wrappedText }; - } - } - - static void UnwrapCSharpSnippets(TextEdit[] snippetEdits) - { - for (var i = 0; i < snippetEdits.Length; i++) - { - var snippetEdit = snippetEdits[i]; - - // Unwrap the cursor marker. - var unwrappedText = snippetEdit.NewText.Replace("/*$0*/", "$0"); - snippetEdits[i] = snippetEdit with { NewText = unwrappedText }; - } - } - } + public override Task FormatSnippetAsync(DocumentUri uri, DocumentSnapshot documentSnapshot, RazorLanguageKind kind, TextEdit[] formattedEdits, FormattingOptions options, CancellationToken cancellationToken) + => ApplyFormattedEditsAsync(uri, documentSnapshot, kind, formattedEdits, options, bypassValidationPasses: true, collapseEdits: true, automaticallyAddUsings: false, cancellationToken: cancellationToken); private async Task ApplyFormattedEditsAsync( DocumentUri uri, diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs index 377909fc31f..4b513b744b8 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs @@ -5,9 +5,11 @@ 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; @@ -42,7 +44,7 @@ internal class InlineCompletionEndpoint : IInlineCompletionHandler private readonly DocumentResolver _documentResolver; private readonly RazorDocumentMappingService _documentMappingService; private readonly ClientNotifierServiceBase _languageServer; - private readonly RazorFormattingService _formattingService; + private readonly AdhocWorkspaceFactory _adhocWorkspaceFactory; private readonly ILogger _logger; [ImportingConstructor] @@ -51,7 +53,7 @@ public InlineCompletionEndpoint( DocumentResolver documentResolver, RazorDocumentMappingService documentMappingService, ClientNotifierServiceBase languageServer, - RazorFormattingService razorFormattingService, + AdhocWorkspaceFactory adhocWorkspaceFactory, ILoggerFactory loggerFactory) { if (projectSnapshotManagerDispatcher is null) @@ -74,9 +76,9 @@ public InlineCompletionEndpoint( throw new ArgumentNullException(nameof(languageServer)); } - if (razorFormattingService is null) + if (adhocWorkspaceFactory is null) { - throw new ArgumentNullException(nameof(razorFormattingService)); + throw new ArgumentNullException(nameof(adhocWorkspaceFactory)); } if (loggerFactory is null) @@ -88,7 +90,7 @@ public InlineCompletionEndpoint( _documentResolver = documentResolver; _documentMappingService = documentMappingService; _languageServer = languageServer; - _formattingService = razorFormattingService; + _adhocWorkspaceFactory = adhocWorkspaceFactory; _logger = loggerFactory.CreateLogger(); } @@ -156,34 +158,29 @@ public RegistrationExtensionResult GetRegistration() } 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 }; - var textEdit = new TextEdit { NewText = item.Text, Range = range }; - - // Remaps the text edits from the generated C# to the razor file, - // as well as applying appropriate formatting. - var formattedEdits = await _formattingService.FormatSnippetAsync( - request.TextDocument.Uri, - document, - RazorLanguageKind.CSharp, - new[] { textEdit }, - s_defaultFormattingOptions, - cancellationToken); + if (!_documentMappingService.TryMapFromProjectedDocumentRange(codeDocument, range, out var rangeInRazorDoc)) + { + _logger.LogWarning($"Could not remap projected range {range} to razor document"); + continue; + } - if (!formattedEdits.Any()) + 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)) { - _logger.LogInformation("Discarding inline completion item after remapping"); continue; } var remappedItem = new InlineCompletionItem { Command = item.Command, - Range = formattedEdits.Single().Range, - Text = formattedEdits.Single().NewText, + Range = rangeInRazorDoc, + Text = newSnippetText.ToString(), TextFormat = item.TextFormat, }; items.Add(remappedItem); @@ -195,5 +192,49 @@ public RegistrationExtensionResult GetRegistration() 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 text = snippetSourceText.GetSubText(line.Span).ToString(); + if (string.IsNullOrEmpty(text)) + { + // 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/RazorLanguageEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageEndpoint.cs index e8ef96df9a5..952d079fc91 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageEndpoint.cs @@ -246,8 +246,18 @@ public async Task Handle(RazorMapToDocumentEdit } else if (request.TextEditKind == TextEditKind.Snippet) { + if (request.Kind == RazorLanguageKind.CSharp) + { + WrapCSharpSnippets(request.ProjectedTextEdits); + } + var mappedEdits = await _razorFormattingService.FormatSnippetAsync(request.RazorDocumentUri, documentSnapshot, request.Kind, request.ProjectedTextEdits, request.FormattingOptions, cancellationToken); + if (request.Kind == RazorLanguageKind.CSharp) + { + UnwrapCSharpSnippets(mappedEdits); + } + return new RazorMapToDocumentEditsResponse() { TextEdits = mappedEdits, @@ -289,6 +299,31 @@ public async Task Handle(RazorMapToDocumentEdit TextEdits = edits.ToArray(), HostDocumentVersion = documentVersion, }; + + static void WrapCSharpSnippets(TextEdit[] snippetEdits) + { + for (var i = 0; i < snippetEdits.Length; i++) + { + var snippetEdit = snippetEdits[i]; + + // Formatting doesn't work with syntax errors caused by the cursor marker ($0). + // So, let's avoid the error by wrapping the cursor marker in a comment. + var wrappedText = snippetEdit.NewText.Replace("$0", "/*$0*/"); + snippetEdits[i] = snippetEdit with { NewText = wrappedText }; + } + } + + static void UnwrapCSharpSnippets(TextEdit[] snippetEdits) + { + for (var i = 0; i < snippetEdits.Length; i++) + { + var snippetEdit = snippetEdits[i]; + + // Unwrap the cursor marker. + var unwrappedText = snippetEdit.NewText.Replace("/*$0*/", "$0"); + snippetEdits[i] = snippetEdit with { NewText = unwrappedText }; + } + } } } } 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 f77f3cc956b..bf355deb25a 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/CompletionHandler.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/CompletionHandler.cs @@ -159,17 +159,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; } @@ -390,7 +379,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 +724,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/HtmlCSharp/InitializeHandler.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InitializeHandler.cs index f4f099440f8..4fc2cf1b9f0 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InitializeHandler.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/InitializeHandler.cs @@ -6,7 +6,6 @@ using System.Composition; using System.Diagnostics; using System.Linq; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/RazorHtmlCSharpLanguageServer.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/RazorHtmlCSharpLanguageServer.cs index d1945b38026..b962f0f1877 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/RazorHtmlCSharpLanguageServer.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/HtmlCSharp/RazorHtmlCSharpLanguageServer.cs @@ -304,17 +304,6 @@ public Task ExitAsync(CancellationToken _) return Task.FromResult(null); } - [JsonRpcMethod(VSInternalMethods.TextDocumentInlineCompletionName, UseSingleObjectParameterDeserialization = true)] - public Task InlineCompletionAsync(VSInternalInlineCompletionRequest inlineCompletionRequest, CancellationToken cancellationToken) - { - if (inlineCompletionRequest is null) - { - throw new ArgumentNullException(nameof(inlineCompletionRequest)); - } - - return ExecuteRequestAsync(VSInternalMethods.TextDocumentInlineCompletionName, inlineCompletionRequest, ClientCapabilities, cancellationToken); - } - // Internal for testing internal Task ExecuteRequestAsync( string methodName, From 9db9ad29205d4afcfd04725e06850a8a23c8019c Mon Sep 17 00:00:00 2001 From: David Barbet Date: Thu, 27 Jan 2022 15:37:39 -0800 Subject: [PATCH 05/10] small fixes --- .../InlineCompletion/InlineCompletionContext.cs | 2 +- .../InlineCompletion/InlineCompletionEndPoint.cs | 16 +++++++++++----- .../InlineCompletion/InlineCompletionItem.cs | 2 +- .../InlineCompletion/InlineCompletionList.cs | 2 +- .../InlineCompletion/InlineCompletionOptions.cs | 2 +- .../InlineCompletion/InlineCompletionRequest.cs | 2 +- .../InlineCompletionTriggerKind.cs | 2 +- .../InlineCompletion/SelectedCompletionInfo.cs | 2 +- 8 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionContext.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionContext.cs index 6ffbe6aca26..2ce7fc1a0da 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionContext.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionContext.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer; /// /// Corresponds to https://devdiv.visualstudio.com/DevDiv/_git/VSLanguageServerClient?path=/src/product/Protocol/LanguageServer.Protocol.Internal/VSInternalInlineCompletionContext.cs /// -public class InlineCompletionContext +internal class InlineCompletionContext { [DataMember(Name = "_vs_triggerKind")] [JsonProperty(Required = Required.Always)] diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs index 4b513b744b8..12610138203 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs @@ -150,8 +150,8 @@ public RegistrationExtensionResult GetRegistration() request.Position = projectedPosition; var response = await _languageServer.SendRequestAsync(LanguageServerConstants.RazorInlineCompletionEndpoint, request).ConfigureAwait(false); - var list = await response.Returning(cancellationToken); - if (list == null) + 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; @@ -186,7 +186,13 @@ public RegistrationExtensionResult GetRegistration() items.Add(remappedItem); } - _logger.LogInformation($"Returning items."); + 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() @@ -211,8 +217,8 @@ private static bool TryGetSnippetWithAdjustedIndentation(FormattingContext forma // Adjust each line, skipping the first since it must start at the snippet keyword. foreach (var line in snippetSourceText.Lines.Skip(1)) { - var text = snippetSourceText.GetSubText(line.Span).ToString(); - if (string.IsNullOrEmpty(text)) + var lineText = snippetSourceText.GetSubText(line.Span); + if (lineText.Length == 0) { // We just have an empty line, nothing to do. continue; diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionItem.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionItem.cs index 0ab2e067676..83d5c178b54 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionItem.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionItem.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer; /// /// Corresponds to https://devdiv.visualstudio.com/DevDiv/_git/VSLanguageServerClient?path=/src/product/Protocol/LanguageServer.Protocol.Internal/VSInternalInlineCompletionItem.cs /// -public class InlineCompletionItem +internal class InlineCompletionItem { [DataMember(Name = "_vs_text")] [JsonProperty(Required = Required.Always)] diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionList.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionList.cs index 20342526533..e28dd815e12 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionList.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionList.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer; -public class InlineCompletionList +internal class InlineCompletionList { /// /// Gets or sets the inline completion items. diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionOptions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionOptions.cs index 25fe027c708..88f2d7e3f44 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionOptions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionOptions.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer; -public class InlineCompletionOptions : ITextDocumentRegistrationOptions +internal class InlineCompletionOptions : ITextDocumentRegistrationOptions { /// /// Gets or sets a regex used by the client to determine when to ask the server for snippets. diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionRequest.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionRequest.cs index 5ec54500d95..80b6d24555b 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionRequest.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionRequest.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer; /// /// Corresponds to https://devdiv.visualstudio.com/DevDiv/_git/VSLanguageServerClient?path=/src/product/Protocol/LanguageServer.Protocol.Internal/VSInternalInlineCompletionRequest.cs /// -public class InlineCompletionRequest : ITextDocumentIdentifierParams, IRequest, IBaseRequest +internal class InlineCompletionRequest : ITextDocumentIdentifierParams, IRequest, IBaseRequest { [DataMember(Name = "_vs_textDocument")] [JsonProperty(Required = Required.Always)] diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionTriggerKind.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionTriggerKind.cs index 2546ec16cb9..f32bd4b1641 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionTriggerKind.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionTriggerKind.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer; /// Corresponds to https://devdiv.visualstudio.com/DevDiv/_git/VSLanguageServerClient?path=/src/product/Protocol/LanguageServer.Protocol.Internal/VSInternalInlineCompletionTriggerKind.cs /// [DataContract] -public enum InlineCompletionTriggerKind +internal enum InlineCompletionTriggerKind { Automatic = 0, diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/SelectedCompletionInfo.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/SelectedCompletionInfo.cs index 84079837184..08913cf69d3 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/SelectedCompletionInfo.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/SelectedCompletionInfo.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer; /// /// Corresponds to https://devdiv.visualstudio.com/DevDiv/_git/VSLanguageServerClient?path=/src/product/Protocol/LanguageServer.Protocol.Internal/VSInternalSelectedCompletionInfo.cs /// -public class SelectedCompletionInfo +internal class SelectedCompletionInfo { [DataMember(Name = "_vs_range")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] From 78a159e57593913f847af470a71cd562885114d5 Mon Sep 17 00:00:00 2001 From: David Barbet Date: Thu, 27 Jan 2022 15:43:45 -0800 Subject: [PATCH 06/10] remove more unnecessary completion code --- .../HtmlCSharp/CompletionHandler.cs | 62 ----------- .../HtmlCSharp/CompletionHandlerTest.cs | 102 ------------------ 2 files changed, 164 deletions(-) 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 bf355deb25a..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; @@ -274,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; 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..88e216e4ff8 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() { @@ -1944,20 +1856,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("|"); From a56c82b8e7e362ac9cc53802cfba90197fb765c7 Mon Sep 17 00:00:00 2001 From: David Barbet Date: Thu, 27 Jan 2022 16:46:51 -0800 Subject: [PATCH 07/10] Use razor type for middle layer bit --- .../InlineCompletionEndPoint.cs | 10 +++++++++- ...ltRazorLanguageServerCustomMessageTarget.cs | 18 +++++++++++++----- .../RazorLanguageServerCustomMessageTarget.cs | 3 ++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs index 12610138203..0f432c690b8 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionEndPoint.cs @@ -148,8 +148,16 @@ public RegistrationExtensionResult GetRegistration() 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, request).ConfigureAwait(false); + var response = await _languageServer.SendRequestAsync(LanguageServerConstants.RazorInlineCompletionEndpoint, razorRequest).ConfigureAwait(false); var list = await response.Returning(cancellationToken).ConfigureAwait(false); if (list == null || !list.Items.Any()) { diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/DefaultRazorLanguageServerCustomMessageTarget.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/DefaultRazorLanguageServerCustomMessageTarget.cs index 0cbed4f8d11..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; @@ -524,14 +526,15 @@ public override async Task RazorWrapWithTagAsync( return response; } - public override async Task ProvideInlineCompletionAsync(VSInternalInlineCompletionRequest inlineCompletionParams, CancellationToken cancellationToken) + public override async Task ProvideInlineCompletionAsync(RazorInlineCompletionRequest inlineCompletionParams, CancellationToken cancellationToken) { if (inlineCompletionParams is null) { throw new ArgumentNullException(nameof(inlineCompletionParams)); } - if (!_documentManager.TryGetDocument(inlineCompletionParams.TextDocument.Uri, out var documentSnapshot)) + var hostDocumentUri = inlineCompletionParams.TextDocument.Uri.ToUri(); + if (!_documentManager.TryGetDocument(hostDocumentUri, out var documentSnapshot)) { return null; } @@ -541,14 +544,19 @@ public override async Task RazorWrapWithTagAsync( return null; } - inlineCompletionParams.TextDocument.Uri = csharpDoc.Uri; + 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( + var request = await _requestInvoker.ReinvokeRequestOnServerAsync( textBuffer, VSInternalMethods.TextDocumentInlineCompletionName, RazorLSPConstants.RazorCSharpLanguageServerName, - inlineCompletionParams, + csharpRequest, cancellationToken).ConfigureAwait(false); return request?.Response; diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/RazorLanguageServerCustomMessageTarget.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServerClient.Razor/RazorLanguageServerCustomMessageTarget.cs index 358060c4f1b..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; @@ -60,6 +61,6 @@ internal abstract class RazorLanguageServerCustomMessageTarget // Called by the Razor Language Server to provide inline completions from the platform. [JsonRpcMethod(LanguageServerConstants.RazorInlineCompletionEndpoint, UseSingleObjectParameterDeserialization = true)] - public abstract Task ProvideInlineCompletionAsync(VSInternalInlineCompletionRequest inlineCompletionParams, CancellationToken cancellationToken); + public abstract Task ProvideInlineCompletionAsync(RazorInlineCompletionRequest inlineCompletionParams, CancellationToken cancellationToken); } } From c2f64b78f261c9d50e025962a5d65f84bf7b2d51 Mon Sep 17 00:00:00 2001 From: David Barbet Date: Thu, 27 Jan 2022 17:11:57 -0800 Subject: [PATCH 08/10] fix warnings --- .../InlineCompletion/InlineCompletionContext.cs | 2 +- .../InlineCompletion/InlineCompletionItem.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionContext.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionContext.cs index 2ce7fc1a0da..75f282c22b2 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionContext.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionContext.cs @@ -19,5 +19,5 @@ internal class InlineCompletionContext [DataMember(Name = "_vs_selectedCompletionInfo")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public SelectedCompletionInfo? SelectedCompletionInfo { get; set; } + public SelectedCompletionInfo SelectedCompletionInfo { get; set; } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionItem.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionItem.cs index 83d5c178b54..146032cba3e 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionItem.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/InlineCompletionItem.cs @@ -20,11 +20,11 @@ internal class InlineCompletionItem [DataMember(Name = "_vs_range")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public Range? Range { get; set; } + public Range Range { get; set; } [DataMember(Name = "_vs_command")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public Command? Command { get; set; } + public Command Command { get; set; } [DataMember(Name = "_vs_insertTextFormat")] [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] From f50497b2d657d35234f9724b6ad3ac44240f1de9 Mon Sep 17 00:00:00 2001 From: David Barbet Date: Thu, 27 Jan 2022 17:23:18 -0800 Subject: [PATCH 09/10] add missing file --- .../RazorInlineCompletionRequest.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/InlineCompletion/RazorInlineCompletionRequest.cs 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; } +} From 2f477d27ce31fc2ff10530ab874d08270b14869f Mon Sep 17 00:00:00 2001 From: David Barbet Date: Thu, 27 Jan 2022 18:18:29 -0800 Subject: [PATCH 10/10] Fixup/remove unneeded completion tests --- .../HtmlCSharp/CompletionHandlerTest.cs | 280 ------------------ 1 file changed, 280 deletions(-) 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 88e216e4ff8..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 @@ -105,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() { @@ -527,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() { @@ -1034,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),