();
+ 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.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);
+
+ 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 7ae774a5d0f..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
@@ -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;
@@ -57,7 +55,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();
@@ -76,7 +74,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);
@@ -95,7 +93,225 @@ 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 int 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 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_ExtractCodeBlock2()
+ {
+ // Arrange
+ var documentPath = new Uri("c:/Test.razor");
+ var contents = """
+ @page "/test"
+
+ @code
+ {
+ private int 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 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_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 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_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));
@@ -126,9 +342,114 @@ 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 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_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);
+ 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]
@@ -136,7 +457,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 contents = """
+ @page "/test"
+
+ @functions {
+ private int x = 1;
+ }
+ """;
var codeDocument = CreateCodeDocument(contents);
Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace));
@@ -167,9 +494,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 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]
@@ -177,7 +518,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 int x = 1;
+ }
+ """;
var codeDocument = CreateCodeDocument(contents);
Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace));
@@ -208,10 +556,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 System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Threading.Tasks;
+ using Microsoft.AspNetCore.Components;
+ using System.Diagnostics;
+
+ namespace test.Pages
+ {
+ public partial class Test
+ {
+ private int x = 1;
+ }
+ }
+ """,
+ editCodeBehindEdit.NewText);
}
[Fact]
@@ -219,7 +581,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 int x = 1;
+ #endregion
+ }
+ """;
var codeDocument = CreateCodeDocument(contents);
Assert.True(codeDocument.TryComputeNamespace(fallbackToRootNamespace: true, out var @namespace));
@@ -250,11 +620,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 System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Threading.Tasks;
+ using Microsoft.AspNetCore.Components;
+
+ namespace test.Pages
+ {
+ public partial class Test
+ {
+ #region TestRegion
+ private int x = 1;
+ #endregion
+ }
+ }
+ """,
+ editCodeBehindEdit.NewText);
}
private static RazorCodeDocument CreateCodeDocument(string text)
@@ -271,7 +655,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,