Skip to content
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

[LSP] Add Razor options provider to Roslyn #53879

Merged
merged 16 commits into from
Jun 9, 2021
19 changes: 14 additions & 5 deletions src/Features/Core/Portable/Completion/CompletionProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,20 @@ public virtual Task<CompletionDescription> GetDescriptionAsync(Document document
/// <param name="item">The item to be committed.</param>
/// <param name="commitKey">The optional key character that caused the commit.</param>
/// <param name="cancellationToken"></param>
public virtual Task<CompletionChange> GetChangeAsync(
Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken)
{
return Task.FromResult(CompletionChange.Create(new TextChange(item.Span, item.DisplayText)));
}
public virtual Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item, char? commitKey, CancellationToken cancellationToken)
=> Task.FromResult(CompletionChange.Create(new TextChange(item.Span, item.DisplayText)));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just changed this to expression body to be more consistent with the style of the other methods in this class


/// <summary>
/// Gets the change to be applied when the specified item is committed. Change is
/// generated via a custom option set instead of using the document's options.
/// </summary>
/// <param name="document">The current document.</param>
/// <param name="optionSet">The set of options to use to generate the change.</param>
/// <param name="item">The item to be committed.</param>
/// <param name="commitKey">The optional key character that caused the commit.</param>
/// <param name="cancellationToken"></param>
internal virtual Task<CompletionChange> GetChangeAsync(Document document, OptionSet optionSet, CompletionItem item, char? commitKey, CancellationToken cancellationToken)
=> GetChangeAsync(document, item, commitKey, cancellationToken);
allisonchou marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// True if the provider produces snippet items.
Expand Down
21 changes: 21 additions & 0 deletions src/Features/Core/Portable/Completion/CompletionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,27 @@ public virtual Task<CompletionChange> GetChangeAsync(
return Task.FromResult(CompletionChange.Create(new TextChange(item.Span, item.DisplayText)));
}

/// <summary>
/// Gets the change to be applied when the item is committed. Change is generated via a
/// custom option set instead of using the document's options.
/// </summary>
/// <param name="document">The document that completion is occurring within.</param>
/// <param name="optionSet">The set of options to use to generate the change.</param>
/// <param name="item">The item to get the change for.</param>
/// <param name="commitCharacter">The typed character that caused the item to be committed.
/// This character may be used as part of the change.
/// This value is null when the commit was caused by the [TAB] or [ENTER] keys.</param>
/// <param name="cancellationToken"></param>
internal virtual Task<CompletionChange> GetChangeAsync(
Document document,
OptionSet optionSet,
CompletionItem item,
char? commitCharacter = null,
CancellationToken cancellationToken = default)
{
return GetChangeAsync(document, item, commitCharacter, cancellationToken);
}

/// <summary>
/// Given a list of completion items that match the current code typed by the user,
/// returns the item that is considered the best match, and whether or not that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,20 @@ public override async Task<CompletionChange> GetChangeAsync(
}
}

internal override async Task<CompletionChange> GetChangeAsync(
Document document, OptionSet optionSet, CompletionItem item, char? commitKey, CancellationToken cancellationToken)
{
var provider = GetProvider(item);
if (provider != null)
{
return await provider.GetChangeAsync(document, optionSet, item, commitKey, cancellationToken).ConfigureAwait(false);
}
else
{
return CompletionChange.Create(new TextChange(item.Span, item.DisplayText));
}
}

bool IEqualityComparer<ImmutableHashSet<string>>.Equals(ImmutableHashSet<string> x, ImmutableHashSet<string> y)
{
if (x == y)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;
Expand All @@ -32,9 +33,24 @@ public AbstractMemberInsertingCompletionProvider()
{
}

public override async Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item, char? commitKey = null, CancellationToken cancellationToken = default)
public override async Task<CompletionChange> GetChangeAsync(
Document document,
CompletionItem item,
char? commitKey = null,
CancellationToken cancellationToken = default)
{
var newDocument = await DetermineNewDocumentAsync(document, item, cancellationToken).ConfigureAwait(false);
var optionSet = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false);
return await GetChangeAsync(document, optionSet, item, commitKey, cancellationToken).ConfigureAwait(false);
}

