Skip to content

Commit

Permalink
Call Roslyn to format new code behind documents (#9263)
Browse files Browse the repository at this point in the history
Fixes #4330
Fixes #8766
Includes #9262 so just review from
8a2b8af
onwards

Goes with dotnet/roslyn#69878 and
dotnet/vscode-csharp#6329

I logged #9264 to follow up with a
better test, though strictly speaking the one I've added id exhaustive
:)

Behaviour of Extract to Code Behind before this change. Note the many
using statements and block scoped namespace.

![ExtractToCodeBehindBefore](https://github.com/dotnet/razor/assets/754264/10ac5595-b3b2-44c2-a1a7-66664d3c8f1b)

Behaviour after this change. Note the file scoped namespace, and file
header.

![ExtractToCodeBehindAfter](https://github.com/dotnet/razor/assets/754264/65160715-bc98-4336-b19d-37f9b14adf94)

The squiggle on `NavigationManager` is due to an `.editorconfig` rule I
have on in that project, requiring `this.` qualification, so is
unrelated.
  • Loading branch information
davidwengier authored Sep 14, 2023
2 parents 3282025 + 5372346 commit da56c44
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 34 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 @@ -93,8 +97,8 @@ 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 codeBlockContent = text.GetSubTextString(new CodeAnalysis.Text.TextSpan(actionParams.ExtractStart, actionParams.ExtractEnd - actionParams.ExtractStart)).Trim();
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";
}
Loading

0 comments on commit da56c44

Please sign in to comment.