Skip to content

Commit 5f32f26

Browse files
authored
Add a code action to promote a using directive (#11241)
Fixes #6155 Me: I really like the cohosting code action tests, it must be really easy to add a new code action now! Me: Oh really? Well, try it and find out, I dare you!! Me: <this PR> (Also me: That wasn't as much fun as I expected. I have thoughts.)
2 parents e4d1b9e + 9850344 commit 5f32f26

File tree

24 files changed

+455
-4
lines changed

24 files changed

+455
-4
lines changed

src/Compiler/Microsoft.CodeAnalysis.Razor.Compiler/src/Mvc/MvcImportProjectFeature.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Extensions;
1414

1515
internal class MvcImportProjectFeature : RazorProjectEngineFeatureBase, IImportProjectFeature
1616
{
17-
private const string ImportsFileName = "_ViewImports.cshtml";
17+
internal const string ImportsFileName = "_ViewImports.cshtml";
1818

1919
public IReadOnlyList<RazorProjectItem> GetImports(RazorProjectItem projectItem)
2020
{

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ public static void AddCodeActionsServices(this IServiceCollection services)
155155
services.AddSingleton<IRazorCodeActionResolver, AddUsingsCodeActionResolver>();
156156
services.AddSingleton<IRazorCodeActionProvider, GenerateMethodCodeActionProvider>();
157157
services.AddSingleton<IRazorCodeActionResolver, GenerateMethodCodeActionResolver>();
158+
services.AddSingleton<IRazorCodeActionProvider, PromoteUsingCodeActionProvider>();
159+
services.AddSingleton<IRazorCodeActionResolver, PromoteUsingCodeActionResolver>();
158160

159161
// Html Code actions
160162
services.AddSingleton<IHtmlCodeActionProvider, HtmlCodeActionProvider>();
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Microsoft.CodeAnalysis.Razor.CodeActions.Models;
7+
8+
internal sealed class PromoteToUsingCodeActionParams
9+
{
10+
[JsonPropertyName("usingStart")]
11+
public required int UsingStart { get; init; }
12+
13+
[JsonPropertyName("usingEnd")]
14+
public required int UsingEnd { get; init; }
15+
16+
[JsonPropertyName("removeStart")]
17+
public required int RemoveStart { get; init; }
18+
19+
[JsonPropertyName("removeEnd")]
20+
public required int RemoveEnd { get; init; }
21+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System.Collections.Immutable;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
8+
using Microsoft.AspNetCore.Razor.Language;
9+
using Microsoft.AspNetCore.Razor.Language.Components;
10+
using Microsoft.AspNetCore.Razor.Language.Syntax;
11+
using Microsoft.AspNetCore.Razor.Threading;
12+
using Microsoft.CodeAnalysis.Razor.CodeActions.Models;
13+
using Microsoft.CodeAnalysis.Razor.CodeActions.Razor;
14+
using Microsoft.CodeAnalysis.Razor.Protocol;
15+
using Microsoft.CodeAnalysis.Text;
16+
17+
namespace Microsoft.CodeAnalysis.Razor.CodeActions;
18+
19+
internal class PromoteUsingCodeActionProvider : IRazorCodeActionProvider
20+
{
21+
public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken)
22+
{
23+
if (context.HasSelection)
24+
{
25+
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
26+
}
27+
28+
var syntaxTree = context.CodeDocument.GetSyntaxTree();
29+
if (syntaxTree?.Root is null)
30+
{
31+
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
32+
}
33+
34+
var owner = syntaxTree.Root.FindNode(TextSpan.FromBounds(context.StartAbsoluteIndex, context.EndAbsoluteIndex));
35+
if (owner is null)
36+
{
37+
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
38+
}
39+
40+
var directive = owner.FirstAncestorOrSelf<RazorDirectiveSyntax>();
41+
if (directive is null || !directive.IsUsingDirective(out _))
42+
{
43+
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>();
44+
}
45+
46+
var importFileName = GetImportsFileName(context.DocumentSnapshot.FileKind);
47+
48+
var line = context.CodeDocument.Source.Text.Lines.GetLineFromPosition(context.StartAbsoluteIndex);
49+
var data = new PromoteToUsingCodeActionParams
50+
{
51+
UsingStart = directive.SpanStart,
52+
UsingEnd = directive.Span.End,
53+
RemoveStart = line.Start,
54+
RemoveEnd = line.EndIncludingLineBreak
55+
};
56+
57+
var resolutionParams = new RazorCodeActionResolutionParams()
58+
{
59+
TextDocument = context.Request.TextDocument,
60+
Action = LanguageServerConstants.CodeActions.PromoteUsingDirective,
61+
Language = RazorLanguageKind.Razor,
62+
DelegatedDocumentUri = context.DelegatedDocumentUri,
63+
Data = data
64+
};
65+
66+
var action = RazorCodeActionFactory.CreatePromoteUsingDirective(importFileName, resolutionParams);
67+
68+
return Task.FromResult<ImmutableArray<RazorVSInternalCodeAction>>([action]);
69+
}
70+
71+
public static string GetImportsFileName(string fileKind)
72+
{
73+
return FileKinds.IsLegacy(fileKind)
74+
? MvcImportProjectFeature.ImportsFileName
75+
: ComponentMetadata.ImportsFileName;
76+
}
77+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.Text.Json;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.AspNetCore.Razor;
10+
using Microsoft.AspNetCore.Razor.PooledObjects;
11+
using Microsoft.AspNetCore.Razor.Utilities;
12+
using Microsoft.CodeAnalysis.Razor.CodeActions.Models;
13+
using Microsoft.CodeAnalysis.Razor.Formatting;
14+
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
15+
using Microsoft.CodeAnalysis.Razor.Protocol;
16+
using Microsoft.CodeAnalysis.Razor.Workspaces;
17+
using Microsoft.CodeAnalysis.Text;
18+
using Microsoft.VisualStudio.LanguageServer.Protocol;
19+
20+
namespace Microsoft.CodeAnalysis.Razor.CodeActions;
21+
22+
internal class PromoteUsingCodeActionResolver(IFileSystem fileSystem) : IRazorCodeActionResolver
23+
{
24+
private readonly IFileSystem _fileSystem = fileSystem;
25+
26+
public string Action => LanguageServerConstants.CodeActions.PromoteUsingDirective;
27+
28+
public async Task<WorkspaceEdit?> ResolveAsync(DocumentContext documentContext, JsonElement data, RazorFormattingOptions options, CancellationToken cancellationToken)
29+
{
30+
var actionParams = data.Deserialize<PromoteToUsingCodeActionParams>();
31+
if (actionParams is null)
32+
{
33+
return null;
34+
}
35+
36+
var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
37+
38+
var importsFileName = PromoteUsingCodeActionProvider.GetImportsFileName(documentContext.FileKind);
39+
40+
var file = FilePathNormalizer.Normalize(documentContext.Uri.GetAbsoluteOrUNCPath());
41+
var folder = Path.GetDirectoryName(file).AssumeNotNull();
42+
var importsFile = Path.GetFullPath(Path.Combine(folder, "..", importsFileName));
43+
var importFileUri = new UriBuilder
44+
{
45+
Scheme = Uri.UriSchemeFile,
46+
Path = importsFile,
47+
Host = string.Empty,
48+
}.Uri;
49+
50+
using var edits = new PooledArrayBuilder<SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>>();
51+
52+
var textToInsert = sourceText.GetSubTextString(TextSpan.FromBounds(actionParams.UsingStart, actionParams.UsingEnd));
53+
var insertLocation = new LinePosition(0, 0);
54+
if (!_fileSystem.FileExists(importsFile))
55+
{
56+
edits.Add(new CreateFile() { Uri = importFileUri });
57+
}
58+
else
59+
{
60+
var st = SourceText.From(_fileSystem.ReadFile(importsFile));
61+
var lastLine = st.Lines[^1];
62+
insertLocation = new LinePosition(lastLine.LineNumber, 0);
63+
if (lastLine.GetFirstNonWhitespaceOffset() is { } nonWhiteSpaceOffset)
64+
{
65+
// Last line isn't blank, so add a newline, and insert at the end
66+
textToInsert = Environment.NewLine + textToInsert;
67+
insertLocation = new LinePosition(insertLocation.Line, lastLine.SpanIncludingLineBreak.Length);
68+
}
69+
}
70+
71+
edits.Add(new TextDocumentEdit
72+
{
73+
TextDocument = new OptionalVersionedTextDocumentIdentifier() { Uri = importFileUri },
74+
Edits = [VsLspFactory.CreateTextEdit(insertLocation, textToInsert)]
75+
});
76+
77+
var removeRange = sourceText.GetRange(actionParams.RemoveStart, actionParams.RemoveEnd);
78+
79+
edits.Add(new TextDocumentEdit
80+
{
81+
TextDocument = new OptionalVersionedTextDocumentIdentifier() { Uri = documentContext.Uri },
82+
Edits = [VsLspFactory.CreateTextEdit(removeRange, string.Empty)]
83+
});
84+
85+
return new WorkspaceEdit
86+
{
87+
DocumentChanges = edits.ToArray()
88+
};
89+
}
90+
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ internal static class RazorCodeActionFactory
1818
private readonly static Guid s_createExtractToComponentTelemetryId = new("af67b0a3-f84b-4808-97a7-b53e85b22c64");
1919
private readonly static Guid s_generateMethodTelemetryId = new("c14fa003-c752-45fc-bb29-3a123ae5ecef");
2020
private readonly static Guid s_generateAsyncMethodTelemetryId = new("9058ca47-98e2-4f11-bf7c-a16a444dd939");
21+
private readonly static Guid s_promoteUsingDirectiveTelemetryId = new("751f9012-e37b-444a-9211-b4ebce91d96e");
22+
23+
public static RazorVSInternalCodeAction CreatePromoteUsingDirective(string importsFileName, RazorCodeActionResolutionParams resolutionParams)
24+
=> new RazorVSInternalCodeAction
25+
{
26+
Title = SR.FormatPromote_using_directive_to(importsFileName),
27+
Data = JsonSerializer.SerializeToElement(resolutionParams),
28+
TelemetryId = s_promoteUsingDirectiveTelemetryId,
29+
Name = LanguageServerConstants.CodeActions.PromoteUsingDirective,
30+
};
2131

2232
public static RazorVSInternalCodeAction CreateAddComponentUsing(string @namespace, string? newTagName, RazorCodeActionResolutionParams resolutionParams)
2333
{

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Protocol/LanguageServerConstants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ public static class CodeActions
4545

4646
public const string AddUsing = "AddUsing";
4747

48+
public const string PromoteUsingDirective = "PromoteUsingDirective";
49+
4850
public const string CodeActionFromVSCode = "CodeActionFromVSCode";
4951

5052
/// <summary>

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/SR.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,4 +196,7 @@
196196
<data name="Statement" xml:space="preserve">
197197
<value>statement</value>
198198
</data>
199+
<data name="Promote_using_directive_to" xml:space="preserve">
200+
<value>Promote using directive to {0}</value>
201+
</data>
199202
</root>

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.cs.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Resources/xlf/SR.de.xlf

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)