internal override async Task<CompletionChange> GetChangeAsync(
Document document,
OptionSet optionSet,
CompletionItem item,
char? commitKey,
CancellationToken cancellationToken)
{
var newDocument = await DetermineNewDocumentAsync(document, optionSet, item, cancellationToken).ConfigureAwait(false);
var newText = await newDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
var newRoot = await newDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

Expand Down Expand Up @@ -64,7 +80,11 @@ public override async Task<CompletionChange> GetChangeAsync(Document document, C
return CompletionChange.Create(change, changesArray, newPosition, includesCommitCharacter: true);
}

private async Task<Document> DetermineNewDocumentAsync(Document document, CompletionItem completionItem, CancellationToken cancellationToken)
private async Task<Document> DetermineNewDocumentAsync(
Document document,
OptionSet optionSet,
CompletionItem completionItem,
CancellationToken cancellationToken)
{
var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);

Expand Down Expand Up @@ -99,7 +119,7 @@ private async Task<Document> DetermineNewDocumentAsync(Document document, Comple
var declaration = GetSyntax(newRoot.FindToken(destinationSpan.End));

document = document.WithSyntaxRoot(newRoot.ReplaceNode(declaration, declaration.WithAdditionalAnnotations(_annotation)));
return await Formatter.FormatAsync(document, _annotation, cancellationToken: cancellationToken).ConfigureAwait(false);
return await Formatter.FormatAsync(document, _annotation, optionSet, cancellationToken).ConfigureAwait(false);
}

private async Task<Document> GenerateMemberAndUsingsAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@

using System;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LanguageServer.Handler.Completion;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.Text.Adornments;
using Newtonsoft.Json.Linq;
using Roslyn.Utilities;
Expand Down Expand Up @@ -90,9 +91,10 @@ public CompletionResolveHandler(CompletionListCache completionListCache)
Contract.ThrowIfTrue(completionItem.TextEdit != null);

var snippetsSupported = context.ClientCapabilities.TextDocument?.Completion?.CompletionItem?.SnippetSupport ?? false;
var optionSet = await GetCompletionFormattingOptionsAsync(document, cancellationToken).ConfigureAwait(false);

completionItem.TextEdit = await GenerateTextEditAsync(
document, completionService, selectedItem, snippetsSupported, cancellationToken).ConfigureAwait(false);
document, completionService, selectedItem, snippetsSupported, optionSet, cancellationToken).ConfigureAwait(false);
}

return completionItem;
Expand Down Expand Up @@ -126,12 +128,13 @@ private static bool MatchesLSPCompletionItem(LSP.CompletionItem lspCompletionIte
CompletionService completionService,
CompletionItem selectedItem,
bool snippetsSupported,
OptionSet optionSet,
CancellationToken cancellationToken)
{
var documentText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);

var completionChange = await completionService.GetChangeAsync(
document, selectedItem, cancellationToken: cancellationToken).ConfigureAwait(false);
document, optionSet, selectedItem, cancellationToken: cancellationToken).ConfigureAwait(false);
var completionChangeSpan = completionChange.TextChange.Span;
var newText = completionChange.TextChange.NewText;
Contract.ThrowIfNull(newText);
Expand Down Expand Up @@ -185,5 +188,40 @@ private static bool MatchesLSPCompletionItem(LSP.CompletionItem lspCompletionIte

return cacheEntry;
}

// Certain language servers such as Razor may want TextEdits formatted using their own options instead of C#/VB options.
private static async Task<OptionSet> GetCompletionFormattingOptionsAsync(Document document, CancellationToken cancellationToken)
allisonchou marked this conversation as resolved.
Show resolved Hide resolved
{
var documentOptions = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false);

var optionsService = document.Services.GetService<IDocumentOptionsProvider>();
if (optionsService == null)
{
return documentOptions;
}

var options = await optionsService.GetOptionsForDocumentAsync(document, cancellationToken).ConfigureAwait(false);
if (options == null)
{
return documentOptions;
}

if (options.TryGetDocumentOption(new OptionKey(FormattingOptions.UseTabs, document.Project.Language), out var useTabs) &&
useTabs != null &&
useTabs.GetType() == typeof(bool))
{
documentOptions = documentOptions.WithChangedOption(FormattingOptions.UseTabs, (bool)useTabs);
}

if (options.TryGetDocumentOption(new OptionKey(FormattingOptions.TabSize, document.Project.Language), out var tabSize) &&
tabSize != null &&
tabSize.GetType() == typeof(int))
{
documentOptions = documentOptions.WithChangedOption(FormattingOptions.TabSize, (int)tabSize);
documentOptions = documentOptions.WithChangedOption(FormattingOptions.IndentationSize, (int)tabSize);
}
allisonchou marked this conversation as resolved.
Show resolved Hide resolved

return documentOptions;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,11 @@ class B : A
using var testLspServer = CreateTestLspServer(markup, out _);

var document = testLspServer.GetCurrentSolution().Projects.First().Documents.First();
var optionSet = await document.GetOptionsAsync().ConfigureAwait(false);

