-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
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.)
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
||
using System.Text.Json.Serialization; | ||
|
||
namespace Microsoft.CodeAnalysis.Razor.CodeActions.Models; | ||
|
||
internal sealed class PromoteToUsingCodeActionParams | ||
{ | ||
[JsonPropertyName("usingStart")] | ||
public required int UsingStart { get; init; } | ||
|
||
[JsonPropertyName("usingEnd")] | ||
public required int UsingEnd { get; init; } | ||
|
||
[JsonPropertyName("removeStart")] | ||
public required int RemoveStart { get; init; } | ||
|
||
[JsonPropertyName("removeEnd")] | ||
public required int RemoveEnd { get; init; } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
||
using System.Collections.Immutable; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Mvc.Razor.Extensions; | ||
using Microsoft.AspNetCore.Razor.Language; | ||
using Microsoft.AspNetCore.Razor.Language.Components; | ||
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.Protocol; | ||
using Microsoft.CodeAnalysis.Text; | ||
|
||
namespace Microsoft.CodeAnalysis.Razor.CodeActions; | ||
|
||
internal class PromoteUsingCodeActionProvider : IRazorCodeActionProvider | ||
{ | ||
public Task<ImmutableArray<RazorVSInternalCodeAction>> ProvideAsync(RazorCodeActionContext context, CancellationToken cancellationToken) | ||
{ | ||
if (context.HasSelection) | ||
{ | ||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>(); | ||
} | ||
|
||
var syntaxTree = context.CodeDocument.GetSyntaxTree(); | ||
if (syntaxTree?.Root is null) | ||
{ | ||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>(); | ||
} | ||
|
||
var owner = syntaxTree.Root.FindNode(TextSpan.FromBounds(context.StartAbsoluteIndex, context.EndAbsoluteIndex)); | ||
if (owner is null) | ||
{ | ||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>(); | ||
} | ||
|
||
var directive = owner.FirstAncestorOrSelf<RazorDirectiveSyntax>(); | ||
if (directive is null || !directive.IsUsingDirective(out _)) | ||
{ | ||
return SpecializedTasks.EmptyImmutableArray<RazorVSInternalCodeAction>(); | ||
} | ||
|
||
var importFileName = GetImportsFileName(context.DocumentSnapshot.FileKind); | ||
|
||
var line = context.CodeDocument.Source.Text.Lines.GetLineFromPosition(context.StartAbsoluteIndex); | ||
var data = new PromoteToUsingCodeActionParams | ||
{ | ||
UsingStart = directive.SpanStart, | ||
UsingEnd = directive.Span.End, | ||
RemoveStart = line.Start, | ||
RemoveEnd = line.EndIncludingLineBreak | ||
}; | ||
|
||
var resolutionParams = new RazorCodeActionResolutionParams() | ||
{ | ||
TextDocument = context.Request.TextDocument, | ||
Action = LanguageServerConstants.CodeActions.PromoteUsingDirective, | ||
Language = RazorLanguageKind.Razor, | ||
DelegatedDocumentUri = context.DelegatedDocumentUri, | ||
Data = data | ||
}; | ||
|
||
var action = RazorCodeActionFactory.CreatePromoteUsingDirective(importFileName, resolutionParams); | ||
|
||
return Task.FromResult<ImmutableArray<RazorVSInternalCodeAction>>([action]); | ||
} | ||
|
||
public static string GetImportsFileName(string fileKind) | ||
{ | ||
return FileKinds.IsLegacy(fileKind) | ||
? MvcImportProjectFeature.ImportsFileName | ||
: ComponentMetadata.ImportsFileName; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the MIT license. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.IO; | ||
using System.Text.Json; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.Razor; | ||
using Microsoft.AspNetCore.Razor.PooledObjects; | ||
using Microsoft.AspNetCore.Razor.Utilities; | ||
using Microsoft.CodeAnalysis.Razor.CodeActions.Models; | ||
using Microsoft.CodeAnalysis.Razor.Formatting; | ||
using Microsoft.CodeAnalysis.Razor.ProjectSystem; | ||
using Microsoft.CodeAnalysis.Razor.Protocol; | ||
using Microsoft.CodeAnalysis.Razor.Workspaces; | ||
using Microsoft.CodeAnalysis.Text; | ||
using Microsoft.VisualStudio.LanguageServer.Protocol; | ||
|
||
namespace Microsoft.CodeAnalysis.Razor.CodeActions; | ||
|
||
internal class PromoteUsingCodeActionResolver(IFileSystem fileSystem) : IRazorCodeActionResolver | ||
{ | ||
private readonly IFileSystem _fileSystem = fileSystem; | ||
|
||
public string Action => LanguageServerConstants.CodeActions.PromoteUsingDirective; | ||
|
||
public async Task<WorkspaceEdit?> ResolveAsync(DocumentContext documentContext, JsonElement data, RazorFormattingOptions options, CancellationToken cancellationToken) | ||
{ | ||
var actionParams = data.Deserialize<PromoteToUsingCodeActionParams>(); | ||
if (actionParams is null) | ||
{ | ||
return null; | ||
} | ||
|
||
var sourceText = await documentContext.GetSourceTextAsync(cancellationToken).ConfigureAwait(false); | ||
|
||
var importsFileName = PromoteUsingCodeActionProvider.GetImportsFileName(documentContext.FileKind); | ||
|
||
var file = FilePathNormalizer.Normalize(documentContext.Uri.GetAbsoluteOrUNCPath()); | ||
var folder = Path.GetDirectoryName(file).AssumeNotNull(); | ||
var importsFile = Path.GetFullPath(Path.Combine(folder, "..", importsFileName)); | ||
var importFileUri = new UriBuilder | ||
{ | ||
Scheme = Uri.UriSchemeFile, | ||
Path = importsFile, | ||
Host = string.Empty, | ||
}.Uri; | ||
|
||
using var edits = new PooledArrayBuilder<SumType<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>>(); | ||
|
||
var textToInsert = sourceText.GetSubTextString(TextSpan.FromBounds(actionParams.UsingStart, actionParams.UsingEnd)); | ||
var insertLocation = new LinePosition(0, 0); | ||
if (!_fileSystem.FileExists(importsFile)) | ||
{ | ||
edits.Add(new CreateFile() { Uri = importFileUri }); | ||
} | ||
else | ||
{ | ||
var st = SourceText.From(_fileSystem.ReadFile(importsFile)); | ||
var lastLine = st.Lines[^1]; | ||
insertLocation = new LinePosition(lastLine.LineNumber, 0); | ||
if (lastLine.GetFirstNonWhitespaceOffset() is { } nonWhiteSpaceOffset) | ||
{ | ||
// Last line isn't blank, so add a newline, and insert at the end | ||
textToInsert = Environment.NewLine + textToInsert; | ||
insertLocation = new LinePosition(insertLocation.Line, lastLine.SpanIncludingLineBreak.Length); | ||
} | ||
} | ||
|
||
edits.Add(new TextDocumentEdit | ||
{ | ||
TextDocument = new OptionalVersionedTextDocumentIdentifier() { Uri = importFileUri }, | ||
Edits = [VsLspFactory.CreateTextEdit(insertLocation, textToInsert)] | ||
}); | ||
|
||
var removeRange = sourceText.GetRange(actionParams.RemoveStart, actionParams.RemoveEnd); | ||
|
||
edits.Add(new TextDocumentEdit | ||
{ | ||
TextDocument = new OptionalVersionedTextDocumentIdentifier() { Uri = documentContext.Uri }, | ||
Edits = [VsLspFactory.CreateTextEdit(removeRange, string.Empty)] | ||
}); | ||
|
||
return new WorkspaceEdit | ||
{ | ||
DocumentChanges = edits.ToArray() | ||
}; | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.