зеркало из https://github.com/dotnet/razor.git
Do some fancy remapping to allow code actions everywhere
This commit is contained in:
Родитель
d8313c95e9
Коммит
8f3cdc448c
|
@ -3,16 +3,9 @@
|
|||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Extensions;
|
||||
using Microsoft.AspNetCore.Razor.Language.Legacy;
|
||||
using Microsoft.AspNetCore.Razor.Language.Syntax;
|
||||
using Microsoft.AspNetCore.Razor.LanguageServer.CodeActions.Models;
|
||||
using Microsoft.AspNetCore.Razor.LanguageServer.Extensions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions
|
||||
{
|
||||
|
@ -25,63 +18,5 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions
|
|||
RazorCodeActionContext context,
|
||||
IEnumerable<RazorCodeAction> codeActions,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
protected static bool InFunctionsBlockThatCantHaveCodeActions(RazorCodeActionContext context)
|
||||
{
|
||||
var change = new SourceChange(context.Location.AbsoluteIndex, length: 0, newText: string.Empty);
|
||||
var syntaxTree = context.CodeDocument.GetSyntaxTree();
|
||||
if (syntaxTree?.Root is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var owner = syntaxTree.Root.LocateOwner(change);
|
||||
if (owner is null)
|
||||
{
|
||||
Debug.Fail("Owner should never be null.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var node = owner.Ancestors().FirstOrDefault(n => n.Kind == SyntaxKind.RazorDirective);
|
||||
if (node is not RazorDirectiveSyntax directiveNode)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (directiveNode.DirectiveDescriptor != FunctionsDirective.Directive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// At this point we know its a functions block, but because of how the source mappings work,
|
||||
// if the opening brace for the functions block is not on the same line as the functions node itself
|
||||
// then we can offer code actions.
|
||||
//
|
||||
// This is because when we have a block like this:
|
||||
//
|
||||
// @functions {
|
||||
// class Goo { }
|
||||
// }
|
||||
//
|
||||
// The source mapping starts at char 13 on the "@functions" line (after the open brace). Unfortunately
|
||||
// and code that is needed on that line, say an attribute that the code action wants to insert, will
|
||||
// start at char 8 because of the indentation of the generated code. This means it starts outside of the
|
||||
// mapping, so is thrown away, which results in data loss.
|
||||
//
|
||||
// When the open brace is on the next line, the source mapping starts at char 2, so the insertion at char 8
|
||||
// is fine.
|
||||
if (directiveNode.Body is RazorDirectiveBodySyntax directiveBody &&
|
||||
directiveBody.CSharpCode.Children.TryGetOpenBraceNode(out var openBrace))
|
||||
{
|
||||
context.SourceText.GetLineAndOffset(directiveNode.SpanStart, out var directiveLine, out _);
|
||||
context.SourceText.GetLineAndOffset(openBrace.SpanStart, out var braceLine, out _);
|
||||
if (braceLine > directiveLine)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,11 +51,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions
|
|||
return EmptyResult;
|
||||
}
|
||||
|
||||
if (InFunctionsBlockThatCantHaveCodeActions(context))
|
||||
{
|
||||
return EmptyResult;
|
||||
}
|
||||
|
||||
var results = new List<RazorCodeAction>();
|
||||
|
||||
foreach (var codeAction in codeActions)
|
||||
|
|
|
@ -18,77 +18,156 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer
|
|||
{
|
||||
internal class DefaultRazorDocumentMappingService : RazorDocumentMappingService
|
||||
{
|
||||
public override bool TryMapFromProjectedDocumentEdit(RazorCodeDocument codeDocument, TextEdit edit, out TextEdit newEdit)
|
||||
public override TextEdit[] GetProjectedDocumentEdits(RazorCodeDocument codeDocument, TextEdit[] edits)
|
||||
{
|
||||
newEdit = default;
|
||||
|
||||
var projectedEdits = new List<TextEdit>();
|
||||
var csharpSourceText = codeDocument.GetCSharpSourceText();
|
||||
var range = edit.Range;
|
||||
if (!IsRangeWithinDocument(range, csharpSourceText))
|
||||
var lastNewLineAddedToLine = 0;
|
||||
|
||||
foreach (var edit in edits)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var startIndex = range.Start.GetAbsoluteIndex(csharpSourceText);
|
||||
var endIndex = range.End.GetAbsoluteIndex(csharpSourceText);
|
||||
var mappedStart = TryMapFromProjectedDocumentPosition(codeDocument, startIndex, out var hostDocumentStart, out _);
|
||||
var mappedEnd = TryMapFromProjectedDocumentPosition(codeDocument, endIndex, out var hostDocumentEnd, out _);
|
||||
|
||||
// Ideal case, both start and end can be mapped so just return the edit
|
||||
if (mappedStart && mappedEnd)
|
||||
{
|
||||
newEdit = new TextEdit()
|
||||
var range = edit.Range;
|
||||
if (!IsRangeWithinDocument(range, csharpSourceText))
|
||||
{
|
||||
NewText = edit.NewText,
|
||||
Range = new Range(hostDocumentStart, hostDocumentEnd)
|
||||
};
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// For the first line of a code block the C# formatter will often return an edit that starts
|
||||
// before our mapping, but ends within. In those cases, when the edit spans multiple lines
|
||||
// we just take the last line and try to use that.
|
||||
//
|
||||
// eg in the C# document you might see:
|
||||
//
|
||||
// protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
|
||||
// {
|
||||
// #nullable restore
|
||||
// #line 1 "/path/to/Document.component"
|
||||
//
|
||||
// var x = DateTime.Now;
|
||||
//
|
||||
// To indent the 'var x' line the formatter will return an edit that starts the line before,
|
||||
// with a NewText of '\n '. The start of that edit is outside our mapping, but we
|
||||
// still want to know how to format the 'var x' line, so we have to break up the edit.
|
||||
if (!mappedStart && mappedEnd && range.SpansMultipleLines())
|
||||
{
|
||||
// Construct a theoretical edit that is just for the last line of the edit that the C# formatter
|
||||
// gave us, and see if we can map that.
|
||||
// The +1 here skips the newline character that is found, but also protects from Substring throwing
|
||||
// if there are no newlines (which should be impossible anyway)
|
||||
var lastNewLine = edit.NewText.LastIndexOfAny(new char[] { '\n', '\r' }) + 1;
|
||||
var startIndex = range.Start.GetAbsoluteIndex(csharpSourceText);
|
||||
var endIndex = range.End.GetAbsoluteIndex(csharpSourceText);
|
||||
var mappedStart = TryMapFromProjectedDocumentPosition(codeDocument, startIndex, out var hostDocumentStart, out _);
|
||||
var mappedEnd = TryMapFromProjectedDocumentPosition(codeDocument, endIndex, out var hostDocumentEnd, out _);
|
||||
|
||||
// Strictly speaking we could be dropping more lines than we need to, because our mapping point could be anywhere within the edit
|
||||
// but we know that the C# formatter will only be returning blank lines up until the first bit of content that needs to be indented
|
||||
// so we can ignore all but the last line. This assert ensures that is true, just in case something changes in Roslyn
|
||||
Debug.Assert(edit.NewText.Substring(0, lastNewLine - 1).All(c => c == '\r' || c == '\n'), "We are throwing away part of an edit that has more than just empty lines!");
|
||||
|
||||
var proposedEdit = new TextEdit()
|
||||
// Ideal case, both start and end can be mapped so just return the edit
|
||||
if (mappedStart && mappedEnd)
|
||||
{
|
||||
NewText = edit.NewText.Substring(lastNewLine),
|
||||
Range = new Range(range.End.Line, 0, range.End.Line, range.End.Character)
|
||||
};
|
||||
// If the previous edit was on the same line, and added a newline, then we need to add a space
|
||||
// between this edit and the previous one, because the normalization will have swallowed it. See
|
||||
// below for a more info.
|
||||
var newText = (lastNewLineAddedToLine == range.Start.Line ? " " : "") + edit.NewText;
|
||||
projectedEdits.Add(new TextEdit()
|
||||
{
|
||||
NewText = newText,
|
||||
Range = new Range(hostDocumentStart, hostDocumentEnd)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// We don't need to worry about the recursion here because we're deliberately constructing a range
|
||||
// that is on a single line, but only recursing if the range spans multiple.
|
||||
if (TryMapFromProjectedDocumentEdit(codeDocument, proposedEdit, out newEdit))
|
||||
// For the first line of a code block the C# formatter will often return an edit that starts
|
||||
// before our mapping, but ends within. In those cases, when the edit spans multiple lines
|
||||
// we just take the last line and try to use that.
|
||||
//
|
||||
// eg in the C# document you might see:
|
||||
//
|
||||
// protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
|
||||
// {
|
||||
// #nullable restore
|
||||
// #line 1 "/path/to/Document.component"
|
||||
//
|
||||
// var x = DateTime.Now;
|
||||
//
|
||||
// To indent the 'var x' line the formatter will return an edit that starts the line before,
|
||||
// with a NewText of '\n '. The start of that edit is outside our mapping, but we
|
||||
// still want to know how to format the 'var x' line, so we have to break up the edit.
|
||||
if (!mappedStart && mappedEnd && range.SpansMultipleLines())
|
||||
{
|
||||
return true;
|
||||
// Construct a theoretical edit that is just for the last line of the edit that the C# formatter
|
||||
// gave us, and see if we can map that.
|
||||
// The +1 here skips the newline character that is found, but also protects from Substring throwing
|
||||
// if there are no newlines (which should be impossible anyway)
|
||||
var lastNewLine = edit.NewText.LastIndexOfAny(new char[] { '\n', '\r' }) + 1;
|
||||
|
||||
// Strictly speaking we could be dropping more lines than we need to, because our mapping point could be anywhere within the edit
|
||||
// but we know that the C# formatter will only be returning blank lines up until the first bit of content that needs to be indented
|
||||
// so we can ignore all but the last line. This assert ensures that is true, just in case something changes in Roslyn
|
||||
Debug.Assert(lastNewLine == 0 || edit.NewText.Substring(0, lastNewLine - 1).All(c => c == '\r' || c == '\n'), "We are throwing away part of an edit that has more than just empty lines!");
|
||||
|
||||
var proposedRange = new Range(range.End.Line, 0, range.End.Line, range.End.Character);
|
||||
startIndex = proposedRange.Start.GetAbsoluteIndex(csharpSourceText);
|
||||
endIndex = proposedRange.End.GetAbsoluteIndex(csharpSourceText);
|
||||
mappedStart = TryMapFromProjectedDocumentPosition(codeDocument, startIndex, out hostDocumentStart, out _);
|
||||
mappedEnd = TryMapFromProjectedDocumentPosition(codeDocument, endIndex, out hostDocumentEnd, out _);
|
||||
|
||||
if (mappedStart && mappedEnd)
|
||||
{
|
||||
projectedEdits.Add(new TextEdit()
|
||||
{
|
||||
NewText = edit.NewText.Substring(lastNewLine),
|
||||
Range = new Range(hostDocumentStart, hostDocumentEnd)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't map either the start or the end then we still might want to do something tricky.
|
||||
// When we have a block like this:
|
||||
//
|
||||
// @functions {
|
||||
// class Goo
|
||||
// {
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// The source mapping starts at char 13 on the "@functions" line (after the open brace). Unfortunately
|
||||
// and code that is needed on that line, say an attribute that the code action wants to insert, will
|
||||
// start at char 8 because of the desired indentation of that new code. This means it starts outside of the
|
||||
// mapping, so is thrown away, which results in data loss.
|
||||
//
|
||||
// To fix this we check and if the mapping would have been successful at the end of the line (char 13 above)
|
||||
// then we insert a newline, and enough indentation to get us back out to where the new code wanted to start (char 8)
|
||||
// and then we're good - we've left the @functions bit alone which razor needs, but we're still able to insert
|
||||
// new code above where the C# code is in the generated document.
|
||||
//
|
||||
// One last hurdle is that sometimes these edits come in as separate edits. So for example replacing "class Goo" above
|
||||
// with "public class Goo" would come in as one edit for "public", one for "class" and one for "Goo", all on the same line.
|
||||
// When we map the edit for "public" we will push everything down a line, so we don't want to do it for other edits
|
||||
// on that line.
|
||||
if (!mappedStart && !mappedEnd && !range.SpansMultipleLines())
|
||||
{
|
||||
// If the new text doesn't have any content we don't care - throwing away invisible whitespace is fine
|
||||
if (string.IsNullOrWhiteSpace(edit.NewText))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var line = csharpSourceText.Lines[range.Start.Line];
|
||||
|
||||
// If the line isn't blank, then this isn't a functions directive
|
||||
if (line.GetFirstNonWhitespaceOffset() is not null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only do anything if the end of the line in question is a valid mapping point (ie, a transition)
|
||||
var endOfLine = line.Span.End;
|
||||
if (TryMapFromProjectedDocumentPosition(codeDocument, endOfLine, out var hostDocumentIndex, out _))
|
||||
{
|
||||
if (range.Start.Line == lastNewLineAddedToLine)
|
||||
{
|
||||
// If we already added a newline to this line, then we don't want to add another one, but
|
||||
// we do need to add a space between this edit and the previous one, because the normalization
|
||||
// will have swallowed it.
|
||||
projectedEdits.Add(new TextEdit()
|
||||
{
|
||||
NewText = " " + edit.NewText,
|
||||
Range = new Range(hostDocumentIndex, hostDocumentIndex)
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise, add a newline and the real content, and remember where we added it
|
||||
lastNewLineAddedToLine = range.Start.Line;
|
||||
projectedEdits.Add(new TextEdit()
|
||||
{
|
||||
NewText = Environment.NewLine + new string(' ', range.Start.Character) + edit.NewText,
|
||||
Range = new Range(hostDocumentIndex, hostDocumentIndex)
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return projectedEdits.ToArray();
|
||||
}
|
||||
|
||||
public override bool TryMapFromProjectedDocumentRange(RazorCodeDocument codeDocument, Range projectedRange, out Range originalRange) => TryMapFromProjectedDocumentRange(codeDocument, projectedRange, MappingBehavior.Strict, out originalRange);
|
||||
|
|
|
@ -107,16 +107,9 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting
|
|||
|
||||
private TextEdit[] MapEditsToHostDocument(RazorCodeDocument codeDocument, TextEdit[] csharpEdits)
|
||||
{
|
||||
var actualEdits = new List<TextEdit>();
|
||||
foreach (var edit in csharpEdits)
|
||||
{
|
||||
if (_documentMappingService.TryMapFromProjectedDocumentEdit(codeDocument, edit, out var newEdit))
|
||||
{
|
||||
actualEdits.Add(newEdit);
|
||||
}
|
||||
}
|
||||
var actualEdits = _documentMappingService.GetProjectedDocumentEdits(codeDocument, csharpEdits);
|
||||
|
||||
return actualEdits.ToArray();
|
||||
return actualEdits;
|
||||
}
|
||||
|
||||
private async Task<TextEdit[]> FormatOnClientAsync(
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
|
@ -67,27 +66,14 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting
|
|||
return projectedTextEdits;
|
||||
}
|
||||
|
||||
var edits = new List<TextEdit>();
|
||||
for (var i = 0; i < projectedTextEdits.Length; i++)
|
||||
if (codeDocument.IsUnsupported())
|
||||
{
|
||||
var projectedRange = projectedTextEdits[i].Range;
|
||||
if (codeDocument.IsUnsupported() ||
|
||||
!DocumentMappingService.TryMapFromProjectedDocumentRange(codeDocument, projectedRange, out var originalRange))
|
||||
{
|
||||
// Can't map range. Discard this edit.
|
||||
continue;
|
||||
}
|
||||
|
||||
var edit = new TextEdit()
|
||||
{
|
||||
Range = originalRange,
|
||||
NewText = projectedTextEdits[i].NewText
|
||||
};
|
||||
|
||||
edits.Add(edit);
|
||||
return Array.Empty<TextEdit>();
|
||||
}
|
||||
|
||||
return edits.ToArray();
|
||||
var edits = DocumentMappingService.GetProjectedDocumentEdits(codeDocument, projectedTextEdits);
|
||||
|
||||
return edits;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer
|
|||
{
|
||||
internal abstract class RazorDocumentMappingService
|
||||
{
|
||||
public abstract bool TryMapFromProjectedDocumentEdit(RazorCodeDocument codeDocument, TextEdit edit, out TextEdit newEdit);
|
||||
public abstract TextEdit[] GetProjectedDocumentEdits(RazorCodeDocument codeDocument, TextEdit[] edits);
|
||||
|
||||
public abstract bool TryMapFromProjectedDocumentRange(RazorCodeDocument codeDocument, Range projectedRange, out Range originalRange);
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProvideAsync_FunctionsBlock_SingleLine_ValidCodeActions_ReturnsEmpty()
|
||||
public async Task ProvideAsync_FunctionsBlock_SingleLine_ValidCodeActions_ReturnsProvidedCodeAction()
|
||||
{
|
||||
// Arrange
|
||||
var documentPath = "c:/Test.razor";
|
||||
|
@ -107,11 +107,14 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.CodeActions
|
|||
var providedCodeActions = await provider.ProvideAsync(context, _supportedCodeActions, default);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(providedCodeActions);
|
||||
Assert.Equal(_supportedCodeActions.Length, providedCodeActions.Count);
|
||||
var providedNames = providedCodeActions.Select(action => action.Name);
|
||||
var expectedNames = _supportedCodeActions.Select(action => action.Name);
|
||||
Assert.Equal(expectedNames, providedNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProvideAsync_FunctionsBlock_OpenBraceSameLine_ValidCodeActions_ReturnsEmpty()
|
||||
public async Task ProvideAsync_FunctionsBlock_OpenBraceSameLine_ValidCodeActions_ReturnsProvidedCodeAction()
|
||||
{
|
||||
// Arrange
|
||||
var documentPath = "c:/Test.razor";
|
||||
|
@ -135,7 +138,10 @@ Path;
|
|||
var providedCodeActions = await provider.ProvideAsync(context, _supportedCodeActions, default);
|
||||
|
||||
// Assert
|
||||
Assert.Empty(providedCodeActions);
|
||||
Assert.Equal(_supportedCodeActions.Length, providedCodeActions.Count);
|
||||
var providedNames = providedCodeActions.Select(action => action.Name);
|
||||
var expectedNames = _supportedCodeActions.Select(action => action.Name);
|
||||
Assert.Equal(expectedNames, providedNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the MIT license. See License.txt in the project root for license information.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting
|
||||
{
|
||||
public class CodeActionFormattingTest : FormattingTestBase
|
||||
{
|
||||
public CodeActionFormattingTest(ITestOutputHelper output)
|
||||
: base(output)
|
||||
{
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddDebuggerDisplay()
|
||||
{
|
||||
await RunCodeActionFormattingTestAsync(
|
||||
input: @"
|
||||
@functions {
|
||||
class Goo$$
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
",
|
||||
codeActionEdits: new []
|
||||
{
|
||||
Edit(7, 17, 7, 17, "Diagnostics;\r\n using System."),
|
||||
Edit(67, 0, 67, 8, ""),
|
||||
Edit(69, 34, 70, 7, "\r\n\r\n [DebuggerDisplay($\"{{{nameof(GetDebuggerDisplay)}(),nq}}\")]"),
|
||||
Edit(71, 0, 71, 4, " "),
|
||||
Edit(72, 5, 72, 5, "\r\n private string GetDebuggerDisplay()\r\n {"),
|
||||
Edit(73, 0, 73, 0, " return ToString();\r\n }\r\n"),
|
||||
Edit(73, 8, 74, 4, "")
|
||||
},
|
||||
expected: @"
|
||||
@functions {
|
||||
[DebuggerDisplay($""{{{nameof(GetDebuggerDisplay)}(),nq}}"")]
|
||||
class Goo
|
||||
{
|
||||
private string GetDebuggerDisplay()
|
||||
{
|
||||
return ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GenerateConstructor()
|
||||
{
|
||||
await RunCodeActionFormattingTestAsync(
|
||||
input: @"
|
||||
@functions {
|
||||
class Goo$$
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
",
|
||||
codeActionEdits: new[]
|
||||
{
|
||||
Edit(67, 0, 67, 8, ""),
|
||||
Edit(69, 34, 69, 34, "\r\n\r\n class Goo"),
|
||||
Edit(70, 0, 70, 12, " {"),
|
||||
Edit(71, 0, 71, 9, " public"),
|
||||
Edit(71, 13, 71, 13, "()"),
|
||||
Edit(72, 0, 72, 4, " "),
|
||||
Edit(73, 0, 73, 4, " }"),
|
||||
Edit(74, 0, 74, 4, " "),
|
||||
},
|
||||
expected: @"
|
||||
@functions {
|
||||
class Goo
|
||||
{
|
||||
public Goo()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OverrideCompletion()
|
||||
{
|
||||
await RunCodeActionFormattingTestAsync(
|
||||
input: @"
|
||||
@functions {
|
||||
override $$
|
||||
}
|
||||
",
|
||||
codeActionEdits: new[]
|
||||
{
|
||||
Edit(65, 0, 72, 0, " {\r\n }\r\n#pragma warning restore 1998\r\n#nullable restore\r\n#line 2 \"e:/Scratch/BlazorApp13/BlazorApp13/Client/Pages/Test.razor\"\r\n\r\n protected override void OnAfterRender(bool firstRender)\r\n {\r\n base.OnAfterRender(firstRender);/*$0*/\r\n }\r\n"),
|
||||
},
|
||||
expected: @"
|
||||
@functions {
|
||||
protected override void OnAfterRender(bool firstRender)
|
||||
{
|
||||
base.OnAfterRender(firstRender);/*$0*/
|
||||
}
|
||||
}
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -126,12 +126,31 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting
|
|||
new XUnitVerifier().EqualOrDiff(expected, actual);
|
||||
}
|
||||
|
||||
protected async Task RunCodeActionFormattingTestAsync(
|
||||
string input,
|
||||
TextEdit[] codeActionEdits,
|
||||
string expected,
|
||||
int tabSize = 4,
|
||||
bool insertSpaces = true,
|
||||
string fileKind = null)
|
||||
{
|
||||
var (razorSourceText, edits) = await GetOnTypeFormattingEditsAsync(input, ' ', tabSize, insertSpaces, fileKind, codeActionEdits, bypassValidationPasses: true);
|
||||
|
||||
// Assert
|
||||
var edited = ApplyEdits(razorSourceText, edits);
|
||||
var actual = edited.ToString();
|
||||
|
||||
new XUnitVerifier().EqualOrDiff(expected, actual);
|
||||
}
|
||||
|
||||
protected async Task<(SourceText, TextEdit[])> GetOnTypeFormattingEditsAsync(
|
||||
string input,
|
||||
char triggerCharacter,
|
||||
int tabSize = 4,
|
||||
bool insertSpaces = true,
|
||||
string fileKind = null)
|
||||
string fileKind = null,
|
||||
TextEdit[] codeActionEdits = null,
|
||||
bool bypassValidationPasses = false)
|
||||
{
|
||||
// Arrange
|
||||
fileKind ??= FileKinds.Component;
|
||||
|
@ -151,8 +170,8 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting
|
|||
throw new InvalidOperationException("Could not map from Razor document to generated document");
|
||||
}
|
||||
|
||||
var projectedEdits = Array.Empty<TextEdit>();
|
||||
if (languageKind == RazorLanguageKind.CSharp)
|
||||
var projectedEdits = codeActionEdits;
|
||||
if (projectedEdits is null && languageKind == RazorLanguageKind.CSharp)
|
||||
{
|
||||
projectedEdits = await GetFormattedCSharpEditsAsync(
|
||||
codeDocument, triggerCharacter, projectedIndex, insertSpaces, tabSize).ConfigureAwait(false);
|
||||
|
@ -171,11 +190,18 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Formatting
|
|||
|
||||
// Act
|
||||
var edits = await formattingService.ApplyFormattedEditsAsync(
|
||||
uri, documentSnapshot, languageKind, projectedEdits, options, CancellationToken.None);
|
||||
uri, documentSnapshot, languageKind, projectedEdits, options, CancellationToken.None, bypassValidationPasses: bypassValidationPasses);
|
||||
|
||||
return (razorSourceText, edits);
|
||||
}
|
||||
|
||||
protected static TextEdit Edit(int startLine, int startChar, int endLine, int endChar, string newText)
|
||||
=> new TextEdit()
|
||||
{
|
||||
Range = new OmniSharp.Extensions.LanguageServer.Protocol.Models.Range(startLine, startChar, endLine, endChar),
|
||||
NewText = newText
|
||||
};
|
||||
|
||||
private static async Task<TextEdit[]> GetFormattedCSharpEditsAsync(
|
||||
RazorCodeDocument codeDocument,
|
||||
char typedChar,
|
||||
|
|
Загрузка…
Ссылка в новой задаче