var selectedItem = CodeAnalysis.Completion.CompletionItem.Create(displayText: "M");
var textEdit = await CompletionResolveHandler.GenerateTextEditAsync(
document, new TestCaretOutOfScopeCompletionService(), selectedItem, snippetsSupported: true, CancellationToken.None).ConfigureAwait(false);
document, new TestCaretOutOfScopeCompletionService(), selectedItem, snippetsSupported: true, optionSet, CancellationToken.None).ConfigureAwait(false);

Assert.Equal(@"public override void M()
{
Expand Down
17 changes: 17 additions & 0 deletions src/Tools/ExternalAccess/Razor/IRazorDocumentOptionsProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.CodeAnalysis.ExternalAccess.Razor
{
internal interface IRazorDocumentOptionsProvider
{
/// <summary>
/// Returns the Razor options for a specific document.
/// </summary>
Task<RazorDocumentOptions> GetDocumentOptionsAsync(Document document, CancellationToken cancellationToken);
}
}
18 changes: 18 additions & 0 deletions src/Tools/ExternalAccess/Razor/RazorDocumentOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace Microsoft.CodeAnalysis.ExternalAccess.Razor
{
internal readonly struct RazorDocumentOptions
{
public readonly bool UseTabs;
public readonly int TabSize;

public RazorDocumentOptions(bool useTabs, int tabSize)
{
UseTabs = useTabs;
TabSize = tabSize;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Options;

namespace Microsoft.CodeAnalysis.ExternalAccess.Razor
{
internal sealed class RazorDocumentOptionsProviderWrapper : IDocumentOptionsProvider
{
private readonly IRazorDocumentOptionsProvider _razorDocumentOptionsProvider;

public RazorDocumentOptionsProviderWrapper(IRazorDocumentOptionsProvider razorDocumentOptionsService)
{
_razorDocumentOptionsProvider = razorDocumentOptionsService ?? throw new ArgumentNullException(nameof(razorDocumentOptionsService));
}

public async Task<IDocumentOptions?> GetOptionsForDocumentAsync(Document document, CancellationToken cancellationToken)
{
var options = await _razorDocumentOptionsProvider.GetDocumentOptionsAsync(document, cancellationToken).ConfigureAwait(false);
var razorDocumentOptions = new RazorDocumentOptions(document, options.UseTabs, options.TabSize);
return razorDocumentOptions;
}

private sealed class RazorDocumentOptions : IDocumentOptions
{
private readonly Document _document;
private readonly bool _useTabs;
private readonly int _tabSize;

public RazorDocumentOptions(Document document, bool useTabs, int tabSize)
{
_document = document;
_useTabs = useTabs;
_tabSize = tabSize;
}

public bool TryGetDocumentOption(OptionKey option, out object? value)
{
if (option.Equals(new OptionKey(FormattingOptions.UseTabs, _document.Project.Language)))
{
value = _useTabs;
return true;
}

if (option.Equals(new OptionKey(FormattingOptions.TabSize, _document.Project.Language)))
{
value = _tabSize;
return true;
}

value = null;
return false;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Options;

namespace Microsoft.CodeAnalysis.ExternalAccess.Razor
{
Expand All @@ -16,6 +17,7 @@ internal sealed class RazorDocumentServiceProviderWrapper : IDocumentServiceProv
private RazorSpanMappingServiceWrapper? _spanMappingService;
private RazorDocumentExcerptServiceWrapper? _excerptService;
private RazorDocumentPropertiesServiceWrapper? _documentPropertiesService;
private RazorDocumentOptionsProviderWrapper? _documentOptionsProvider;

public RazorDocumentServiceProviderWrapper(IRazorDocumentServiceProvider innerDocumentServiceProvider)
{
Expand Down Expand Up @@ -103,6 +105,30 @@ public RazorDocumentServiceProviderWrapper(IRazorDocumentServiceProvider innerDo
return (TService)(object)_documentPropertiesService;
}

if (typeof(TService) == typeof(IDocumentOptionsProvider))
{
if (_documentOptionsProvider == null)
{
lock (_lock)
{
if (_documentOptionsProvider == null)
{
var razorOptionsService = _innerDocumentServiceProvider.GetService<IRazorDocumentOptionsProvider>();
if (razorOptionsService != null)
{
_documentOptionsProvider = new RazorDocumentOptionsProviderWrapper(razorOptionsService);
}
else
{
return this as TService;
}
}
}
}

return (TService)(object)_documentOptionsProvider;
}

return this as TService;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.ExternalAccess.Razor
{
Expand Down
Loading