1010using Microsoft . AspNetCore . Razor . PooledObjects ;
1111using Microsoft . AspNetCore . Razor . Telemetry ;
1212using Microsoft . AspNetCore . Razor . Test . Common ;
13+ using Microsoft . AspNetCore . Razor . Utilities ;
14+ using Microsoft . CodeAnalysis ;
1315using Microsoft . CodeAnalysis . ExternalAccess . Razor ;
16+ using Microsoft . CodeAnalysis . Razor ;
1417using Microsoft . CodeAnalysis . Razor . CodeActions . Models ;
1518using Microsoft . CodeAnalysis . Razor . Protocol . CodeActions ;
1619using Microsoft . CodeAnalysis . Razor . Workspaces ;
2225using Xunit ;
2326using Xunit . Abstractions ;
2427using WorkspacesSR = Microsoft . CodeAnalysis . Razor . Workspaces . Resources . SR ;
28+ using LspDiagnostic = Microsoft . VisualStudio . LanguageServer . Protocol . Diagnostic ;
2529
2630namespace 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