Skip to content

Commit

Permalink
Call Roslyn to format new documents when they're created
Browse files Browse the repository at this point in the history
  • Loading branch information
davidwengier committed Sep 11, 2023
1 parent 5a72d94 commit 8a2b8af
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models;
using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor;
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
using Microsoft.AspNetCore.Razor.LanguageServer.Extensions;
using Microsoft.AspNetCore.Razor.PooledObjects;
Expand All @@ -29,13 +30,16 @@ internal sealed class ExtractToCodeBehindCodeActionResolver : IRazorCodeActionRe

private readonly DocumentContextFactory _documentContextFactory;
private readonly LanguageServerFeatureOptions _languageServerFeatureOptions;
private readonly ClientNotifierServiceBase _languageServer;

public ExtractToCodeBehindCodeActionResolver(
DocumentContextFactory documentContextFactory,
LanguageServerFeatureOptions languageServerFeatureOptions)
LanguageServerFeatureOptions languageServerFeatureOptions,
ClientNotifierServiceBase languageServer)
{
_documentContextFactory = documentContextFactory ?? throw new ArgumentNullException(nameof(documentContextFactory));
_languageServerFeatureOptions = languageServerFeatureOptions;
_languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions));
_languageServer = languageServer ?? throw new ArgumentNullException(nameof(languageServer));
}

public string Action => LanguageServerConstants.CodeActions.ExtractToCodeBehindAction;
Expand Down Expand Up @@ -94,7 +98,7 @@ public ExtractToCodeBehindCodeActionResolver(

var className = Path.GetFileNameWithoutExtension(path);
var codeBlockContent = text.GetSubTextString(new CodeAnalysis.Text.TextSpan(actionParams.ExtractStart, actionParams.ExtractEnd - actionParams.ExtractStart));
var codeBehindContent = GenerateCodeBehindClass(className, actionParams.Namespace, codeBlockContent, codeDocument);
var codeBehindContent = await GenerateCodeBehindClassAsync(documentContext.Project, codeBehindUri, className, actionParams.Namespace, codeBlockContent, codeDocument, cancellationToken).ConfigureAwait(false);

var start = codeDocument.Source.Lines.GetLocation(actionParams.RemoveStart);
var end = codeDocument.Source.Lines.GetLocation(actionParams.RemoveEnd);
Expand Down Expand Up @@ -169,17 +173,7 @@ private static string GenerateCodeBehindPath(string path)
return codeBehindPath;
}

