diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index f3ee8b157e4..cbafe0b0244 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -153,6 +153,7 @@ public static void AddCodeActionsServices(this IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/AddUsingsCodeActionResolver.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/AddUsingsCodeActionResolver.cs index da852edaa91..4accc59cbed 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/AddUsingsCodeActionResolver.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/AddUsingsCodeActionResolver.cs @@ -62,13 +62,19 @@ internal static bool TryCreateAddUsingResolutionParams(string fullyQualifiedName return false; } + resolutionParams = CreateAddUsingResolutionParams(@namespace, textDocument, additionalEdit, delegatedDocumentUri); + return true; + } + + internal static RazorCodeActionResolutionParams CreateAddUsingResolutionParams(string @namespace, VSTextDocumentIdentifier textDocument, TextDocumentEdit? additionalEdit, Uri? delegatedDocumentUri) + { var actionParams = new AddUsingsCodeActionParams { Namespace = @namespace, AdditionalEdit = additionalEdit }; - resolutionParams = new RazorCodeActionResolutionParams + return new RazorCodeActionResolutionParams { TextDocument = textDocument, Action = LanguageServerConstants.CodeActions.AddUsing, @@ -76,8 +82,6 @@ internal static bool TryCreateAddUsingResolutionParams(string fullyQualifiedName DelegatedDocumentUri = delegatedDocumentUri, Data = actionParams, }; - - return true; } // Internal for testing diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/UnboundDirectiveAttributeAddUsingCodeActionProvider.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/UnboundDirectiveAttributeAddUsingCodeActionProvider.cs new file mode 100644 index 00000000000..2712c9d74a6 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/CodeActions/Razor/UnboundDirectiveAttributeAddUsingCodeActionProvider.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.AspNetCore.Razor.Threading; +using Microsoft.CodeAnalysis.Razor.CodeActions.Models; +using Microsoft.CodeAnalysis.Razor.CodeActions.Razor; +using Microsoft.CodeAnalysis.Razor.Workspaces; + +namespace Microsoft.CodeAnalysis.Razor.CodeActions; + +internal class UnboundDirectiveAttributeAddUsingCodeActionProvider : IRazorCodeActionProvider +{ + public Task> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken) + { + if (context.HasSelection) + { + return SpecializedTasks.EmptyImmutableArray(); + } + + // Only work in component files + if (!FileKinds.IsComponent(context.CodeDocument.FileKind)) + { + return SpecializedTasks.EmptyImmutableArray(); + } + + if (!context.CodeDocument.TryGetSyntaxRoot(out var syntaxRoot)) + { + return SpecializedTasks.EmptyImmutableArray(); + } + + // Find the node at the cursor position + var owner = syntaxRoot.FindInnermostNode(context.StartAbsoluteIndex, includeWhitespace: false); + if (owner is null) + { + return SpecializedTasks.EmptyImmutableArray(); + } + + // Find a regular markup attribute (not a tag helper attribute) that starts with '@' + // Unbound directive attributes are just regular attributes that happen to start with '@' + var attributeBlock = owner.FirstAncestorOrSelf(); + if (attributeBlock is null) + { + return SpecializedTasks.EmptyImmutableArray(); + } + + // Make sure the cursor is actually on the name part, since the attribute block is the whole attribute, including + // value and even some whitespace + var nameSpan = attributeBlock.Name.Span; + if (context.StartAbsoluteIndex < nameSpan.Start || context.StartAbsoluteIndex > nameSpan.End) + { + return SpecializedTasks.EmptyImmutableArray(); + } + + // Try to find the missing namespace for this directive attribute + if (!TryGetMissingDirectiveAttributeNamespace(context.CodeDocument, attributeBlock, out var missingNamespace)) + { + return SpecializedTasks.EmptyImmutableArray(); + } + + // Create the code action + var resolutionParams = AddUsingsCodeActionResolver.CreateAddUsingResolutionParams( + missingNamespace, + context.Request.TextDocument, + additionalEdit: null, + context.DelegatedDocumentUri); + + var addUsingCodeAction = RazorCodeActionFactory.CreateAddComponentUsing( + missingNamespace, + newTagName: null, + resolutionParams); + + return Task.FromResult>([addUsingCodeAction]); + } + + private static bool TryGetMissingDirectiveAttributeNamespace( + RazorCodeDocument codeDocument, + MarkupAttributeBlockSyntax attributeBlock, + [NotNullWhen(true)] out string? missingNamespace) + { + missingNamespace = null; + + // Check if this is a directive attribute (starts with '@') + var attributeName = attributeBlock.Name.GetContent(); + if (attributeName is not ['@', ..]) + { + return false; + } + + // Get all tag helpers, not just those in scope, since we want to suggest adding a using + if (!codeDocument.TryGetTagHelpers(out var tagHelpers)) + { + return false; + } + + // For attributes with parameters (e.g., @bind:after), extract just the base attribute name + var baseAttributeName = attributeName.AsSpan(); + var colonIndex = baseAttributeName.IndexOf(':'); + if (colonIndex > 0) + { + baseAttributeName = baseAttributeName[..colonIndex]; + } + + // Search for matching bound attribute descriptors in all available tag helpers + foreach (var tagHelper in tagHelpers) + { + if (!tagHelper.IsAttributeDescriptor()) + { + continue; + } + + foreach (var boundAttribute in tagHelper.BoundAttributes) + { + // No need to worry about multiple matches, because Razor syntax has no way to disambiguate anyway. + // Currently only compiler can create directive attribute tag helpers anyway. + if (boundAttribute.IsDirectiveAttribute && + boundAttribute.Name.AsSpan().SequenceEqual(baseAttributeName)) + { + if (boundAttribute.Parent.TypeNamespace is { } typeNamespace) + { + missingNamespace = typeNamespace; + return true; + } + + // This is unexpected, but if for some reason we can't find a namespace, there is no point looking further + break; + } + } + } + + return false; + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteServices.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteServices.cs index fb05912ea9e..bd1a9b2293a 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteServices.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/CodeActions/RemoteServices.cs @@ -56,6 +56,9 @@ internal sealed class OOPSimplifyFullyQualifiedComponentCodeActionProvider : Sim [method: ImportingConstructor] internal sealed class OOPComponentAccessibilityCodeActionProvider(IFileSystem fileSystem) : ComponentAccessibilityCodeActionProvider(fileSystem); +[Export(typeof(IRazorCodeActionProvider)), Shared] +internal sealed class OOPUnboundDirectiveAttributeAddUsingCodeActionProvider : UnboundDirectiveAttributeAddUsingCodeActionProvider; + [Export(typeof(IRazorCodeActionProvider)), Shared] internal sealed class OOPGenerateMethodCodeActionProvider : GenerateMethodCodeActionProvider; diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Cohosting/CohostTestBase.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Cohosting/CohostTestBase.cs index 6b9c368f408..81eba868559 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Cohosting/CohostTestBase.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Cohosting/CohostTestBase.cs @@ -152,7 +152,8 @@ protected abstract TextDocument CreateProjectAndRazorDocument( string? documentFilePath = null, (string fileName, string contents)[]? additionalFiles = null, bool inGlobalNamespace = false, - bool miscellaneousFile = false); + bool miscellaneousFile = false, + bool addDefaultImports = true); protected TextDocument CreateProjectAndRazorDocument( CodeAnalysis.Workspace remoteWorkspace, @@ -161,7 +162,8 @@ protected TextDocument CreateProjectAndRazorDocument( string? documentFilePath = null, (string fileName, string contents)[]? additionalFiles = null, bool inGlobalNamespace = false, - bool miscellaneousFile = false) + bool miscellaneousFile = false, + bool addDefaultImports = true) { // Using IsLegacy means null == component, so easier for test authors var isComponent = fileKind != RazorFileKind.Legacy; @@ -173,15 +175,15 @@ protected TextDocument CreateProjectAndRazorDocument( var projectId = ProjectId.CreateNewId(debugName: TestProjectData.SomeProject.DisplayName); var documentId = DocumentId.CreateNewId(projectId, debugName: documentFilePath); - return CreateProjectAndRazorDocument(remoteWorkspace, projectId, miscellaneousFile, documentId, documentFilePath, contents, additionalFiles, inGlobalNamespace); + return CreateProjectAndRazorDocument(remoteWorkspace, projectId, miscellaneousFile, documentId, documentFilePath, contents, additionalFiles, inGlobalNamespace, addDefaultImports); } - protected static TextDocument CreateProjectAndRazorDocument(CodeAnalysis.Workspace workspace, ProjectId projectId, bool miscellaneousFile, DocumentId documentId, string documentFilePath, string contents, (string fileName, string contents)[]? additionalFiles, bool inGlobalNamespace) + protected static TextDocument CreateProjectAndRazorDocument(CodeAnalysis.Workspace workspace, ProjectId projectId, bool miscellaneousFile, DocumentId documentId, string documentFilePath, string contents, (string fileName, string contents)[]? additionalFiles, bool inGlobalNamespace, bool addDefaultImports) { - return AddProjectAndRazorDocument(workspace.CurrentSolution, TestProjectData.SomeProject.FilePath, projectId, miscellaneousFile, documentId, documentFilePath, contents, additionalFiles, inGlobalNamespace); + return AddProjectAndRazorDocument(workspace.CurrentSolution, TestProjectData.SomeProject.FilePath, projectId, miscellaneousFile, documentId, documentFilePath, contents, additionalFiles, inGlobalNamespace, addDefaultImports); } - protected static TextDocument AddProjectAndRazorDocument(Solution solution, [DisallowNull] string? projectFilePath, ProjectId projectId, bool miscellaneousFile, DocumentId documentId, string documentFilePath, string contents, (string fileName, string contents)[]? additionalFiles, bool inGlobalNamespace) + protected static TextDocument AddProjectAndRazorDocument(Solution solution, [DisallowNull] string? projectFilePath, ProjectId projectId, bool miscellaneousFile, DocumentId documentId, string documentFilePath, string contents, (string fileName, string contents)[]? additionalFiles, bool inGlobalNamespace, bool addDefaultImports) { var builder = new RazorProjectBuilder(projectId); @@ -202,7 +204,9 @@ protected static TextDocument AddProjectAndRazorDocument(Solution solution, [Dis builder.RootNamespace = TestProjectData.SomeProject.RootNamespace; } - builder.AddAdditionalDocument( + if (addDefaultImports) + { + builder.AddAdditionalDocument( filePath: TestProjectData.SomeProjectComponentImportFile1.FilePath, text: SourceText.From(""" @using Microsoft.AspNetCore.Components @@ -211,11 +215,12 @@ @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web """)); - builder.AddAdditionalDocument( + builder.AddAdditionalDocument( filePath: TestProjectData.SomeProjectImportFile.FilePath, text: SourceText.From(""" @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers """)); + } if (additionalFiles is not null) { diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs index fc2c00f62f5..cba1dc26f9a 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostEndpointTestBase.cs @@ -97,10 +97,11 @@ protected override TextDocument CreateProjectAndRazorDocument( string? documentFilePath = null, (string fileName, string contents)[]? additionalFiles = null, bool inGlobalNamespace = false, - bool miscellaneousFile = false) + bool miscellaneousFile = false, + bool addDefaultImports = true) { var remoteWorkspace = RemoteWorkspaceProvider.Instance.GetWorkspace(); - var remoteDocument = base.CreateProjectAndRazorDocument(remoteWorkspace, contents, fileKind, documentFilePath, additionalFiles, inGlobalNamespace, miscellaneousFile); + var remoteDocument = base.CreateProjectAndRazorDocument(remoteWorkspace, contents, fileKind, documentFilePath, additionalFiles, inGlobalNamespace, miscellaneousFile, addDefaultImports); // In this project we simulate remote services running OOP by creating a different workspace with a different // set of services to represent the devenv Roslyn side of things. We don't have any actual solution syncing set @@ -114,7 +115,8 @@ protected override TextDocument CreateProjectAndRazorDocument( remoteDocument.FilePath.AssumeNotNull(), contents, additionalFiles, - inGlobalNamespace); + inGlobalNamespace, + addDefaultImports); } private TextDocument CreateLocalProjectAndRazorDocument( @@ -125,9 +127,10 @@ private TextDocument CreateLocalProjectAndRazorDocument( string documentFilePath, string contents, (string fileName, string contents)[]? additionalFiles, - bool inGlobalNamespace) + bool inGlobalNamespace, + bool addDefaultImports) { - var razorDocument = CreateProjectAndRazorDocument(LocalWorkspace, projectId, miscellaneousFile, documentId, documentFilePath, contents, additionalFiles, inGlobalNamespace); + var razorDocument = CreateProjectAndRazorDocument(LocalWorkspace, projectId, miscellaneousFile, documentId, documentFilePath, contents, additionalFiles, inGlobalNamespace, addDefaultImports); // If we're creating remote and local workspaces, then we'll return the local document, and have to allow // the remote service invoker to map from the local solution to the remote one. diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/RetryProjectTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/RetryProjectTest.cs index b9885e9d31f..d00b9af95da 100644 --- a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/RetryProjectTest.cs +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/RetryProjectTest.cs @@ -72,7 +72,7 @@ public async Task HoverRequest_MultipleProjects_ReturnsResults() var projectId = ProjectId.CreateNewId(debugName: TestProjectData.SomeProject.DisplayName); var documentFilePath = TestProjectData.AnotherProjectComponentFile1.FilePath; var documentId = DocumentId.CreateNewId(projectId, debugName: documentFilePath); - var otherDocument = AddProjectAndRazorDocument(document.Project.Solution, TestProjectData.AnotherProject.FilePath, projectId, miscellaneousFile: false, documentId, documentFilePath, otherInput.Text, additionalFiles: null, inGlobalNamespace: false); + var otherDocument = AddProjectAndRazorDocument(document.Project.Solution, TestProjectData.AnotherProject.FilePath, projectId, miscellaneousFile: false, documentId, documentFilePath, otherInput.Text, additionalFiles: null, inGlobalNamespace: false, addDefaultImports: true); // Make sure we have the document from our new fork document = otherDocument.Project.Solution.GetAdditionalDocument(document.Id).AssumeNotNull(); diff --git a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/CohostEndpointTestBase.cs b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/CohostEndpointTestBase.cs index 9a33c2a7b2d..a9ffb6472e9 100644 --- a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/CohostEndpointTestBase.cs +++ b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/CohostEndpointTestBase.cs @@ -87,8 +87,9 @@ protected override TextDocument CreateProjectAndRazorDocument( string? documentFilePath = null, (string fileName, string contents)[]? additionalFiles = null, bool inGlobalNamespace = false, - bool miscellaneousFile = false) + bool miscellaneousFile = false, + bool addDefaultImports = true) { - return CreateProjectAndRazorDocument(LocalWorkspace, contents, fileKind, documentFilePath, additionalFiles, inGlobalNamespace, miscellaneousFile); + return CreateProjectAndRazorDocument(LocalWorkspace, contents, fileKind, documentFilePath, additionalFiles, inGlobalNamespace, miscellaneousFile, addDefaultImports); } } diff --git a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CodeActions/CohostCodeActionsEndpointTestBase.cs b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CodeActions/CohostCodeActionsEndpointTestBase.cs index fd1e797a3dc..a853530e68c 100644 --- a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CodeActions/CohostCodeActionsEndpointTestBase.cs +++ b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CodeActions/CohostCodeActionsEndpointTestBase.cs @@ -36,9 +36,10 @@ private protected async Task VerifyCodeActionAsync( RazorFileKind? fileKind = null, string? documentFilePath = null, (string filePath, string contents)[]? additionalFiles = null, - (Uri fileUri, string contents)[]? additionalExpectedFiles = null) + (Uri fileUri, string contents)[]? additionalExpectedFiles = null, + bool addDefaultImports = true) { - var document = CreateRazorDocument(input, fileKind, documentFilePath, additionalFiles); + var document = CreateRazorDocument(input, fileKind, documentFilePath, additionalFiles, addDefaultImports: addDefaultImports); var codeAction = await VerifyCodeActionRequestAsync(document, input, codeActionName, childActionIndex, expectOffer: expected is not null); @@ -55,7 +56,7 @@ private protected async Task VerifyCodeActionAsync( await VerifyCodeActionResultAsync(document, workspaceEdit, expected, additionalExpectedFiles); } - private protected TextDocument CreateRazorDocument(TestCode input, RazorFileKind? fileKind = null, string? documentFilePath = null, (string filePath, string contents)[]? additionalFiles = null) + private protected TextDocument CreateRazorDocument(TestCode input, RazorFileKind? fileKind = null, string? documentFilePath = null, (string filePath, string contents)[]? additionalFiles = null, bool addDefaultImports = true) { var fileSystem = (RemoteFileSystem)OOPExportProvider.GetExportedValue(); fileSystem.GetTestAccessor().SetFileSystem(new TestFileSystem(additionalFiles)); @@ -73,7 +74,7 @@ private protected TextDocument CreateRazorDocument(TestCode input, RazorFileKind return options; }); - return CreateProjectAndRazorDocument(input.Text, fileKind, documentFilePath, additionalFiles: additionalFiles); + return CreateProjectAndRazorDocument(input.Text, fileKind, documentFilePath, additionalFiles: additionalFiles, addDefaultImports: addDefaultImports); } private async Task VerifyCodeActionRequestAsync(TextDocument document, TestCode input, string codeActionName, int childActionIndex, bool expectOffer) diff --git a/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CodeActions/UnboundDirectiveAttributeAddUsingTests.cs b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CodeActions/UnboundDirectiveAttributeAddUsingTests.cs new file mode 100644 index 00000000000..385a30ed965 --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudioCode.RazorExtension.Test/Endpoints/Shared/CodeActions/UnboundDirectiveAttributeAddUsingTests.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Xunit; +using Xunit.Abstractions; +using WorkItemAttribute = Roslyn.Test.Utilities.WorkItemAttribute; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost.CodeActions; + +public class UnboundDirectiveAttributeAddUsingTests(ITestOutputHelper testOutputHelper) : CohostCodeActionsEndpointTestBase(testOutputHelper) +{ + [Fact, WorkItem("https://github.com/dotnet/razor/issues/9747")] + public async Task AddUsing_OnClick() + { + var input = """ + + """; + + var expected = """ + @using Microsoft.AspNetCore.Components.Web + + """; + + await VerifyCodeActionAsync(input, expected, LanguageServerConstants.CodeActions.AddUsing, addDefaultImports: false); + } + + [Fact, WorkItem("https://github.com/dotnet/razor/issues/9747")] + public async Task AddUsing_OnClick_CursorAtEnd() + { + var input = """ + + """; + + var expected = """ + @using Microsoft.AspNetCore.Components.Web + + """; + + await VerifyCodeActionAsync(input, expected, LanguageServerConstants.CodeActions.AddUsing, addDefaultImports: false); + } + + [Fact, WorkItem("https://github.com/dotnet/razor/issues/9747")] + public async Task AddUsing_OnClick_WithExisting() + { + var input = """ + @using System + + + """; + + var expected = """ + @using System + @using Microsoft.AspNetCore.Components.Web + + + """; + + await VerifyCodeActionAsync(input, expected, LanguageServerConstants.CodeActions.AddUsing, addDefaultImports: false); + } + + [Fact, WorkItem("https://github.com/dotnet/razor/issues/9747")] + public async Task AddUsing_OnChange() + { + var input = """ + + """; + + var expected = """ + @using Microsoft.AspNetCore.Components.Web + + """; + + await VerifyCodeActionAsync(input, expected, LanguageServerConstants.CodeActions.AddUsing, addDefaultImports: false); + } + + [Fact, WorkItem("https://github.com/dotnet/razor/issues/9747")] + public async Task NoCodeAction_WhenBoundAttribute() + { + var input = """ + @using Microsoft.AspNetCore.Components.Web + + + """; + + await VerifyCodeActionAsync(input, expected: null, LanguageServerConstants.CodeActions.AddUsing, addDefaultImports: false); + } + + [Fact, WorkItem("https://github.com/dotnet/razor/issues/9747")] + public async Task NoCodeAction_WhenNotOnDirectiveAttribute() + { + var input = """ + + """; + + await VerifyCodeActionAsync(input, expected: null, LanguageServerConstants.CodeActions.AddUsing, addDefaultImports: false); + } + + [Fact, WorkItem("https://github.com/dotnet/razor/issues/9747")] + public async Task NoCodeAction_WhenNotOnAttributeName() + { + var input = """ + + """; + + await VerifyCodeActionAsync(input, expected: null, LanguageServerConstants.CodeActions.AddUsing, addDefaultImports: false); + } + + [Fact, WorkItem("https://github.com/dotnet/razor/issues/9747")] + public async Task AddUsing_Bind() + { + var input = """ + + """; + + var expected = """ + @using Microsoft.AspNetCore.Components.Web + + """; + + await VerifyCodeActionAsync(input, expected, LanguageServerConstants.CodeActions.AddUsing, addDefaultImports: false); + } + + [Fact, WorkItem("https://github.com/dotnet/razor/issues/9747")] + public async Task AddUsing_BindWithParameter() + { + var input = """ + + """; + + var expected = """ + @using Microsoft.AspNetCore.Components.Web + + """; + + await VerifyCodeActionAsync(input, expected, LanguageServerConstants.CodeActions.AddUsing, addDefaultImports: false); + } +}