Skip to content

Commit f827eda

Browse files
committed
Move component accessibility code action provider to IFileSystem, and add tests
1 parent f195130 commit f827eda

File tree

7 files changed

+238
-31
lines changed

7 files changed

+238
-31
lines changed

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ namespace Microsoft.CodeAnalysis.Razor.CodeActions;
2525

2626
using SyntaxNode = Microsoft.AspNetCore.Razor.Language.Syntax.SyntaxNode;
2727

28-
internal class ComponentAccessibilityCodeActionProvider : IRazorCodeActionProvider
28+
internal class ComponentAccessibilityCodeActionProvider(IFileSystem fileSystem) : IRazorCodeActionProvider
2929
{
30+
private readonly IFileSystem _fileSystem = fileSystem;
31+
3032
public async Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken)
3133
{
3234
// Locate cursor
@@ -89,7 +91,7 @@ private static bool IsApplicableTag(IStartTagSyntaxNode startTag)
8991
return true;
9092
}
9193

92-
private static void AddCreateComponentFromTag(RazorCodeActionContext context, IStartTagSyntaxNode startTag, List<RazorVSInternalCodeAction> container)
94+
private void AddCreateComponentFromTag(RazorCodeActionContext context, IStartTagSyntaxNode startTag, List<RazorVSInternalCodeAction> container)
9395
{
9496
if (!context.SupportsFileCreation)
9597
{
@@ -103,7 +105,7 @@ private static void AddCreateComponentFromTag(RazorCodeActionContext context, IS
103105
Assumes.NotNull(directoryName);
104106

105107
var newComponentPath = Path.Combine(directoryName, $"{startTag.Name.Content}.razor");
106-
if (File.Exists(newComponentPath))
108+
if (_fileSystem.FileExists(newComponentPath))
107109
{
108110
return;
109111
}

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,14 @@
66

77
namespace Microsoft.CodeAnalysis.Razor.Workspaces;
88

9-
internal class FileSystem : IFileSystem
9+
internal sealed class FileSystem : IFileSystem
1010
{
1111
public IEnumerable<string> GetFiles(string workspaceDirectory, string searchPattern, SearchOption searchOption)
12-
{
13-
return Directory.GetFiles(workspaceDirectory, searchPattern, searchOption);
14-
}
12+
=> Directory.GetFiles(workspaceDirectory, searchPattern, searchOption);
1513

1614
public IEnumerable<string> GetDirectories(string workspaceDirectory)
17-
{
18-
return Directory.GetDirectories(workspaceDirectory);
19-
}
15+
=> Directory.GetDirectories(workspaceDirectory);
16+
17+
public bool FileExists(string filePath)
18+
=> File.Exists(filePath);
2019
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ internal interface IFileSystem
1111
public IEnumerable<string> GetFiles(string workspaceDirectory, string searchPattern, SearchOption searchOption);
1212

1313
public IEnumerable<string> GetDirectories(string workspaceDirectory);
14+
15+
bool FileExists(string filePath);
1416
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ internal sealed class OOPExtractToCodeBehindCodeActionProvider(ILoggerFactory lo
4343
internal sealed class OOPExtractToComponentCodeActionProvider : ExtractToComponentCodeActionProvider;
4444

4545
[Export(typeof(IRazorCodeActionProvider)), Shared]
46-
internal sealed class OOPComponentAccessibilityCodeActionProvider : ComponentAccessibilityCodeActionProvider;
46+
[method: ImportingConstructor]
47+
internal sealed class OOPComponentAccessibilityCodeActionProvider(IFileSystem fileSystem) : ComponentAccessibilityCodeActionProvider(fileSystem);
4748

4849
[Export(typeof(IRazorCodeActionProvider)), Shared]
4950
internal sealed class OOPGenerateMethodCodeActionProvider : GenerateMethodCodeActionProvider;
Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,34 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT license. See License.txt in the project root for license information.
33

4+
using System.Collections.Generic;
45
using System.Composition;
6+
using System.IO;
57
using Microsoft.CodeAnalysis.Razor.Workspaces;
68

79
namespace Microsoft.CodeAnalysis.Remote.Razor;
810

911
[Export(typeof(IFileSystem)), Shared]
10-
internal class RemoteFileSystem : FileSystem
12+
internal class RemoteFileSystem : IFileSystem
1113
{
14+
private IFileSystem _fileSystem = new FileSystem();
15+
16+
public bool FileExists(string filePath) =>
17+
_fileSystem.FileExists(filePath);
18+
19+
public IEnumerable<string> GetDirectories(string workspaceDirectory)
20+
=> _fileSystem.GetDirectories(workspaceDirectory);
21+
22+
public IEnumerable<string> GetFiles(string workspaceDirectory, string searchPattern, SearchOption searchOption)
23+
=> _fileSystem.GetFiles(workspaceDirectory, searchPattern, searchOption);
24+
25+
internal TestAccessor GetTestAccessor() => new(this);
26+
27+
internal readonly struct TestAccessor(RemoteFileSystem instance)
28+
{
29+
public void SetFileSystem(IFileSystem fileSystem)
30+
{
31+
instance._fileSystem = fileSystem;
32+
}
33+
}
1234
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT license. See License.txt in the project root for license information.
33

4+
using System;
45
using System.Collections.Generic;
56
using System.IO;
67
using System.Linq;
@@ -96,5 +97,8 @@ public IEnumerable<string> GetFiles(string workspaceDirectory, string searchPatt
9697
throw new DirectoryNotFoundException();
9798
}
9899
}
100+
101+
public bool FileExists(string filePath)
102+
=> throw new NotImplementedException();
99103
}
100104
}

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

Lines changed: 196 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.IO;
67
using System.Linq;
78
using System.Threading.Tasks;
89
using Microsoft.AspNetCore.Razor;
@@ -12,6 +13,8 @@
1213
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
1314
using Microsoft.CodeAnalysis.Razor.CodeActions.Models;
1415
using Microsoft.CodeAnalysis.Razor.Protocol.CodeActions;
16+
using Microsoft.CodeAnalysis.Razor.Workspaces;
17+
using Microsoft.CodeAnalysis.Remote.Razor;
1518
using Microsoft.CodeAnalysis.Text;
1619
using Microsoft.VisualStudio.LanguageServer.Protocol;
1720
using Microsoft.VisualStudio.Razor.Settings;
@@ -408,6 +411,27 @@ @using System.Text
408411
await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeFixProviderNames.AddImport);
409412
}
410413

414+
[Fact]
415+
public async Task AddUsing_Typo()
416+
{
417+
var input = """
418+
@code
419+
{
420+
private [||]Stringbuilder _x = new Stringbuilder();
421+
}
422+
""";
423+
424+
var expected = """
425+
@using System.Text
426+
@code
427+
{
428+
private StringBuilder _x = new Stringbuilder();
429+
}
430+
""";
431+
432+
await VerifyCodeActionAsync(input, expected, RazorPredefinedCodeFixProviderNames.AddImport);
433+
}
434+
411435
[Fact]
412436
public async Task AddUsing_WithExisting()
413437
{
@@ -527,6 +551,114 @@ private Task DoesNotExist(MouseEventArgs e)
527551
await VerifyCodeActionAsync(input, expected, WorkspacesSR.FormatGenerate_Async_Event_Handler_Title("DoesNotExist"));
528552
}
529553

554+
[Fact]
555+
public async Task CreateComponentFromTag()
556+
{
557+
await VerifyCodeActionAsync(
558+
input: """
559+
<div></div>
560+
561+
<He[||]llo></Hello>
562+
""",
563+
expected: """
564+
<div></div>
565+
566+
<Hello><Hello>
567+
""",
568+
codeActionName: WorkspacesSR.Create_Component_FromTag_Title,
569+
additionalExpectedFiles: [
570+
(FileUri("Hello.razor"), "")]);
571+
}
572+
573+
[Fact]
574+
public async Task CreateComponentFromTag_Attribute()
575+
{
576+
await VerifyCodeActionAsync(
577+
input: """
578+
<div></div>
579+
580+
<Hello wor[||]ld="true"></Hello>
581+
""",
582+
expected: """
583+
<div></div>
584+
585+
<Hello><Hello>
586+
""",
587+
codeActionName: WorkspacesSR.Create_Component_FromTag_Title,
588+
additionalExpectedFiles: [
589+
(FileUri("Hello.razor"), "")]);
590+
}
591+
592+
[Fact]
593+
public async Task ComponentAccessibility_FixCasing()
594+
{
595+
await VerifyCodeActionAsync(
596+
input: """
597+
<div></div>
598+
599+
<Edit[||]form></Editform>
600+
""",
601+
expected: """
602+
<div></div>
603+
604+
<EditForm></EditForm>
605+
""",
606+
codeActionName: "EditForm");
607+
}
608+
609+
[Fact]
610+
public async Task ComponentAccessibility_FullyQualify()
611+
{
612+
await VerifyCodeActionAsync(
613+
input: """
614+
<div></div>
615+
616+
<Section[||]Outlet></SectionOutlet>
617+
""",
618+
expected: """
619+
<div></div>
620+
621+
<Microsoft.AspNetCore.Components.Sections.SectionOutlet></Microsoft.AspNetCore.Components.Sections.SectionOutlet>
622+
""",
623+
codeActionName: "Microsoft.AspNetCore.Components.Sections.SectionOutlet");
624+
}
625+
626+
[Fact]
627+
public async Task ComponentAccessibility_AddUsing()
628+
{
629+
await VerifyCodeActionAsync(
630+
input: """
631+
<div></div>
632+
633+
<Section[||]Outlet></SectionOutlet>
634+
""",
635+
expected: """
636+
@using Microsoft.AspNetCore.Components.Sections
637+
<div></div>
638+
639+
<SectionOutlet></SectionOutlet>
640+
""",
641+
codeActionName: "@using Microsoft.AspNetCore.Components.Sections");
642+
}
643+
644+
[Fact]
645+
public async Task ComponentAccessibility_AddUsing_FixTypo()
646+
{
647+
await VerifyCodeActionAsync(
648+
input: """
649+
<div></div>
650+
651+
<Section[||]outlet></Sectionoutlet>
652+
""",
653+
expected: """
654+
@using Microsoft.AspNetCore.Components.Sections
655+
<div></div>
656+
657+
<SectionOutlet></SectionOutlet>
658+
""",
659+
codeActionName: "SectionOutlet - @using Microsoft.AspNetCore.Components.Sections");
660+
}
661+
530662
[Fact]
531663
public async Task ExtractToCodeBehind()
532664
{
@@ -588,6 +720,9 @@ Hello World
588720

589721
private async Task VerifyCodeActionAsync(TestCode input, string? expected, string codeActionName, int childActionIndex = 0, string? fileKind = null, (Uri fileUri, string contents)[]? additionalExpectedFiles = null)
590722
{
723+
var fileSystem = (RemoteFileSystem)OOPExportProvider.GetExportedValue<IFileSystem>();
724+
fileSystem.GetTestAccessor().SetFileSystem(new TestFileSystem(additionalExpectedFiles));
725+
591726
UpdateClientLSPInitializationOptions(options =>
592727
{
593728
options.ClientCapabilities.TextDocument = new()
@@ -611,7 +746,11 @@ private async Task VerifyCodeActionAsync(TestCode input, string? expected, strin
611746
return;
612747
}
613748

614-
await VerifyCodeActionResolveAsync(document, codeAction, expected, additionalExpectedFiles);
749+
var workspaceEdit = codeAction.Data is null
750+
? codeAction.Edit.AssumeNotNull()
751+
: await ResolveCodeActionAsync(document, codeAction);
752+
753+
await VerifyCodeActionResultAsync(document, workspaceEdit, expected, additionalExpectedFiles);
615754
}
616755

617756
private async Task<CodeAction?> VerifyCodeActionRequestAsync(CodeAnalysis.TextDocument document, TestCode input, string codeActionName, int childActionIndex)
@@ -673,36 +812,74 @@ private async Task VerifyCodeActionAsync(TestCode input, string? expected, strin
673812
return codeActionToRun;
674813
}
675814

676-
private async Task VerifyCodeActionResolveAsync(CodeAnalysis.TextDocument document, CodeAction codeAction, string? expected, (Uri fileUri, string contents)[]? additionalExpectedFiles = null)
815+
private async Task VerifyCodeActionResultAsync(CodeAnalysis.TextDocument document, WorkspaceEdit workspaceEdit, string? expected, (Uri fileUri, string contents)[]? additionalExpectedFiles = null)
816+
{
817+
var validated = false;
818+
if (workspaceEdit.TryGetTextDocumentEdits(out var documentEdits))
819+
{
820+
var documentUri = document.CreateUri();
821+
var sourceText = await document.GetTextAsync(DisposalToken).ConfigureAwait(false);
822+
823+
foreach (var edit in documentEdits)
824+
{
825+
if (edit.TextDocument.Uri == documentUri)
826+
{
827+
sourceText = sourceText.WithChanges(edit.Edits.Select(sourceText.GetTextChange));
828+
}
829+
else
830+
{
831+
var contents = Assert.Single(additionalExpectedFiles.AssumeNotNull(), f => f.fileUri == edit.TextDocument.Uri).contents;
832+
AssertEx.EqualOrDiff(contents, Assert.Single(edit.Edits).NewText);
833+
}
834+
}
835+
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)
844+
{
845+
if (sumType.Value is CreateFile createFile)
846+
{
847+
validated = true;
848+
Assert.Single(additionalExpectedFiles.AssumeNotNull(), f => f.fileUri == createFile.Uri);
849+
}
850+
}
851+
}
852+
853+
Assert.True(validated, "Test did not validate anything. Code action response type is presumably not supported.");
854+
}
855+
856+
private async Task<WorkspaceEdit> ResolveCodeActionAsync(CodeAnalysis.TextDocument document, CodeAction codeAction)
677857
{
678858
var requestInvoker = new TestLSPRequestInvoker();
679859
var clientSettingsManager = new ClientSettingsManager(changeTriggers: []);
680-
681860
var endpoint = new CohostCodeActionsResolveEndpoint(RemoteServiceInvoker, ClientCapabilitiesService, clientSettingsManager, TestHtmlDocumentSynchronizer.Instance, requestInvoker);
682861

683862
var result = await endpoint.GetTestAccessor().HandleRequestAsync(document, codeAction, DisposalToken);
684863

685864
Assert.NotNull(result?.Edit);
865+
return result.Edit;
866+
}
686867

687-
var workspaceEdit = result.Edit;
688-
Assert.True(workspaceEdit.TryGetTextDocumentEdits(out var documentEdits));
689-
690-
var documentUri = document.CreateUri();
691-
var sourceText = await document.GetTextAsync(DisposalToken).ConfigureAwait(false);
868+
private class TestFileSystem((Uri fileUri, string contents)[]? files) : IFileSystem
869+
{
870+
public bool FileExists(string filePath)
871+
{
872+
return false;
873+
}
692874

693-
foreach (var edit in documentEdits)
875+
public IEnumerable<string> GetDirectories(string workspaceDirectory)
694876
{
695-
if (edit.TextDocument.Uri == documentUri)
696-
{
697-
sourceText = sourceText.WithChanges(edit.Edits.Select(sourceText.GetTextChange));
698-
}
699-
else
700-
{
701-
var contents = Assert.Single(additionalExpectedFiles.AssumeNotNull(), f => f.fileUri == edit.TextDocument.Uri).contents;
702-
AssertEx.EqualOrDiff(contents, Assert.Single(edit.Edits).NewText);
703-
}
877+
throw new NotImplementedException();
704878
}
705879

706-
AssertEx.EqualOrDiff(expected, sourceText.ToString());
880+
public IEnumerable<string> GetFiles(string workspaceDirectory, string searchPattern, SearchOption searchOption)
881+
{
882+
throw new NotImplementedException();
883+
}
707884
}
708885
}

0 commit comments

Comments
 (0)