/// <summary>
/// Generate a complete C# compilation unit containing a partial class
/// with the given name, body contents, and the namespace and all
/// usings from the existing code document.
/// </summary>
/// <param name="className">Name of the resultant partial class.</param>
/// <param name="namespaceName">Name of the namespace to put the resultant class in.</param>
/// <param name="contents">Class body contents.</param>
/// <param name="razorCodeDocument">Existing code document we're extracting from.</param>
/// <returns></returns>
private string GenerateCodeBehindClass(string className, string namespaceName, string contents, RazorCodeDocument razorCodeDocument)
private async Task<string> GenerateCodeBehindClassAsync(CodeAnalysis.Razor.ProjectSystem.IProjectSnapshot project, Uri codeBehindUri, string className, string namespaceName, string contents, RazorCodeDocument razorCodeDocument, CancellationToken cancellationToken)
{
using var _ = StringBuilderPool.GetPooledObject(out var builder);

Expand Down Expand Up @@ -210,12 +204,32 @@ private string GenerateCodeBehindClass(string className, string namespaceName, s
builder.AppendLine(contents);
builder.Append('}');

// Sadly we can't use a "real" workspace here, because we don't have access. If we use our workspace, it wouldn't have the right settings
// for C# formatting, only Razor formatting, and we have no access to Roslyn's real workspace, since it could be in another process.
// TODO: Rather than format here, call Roslyn via LSP to format, and remove and sort usings: https://github.com/dotnet/razor/issues/8766
var node = CSharpSyntaxTree.ParseText(builder.ToString()).GetRoot();
node = Formatter.Format(node, s_workspace);
var newFileContent = builder.ToString();

var parameters = new FormatNewFileParams()
{
Project = new TextDocumentIdentifier
{
Uri = new Uri(project.FilePath, UriKind.Absolute)
},
Document = new TextDocumentIdentifier
{
Uri = codeBehindUri
},
Contents = newFileContent
};
var fixedContent = await _languageServer.SendRequestAsync<FormatNewFileParams, string?>(CustomMessageNames.RazorFormatNewFileEndpointName, parameters, cancellationToken).ConfigureAwait(false);

if (fixedContent is null)
{
// Sadly we can't use a "real" workspace here, because we don't have access. If we use our workspace, it wouldn't have the right settings
// for C# formatting, only Razor formatting, and we have no access to Roslyn's real workspace, since it could be in another process.
var node = await CSharpSyntaxTree.ParseText(newFileContent).GetRootAsync(cancellationToken).ConfigureAwait(false);
node = Formatter.Format(node, s_workspace);

return node.ToFullString();
}

return node.ToFullString();
return fixedContent;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Runtime.Serialization;
using Microsoft.VisualStudio.LanguageServer.Protocol;

namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor;

[DataContract]
internal record FormatNewFileParams
{
[DataMember(Name = "document")]
public required TextDocumentIdentifier Document { get; set; }

[DataMember(Name = "project")]
public required TextDocumentIdentifier Project { get; set; }

[DataMember(Name = "contents")]
public required string Contents { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ internal static class CustomMessageNames
public const string RazorFoldingRangeEndpoint = "razor/foldingRange";
public const string RazorHtmlFormattingEndpoint = "razor/htmlFormatting";
public const string RazorHtmlOnTypeFormattingEndpoint = "razor/htmlOnTypeFormatting";
public const string RazorSimplifyMethodEndpointName = "razor/simplifyMethod";
public const string RazorFormatNewFileEndpointName = "razor/formatNewFile";

// VS Windows only at the moment, but could/should be migrated
public const string RazorDocumentSymbolEndpoint = "razor/documentSymbol";
Expand All @@ -52,8 +54,6 @@ internal static class CustomMessageNames

public const string RazorReferencesEndpointName = "razor/references";

public const string RazorSimplifyMethodEndpointName = "razor/simplifyMethod";

// Called to get C# diagnostics from Roslyn when publishing diagnostics for VS Code
public const string RazorCSharpPullDiagnosticsEndpointName = "razor/csharpPullDiagnostics";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Razor;
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Newtonsoft.Json.Linq;
using StreamJsonRpc;

namespace Microsoft.VisualStudio.LanguageServerClient.Razor;

internal partial class RazorCustomMessageTarget
{
[JsonRpcMethod(CustomMessageNames.RazorFormatNewFileEndpointName, UseSingleObjectParameterDeserialization = true)]
public async Task<string?> FormatNewFileAsync(FormatNewFileParams request, CancellationToken cancellationToken)
{
// This endpoint is special because it deals with a file that doesn't exist yet, so there is no document syncing necessary!
var response = await _requestInvoker.ReinvokeRequestOnServerAsync<FormatNewFileParams, string?>(
RazorLSPConstants.RoslynFormatNewFileEndpointName,
RazorLSPConstants.RazorCSharpLanguageServerName,
SupportsFormatNewFile,
request,
cancellationToken).ConfigureAwait(false);

return response.Result;
}

private static bool SupportsFormatNewFile(JToken token)
{
var serverCapabilities = token.ToObject<VSInternalServerCapabilities>();

return serverCapabilities?.Experimental is JObject experimental
&& experimental.TryGetValue(RazorLSPConstants.RoslynFormatNewFileEndpointName, out var supportsFormatNewFile)
&& supportsFormatNewFile.ToObject<bool>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ internal static class RazorLSPConstants
public const string HtmlLSPDelegationContentTypeName = "html-delegation";

public const string RoslynSimplifyMethodEndpointName = "roslyn/simplifyMethod";

public const string RoslynFormatNewFileEndpointName = "roslyn/formatNewFile";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models;
using Microsoft.AspNetCore.Razor.LanguageServer.Common;
using Microsoft.AspNetCore.Razor.LanguageServer.Extensions;
using Microsoft.AspNetCore.Razor.LanguageServer.Test;
using Microsoft.AspNetCore.Razor.LanguageServer.Test.Common;
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.CodeAnalysis;
Expand All @@ -21,18 +24,23 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions;
public class ExtractToCodeBehindCodeActionResolverTest : LanguageServerTestBase
{
private readonly DocumentContextFactory _emptyDocumentContextFactory;
private readonly TestLanguageServer _languageServer;

public ExtractToCodeBehindCodeActionResolverTest(ITestOutputHelper testOutput)
: base(testOutput)
{
_emptyDocumentContextFactory = new TestDocumentContextFactory();
_languageServer = new TestLanguageServer(new Dictionary<string, Func<object?, Task<object>>>()
{
[CustomMessageNames.RazorFormatNewFileEndpointName] = c => Task.FromResult<object>(null!),
});
}

[Fact]
public async Task Handle_MissingFile()
{
// Arrange
var resolver = new ExtractToCodeBehindCodeActionResolver(_emptyDocumentContextFactory, TestLanguageServerFeatureOptions.Instance);
var resolver = new ExtractToCodeBehindCodeActionResolver(_emptyDocumentContextFactory, TestLanguageServerFeatureOptions.Instance, _languageServer);
var data = JObject.FromObject(new ExtractToCodeBehindCodeActionParams()
{
Uri = new Uri("c:/Test.razor"),
Expand All @@ -59,7 +67,7 @@ public async Task Handle_Unsupported()
var codeDocument = CreateCodeDocument(contents);
codeDocument.SetUnsupported();

var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance);
var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance, _languageServer);
var data = JObject.FromObject(CreateExtractToCodeBehindCodeActionParams(new Uri("c:/Test.razor"), contents, "@code", "Test"));

// Act
Expand All @@ -78,7 +86,7 @@ public async Task Handle_InvalidFileKind()
var codeDocument = CreateCodeDocument(contents);
codeDocument.SetFileKind(FileKinds.Legacy);

var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance);
var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance, _languageServer);
var data = JObject.FromObject(CreateExtractToCodeBehindCodeActionParams(new Uri("c:/Test.razor"), contents, "@code", "Test"));

// Act
Expand All @@ -103,7 +111,7 @@ public async Task Handle_ExtractCodeBlock()
var codeDocument = CreateCodeDocument(contents);
Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace));

var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance);
var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance, _languageServer);
var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace);
var data = JObject.FromObject(actionParams);

Expand Down Expand Up @@ -165,7 +173,7 @@ public async Task Handle_ExtractCodeBlock2()
var codeDocument = CreateCodeDocument(contents);
Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace));

