Skip to content
2 changes: 1 addition & 1 deletion eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
<MicrosoftVisualStudioShellPackagesVersion>17.0.31723.112</MicrosoftVisualStudioShellPackagesVersion>
<MicrosoftVisualStudioPackagesVersion>17.0.487</MicrosoftVisualStudioPackagesVersion>
<RoslynPackageVersion>4.1.0-1.21471.13</RoslynPackageVersion>
<VisualStudioLanguageServerProtocolVersion>17.1.2</VisualStudioLanguageServerProtocolVersion>
<VisualStudioLanguageServerProtocolVersion>17.1.8</VisualStudioLanguageServerProtocolVersion>
</PropertyGroup>
<PropertyGroup Label="Manual">
<MicrosoftExtensionsNonCapturingTimerSourcesPackageVersion>5.0.0-preview.4.20205.1</MicrosoftExtensionsNonCapturingTimerSourcesPackageVersion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public static class LanguageServerConstants

public const string RazorServerReadyEndpoint = "razor/serverReady";

public const string RazorInlineCompletionEndpoint = "razor/inlineCompletion";

// This needs to be the same as in Web Tools, that is used by the HTML editor, because
// we actually respond to the Web Tools "Wrap With Div" command handler, which sends this message
// to all servers. We then take the message, get the HTML virtual document, and send it
Expand Down
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;
Comment thread
dibarbet marked this conversation as resolved.

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()
Comment thread
NTaylorMullen marked this conversation as resolved.
{
TabSize = 4,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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)
Comment thread
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))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 razorFormatter.FormatSnippet(...). Only bringing it up because I can envisions us wanting to re-use this logic (override completion comes to mind but of course that only has $0 typically).

Copy link
Copy Markdown
Member Author

@dibarbet dibarbet Jan 28, 2022

Choose a reason for hiding this comment

The 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
https://github.com/microsoft/vscode/blob/075ba020e8493f40dba89891b1a08453f2c067e9/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts#L82

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

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);
Comment thread
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; }
}
Loading