Skip to content

Commit 76f78df

Browse files
committed
Move generate method code action to use IFileSystem and add tests
1 parent f827eda commit 76f78df

File tree

7 files changed

+172
-41
lines changed

7 files changed

+172
-41
lines changed

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/GenerateMethodCodeActionResolver.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Microsoft.CodeAnalysis.Razor.Formatting;
2020
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
2121
using Microsoft.CodeAnalysis.Razor.Protocol;
22+
using Microsoft.CodeAnalysis.Razor.Workspaces;
2223
using Microsoft.VisualStudio.LanguageServer.Protocol;
2324
using CSharpSyntaxFactory = Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
2425

@@ -27,11 +28,13 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions.Razor;
2728
internal class GenerateMethodCodeActionResolver(
2829
IRoslynCodeActionHelpers roslynCodeActionHelpers,
2930
IDocumentMappingService documentMappingService,
30-
IRazorFormattingService razorFormattingService) : IRazorCodeActionResolver
31+
IRazorFormattingService razorFormattingService,
32+
IFileSystem fileSystem) : IRazorCodeActionResolver
3133
{
3234
private readonly IRoslynCodeActionHelpers _roslynCodeActionHelpers = roslynCodeActionHelpers;
3335
private readonly IDocumentMappingService _documentMappingService = documentMappingService;
3436
private readonly IRazorFormattingService _razorFormattingService = razorFormattingService;
37+
private readonly IFileSystem _fileSystem = fileSystem;
3538

3639
private const string ReturnType = "$$ReturnType$$";
3740
private const string MethodName = "$$MethodName$$";
@@ -58,7 +61,7 @@ internal class GenerateMethodCodeActionResolver(
5861
var razorClassName = Path.GetFileNameWithoutExtension(uriPath);
5962
var codeBehindPath = $"{uriPath}.cs";
6063

61-
if (!File.Exists(codeBehindPath) ||
64+
if (!_fileSystem.FileExists(codeBehindPath) ||
6265
razorClassName is null ||
6366
!code.TryComputeNamespace(fallbackToRootNamespace: true, out var razorNamespace))
6467
{
@@ -72,7 +75,7 @@ razorClassName is null ||
7275
cancellationToken).ConfigureAwait(false);
7376
}
7477

75-
var content = File.ReadAllText(codeBehindPath);
78+
var content = _fileSystem.ReadFile(codeBehindPath);
7679
if (GetCSharpClassDeclarationSyntax(content, razorNamespace, razorClassName) is not { } @class)
7780
{
7881
// The code behind file is malformed, generate the code in the razor file instead.

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/FileSystem.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,7 @@ public IEnumerable<string> GetDirectories(string workspaceDirectory)
1616

1717
public bool FileExists(string filePath)
1818
=> File.Exists(filePath);
19+
20+
public string ReadFile(string filePath)
21+
=> File.ReadAllText(filePath);
1922
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/IFileSystem.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,6 @@ internal interface IFileSystem
1313
public IEnumerable<string> GetDirectories(string workspaceDirectory);
1414

1515
bool FileExists(string filePath);
16+
17+
string ReadFile(string filePath);
1618
}

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteServices.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,9 @@ internal sealed class OOPAddUsingsCodeActionResolver : AddUsingsCodeActionResolv
8585
internal sealed class OOPGenerateMethodCodeActionResolver(
8686
IRoslynCodeActionHelpers roslynCodeActionHelpers,
8787
IDocumentMappingService documentMappingService,
88-
IRazorFormattingService razorFormattingService)
89-
: GenerateMethodCodeActionResolver(roslynCodeActionHelpers, documentMappingService, razorFormattingService);
88+
IRazorFormattingService razorFormattingService,
89+
IFileSystem fileSystem)
90+
: GenerateMethodCodeActionResolver(roslynCodeActionHelpers, documentMappingService, razorFormattingService, fileSystem);
9091

9192
[Export(typeof(ICSharpCodeActionResolver)), Shared]
9293
[method: ImportingConstructor]

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/RemoteFileSystem.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ internal class RemoteFileSystem : IFileSystem
1313
{
1414
private IFileSystem _fileSystem = new FileSystem();
1515

16-
public bool FileExists(string filePath) =>
17-
_fileSystem.FileExists(filePath);
16+
public bool FileExists(string filePath)
17+
=> _fileSystem.FileExists(filePath);
18+
19+
public string ReadFile(string filePath)
20+
=> _fileSystem.ReadFile(filePath);
1821

1922
public IEnumerable<string> GetDirectories(string workspaceDirectory)
2023
=> _fileSystem.GetDirectories(workspaceDirectory);

src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/IFileSystemExtensionsTest.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,8 @@ public IEnumerable<string> GetFiles(string workspaceDirectory, string searchPatt
100100

101101
public bool FileExists(string filePath)
102102
=> throw new NotImplementedException();
103+
104+
public string ReadFile(string filePath)
105+
=> throw new NotImplementedException();
103106
}
104107
}

src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostCodeActionsEndpointTest.cs

Lines changed: 150 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
using Microsoft.AspNetCore.Razor.PooledObjects;
1111
using Microsoft.AspNetCore.Razor.Telemetry;
1212
using Microsoft.AspNetCore.Razor.Test.Common;
13+
using Microsoft.AspNetCore.Razor.Utilities;
14+
using Microsoft.CodeAnalysis;
1315
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
16+
using Microsoft.CodeAnalysis.Razor;
1417
using Microsoft.CodeAnalysis.Razor.CodeActions.Models;
1518
using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions;
1619
using Microsoft.CodeAnalysis.Razor.Workspaces;
@@ -22,6 +25,7 @@
2225
using Xunit;
2326
using Xunit.Abstractions;
2427
using WorkspacesSR = Microsoft.CodeAnalysis.Razor.Workspaces.Resources.SR;
28+
using LspDiagnostic = Microsoft.VisualStudio.LanguageServer.Protocol.Diagnostic;
2529

2630
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
2731

@@ -505,6 +509,106 @@ private void DoesNotExist(MouseEventArgs e)
505509
await VerifyCodeActionAsync(input, expected, WorkspacesSR.FormatGenerate_Event_Handler_Title("DoesNotExist"));
506510
}
507511