var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance);
var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance, _languageServer);
var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace);
var data = JObject.FromObject(actionParams);

Expand Down Expand Up @@ -235,7 +243,7 @@ private void M()
var codeDocument = CreateCodeDocument(contents);
Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace));

var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance);
var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance, _languageServer);
var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace);
var data = JObject.FromObject(actionParams);

Expand Down Expand Up @@ -315,7 +323,7 @@ private void M()
var codeDocument = CreateCodeDocument(contents);
Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace));

var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance);
var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance, _languageServer);
var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace);
var data = JObject.FromObject(actionParams);

Expand Down Expand Up @@ -397,7 +405,7 @@ private void M()
var codeDocument = CreateCodeDocument(contents);
Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace));

var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance);
var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance, _languageServer);
var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace);
var data = JObject.FromObject(actionParams);

Expand Down Expand Up @@ -467,7 +475,7 @@ public async Task Handle_ExtractFunctionsBlock()
var codeDocument = CreateCodeDocument(contents);
Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace));

var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance);
var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance, _languageServer);
var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@functions", @namespace);
var data = JObject.FromObject(actionParams);

Expand Down Expand Up @@ -529,7 +537,7 @@ @using System.Diagnostics
var codeDocument = CreateCodeDocument(contents);
Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace));

var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance);
var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance, _languageServer);
var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace);
var data = JObject.FromObject(actionParams);

Expand Down Expand Up @@ -593,7 +601,7 @@ public async Task Handle_ExtractCodeBlockWithDirectives()
var codeDocument = CreateCodeDocument(contents);
Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace));

var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance);
var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance, _languageServer);
var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace);
var data = JObject.FromObject(actionParams);

Expand Down

0 comments on commit 8a2b8af

Please sign in to comment.