-
Notifications
You must be signed in to change notification settings - Fork 238
Add support for invoking C# inline completions #6012
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
86459fd
059ad97
12e617b
87e5a91
9db9ad2
78a159e
a56c82b
8a16091
c2f64b7
f50497b
2f477d2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<InlineCompletionRequest, InlineCompletionList?>, IRegistrationExtension | ||
| { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
|
||
| /// <summary> | ||
| /// Corresponds to https://devdiv.visualstudio.com/DevDiv/_git/VSLanguageServerClient?path=/src/product/Protocol/LanguageServer.Protocol.Internal/VSInternalInlineCompletionContext.cs | ||
| /// </summary> | ||
| internal class InlineCompletionContext | ||
| { | ||
| [DataMember(Name = "_vs_triggerKind")] | ||
| [JsonProperty(Required = Required.Always)] | ||
| public InlineCompletionTriggerKind TriggerKind { get; set; } = InlineCompletionTriggerKind.Explicit; | ||
|
|
||
| [DataMember(Name = "_vs_selectedCompletionInfo")] | ||
| [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] | ||
| public SelectedCompletionInfo SelectedCompletionInfo { get; set; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,254 @@ | ||
| // Copyright (c) .NET Foundation. All rights reserved. | ||
| // Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.Collections.Immutable; | ||
| using System.Composition; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Linq; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using Microsoft.AspNetCore.Razor.Language; | ||
| using Microsoft.AspNetCore.Razor.LanguageServer.Common; | ||
| using Microsoft.AspNetCore.Razor.LanguageServer.Common.Extensions; | ||
| using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; | ||
| using Microsoft.AspNetCore.Razor.LanguageServer.Formatting; | ||
| using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem; | ||
| using Microsoft.CodeAnalysis.Razor; | ||
| using Microsoft.CodeAnalysis.Text; | ||
| using Microsoft.Extensions.Logging; | ||
| using OmniSharp.Extensions.LanguageServer.Protocol.Models; | ||
|
|
||
| namespace Microsoft.AspNetCore.Razor.LanguageServer; | ||
|
|
||
| internal class InlineCompletionEndpoint : IInlineCompletionHandler | ||
| { | ||
| // Usually when we need to format code, we utilize the formatting options provided | ||
| // by the platform. Similar to DefaultCSharpCodeActionResolver we do not have any, so use defaults. | ||
| private static readonly FormattingOptions s_defaultFormattingOptions = new FormattingOptions() | ||
|
NTaylorMullen marked this conversation as resolved.
|
||
| { | ||
| TabSize = 4, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @allisonchou IIRC we had an options store on the Razor language server side that was regularly updated. Am I remembering wrong? Wondering if there's a way @dibarbet can hook into that
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we do get the razor formatting options though, I think they still need to be passed to C# so C# formats the snippet with the right settings. So regardless I think the best way is to just include them on the request. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahhhh good point. Agreed as a good follow up! |
||
| InsertSpaces = true, | ||
| TrimTrailingWhitespace = true, | ||
| InsertFinalNewline = true, | ||
| TrimFinalNewlines = true | ||
| }; | ||
|
|
||
| private static readonly ImmutableHashSet<string> s_cSharpKeywords = ImmutableHashSet.Create( | ||
| "~", "Attribute", "checked", "class", "ctor", "cw", "do", "else", "enum", "equals", "Exception", "for", "foreach", "forr", | ||
| "if", "indexer", "interface", "invoke", "iterator", "iterindex", "lock", "mbox", "namespace", "#if", "#region", "prop", | ||
| "propfull", "propg", "sim", "struct", "svm", "switch", "try", "tryf", "unchecked", "unsafe", "using", "while"); | ||
|
|
||
| private readonly ProjectSnapshotManagerDispatcher _projectSnapshotManagerDispatcher; | ||
| private readonly DocumentResolver _documentResolver; | ||
| private readonly RazorDocumentMappingService _documentMappingService; | ||
| private readonly ClientNotifierServiceBase _languageServer; | ||
| private readonly AdhocWorkspaceFactory _adhocWorkspaceFactory; | ||
| private readonly ILogger _logger; | ||
|
|
||
| [ImportingConstructor] | ||
| public InlineCompletionEndpoint( | ||
| ProjectSnapshotManagerDispatcher projectSnapshotManagerDispatcher, | ||
| DocumentResolver documentResolver, | ||
| RazorDocumentMappingService documentMappingService, | ||
| ClientNotifierServiceBase languageServer, | ||
| AdhocWorkspaceFactory adhocWorkspaceFactory, | ||
| ILoggerFactory loggerFactory) | ||
| { | ||
| if (projectSnapshotManagerDispatcher is null) | ||
| { | ||
| throw new ArgumentNullException(nameof(projectSnapshotManagerDispatcher)); | ||
| } | ||
|
|
||
| if (documentResolver is null) | ||
| { | ||
| throw new ArgumentNullException(nameof(documentResolver)); | ||
| } | ||
|
|
||
| if (documentMappingService is null) | ||
| { | ||
| throw new ArgumentNullException(nameof(documentMappingService)); | ||
| } | ||
|
|
||
| if (languageServer is null) | ||
| { | ||
| throw new ArgumentNullException(nameof(languageServer)); | ||
| } | ||
|
|
||
| if (adhocWorkspaceFactory is null) | ||
| { | ||
| throw new ArgumentNullException(nameof(adhocWorkspaceFactory)); | ||
| } | ||
|
|
||
| if (loggerFactory is null) | ||
| { | ||
| throw new ArgumentNullException(nameof(loggerFactory)); | ||
| } | ||
|
|
||
| _projectSnapshotManagerDispatcher = projectSnapshotManagerDispatcher; | ||
| _documentResolver = documentResolver; | ||
| _documentMappingService = documentMappingService; | ||
| _languageServer = languageServer; | ||
| _adhocWorkspaceFactory = adhocWorkspaceFactory; | ||
| _logger = loggerFactory.CreateLogger<InlineCompletionEndpoint>(); | ||
| } | ||
|
|
||
| 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<InlineCompletionList?> Handle(InlineCompletionRequest request, CancellationToken cancellationToken) | ||
| { | ||
| if (request is null) | ||
| { | ||
| throw new ArgumentNullException(nameof(request)); | ||
| } | ||
|
|
||
| _logger.LogInformation($"Starting request for {request.TextDocument.Uri} at {request.Position}."); | ||
|
|
||
| var document = await _projectSnapshotManagerDispatcher.RunOnDispatcherThreadAsync(() => | ||
| { | ||
| _documentResolver.TryResolveDocument(request.TextDocument.Uri.GetAbsoluteOrUNCPath(), out var documentSnapshot); | ||
|
|
||
| return documentSnapshot; | ||
| }, cancellationToken).ConfigureAwait(false); | ||
|
|
||
| if (document is null) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var codeDocument = await document.GetGeneratedOutputAsync(); | ||
| if (codeDocument.IsUnsupported()) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var sourceText = await document.GetTextAsync(); | ||
| var linePosition = new LinePosition(request.Position.Line, request.Position.Character); | ||
| var hostDocumentIndex = sourceText.Lines.GetPosition(linePosition); | ||
|
|
||
| var languageKind = _documentMappingService.GetLanguageKind(codeDocument, hostDocumentIndex); | ||
|
|
||
| // Map to the location in the C# document. | ||
| if (languageKind != RazorLanguageKind.CSharp || | ||
| !_documentMappingService.TryMapToProjectedDocumentPosition(codeDocument, hostDocumentIndex, out var projectedPosition, out _)) | ||
| { | ||
| _logger.LogInformation($"Unsupported location for {request.TextDocument.Uri}."); | ||
| return null; | ||
| } | ||
|
|
||
| var razorRequest = new RazorInlineCompletionRequest | ||
| { | ||
| TextDocument = request.TextDocument, | ||
| Context = request.Context, | ||
| Position = projectedPosition, | ||
| Kind = languageKind, | ||
| }; | ||
|
|
||
| request.Position = projectedPosition; | ||
| var response = await _languageServer.SendRequestAsync(LanguageServerConstants.RazorInlineCompletionEndpoint, razorRequest).ConfigureAwait(false); | ||
| var list = await response.Returning<InlineCompletionList>(cancellationToken).ConfigureAwait(false); | ||
| if (list == null || !list.Items.Any()) | ||
| { | ||
| _logger.LogInformation($"Did not get any inline completions from delegation."); | ||
| return null; | ||
| } | ||
|
|
||
| var items = new List<InlineCompletionItem>(); | ||
| var csharpDocOptions = codeDocument.GetCSharpDocument(); | ||
| foreach (var item in list.Items) | ||
| { | ||
| var containsSnippet = item.TextFormat == InsertTextFormat.Snippet; | ||
| var range = item.Range ?? new Range { Start = projectedPosition, End = projectedPosition }; | ||
|
|
||
| if (!_documentMappingService.TryMapFromProjectedDocumentRange(codeDocument, range, out var rangeInRazorDoc)) | ||
| { | ||
| _logger.LogWarning($"Could not remap projected range {range} to razor document"); | ||
| continue; | ||
| } | ||
|
|
||
| using var formattingContext = FormattingContext.Create(request.TextDocument.Uri, document, codeDocument, s_defaultFormattingOptions, _adhocWorkspaceFactory, isFormatOnType: true, automaticallyAddUsings: false); | ||
| if (!TryGetSnippetWithAdjustedIndentation(formattingContext, item.Text, hostDocumentIndex, out var newSnippetText)) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| var remappedItem = new InlineCompletionItem | ||
| { | ||
| Command = item.Command, | ||
| Range = rangeInRazorDoc, | ||
| Text = newSnippetText.ToString(), | ||
| TextFormat = item.TextFormat, | ||
| }; | ||
| items.Add(remappedItem); | ||
| } | ||
|
|
||
| if (items.Count == 0) | ||
| { | ||
| _logger.LogInformation($"Could not format / map the items from delegation."); | ||
| return null; | ||
| } | ||
|
|
||
| _logger.LogInformation($"Returning {items.Count} items."); | ||
| return new InlineCompletionList | ||
| { | ||
| Items = items.ToArray() | ||
| }; | ||
| } | ||
|
|
||
| private static bool TryGetSnippetWithAdjustedIndentation(FormattingContext formattingContext, string snippetText, int hostDocumentIndex, [NotNullWhen(true)] out string? newSnippetText) | ||
|
NTaylorMullen marked this conversation as resolved.
|
||
| { | ||
| 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<TextChange> indentationChanges = new(); | ||
| // Adjust each line, skipping the first since it must start at the snippet keyword. | ||
| foreach (var line in snippetSourceText.Lines.Skip(1)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm do we have to skip the first line? Asking because I'd hope that whatever path we take in formatting a snippet could be pulled into the formatting engine so someone can do something like
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's possible, but would need to be special cased. Because the text edit that C# returns starts at the snippet keyword, and only replaces the snippet keyword. So we'd have to extend the range to the rest of the line (including possible text before the keyword), then modify that indentation, which seemed more complicated than it was worth. Especially since the range for inlien completions should start at that keyword I think combining the logic for formatting snippets (at least for C#) is a good idea - however I would like to battle test this formatting code before we unify things There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Totally fair! |
||
| { | ||
| var lineText = snippetSourceText.GetSubText(line.Span); | ||
| if (lineText.Length == 0) | ||
| { | ||
| // We just have an empty line, nothing to do. | ||
| continue; | ||
| } | ||
|
|
||
| // Get the indentation of the line in the C# document based on what options the C# document was generated with. | ||
| var csharpLineIndentationSize = line.GetIndentationSize(formattingContext.Options.TabSize); | ||
| var csharpIndentationLevel = csharpLineIndentationSize / formattingContext.Options.TabSize; | ||
|
|
||
| // Get the new indentation level based on the context in the razor document. | ||
| var newIndentationLevel = csharpIndentationLevel + amountToAddToCSharpIndentation; | ||
| var newIndentationString = formattingContext.GetIndentationLevelString(newIndentationLevel); | ||
|
|
||
| // Replace the current indentation with the new indentation. | ||
| var spanToReplace = new TextSpan(line.Start, line.GetFirstNonWhitespaceOffset() ?? line.Span.End); | ||
| var textChange = new TextChange(spanToReplace, newIndentationString); | ||
| indentationChanges.Add(textChange); | ||
|
davidwengier marked this conversation as resolved.
|
||
| } | ||
|
|
||
| var newSnippetSourceText = snippetSourceText.WithChanges(indentationChanges); | ||
| newSnippetText = newSnippetSourceText.ToString(); | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
|
||
| /// <summary> | ||
| /// Corresponds to https://devdiv.visualstudio.com/DevDiv/_git/VSLanguageServerClient?path=/src/product/Protocol/LanguageServer.Protocol.Internal/VSInternalInlineCompletionItem.cs | ||
| /// </summary> | ||
| internal class InlineCompletionItem | ||
| { | ||
| [DataMember(Name = "_vs_text")] | ||
| [JsonProperty(Required = Required.Always)] | ||
| public string Text { get; set; } | ||
|
|
||
| [DataMember(Name = "_vs_range")] | ||
| [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] | ||
| public Range Range { get; set; } | ||
|
|
||
| [DataMember(Name = "_vs_command")] | ||
| [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] | ||
| public Command Command { get; set; } | ||
|
|
||
| [DataMember(Name = "_vs_insertTextFormat")] | ||
| [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] | ||
| public InsertTextFormat? TextFormat { get; set; } = InsertTextFormat.PlainText; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| // Copyright (c) .NET Foundation. All rights reserved. | ||
| // Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
|
||
| #nullable disable | ||
|
|
||
| using System.Runtime.Serialization; | ||
| using Newtonsoft.Json; | ||
|
|
||
| namespace Microsoft.AspNetCore.Razor.LanguageServer; | ||
|
|
||
| internal class InlineCompletionList | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets the inline completion items. | ||
| /// </summary> | ||
| [DataMember(Name = "_vs_items")] | ||
| [JsonProperty(Required = Required.Always)] | ||
| public InlineCompletionItem[] Items { get; set; } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| // Copyright (c) .NET Foundation. All rights reserved. | ||
| // Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
|
||
| #nullable disable | ||
|
|
||
| using System.Runtime.Serialization; | ||
| using System.Text.RegularExpressions; | ||
| using Newtonsoft.Json; | ||
| using Newtonsoft.Json.Converters; | ||
| using OmniSharp.Extensions.LanguageServer.Protocol.Models; | ||
|
|
||
| namespace Microsoft.AspNetCore.Razor.LanguageServer; | ||
|
|
||
| internal class InlineCompletionOptions : ITextDocumentRegistrationOptions | ||
| { | ||
| /// <summary> | ||
| /// Gets or sets a regex used by the client to determine when to ask the server for snippets. | ||
| /// </summary> | ||
| [DataMember(Name = "_vs_pattern")] | ||
| [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] | ||
| public string Pattern { get; set; } | ||
|
|
||
| public DocumentSelector DocumentSelector { get; set; } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.