512+
[Fact]
513+
public async Task GenerateEventHandler_BadCodeBehind()
514+
{
515+
await VerifyCodeActionAsync(
516+
input: """
517+
<button @onclick="{|CS0103:Does[||]NotExist|}"></button>
518+
""",
519+
expected: """
520+
<button @onclick="DoesNotExist"></button>
521+
@code {
522+
private void DoesNotExist(MouseEventArgs e)
523+
{
524+
throw new NotImplementedException();
525+
}
526+
}
527+
""",
528+
additionalFiles: [
529+
(FilePath("File1.razor.cs"), """
530+
namespace Goo
531+
{
532+
public partial class NotAComponent
533+
{
534+
}
535+
}
536+
""")],
537+
codeActionName: WorkspacesSR.FormatGenerate_Event_Handler_Title("DoesNotExist"));
538+
}
539+
540+
[Fact]
541+
public async Task GenerateEventHandler_CodeBehind()
542+
{
543+
await VerifyCodeActionAsync(
544+
input: """
545+
<button @onclick="{|CS0103:Does[||]NotExist|}"></button>
546+
""",
547+
expected: """
548+
<button @onclick="DoesNotExist"></button>
549+
""",
550+
additionalFiles: [
551+
(FilePath("File1.razor.cs"), """
552+
namespace SomeProject
553+
554+
public partial class File1
555+
{
556+
public void M()
557+
{
558+
}
559+
}
560+
""")],
561+
additionalExpectedFiles: [
562+
(FileUri("File1.razor.cs"), """
563+
namespace SomeProject
564+
565+
public partial class File1
566+
{
567+
public void M()
568+
{
569+
}
570+
private void DoesNotExist(Microsoft.AspNetCore.Components.Web.MouseEventArgs e)
571+
{
572+
throw new System.NotImplementedException();
573+
}
574+
}
575+
""")],
576+
codeActionName: WorkspacesSR.FormatGenerate_Event_Handler_Title("DoesNotExist"));
577+
}
578+
579+
[Fact]
580+
public async Task GenerateEventHandler_EmptyCodeBehind()
581+
{
582+
await VerifyCodeActionAsync(
583+
input: """
584+
<button @onclick="{|CS0103:Does[||]NotExist|}"></button>
585+
""",
586+
expected: """
587+
<button @onclick="DoesNotExist"></button>
588+
""",
589+
additionalFiles: [
590+
(FilePath("File1.razor.cs"), """
591+
namespace SomeProject
592+
593+
public partial class File1
594+
{
595+
}
596+
""")],
597+
additionalExpectedFiles: [
598+
(FileUri("File1.razor.cs"), """
599+
namespace SomeProject
600+
601+
public partial class File1
602+
{
603+
private void DoesNotExist(Microsoft.AspNetCore.Components.Web.MouseEventArgs e)
604+
{
605+
throw new System.NotImplementedException();
606+
}
607+
}
608+
""")],
609+
codeActionName: WorkspacesSR.FormatGenerate_Event_Handler_Title("DoesNotExist"));
610+
}
611+
508612
[Fact]
509613
public async Task GenerateAsyncEventHandler_NoCodeBlock()
510614
{
@@ -718,10 +822,10 @@ Hello World
718822
""")]);
719823
}
720824

721-
private async Task VerifyCodeActionAsync(TestCode input, string? expected, string codeActionName, int childActionIndex = 0, string? fileKind = null, (Uri fileUri, string contents)[]? additionalExpectedFiles = null)
825+
private async Task VerifyCodeActionAsync(TestCode input, string? expected, string codeActionName, int childActionIndex = 0, string? fileKind = null, (string filePath, string contents)[]? additionalFiles = null, (Uri fileUri, string contents)[]? additionalExpectedFiles = null)
722826
{
723827
var fileSystem = (RemoteFileSystem)OOPExportProvider.GetExportedValue<IFileSystem>();
724-
fileSystem.GetTestAccessor().SetFileSystem(new TestFileSystem(additionalExpectedFiles));
828+
fileSystem.GetTestAccessor().SetFileSystem(new TestFileSystem(additionalFiles));
725829

726830
UpdateClientLSPInitializationOptions(options =>
727831
{
@@ -736,7 +840,7 @@ private async Task VerifyCodeActionAsync(TestCode input, string? expected, strin
736840
return options;
737841
});
738842

739-
var document = await CreateProjectAndRazorDocumentAsync(input.Text, fileKind, createSeparateRemoteAndLocalWorkspaces: true);
843+
var document = await CreateProjectAndRazorDocumentAsync(input.Text, fileKind, createSeparateRemoteAndLocalWorkspaces: true, additionalFiles: additionalFiles);
740844

741845
var codeAction = await VerifyCodeActionRequestAsync(document, input, codeActionName, childActionIndex);
742846

@@ -759,7 +863,7 @@ private async Task VerifyCodeActionAsync(TestCode input, string? expected, strin
759863
var endpoint = new CohostCodeActionsEndpoint(RemoteServiceInvoker, ClientCapabilitiesService, TestHtmlDocumentSynchronizer.Instance, requestInvoker, NoOpTelemetryReporter.Instance);
760864
var inputText = await document.GetTextAsync(DisposalToken);
761865

762-
using var diagnostics = new PooledArrayBuilder<Diagnostic>();
866+
using var diagnostics = new PooledArrayBuilder<LspDiagnostic>();
763867
foreach (var (code, spans) in input.NamedSpans)
764868
{
765869
if (code.Length == 0)
@@ -769,7 +873,7 @@ private async Task VerifyCodeActionAsync(TestCode input, string? expected, strin
769873

770874
foreach (var diagnosticSpan in spans)
771875
{
772-
diagnostics.Add(new Diagnostic
876+
diagnostics.Add(new LspDiagnostic
773877
{
774878
Code = code,
775879
Range = inputText.GetRange(diagnosticSpan)
@@ -812,42 +916,57 @@ private async Task VerifyCodeActionAsync(TestCode input, string? expected, strin
812916
return codeActionToRun;
813917
}
814918

815-
private async Task VerifyCodeActionResultAsync(CodeAnalysis.TextDocument document, WorkspaceEdit workspaceEdit, string? expected, (Uri fileUri, string contents)[]? additionalExpectedFiles = null)
919+
private async Task VerifyCodeActionResultAsync(TextDocument document, WorkspaceEdit workspaceEdit, string? expected, (Uri fileUri, string contents)[]? additionalExpectedFiles = null)
816920
{
921+
var solution = document.Project.Solution;
817922
var validated = false;
818-
if (workspaceEdit.TryGetTextDocumentEdits(out var documentEdits))
923+
924+
if (workspaceEdit.DocumentChanges?.Value is SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>[] sumTypeArray)
819925
{
820-
var documentUri = document.CreateUri();
821-
var sourceText = await document.GetTextAsync(DisposalToken).ConfigureAwait(false);
926+
using var builder = new PooledArrayBuilder<TextDocumentEdit>();
927+
foreach (var sumType in sumTypeArray)
928+
{
929+
if (sumType.Value is CreateFile createFile)
930+
{
931+
validated = true;
932+
Assert.Single(additionalExpectedFiles.AssumeNotNull(), f => f.fileUri == createFile.Uri);
933+
var documentId = DocumentId.CreateNewId(document.Project.Id);
934+
var filePath = createFile.Uri.GetDocumentFilePath();
935+
var documentInfo = DocumentInfo.Create(documentId, filePath, filePath: filePath);
936+
solution = solution.AddDocument(documentInfo);
937+
}
938+
}
939+
}
822940

941+
if (workspaceEdit.TryGetTextDocumentEdits(out var documentEdits))
942+
{
823943
foreach (var edit in documentEdits)
824944
{
825-
if (edit.TextDocument.Uri == documentUri)
945+
var textDocument = solution.GetTextDocuments(edit.TextDocument.Uri).First();
946+
var text = await textDocument.GetTextAsync(DisposalToken).ConfigureAwait(false);
947+
if (textDocument is Document)
826948
{
827-
sourceText = sourceText.WithChanges(edit.Edits.Select(sourceText.GetTextChange));
949+
solution = solution.WithDocumentText(textDocument.Id, text.WithChanges(edit.Edits.Select(text.GetTextChange)));
828950
}
829951
else
830952
{
831-
var contents = Assert.Single(additionalExpectedFiles.AssumeNotNull(), f => f.fileUri == edit.TextDocument.Uri).contents;
832-
AssertEx.EqualOrDiff(contents, Assert.Single(edit.Edits).NewText);
953+
solution = solution.WithAdditionalDocumentText(textDocument.Id, text.WithChanges(edit.Edits.Select(text.GetTextChange)));
833954
}
834955
}
835956

836-
validated = true;
837-
AssertEx.EqualOrDiff(expected, sourceText.ToString());
838-
}
839-
840-
if (workspaceEdit.DocumentChanges?.Value is SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>[] sumTypeArray)
841-
{
842-
using var builder = new PooledArrayBuilder<TextDocumentEdit>();
843-
foreach (var sumType in sumTypeArray)
957+
if (additionalExpectedFiles is not null)
844958
{
845-
if (sumType.Value is CreateFile createFile)
959+
foreach (var (uri, contents) in additionalExpectedFiles)
846960
{
847-
validated = true;
848-
Assert.Single(additionalExpectedFiles.AssumeNotNull(), f => f.fileUri == createFile.Uri);
961+
var additionalDocument = solution.GetTextDocuments(uri).First();
962+
var text = await additionalDocument.GetTextAsync(DisposalToken).ConfigureAwait(false);
963+
AssertEx.EqualOrDiff(contents, text.ToString());
849964
}
850965
}
966+
967+
validated = true;
968+
var actual = await solution.GetAdditionalDocument(document.Id).AssumeNotNull().GetTextAsync(DisposalToken).ConfigureAwait(false);
969+
AssertEx.EqualOrDiff(expected, actual.ToString());
851970
}
852971

853972
Assert.True(validated, "Test did not validate anything. Code action response type is presumably not supported.");
@@ -865,21 +984,18 @@ private async Task<WorkspaceEdit> ResolveCodeActionAsync(CodeAnalysis.TextDocume
865984
return result.Edit;
866985
}
867986

868-
private class TestFileSystem((Uri fileUri, string contents)[]? files) : IFileSystem
987+
private class TestFileSystem((string filePath, string contents)[]? files) : IFileSystem
869988
{
870989
public bool FileExists(string filePath)
871-
{
872-
return false;
873-
}
990+
=> files?.Any(f => FilePathNormalizingComparer.Instance.Equals(f.filePath, filePath)) ?? false;
991+
992+
public string ReadFile(string filePath)
993+
=> files.AssumeNotNull().Single(f => FilePathNormalizingComparer.Instance.Equals(f.filePath, filePath)).contents;
874994

875995
public IEnumerable<string> GetDirectories(string workspaceDirectory)
876-
{
877-
throw new NotImplementedException();
878-
}
996+
=> throw new NotImplementedException();
879997

880998
public IEnumerable<string> GetFiles(string workspaceDirectory, string searchPattern, SearchOption searchOption)
881-
{
882-
throw new NotImplementedException();
883-
}
999+
=> throw new NotImplementedException();
8841000
}
8851001
}

0 commit comments

Comments
 (0)