From 3f97d5859dfb23c0ecb1c3c8d94a3a7ced418976 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 8 Sep 2023 21:58:59 +1000 Subject: [PATCH 01/10] Update tests to a nicer format --- ...tractToCodeBehindCodeActionResolverTest.cs | 126 ++++++++++++++---- 1 file changed, 103 insertions(+), 23 deletions(-) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs index 7ae774a5d0f..c0fb6c4da94 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs @@ -3,7 +3,6 @@ using System; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; @@ -12,9 +11,8 @@ using Microsoft.AspNetCore.Razor.Test.Common; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor.Workspaces.Extensions; -using Microsoft.VisualStudio.LanguageServer.Protocol; -using Moq; using Newtonsoft.Json.Linq; +using Roslyn.Test.Utilities; using Xunit; using Xunit.Abstractions; @@ -95,7 +93,13 @@ public async Task Handle_ExtractCodeBlock() { // Arrange var documentPath = new Uri("c:/Test.razor"); - var contents = $"@page \"/test\"{Environment.NewLine}@code {{ private var x = 1; }}"; + var contents = """ + @page "/test" + + @code { + private var x = 1; + } + """; var codeDocument = CreateCodeDocument(contents); Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace)); @@ -126,9 +130,23 @@ public async Task Handle_ExtractCodeBlock() var editCodeBehindChange = documentChanges[2]; Assert.True(editCodeBehindChange.TryGetFirst(out var textDocumentEdit2)); var editCodeBehindEdit = textDocumentEdit2!.Edits.First(); - Assert.Contains("public partial class Test", editCodeBehindEdit.NewText, StringComparison.Ordinal); - Assert.Contains("private var x = 1", editCodeBehindEdit.NewText, StringComparison.Ordinal); - Assert.Contains("namespace test.Pages", editCodeBehindEdit.NewText, StringComparison.Ordinal); + + AssertEx.EqualOrDiff(""" + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::System.Threading.Tasks; + using global::Microsoft.AspNetCore.Components; + + namespace test.Pages + { + public partial class Test + { + private var x = 1; + } + } + """, + editCodeBehindEdit.NewText); } [Fact] @@ -136,8 +154,13 @@ public async Task Handle_ExtractFunctionsBlock() { // Arrange var documentPath = new Uri("c:/Test.razor"); - var contents = $"@page \"/test\"{Environment.NewLine}@functions {{ private var x = 1; }}"; - var codeDocument = CreateCodeDocument(contents); + var contents = """ + @page "/test" + + @functions { + private var x = 1; + } + """; var codeDocument = CreateCodeDocument(contents); Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace)); var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance); @@ -167,9 +190,23 @@ public async Task Handle_ExtractFunctionsBlock() var editCodeBehindChange = documentChanges[2]; Assert.True(editCodeBehindChange.TryGetFirst(out var editCodeBehind)); var editCodeBehindEdit = editCodeBehind!.Edits.First(); - Assert.Contains("public partial class Test", editCodeBehindEdit.NewText, StringComparison.Ordinal); - Assert.Contains("private var x = 1", editCodeBehindEdit.NewText, StringComparison.Ordinal); - Assert.Contains("namespace test.Pages", editCodeBehindEdit.NewText, StringComparison.Ordinal); + + AssertEx.EqualOrDiff(""" + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::System.Threading.Tasks; + using global::Microsoft.AspNetCore.Components; + + namespace test.Pages + { + public partial class Test + { + private var x = 1; + } + } + """, + editCodeBehindEdit.NewText); } [Fact] @@ -177,7 +214,14 @@ public async Task Handle_ExtractCodeBlockWithUsing() { // Arrange var documentPath = new Uri("c:/Test.razor"); - var contents = $"@page \"/test\"\n@using System.Diagnostics{Environment.NewLine}@code {{ private var x = 1; }}"; + var contents = """ + @page "/test" + @using System.Diagnostics + + @code { + private var x = 1; + } + """; var codeDocument = CreateCodeDocument(contents); Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace)); @@ -208,10 +252,24 @@ public async Task Handle_ExtractCodeBlockWithUsing() var editCodeBehindChange = documentChanges[2]; Assert.True(editCodeBehindChange.TryGetFirst(out var editCodeBehind)); var editCodeBehindEdit = editCodeBehind!.Edits.First(); - Assert.Contains("using System.Diagnostics", editCodeBehindEdit.NewText, StringComparison.Ordinal); - Assert.Contains("public partial class Test", editCodeBehindEdit.NewText, StringComparison.Ordinal); - Assert.Contains("private var x = 1", editCodeBehindEdit.NewText, StringComparison.Ordinal); - Assert.Contains("namespace test.Pages", editCodeBehindEdit.NewText, StringComparison.Ordinal); + + AssertEx.EqualOrDiff(""" + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::System.Threading.Tasks; + using global::Microsoft.AspNetCore.Components; + using System.Diagnostics; + + namespace test.Pages + { + public partial class Test + { + private var x = 1; + } + } + """, + editCodeBehindEdit.NewText); } [Fact] @@ -219,7 +277,15 @@ public async Task Handle_ExtractCodeBlockWithDirectives() { // Arrange var documentPath = new Uri("c:/Test.razor"); - var contents = $"@page \"/test\"{Environment.NewLine}@code {{ {Environment.NewLine} #region TestRegion {Environment.NewLine} private var x = 1; {Environment.NewLine} #endregion {Environment.NewLine}}}"; + var contents = """ + @page "/test" + + @code { + #region TestRegion + private var x = 1; + #endregion + } + """; var codeDocument = CreateCodeDocument(contents); Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace)); @@ -250,11 +316,25 @@ public async Task Handle_ExtractCodeBlockWithDirectives() var editCodeBehindChange = documentChanges[2]; Assert.True(editCodeBehindChange.TryGetFirst(out var textDocumentEdit2)); var editCodeBehindEdit = textDocumentEdit2!.Edits.First(); - Assert.Contains("public partial class Test", editCodeBehindEdit.NewText, StringComparison.Ordinal); - Assert.Contains("#region TestRegion", editCodeBehindEdit.NewText, StringComparison.Ordinal); - Assert.Contains("private var x = 1", editCodeBehindEdit.NewText, StringComparison.Ordinal); - Assert.Contains("#endregion", editCodeBehindEdit.NewText, StringComparison.Ordinal); - Assert.Contains("namespace test.Pages", editCodeBehindEdit.NewText, StringComparison.Ordinal); + + AssertEx.EqualOrDiff(""" + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::System.Threading.Tasks; + using global::Microsoft.AspNetCore.Components; + + namespace test.Pages + { + public partial class Test + { + #region TestRegion + private var x = 1; + #endregion + } + } + """, + editCodeBehindEdit.NewText); } private static RazorCodeDocument CreateCodeDocument(string text) From c7d3bbd4cdef96624aec61d2b6b7d50de9136524 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 8 Sep 2023 22:08:00 +1000 Subject: [PATCH 02/10] Add some more tests to validate current behaviour --- ...tractToCodeBehindCodeActionResolverTest.cs | 219 +++++++++++++++++- 1 file changed, 218 insertions(+), 1 deletion(-) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs index c0fb6c4da94..060c4d5e3da 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs @@ -149,6 +149,223 @@ public partial class Test editCodeBehindEdit.NewText); } + [Fact] + public async Task Handle_ExtractCodeBlock2() + { + // Arrange + var documentPath = new Uri("c:/Test.razor"); + var contents = """ + @page "/test" + + @code + { + private var x = 1; + } + """; + var codeDocument = CreateCodeDocument(contents); + Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace)); + + var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance); + var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); + var data = JObject.FromObject(actionParams); + + // Act + var workspaceEdit = await resolver.ResolveAsync(data, default); + + // Assert + Assert.NotNull(workspaceEdit); + Assert.NotNull(workspaceEdit!.DocumentChanges); + Assert.Equal(3, workspaceEdit.DocumentChanges!.Value.Count()); + + var documentChanges = workspaceEdit.DocumentChanges!.Value.ToArray(); + var createFileChange = documentChanges[0]; + Assert.True(createFileChange.TryGetSecond(out var _)); + + var editCodeDocumentChange = documentChanges[1]; + Assert.True(editCodeDocumentChange.TryGetFirst(out var textDocumentEdit1)); + var editCodeDocumentEdit = textDocumentEdit1!.Edits.First(); + Assert.True(editCodeDocumentEdit.Range.Start.TryGetAbsoluteIndex(codeDocument.GetSourceText(), Logger, out var removeStart)); + Assert.Equal(actionParams.RemoveStart, removeStart); + Assert.True(editCodeDocumentEdit.Range.End.TryGetAbsoluteIndex(codeDocument.GetSourceText(), Logger, out var removeEnd)); + Assert.Equal(actionParams.RemoveEnd, removeEnd); + + var editCodeBehindChange = documentChanges[2]; + Assert.True(editCodeBehindChange.TryGetFirst(out var textDocumentEdit2)); + var editCodeBehindEdit = textDocumentEdit2!.Edits.First(); + + AssertEx.EqualOrDiff(""" + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::System.Threading.Tasks; + using global::Microsoft.AspNetCore.Components; + + namespace test.Pages + { + public partial class Test + { + private var x = 1; + } + } + """, + editCodeBehindEdit.NewText); + } + + [Fact] + public async Task Handle_ExtractCodeBlock_MultipleMembers() + { + // Arrange + var documentPath = new Uri("c:/Test.razor"); + var contents = """ + @page "/test" + + @code { + private int x = 1; + private int z = 2; + + private string y = "hello"; + + // Here is a comment + private void M() + { + // okay + } + } + """; + var codeDocument = CreateCodeDocument(contents); + Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace)); + + var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance); + var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); + var data = JObject.FromObject(actionParams); + + // Act + var workspaceEdit = await resolver.ResolveAsync(data, default); + + // Assert + Assert.NotNull(workspaceEdit); + Assert.NotNull(workspaceEdit!.DocumentChanges); + Assert.Equal(3, workspaceEdit.DocumentChanges!.Value.Count()); + + var documentChanges = workspaceEdit.DocumentChanges!.Value.ToArray(); + var createFileChange = documentChanges[0]; + Assert.True(createFileChange.TryGetSecond(out var _)); + + var editCodeDocumentChange = documentChanges[1]; + Assert.True(editCodeDocumentChange.TryGetFirst(out var textDocumentEdit1)); + var editCodeDocumentEdit = textDocumentEdit1!.Edits.First(); + Assert.True(editCodeDocumentEdit.Range.Start.TryGetAbsoluteIndex(codeDocument.GetSourceText(), Logger, out var removeStart)); + Assert.Equal(actionParams.RemoveStart, removeStart); + Assert.True(editCodeDocumentEdit.Range.End.TryGetAbsoluteIndex(codeDocument.GetSourceText(), Logger, out var removeEnd)); + Assert.Equal(actionParams.RemoveEnd, removeEnd); + + var editCodeBehindChange = documentChanges[2]; + Assert.True(editCodeBehindChange.TryGetFirst(out var textDocumentEdit2)); + var editCodeBehindEdit = textDocumentEdit2!.Edits.First(); + + AssertEx.EqualOrDiff(""" + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::System.Threading.Tasks; + using global::Microsoft.AspNetCore.Components; + + namespace test.Pages + { + public partial class Test + { + private int x = 1; + private int z = 2; + private string y = "hello"; + // Here is a comment + private void M() + { + // okay + } + } + } + """, + editCodeBehindEdit.NewText); + } + + [Fact] + public async Task Handle_ExtractCodeBlock_MultipleMembers2() + { + // Arrange + var documentPath = new Uri("c:/Test.razor"); + var contents = """ + @page "/test" + + @code + { + private int x = 1; + private int z = 2; + + private string y = "hello"; + + // Here is a comment + private void M() + { + // okay + } + } + """; + var codeDocument = CreateCodeDocument(contents); + Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace)); + + var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance); + var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); + var data = JObject.FromObject(actionParams); + + // Act + var workspaceEdit = await resolver.ResolveAsync(data, default); + + // Assert + Assert.NotNull(workspaceEdit); + Assert.NotNull(workspaceEdit!.DocumentChanges); + Assert.Equal(3, workspaceEdit.DocumentChanges!.Value.Count()); + + var documentChanges = workspaceEdit.DocumentChanges!.Value.ToArray(); + var createFileChange = documentChanges[0]; + Assert.True(createFileChange.TryGetSecond(out var _)); + + var editCodeDocumentChange = documentChanges[1]; + Assert.True(editCodeDocumentChange.TryGetFirst(out var textDocumentEdit1)); + var editCodeDocumentEdit = textDocumentEdit1!.Edits.First(); + Assert.True(editCodeDocumentEdit.Range.Start.TryGetAbsoluteIndex(codeDocument.GetSourceText(), Logger, out var removeStart)); + Assert.Equal(actionParams.RemoveStart, removeStart); + Assert.True(editCodeDocumentEdit.Range.End.TryGetAbsoluteIndex(codeDocument.GetSourceText(), Logger, out var removeEnd)); + Assert.Equal(actionParams.RemoveEnd, removeEnd); + + var editCodeBehindChange = documentChanges[2]; + Assert.True(editCodeBehindChange.TryGetFirst(out var textDocumentEdit2)); + var editCodeBehindEdit = textDocumentEdit2!.Edits.First(); + + AssertEx.EqualOrDiff(""" + using global::System; + using global::System.Collections.Generic; + using global::System.Linq; + using global::System.Threading.Tasks; + using global::Microsoft.AspNetCore.Components; + + namespace test.Pages + { + public partial class Test + { + private int x = 1; + private int z = 2; + private string y = "hello"; + // Here is a comment + private void M() + { + // okay + } + } + } + """, + editCodeBehindEdit.NewText); + } + [Fact] public async Task Handle_ExtractFunctionsBlock() { @@ -351,7 +568,7 @@ private static RazorCodeDocument CreateCodeDocument(string text) private static ExtractToCodeBehindCodeActionParams CreateExtractToCodeBehindCodeActionParams(Uri uri, string contents, string removeStart, string @namespace) { // + 1 to ensure we do not cut off the '}'. - var endIndex = contents.IndexOf("}", StringComparison.Ordinal) + 1; + var endIndex = contents.LastIndexOf("}", StringComparison.Ordinal) + 1; return new ExtractToCodeBehindCodeActionParams { Uri = uri, From a400c2283959ae7d9320b9c3fe23e5bd674e1559 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 8 Sep 2023 22:14:58 +1000 Subject: [PATCH 03/10] Make using directives nicer --- .../ExtractToCodeBehindCodeActionResolver.cs | 11 ++- ...tractToCodeBehindCodeActionResolverTest.cs | 70 +++++++++---------- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs index 28268895c68..002f513100f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs @@ -181,7 +181,16 @@ private static IEnumerable FindUsings(RazorCodeDocument razorCodeDocumen return razorCodeDocument .GetDocumentIntermediateNode() .FindDescendantNodes() - .Select(n => n.Content); + .Select(n => + { + var content = n.Content; + if (content.StartsWith("global::", StringComparison.Ordinal)) + { + return content.Substring(8); + } + + return content; + }); } /// diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs index 060c4d5e3da..953ec92404c 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs @@ -132,11 +132,11 @@ public async Task Handle_ExtractCodeBlock() var editCodeBehindEdit = textDocumentEdit2!.Edits.First(); AssertEx.EqualOrDiff(""" - using global::System; - using global::System.Collections.Generic; - using global::System.Linq; - using global::System.Threading.Tasks; - using global::Microsoft.AspNetCore.Components; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Components; namespace test.Pages { @@ -194,11 +194,11 @@ public async Task Handle_ExtractCodeBlock2() var editCodeBehindEdit = textDocumentEdit2!.Edits.First(); AssertEx.EqualOrDiff(""" - using global::System; - using global::System.Collections.Generic; - using global::System.Linq; - using global::System.Threading.Tasks; - using global::Microsoft.AspNetCore.Components; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Components; namespace test.Pages { @@ -264,11 +264,11 @@ private void M() var editCodeBehindEdit = textDocumentEdit2!.Edits.First(); AssertEx.EqualOrDiff(""" - using global::System; - using global::System.Collections.Generic; - using global::System.Linq; - using global::System.Threading.Tasks; - using global::Microsoft.AspNetCore.Components; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Components; namespace test.Pages { @@ -342,11 +342,11 @@ private void M() var editCodeBehindEdit = textDocumentEdit2!.Edits.First(); AssertEx.EqualOrDiff(""" - using global::System; - using global::System.Collections.Generic; - using global::System.Linq; - using global::System.Threading.Tasks; - using global::Microsoft.AspNetCore.Components; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Components; namespace test.Pages { @@ -409,11 +409,11 @@ public async Task Handle_ExtractFunctionsBlock() var editCodeBehindEdit = editCodeBehind!.Edits.First(); AssertEx.EqualOrDiff(""" - using global::System; - using global::System.Collections.Generic; - using global::System.Linq; - using global::System.Threading.Tasks; - using global::Microsoft.AspNetCore.Components; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Components; namespace test.Pages { @@ -471,11 +471,11 @@ @using System.Diagnostics var editCodeBehindEdit = editCodeBehind!.Edits.First(); AssertEx.EqualOrDiff(""" - using global::System; - using global::System.Collections.Generic; - using global::System.Linq; - using global::System.Threading.Tasks; - using global::Microsoft.AspNetCore.Components; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Components; using System.Diagnostics; namespace test.Pages @@ -535,11 +535,11 @@ public async Task Handle_ExtractCodeBlockWithDirectives() var editCodeBehindEdit = textDocumentEdit2!.Edits.First(); AssertEx.EqualOrDiff(""" - using global::System; - using global::System.Collections.Generic; - using global::System.Linq; - using global::System.Threading.Tasks; - using global::Microsoft.AspNetCore.Components; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Components; namespace test.Pages { From aea3842c99d3c3a724fea75755111b93c66fa6e4 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sat, 9 Sep 2023 16:23:03 +1000 Subject: [PATCH 04/10] Generate nicer looking C# Calling NormalizeWhitespace is the lazy way to not have to worry about trivia when using SyntaxFactory, but it also destroys any formatting the user might have done. Better to use the actual formatter to fix indentation and things, and just generate the code as a string. Using syntax factory wasn't really helping too much. --- .../ExtractToCodeBehindCodeActionResolver.cs | 93 +++++++++---------- ...tractToCodeBehindCodeActionResolverTest.cs | 36 ++++--- 2 files changed, 64 insertions(+), 65 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs index 002f513100f..1f30b946336 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs @@ -2,10 +2,8 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.Language; @@ -13,16 +11,14 @@ using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; using Microsoft.AspNetCore.Razor.LanguageServer.Common; using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.AspNetCore.Razor.Utilities; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Razor; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.VisualStudio.LanguageServer.Protocol; using Newtonsoft.Json.Linq; -using CSharpSyntaxFactory = Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -using CSharpSyntaxKind = Microsoft.CodeAnalysis.CSharp.SyntaxKind; namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions; @@ -30,13 +26,16 @@ internal sealed class ExtractToCodeBehindCodeActionResolver : IRazorCodeActionRe { private readonly DocumentContextFactory _documentContextFactory; private readonly LanguageServerFeatureOptions _languageServerFeatureOptions; + private readonly ProjectSnapshotManagerAccessor _projectSnapshotManagerAccessor; public ExtractToCodeBehindCodeActionResolver( DocumentContextFactory documentContextFactory, - LanguageServerFeatureOptions languageServerFeatureOptions) + LanguageServerFeatureOptions languageServerFeatureOptions, + ProjectSnapshotManagerAccessor projectSnapshotManagerAccessor) { _documentContextFactory = documentContextFactory ?? throw new ArgumentNullException(nameof(documentContextFactory)); _languageServerFeatureOptions = languageServerFeatureOptions; + _projectSnapshotManagerAccessor = projectSnapshotManagerAccessor; } public string Action => LanguageServerConstants.CodeActions.ExtractToCodeBehindAction; @@ -170,29 +169,6 @@ private static string GenerateCodeBehindPath(string path) return codeBehindPath; } - /// - /// Determine all explicit and implicit using statements in the code - /// document using the intermediate node. - /// - /// The code document to analyze. - /// An enumerable of the qualified namespaces. - private static IEnumerable FindUsings(RazorCodeDocument razorCodeDocument) - { - return razorCodeDocument - .GetDocumentIntermediateNode() - .FindDescendantNodes() - .Select(n => - { - var content = n.Content; - if (content.StartsWith("global::", StringComparison.Ordinal)) - { - return content.Substring(8); - } - - return content; - }); - } - /// /// Generate a complete C# compilation unit containing a partial class /// with the given name, body contents, and the namespace and all @@ -203,27 +179,42 @@ private static IEnumerable FindUsings(RazorCodeDocument razorCodeDocumen /// Class body contents. /// Existing code document we're extracting from. /// - private static string GenerateCodeBehindClass(string className, string namespaceName, string contents, RazorCodeDocument razorCodeDocument) + private string GenerateCodeBehindClass(string className, string namespaceName, string contents, RazorCodeDocument razorCodeDocument) { - var mock = (ClassDeclarationSyntax)CSharpSyntaxFactory.ParseMemberDeclaration($"class Class {contents}")!; - var @class = CSharpSyntaxFactory - .ClassDeclaration(className) - .AddModifiers(CSharpSyntaxFactory.Token(CSharpSyntaxKind.PublicKeyword), CSharpSyntaxFactory.Token(CSharpSyntaxKind.PartialKeyword)) - .AddMembers(mock.Members.ToArray()) - .WithCloseBraceToken(mock.CloseBraceToken); - - var @namespace = CSharpSyntaxFactory - .NamespaceDeclaration(CSharpSyntaxFactory.ParseName(namespaceName)) - .AddMembers(@class); - - var usings = FindUsings(razorCodeDocument) - .Select(u => CSharpSyntaxFactory.UsingDirective(CSharpSyntaxFactory.ParseName(u))) - .ToArray(); - var compilationUnit = CSharpSyntaxFactory - .CompilationUnit() - .AddUsings(usings) - .AddMembers(@namespace); - - return compilationUnit.NormalizeWhitespace().ToFullString(); + using var _ = StringBuilderPool.GetPooledObject(out var builder); + + var usingDirectives = razorCodeDocument + .GetDocumentIntermediateNode() + .FindDescendantNodes(); + foreach (var usingDirective in usingDirectives) + { + builder.Append("using "); + + var content = usingDirective.Content; + var startIndex = content.StartsWith("global::", StringComparison.Ordinal) + ? 8 + : 0; + + builder.Append(content, startIndex, content.Length - startIndex); + builder.Append(';'); + builder.AppendLine(); + } + + builder.AppendLine(); + builder.Append("namespace "); + builder.AppendLine(namespaceName); + builder.Append('{'); + builder.AppendLine(); + builder.Append(" public partial class "); + builder.AppendLine(className); + builder.Append(" "); + builder.AppendLine(contents); + builder.Append('}'); + + // 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, _projectSnapshotManagerAccessor.Instance.Workspace); + + return node.ToFullString(); } } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs index 953ec92404c..41f30241f1c 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; 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; @@ -21,18 +22,21 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions; public class ExtractToCodeBehindCodeActionResolverTest : LanguageServerTestBase { private readonly DocumentContextFactory _emptyDocumentContextFactory; + private TestProjectSnapshotManagerAccessor _projectSnapshotManagerAccessor; public ExtractToCodeBehindCodeActionResolverTest(ITestOutputHelper testOutput) : base(testOutput) { _emptyDocumentContextFactory = new TestDocumentContextFactory(); + var manager = TestProjectSnapshotManager.Create(ErrorReporter, Dispatcher); + _projectSnapshotManagerAccessor = new TestProjectSnapshotManagerAccessor(manager); } [Fact] public async Task Handle_MissingFile() { // Arrange - var resolver = new ExtractToCodeBehindCodeActionResolver(_emptyDocumentContextFactory, TestLanguageServerFeatureOptions.Instance); + var resolver = new ExtractToCodeBehindCodeActionResolver(_emptyDocumentContextFactory, TestLanguageServerFeatureOptions.Instance, _projectSnapshotManagerAccessor); var data = JObject.FromObject(new ExtractToCodeBehindCodeActionParams() { Uri = new Uri("c:/Test.razor"), @@ -59,7 +63,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, _projectSnapshotManagerAccessor); var data = JObject.FromObject(CreateExtractToCodeBehindCodeActionParams(new Uri("c:/Test.razor"), contents, "@code", "Test")); // Act @@ -78,7 +82,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, _projectSnapshotManagerAccessor); var data = JObject.FromObject(CreateExtractToCodeBehindCodeActionParams(new Uri("c:/Test.razor"), contents, "@code", "Test")); // Act @@ -103,7 +107,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, _projectSnapshotManagerAccessor); var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); var data = JObject.FromObject(actionParams); @@ -165,7 +169,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, _projectSnapshotManagerAccessor); var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); var data = JObject.FromObject(actionParams); @@ -235,7 +239,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, _projectSnapshotManagerAccessor); var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); var data = JObject.FromObject(actionParams); @@ -276,11 +280,13 @@ public partial class Test { private int x = 1; private int z = 2; + private string y = "hello"; + // Here is a comment private void M() { - // okay + // okay } } } @@ -313,7 +319,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, _projectSnapshotManagerAccessor); var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); var data = JObject.FromObject(actionParams); @@ -354,11 +360,13 @@ public partial class Test { private int x = 1; private int z = 2; + private string y = "hello"; + // Here is a comment private void M() { - // okay + // okay } } } @@ -380,7 +388,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, _projectSnapshotManagerAccessor); var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@functions", @namespace); var data = JObject.FromObject(actionParams); @@ -442,7 +450,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, _projectSnapshotManagerAccessor); var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); var data = JObject.FromObject(actionParams); @@ -506,7 +514,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, _projectSnapshotManagerAccessor); var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); var data = JObject.FromObject(actionParams); @@ -545,9 +553,9 @@ namespace test.Pages { public partial class Test { - #region TestRegion + #region TestRegion private var x = 1; - #endregion + #endregion } } """, From 3ffbeea26ea884883a23c1d39b2298a85140b5b8 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sat, 9 Sep 2023 16:23:32 +1000 Subject: [PATCH 05/10] Add another test Just to make sure the formatter is doing the right thing --- ...tractToCodeBehindCodeActionResolverTest.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs index 41f30241f1c..68fdf19716f 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs @@ -374,6 +374,88 @@ private void M() editCodeBehindEdit.NewText); } + [Fact] + public async Task Handle_ExtractCodeBlock_MultipleMembers3() + { + // Arrange + var documentPath = new Uri("c:/Test.razor"); + var contents = """ + @page "/test" + +
+ @code + { + private int x = 1; + private int z = 2; + + private string y = "hello"; + + // Here is a comment + private void M() + { + // okay + } + } +
+ """; + var codeDocument = CreateCodeDocument(contents); + Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace)); + + var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance, _projectSnapshotManagerAccessor); + var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); + var data = JObject.FromObject(actionParams); + + // Act + var workspaceEdit = await resolver.ResolveAsync(data, default); + + // Assert + Assert.NotNull(workspaceEdit); + Assert.NotNull(workspaceEdit!.DocumentChanges); + Assert.Equal(3, workspaceEdit.DocumentChanges!.Value.Count()); + + var documentChanges = workspaceEdit.DocumentChanges!.Value.ToArray(); + var createFileChange = documentChanges[0]; + Assert.True(createFileChange.TryGetSecond(out var _)); + + var editCodeDocumentChange = documentChanges[1]; + Assert.True(editCodeDocumentChange.TryGetFirst(out var textDocumentEdit1)); + var editCodeDocumentEdit = textDocumentEdit1!.Edits.First(); + Assert.True(editCodeDocumentEdit.Range.Start.TryGetAbsoluteIndex(codeDocument.GetSourceText(), Logger, out var removeStart)); + Assert.Equal(actionParams.RemoveStart, removeStart); + Assert.True(editCodeDocumentEdit.Range.End.TryGetAbsoluteIndex(codeDocument.GetSourceText(), Logger, out var removeEnd)); + Assert.Equal(actionParams.RemoveEnd, removeEnd); + + var editCodeBehindChange = documentChanges[2]; + Assert.True(editCodeBehindChange.TryGetFirst(out var textDocumentEdit2)); + var editCodeBehindEdit = textDocumentEdit2!.Edits.First(); + + AssertEx.EqualOrDiff(""" + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Components; + + namespace test.Pages + { + public partial class Test + { + private int x = 1; + private int z = 2; + + private string y = "hello"; + + // Here is a comment + private void M() + { + // okay + } + } + } + """, + editCodeBehindEdit.NewText); + } + [Fact] public async Task Handle_ExtractFunctionsBlock() { From e549c4336b88d09a9587b54d3f5b916915146d58 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Sat, 9 Sep 2023 16:32:30 +1000 Subject: [PATCH 06/10] Fix up the old tests The invalid C# code annoyed me :P --- ...tractToCodeBehindCodeActionResolverTest.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs index 68fdf19716f..342410bd060 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs @@ -59,7 +59,7 @@ public async Task Handle_Unsupported() { // Arrange var documentPath = new Uri("c:\\Test.razor"); - var contents = $"@page \"/test\"{Environment.NewLine}@code {{ private var x = 1; }}"; + var contents = $"@page \"/test\"{Environment.NewLine}@code {{ private int x = 1; }}"; var codeDocument = CreateCodeDocument(contents); codeDocument.SetUnsupported(); @@ -78,7 +78,7 @@ public async Task Handle_InvalidFileKind() { // Arrange var documentPath = new Uri("c:\\Test.razor"); - var contents = $"@page \"/test\"{Environment.NewLine}@code {{ private var x = 1; }}"; + var contents = $"@page \"/test\"{Environment.NewLine}@code {{ private int x = 1; }}"; var codeDocument = CreateCodeDocument(contents); codeDocument.SetFileKind(FileKinds.Legacy); @@ -101,7 +101,7 @@ public async Task Handle_ExtractCodeBlock() @page "/test" @code { - private var x = 1; + private int x = 1; } """; var codeDocument = CreateCodeDocument(contents); @@ -146,7 +146,7 @@ namespace test.Pages { public partial class Test { - private var x = 1; + private int x = 1; } } """, @@ -163,7 +163,7 @@ public async Task Handle_ExtractCodeBlock2() @code { - private var x = 1; + private int x = 1; } """; var codeDocument = CreateCodeDocument(contents); @@ -208,7 +208,7 @@ namespace test.Pages { public partial class Test { - private var x = 1; + private int x = 1; } } """, @@ -465,7 +465,7 @@ public async Task Handle_ExtractFunctionsBlock() @page "/test" @functions { - private var x = 1; + private int x = 1; } """; var codeDocument = CreateCodeDocument(contents); Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace)); @@ -509,7 +509,7 @@ namespace test.Pages { public partial class Test { - private var x = 1; + private int x = 1; } } """, @@ -526,7 +526,7 @@ public async Task Handle_ExtractCodeBlockWithUsing() @using System.Diagnostics @code { - private var x = 1; + private int x = 1; } """; var codeDocument = CreateCodeDocument(contents); @@ -572,7 +572,7 @@ namespace test.Pages { public partial class Test { - private var x = 1; + private int x = 1; } } """, @@ -589,7 +589,7 @@ public async Task Handle_ExtractCodeBlockWithDirectives() @code { #region TestRegion - private var x = 1; + private int x = 1; #endregion } """; @@ -636,7 +636,7 @@ namespace test.Pages public partial class Test { #region TestRegion - private var x = 1; + private int x = 1; #endregion } } From 7f95457ae5d86659f7eb2b63d160d5d670d5fda3 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 11 Sep 2023 09:41:48 +1000 Subject: [PATCH 07/10] Add test for tab indent --- ...tractToCodeBehindCodeActionResolverTest.cs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs index 342410bd060..30ff18ea616 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs @@ -92,6 +92,74 @@ public async Task Handle_InvalidFileKind() Assert.Null(workspaceEdit); } + [Fact] + public async Task Handle_ExtractCodeBlock_Tabs() + { + // Arrange + var documentPath = new Uri("c:/Test.razor"); + var contents = """ + @page "/test" + + @code { + private int x = 1; + } + """; + + var workspace = _projectSnapshotManagerAccessor.Instance.Workspace; + var newOptions = workspace.Options.WithChangedOption(CodeAnalysis.Formatting.FormattingOptions.TabSize, LanguageNames.CSharp, 4) + .WithChangedOption(CodeAnalysis.Formatting.FormattingOptions.IndentationSize, LanguageNames.CSharp, 4) + .WithChangedOption(CodeAnalysis.Formatting.FormattingOptions.UseTabs, LanguageNames.CSharp, true); + workspace.TryApplyChanges(workspace.CurrentSolution.WithOptions(newOptions)); + + var codeDocument = CreateCodeDocument(contents); + Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace)); + + var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance, _projectSnapshotManagerAccessor); + var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); + var data = JObject.FromObject(actionParams); + + // Act + var workspaceEdit = await resolver.ResolveAsync(data, default); + + // Assert + Assert.NotNull(workspaceEdit); + Assert.NotNull(workspaceEdit!.DocumentChanges); + Assert.Equal(3, workspaceEdit.DocumentChanges!.Value.Count()); + + var documentChanges = workspaceEdit.DocumentChanges!.Value.ToArray(); + var createFileChange = documentChanges[0]; + Assert.True(createFileChange.TryGetSecond(out var _)); + + var editCodeDocumentChange = documentChanges[1]; + Assert.True(editCodeDocumentChange.TryGetFirst(out var textDocumentEdit1)); + var editCodeDocumentEdit = textDocumentEdit1!.Edits.First(); + Assert.True(editCodeDocumentEdit.Range.Start.TryGetAbsoluteIndex(codeDocument.GetSourceText(), Logger, out var removeStart)); + Assert.Equal(actionParams.RemoveStart, removeStart); + Assert.True(editCodeDocumentEdit.Range.End.TryGetAbsoluteIndex(codeDocument.GetSourceText(), Logger, out var removeEnd)); + Assert.Equal(actionParams.RemoveEnd, removeEnd); + + var editCodeBehindChange = documentChanges[2]; + Assert.True(editCodeBehindChange.TryGetFirst(out var textDocumentEdit2)); + var editCodeBehindEdit = textDocumentEdit2!.Edits.First(); + + AssertEx.EqualOrDiff(""" + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Components; + + namespace test.Pages + { + public partial class Test + { + private int x = 1; + } + } + """, + editCodeBehindEdit.NewText); + } + [Fact] public async Task Handle_ExtractCodeBlock() { From e7725a32a004152bcdb85cdf5e5f4e75a4b08c90 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 11 Sep 2023 14:56:22 +1000 Subject: [PATCH 08/10] Don't pass a workspace to the formatter Turns out this test was invalid, and this behaviour is not possible to do on our side. The good news is the old code was just as wrong, and I have a better solution in the works :) --- .../ExtractToCodeBehindCodeActionResolver.cs | 12 ++- ...tractToCodeBehindCodeActionResolverTest.cs | 97 +++---------------- 2 files changed, 20 insertions(+), 89 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs index 1f30b946336..36ae3f5f0a2 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.AspNetCore.Razor.Utilities; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Razor; @@ -24,18 +25,17 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions; internal sealed class ExtractToCodeBehindCodeActionResolver : IRazorCodeActionResolver { + private static readonly Workspace s_workspace = new AdhocWorkspace(); + private readonly DocumentContextFactory _documentContextFactory; private readonly LanguageServerFeatureOptions _languageServerFeatureOptions; - private readonly ProjectSnapshotManagerAccessor _projectSnapshotManagerAccessor; public ExtractToCodeBehindCodeActionResolver( DocumentContextFactory documentContextFactory, - LanguageServerFeatureOptions languageServerFeatureOptions, - ProjectSnapshotManagerAccessor projectSnapshotManagerAccessor) + LanguageServerFeatureOptions languageServerFeatureOptions) { _documentContextFactory = documentContextFactory ?? throw new ArgumentNullException(nameof(documentContextFactory)); _languageServerFeatureOptions = languageServerFeatureOptions; - _projectSnapshotManagerAccessor = projectSnapshotManagerAccessor; } public string Action => LanguageServerConstants.CodeActions.ExtractToCodeBehindAction; @@ -211,9 +211,11 @@ 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, _projectSnapshotManagerAccessor.Instance.Workspace); + node = Formatter.Format(node, s_workspace); return node.ToFullString(); } diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs index 30ff18ea616..00caaecd14a 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/CodeActions/Razor/ExtractToCodeBehindCodeActionResolverTest.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models; 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; @@ -22,21 +21,18 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions; public class ExtractToCodeBehindCodeActionResolverTest : LanguageServerTestBase { private readonly DocumentContextFactory _emptyDocumentContextFactory; - private TestProjectSnapshotManagerAccessor _projectSnapshotManagerAccessor; public ExtractToCodeBehindCodeActionResolverTest(ITestOutputHelper testOutput) : base(testOutput) { _emptyDocumentContextFactory = new TestDocumentContextFactory(); - var manager = TestProjectSnapshotManager.Create(ErrorReporter, Dispatcher); - _projectSnapshotManagerAccessor = new TestProjectSnapshotManagerAccessor(manager); } [Fact] public async Task Handle_MissingFile() { // Arrange - var resolver = new ExtractToCodeBehindCodeActionResolver(_emptyDocumentContextFactory, TestLanguageServerFeatureOptions.Instance, _projectSnapshotManagerAccessor); + var resolver = new ExtractToCodeBehindCodeActionResolver(_emptyDocumentContextFactory, TestLanguageServerFeatureOptions.Instance); var data = JObject.FromObject(new ExtractToCodeBehindCodeActionParams() { Uri = new Uri("c:/Test.razor"), @@ -63,7 +59,7 @@ public async Task Handle_Unsupported() var codeDocument = CreateCodeDocument(contents); codeDocument.SetUnsupported(); - var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance, _projectSnapshotManagerAccessor); + var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance); var data = JObject.FromObject(CreateExtractToCodeBehindCodeActionParams(new Uri("c:/Test.razor"), contents, "@code", "Test")); // Act @@ -82,7 +78,7 @@ public async Task Handle_InvalidFileKind() var codeDocument = CreateCodeDocument(contents); codeDocument.SetFileKind(FileKinds.Legacy); - var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance, _projectSnapshotManagerAccessor); + var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance); var data = JObject.FromObject(CreateExtractToCodeBehindCodeActionParams(new Uri("c:/Test.razor"), contents, "@code", "Test")); // Act @@ -92,74 +88,6 @@ public async Task Handle_InvalidFileKind() Assert.Null(workspaceEdit); } - [Fact] - public async Task Handle_ExtractCodeBlock_Tabs() - { - // Arrange - var documentPath = new Uri("c:/Test.razor"); - var contents = """ - @page "/test" - - @code { - private int x = 1; - } - """; - - var workspace = _projectSnapshotManagerAccessor.Instance.Workspace; - var newOptions = workspace.Options.WithChangedOption(CodeAnalysis.Formatting.FormattingOptions.TabSize, LanguageNames.CSharp, 4) - .WithChangedOption(CodeAnalysis.Formatting.FormattingOptions.IndentationSize, LanguageNames.CSharp, 4) - .WithChangedOption(CodeAnalysis.Formatting.FormattingOptions.UseTabs, LanguageNames.CSharp, true); - workspace.TryApplyChanges(workspace.CurrentSolution.WithOptions(newOptions)); - - var codeDocument = CreateCodeDocument(contents); - Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace)); - - var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance, _projectSnapshotManagerAccessor); - var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); - var data = JObject.FromObject(actionParams); - - // Act - var workspaceEdit = await resolver.ResolveAsync(data, default); - - // Assert - Assert.NotNull(workspaceEdit); - Assert.NotNull(workspaceEdit!.DocumentChanges); - Assert.Equal(3, workspaceEdit.DocumentChanges!.Value.Count()); - - var documentChanges = workspaceEdit.DocumentChanges!.Value.ToArray(); - var createFileChange = documentChanges[0]; - Assert.True(createFileChange.TryGetSecond(out var _)); - - var editCodeDocumentChange = documentChanges[1]; - Assert.True(editCodeDocumentChange.TryGetFirst(out var textDocumentEdit1)); - var editCodeDocumentEdit = textDocumentEdit1!.Edits.First(); - Assert.True(editCodeDocumentEdit.Range.Start.TryGetAbsoluteIndex(codeDocument.GetSourceText(), Logger, out var removeStart)); - Assert.Equal(actionParams.RemoveStart, removeStart); - Assert.True(editCodeDocumentEdit.Range.End.TryGetAbsoluteIndex(codeDocument.GetSourceText(), Logger, out var removeEnd)); - Assert.Equal(actionParams.RemoveEnd, removeEnd); - - var editCodeBehindChange = documentChanges[2]; - Assert.True(editCodeBehindChange.TryGetFirst(out var textDocumentEdit2)); - var editCodeBehindEdit = textDocumentEdit2!.Edits.First(); - - AssertEx.EqualOrDiff(""" - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Components; - - namespace test.Pages - { - public partial class Test - { - private int x = 1; - } - } - """, - editCodeBehindEdit.NewText); - } - [Fact] public async Task Handle_ExtractCodeBlock() { @@ -175,7 +103,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, _projectSnapshotManagerAccessor); + var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance); var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); var data = JObject.FromObject(actionParams); @@ -237,7 +165,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, _projectSnapshotManagerAccessor); + var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance); var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); var data = JObject.FromObject(actionParams); @@ -307,7 +235,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, _projectSnapshotManagerAccessor); + var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance); var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); var data = JObject.FromObject(actionParams); @@ -387,7 +315,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, _projectSnapshotManagerAccessor); + var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance); var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); var data = JObject.FromObject(actionParams); @@ -469,7 +397,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, _projectSnapshotManagerAccessor); + var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance); var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); var data = JObject.FromObject(actionParams); @@ -535,10 +463,11 @@ public async Task Handle_ExtractFunctionsBlock() @functions { private int x = 1; } - """; var codeDocument = CreateCodeDocument(contents); + """; + var codeDocument = CreateCodeDocument(contents); Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace)); - var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance, _projectSnapshotManagerAccessor); + var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance); var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@functions", @namespace); var data = JObject.FromObject(actionParams); @@ -600,7 +529,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, _projectSnapshotManagerAccessor); + var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance); var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); var data = JObject.FromObject(actionParams); @@ -664,7 +593,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, _projectSnapshotManagerAccessor); + var resolver = new ExtractToCodeBehindCodeActionResolver(CreateDocumentContextFactory(documentPath, codeDocument), TestLanguageServerFeatureOptions.Instance); var actionParams = CreateExtractToCodeBehindCodeActionParams(documentPath, contents, "@code", @namespace); var data = JObject.FromObject(actionParams); From 5a72d9493a0b12557fb03f75cf7e15af25ca0f28 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Mon, 11 Sep 2023 14:56:38 +1000 Subject: [PATCH 09/10] Fix code generation --- .../CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs index 36ae3f5f0a2..3af83c9b2db 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs @@ -205,9 +205,8 @@ private string GenerateCodeBehindClass(string className, string namespaceName, s builder.AppendLine(namespaceName); builder.Append('{'); builder.AppendLine(); - builder.Append(" public partial class "); + builder.Append("public partial class "); builder.AppendLine(className); - builder.Append(" "); builder.AppendLine(contents); builder.Append('}'); From cbfbb831e454ced93a14c58b0e12254302c77d9c Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 13 Sep 2023 07:08:38 +1000 Subject: [PATCH 10/10] Fix whitespace in my PR that fixes whitespace --- .../Razor/ExtractToCodeBehindCodeActionResolver.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs index 3af83c9b2db..f5271385a47 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/CodeActions/Razor/ExtractToCodeBehindCodeActionResolver.cs @@ -184,8 +184,8 @@ private string GenerateCodeBehindClass(string className, string namespaceName, s using var _ = StringBuilderPool.GetPooledObject(out var builder); var usingDirectives = razorCodeDocument - .GetDocumentIntermediateNode() - .FindDescendantNodes(); + .GetDocumentIntermediateNode() + .FindDescendantNodes(); foreach (var usingDirective in usingDirectives) { builder.Append("using ");