diff --git a/src/EditorFeatures/CSharp/Wrapping/CallExpression/CSharpCallExpressionWrapper.cs b/src/EditorFeatures/CSharp/Wrapping/CallExpression/CSharpCallExpressionWrapper.cs index 035f808dfd1db2b56335778d9bd6c6daff3023f8..511a709ff4a5f2f51a767d0b804f93811cae8d83 100644 --- a/src/EditorFeatures/CSharp/Wrapping/CallExpression/CSharpCallExpressionWrapper.cs +++ b/src/EditorFeatures/CSharp/Wrapping/CallExpression/CSharpCallExpressionWrapper.cs @@ -15,7 +15,7 @@ internal class CSharpCallExpressionWrapper : AbstractCallExpressionWrapper< BaseArgumentListSyntax> { public CSharpCallExpressionWrapper() - : base(CSharpSyntaxFactsService.Instance) + : base(CSharpSyntaxFactsService.Instance, (int)SyntaxKind.DotToken, (int)SyntaxKind.QuestionToken) { } diff --git a/src/EditorFeatures/CSharpTest/Wrapping/CallExpressionWrappingTests.cs b/src/EditorFeatures/CSharpTest/Wrapping/CallExpressionWrappingTests.cs index dacba6ca9ee8b8943a2f7515de1f9ddba1a6a90d..79d57574c97dbc6f7bb1198615d9989fd2366d87 100644 --- a/src/EditorFeatures/CSharpTest/Wrapping/CallExpressionWrappingTests.cs +++ b/src/EditorFeatures/CSharpTest/Wrapping/CallExpressionWrappingTests.cs @@ -46,8 +46,8 @@ public async Task TestWithEnoughChunks() }", @"class C { void Bar() { - the.quick.brown() - .fox.jumped(); + the.quick.brown().fox + .jumped(); } }"); } @@ -63,8 +63,8 @@ public async Task TestElementAccess() }", @"class C { void Bar() { - the.quick.brown[1, 2, 3] - .fox.jumped[1][2][3]; + the.quick.brown[1, 2, 3].fox + .jumped[1][2][3]; } }"); } @@ -75,8 +75,8 @@ public async Task TestUnwrap() await TestAllWrappingCasesAsync( @"class C { void Bar() { - [||]the.quick.brown[1, 2, 3] - .fox.jumped[1][2][3]; + [||]the.quick.brown[1, 2, 3].fox + .jumped[1][2][3]; } }", @"class C { @@ -99,8 +99,8 @@ public async Task TestWrapAndUnwrap() }", @"class C { void Bar() { - the.quick.brown[1, 2, 3] - .fox.jumped[1][2][3]; + the.quick.brown[1, 2, 3].fox + .jumped[1][2][3]; } }", @"class C { @@ -121,8 +121,8 @@ public async Task TestChunkMustHaveDottedSection() }", @"class C { void Bar() { - the().quick.brown[1, 2, 3] - .fox.jumped[1][2][3]; + the().quick.brown[1, 2, 3].fox + .jumped[1][2][3]; } }"); } @@ -138,8 +138,8 @@ public async Task TrailingNonCallIsNotWrapped() }", @"class C { void Bar() { - the.quick.brown() - .fox.jumped().over; + the.quick.brown().fox + .jumped().over; } }"); } @@ -156,19 +156,19 @@ public async Task TrailingLongWrapping1() GetIndentionColumn(35), @"class C { void Bar() { - the.quick.brown() - .fox.jumped() - .over.the() - .lazy() - .dog(); + the.quick.brown().fox + .jumped().over + .the() + .lazy() + .dog(); } }", @"class C { void Bar() { - the.quick.brown() - .fox.jumped() - .over.the().lazy() - .dog(); + the.quick.brown().fox + .jumped().over + .the().lazy() + .dog(); } }"); } @@ -185,17 +185,18 @@ public async Task TrailingLongWrapping2() GetIndentionColumn(40), @"class C { void Bar() { - the.quick.brown() - .fox.jumped() - .over.the() - .lazy() - .dog(); + the.quick.brown().fox + .jumped().over + .the() + .lazy() + .dog(); } }", @"class C { void Bar() { - the.quick.brown().fox.jumped() - .over.the().lazy().dog(); + the.quick.brown().fox + .jumped().over.the() + .lazy().dog(); } }"); } @@ -212,17 +213,86 @@ public async Task TrailingLongWrapping3() GetIndentionColumn(60), @"class C { void Bar() { - the.quick.brown() - .fox.jumped() - .over.the() - .lazy() - .dog(); + the.quick.brown().fox + .jumped().over + .the() + .lazy() + .dog(); } }", @"class C { void Bar() { the.quick.brown().fox.jumped().over.the().lazy() - .dog(); + .dog(); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsWrapping)] + public async Task TestInConditionalAccess() + { + await TestAllWrappingCasesAsync( +@"class C { + void Bar() { + the?.[||]quick.brown().fox.jumped(); + } +}", +@"class C { + void Bar() { + the?.quick.brown().fox + .jumped(); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsWrapping)] + public async Task TestInConditionalAccess2() + { + await TestAllWrappingCasesAsync( +@"class C { + void Bar() { + the?.[||]quick.brown()?.fox.jumped(); + } +}", +@"class C { + void Bar() { + the?.quick.brown()?.fox + .jumped(); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsWrapping)] + public async Task TestInConditionalAccess3() + { + await TestAllWrappingCasesAsync( +@"class C { + void Bar() { + the?.[||]quick.brown()?.fox().jumped(); + } +}", +@"class C { + void Bar() { + the?.quick.brown()?.fox() + .jumped(); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsWrapping)] + public async Task TestInConditionalAccess4() + { + await TestAllWrappingCasesAsync( +@"class C { + void Bar() { + [||]the?.quick().brown()?.fox().jumped(); + } +}", +@"class C { + void Bar() { + the?.quick() + .brown()?.fox() + .jumped(); } }"); } diff --git a/src/EditorFeatures/Core/Wrapping/CallExpression/AbstractCallExpressionWrapper.cs b/src/EditorFeatures/Core/Wrapping/CallExpression/AbstractCallExpressionWrapper.cs index b7fe088cfbbc562efef5423d9289a72b242cb4b9..1ef6e83cf20621c3f8cfa38ac9564392f3038f80 100644 --- a/src/EditorFeatures/Core/Wrapping/CallExpression/AbstractCallExpressionWrapper.cs +++ b/src/EditorFeatures/Core/Wrapping/CallExpression/AbstractCallExpressionWrapper.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; @@ -75,24 +76,30 @@ internal abstract partial class AbstractCallExpressionWrapper< where TBaseArgumentListSyntax : SyntaxNode { private readonly ISyntaxFactsService _syntaxFacts; + private readonly int _dotTokenKind; + private readonly int _questionTokenKind; protected AbstractCallExpressionWrapper( - ISyntaxFactsService syntaxFacts) + ISyntaxFactsService syntaxFacts, + int dotTokenKind, + int questionTokenKind) { _syntaxFacts = syntaxFacts; + _dotTokenKind = dotTokenKind; + _questionTokenKind = questionTokenKind; } public sealed override async Task TryCreateComputerAsync( Document document, int position, SyntaxNode node, CancellationToken cancellationToken) { - // has to either be `expr(...)` or `expr[...]` - if (!IsCallExpression(node)) + // We have to be on a chain part. + if (!IsChainPart(node)) { return null; } - // Has to be the topmost invocation/element-access. - if (IsCallExpression(node.Parent)) + // Has to be the topmost chain part. + if (IsChainPart(node.Parent)) { return null; } @@ -101,167 +108,327 @@ internal abstract partial class AbstractCallExpressionWrapper< // i.e. if we only have `this.Goo(...)` there's nothing to wrap. However, we can // wrap when we have `this.Goo(...).Bar(...)`. Grab the chunks of `.Name(...)` as // that's what we're going to be wrapping/aligning. - var callChunks = GetCallChunks(node); - if (callChunks.Length <= 1) + var chunks = GetChainChunks(node); + if (chunks.Length <= 1) { return null; } - // Don't process this call expression if it's contained in some higher member - // call-chunk expression. We'll take care of this when we hit the parent. - var current = node; - while (current.Parent is TMemberAccessExpressionSyntax) + // If any of these chunk parts are unformattable, then we don't want to offer anything + // here as we may make formatting worse for this construct. + foreach (var chunk in chunks) { - current = current.Parent; + var unformattable = await ContainsUnformattableContentAsync( + document, chunk, cancellationToken).ConfigureAwait(false); + if (unformattable) + { + return null; + } } - if (IsCallExpression(current.Parent)) + // Looks good. Crate the action computer which will actually determine + // the set of wrapping options to provide. + var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + var options = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false); + return new CallExpressionCodeActionComputer( + this, document, sourceText, options, chunks, cancellationToken); + } + + private ImmutableArray> GetChainChunks(SyntaxNode node) + { + // First, just take the topmost chain node and break into the individual + // nodes and tokens we want to treat as individual elements. i.e. an + // element that would be kept together. For example, the arg-list of an + // invocation is an element we do not want to ever break-up/wrap. + var pieces = ArrayBuilder.GetInstance(); + BreakIntoPieces(node, pieces); + + // Now that we have the pieces, find 'chunks' similar to the form: + // + // . Name (...) Remainder... + // + // These will be the chunks that are wrapped and aligned on that dot. + // + // Here 'remainder' is everything up to the next `. Name (...)` chunk. + + var chunks = ArrayBuilder>.GetInstance(); + BreakPiecesIntoChunks(pieces, chunks); + + pieces.Free(); + return chunks.ToImmutableAndFree(); + } + + private void BreakPiecesIntoChunks( + ArrayBuilder pieces, + ArrayBuilder> chunks) + { + // Have to look for the first chunk after the first piece. i.e. if the pieces + // starts with `.Foo().Bar().Baz()` then the chunks would be `.Bar()` and `.Baz()`. + // However, if we had `this.Foo().Bar().Baz()` then the chunks would be `.Foo()` + // `.Bar()` and `.Baz()`. + var currentChunkStart = FindNextChunkStart(pieces, firstChunk: true, index: 1); + if (currentChunkStart < 0) { - return null; + return; } - // If any of these chunk parts are unformattable, then we don't want to offer anything - // here as we may make formatting worse for this construct. - foreach (var callChunk in callChunks) + while (true) { - foreach (var memberChunk in callChunk.MemberChunks) + // Look for the next chunk starting after the current chunk we're on. + var nextChunkStart = FindNextChunkStart(pieces, firstChunk: false, index: currentChunkStart + 1); + if (nextChunkStart < 0) { - var unformattable = await ContainsUnformattableContentAsync( - document, new SyntaxNodeOrToken[] { memberChunk.DotToken, memberChunk.Name }, cancellationToken).ConfigureAwait(false); - if (unformattable) - { - return null; - } + // No next chunk after the current one. The current chunk just + // extends to the end of the pieces. + chunks.Add(GetSubRange(pieces, currentChunkStart, end: pieces.Count)); + return; } - foreach (var argumentList in callChunk.ArgumentLists) - { - var unformattable = await ContainsUnformattableContentAsync( - document, new SyntaxNodeOrToken[] { argumentList }, cancellationToken).ConfigureAwait(false); + chunks.Add(GetSubRange(pieces, currentChunkStart, end: nextChunkStart)); + currentChunkStart = nextChunkStart; + } + } - if (unformattable) + /// + /// Looks for the next sequence of `. Name (ArgList)`. Note, except for the first + /// chunk, this cannot be of the form `? . Name (ArgList)` as we do not want to + /// wrap before a dot in a `?.` form. This doesn't matter for the first chunk as + /// we won't be wrapping that one. + /// + private int FindNextChunkStart( + ArrayBuilder pieces, bool firstChunk, int index) + { + for (var i = index; i < pieces.Count; i++) + { + if (IsToken(_dotTokenKind, pieces, i) && + IsNode(pieces, i + 1) && + IsNode(pieces, i + 2)) + { + if (firstChunk || !IsToken(_questionTokenKind, pieces, i - 1)) { - return null; + return i; } } } - // Looks good. Crate the action computer which will actually determine - // the set of wrapping options to provide. - var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); - var options = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false); - return new CallExpressionCodeActionComputer( - this, document, sourceText, options, callChunks, cancellationToken); + return -1; } - private bool IsCallExpression(SyntaxNode node) - => IsCallExpression(node, out _, out _); - - private bool IsCallExpression( - SyntaxNode node, out TExpressionSyntax expression, out TBaseArgumentListSyntax argumentList) + private bool IsNode(ArrayBuilder pieces, int index) { - if (IsCallExpressionWorker( - node, out var expressionNode, out var argumentListNode)) + if (index < pieces.Count) { - expression = (TExpressionSyntax)expressionNode; - argumentList = (TBaseArgumentListSyntax)argumentListNode; - return true; + var piece = pieces[index]; + return piece.IsNode && piece.AsNode() is TNode; } - expression = null; - argumentList = null; return false; } - private bool IsCallExpressionWorker( - SyntaxNode node, out SyntaxNode expression, out SyntaxNode argumentList) + private bool IsToken(int tokenKind, ArrayBuilder pieces, int index) { - if (node is TInvocationExpressionSyntax) + if (index < pieces.Count) { - _syntaxFacts.GetPartsOfInvocationExpression(node, out expression, out argumentList); - return true; + var piece = pieces[index]; + return piece.IsToken && piece.AsToken().RawKind == tokenKind; } - if (node is TElementAccessExpressionSyntax) + return false; + } + + private ImmutableArray GetSubRange( + ArrayBuilder pieces, int start, int end) + { + var result = ArrayBuilder.GetInstance(end - start); + for (var i = start; i < end; i++) { - _syntaxFacts.GetPartsOfElementAccessExpression(node, out expression, out argumentList); - return true; + result.Add(pieces[i]); } - expression = null; - argumentList = null; - return false; + return result.ToImmutableAndFree(); } - private ImmutableArray GetCallChunks(SyntaxNode node) + private bool IsChainPart(SyntaxNode node) { - var chunks = ArrayBuilder.GetInstance(); - AddChunks(node, chunks); - return chunks.ToImmutableAndFree(); + if (_syntaxFacts.IsAnyMemberAccessExpression(node) || + _syntaxFacts.IsInvocationExpression(node) || + _syntaxFacts.IsElementAccessExpression(node) || + _syntaxFacts.IsPostfixUnaryExpression(node) || + _syntaxFacts.IsConditionalAccessExpression(node) || + _syntaxFacts.IsMemberBindingExpression(node)) + { + return true; + } + + return false; } - private void AddChunks(SyntaxNode node, ArrayBuilder chunks) + private void BreakIntoPieces(SyntaxNode node, ArrayBuilder pieces) { - var argumentLists = ArrayBuilder.GetInstance(); - var memberChunks = ArrayBuilder.GetInstance(); - - // Walk downwards, consuming argument lists. - // Note: because of how we walk down, the arg lists will be reverse order. - // We take care of that below. - while (IsCallExpression(node, out var expression, out var argumentList)) + if (node is null) { - argumentLists.Add(argumentList); - node = expression; + return; } - // Walk down the left side eating up `.Name` member-chunks. - // Note: because of how we walk down, the member chunks will be reverse order. - // We take care of that below. - while (node is TMemberAccessExpressionSyntax memberAccess) + // Keep in sync with IsChainPart + if (_syntaxFacts.IsAnyMemberAccessExpression(node)) { _syntaxFacts.GetPartsOfMemberAccessExpression( - node, out var left, out var operatorToken, out var name); - - // We could have a leading member without an 'left' side before - // the dot. This happens in vb 'with' statements. In that case - // we don't want to consider this part the one to align with. i.e. - // we don't want to generate: - // - // with goo - // .bar.baz() - // .quux() - // - // We want to generate: - // - // with goo - // .bar.baz() - // .quux() - if (left != null) - { - memberChunks.Add(new MemberChunk(operatorToken, (TNameSyntax)name)); - } - - node = left; + node, out var expression, out var operatorToken, out var name); + BreakIntoPieces(expression, pieces); + pieces.Add(operatorToken); + pieces.Add(name); } - - // Had to have at least one argument list and at least one member chunk. - if (argumentLists.Count == 0 || memberChunks.Count == 0) + else if (_syntaxFacts.IsMemberBindingExpression(node)) { - argumentLists.Free(); - memberChunks.Free(); - return; + _syntaxFacts.GetPartsOfMemberBindingExpression( + node, out var dotToken, out var name); + pieces.Add(dotToken); + pieces.Add(name); + } + else if (_syntaxFacts.IsInvocationExpression(node)) + { + _syntaxFacts.GetPartsOfInvocationExpression( + node, out var expression, out var argumentList); + BreakIntoPieces(expression, pieces); + pieces.Add(argumentList); + } + else if (_syntaxFacts.IsElementAccessExpression(node)) + { + _syntaxFacts.GetPartsOfElementAccessExpression( + node, out var expression, out var argumentList); + BreakIntoPieces(expression, pieces); + pieces.Add(argumentList); } + else if (_syntaxFacts.IsPostfixUnaryExpression(node)) + { + _syntaxFacts.GetPartsOfPostfixUnaryExpression( + node, out var operand, out var operatorToken); + BreakIntoPieces(operand, pieces); + pieces.Add(operatorToken); + } + else if (_syntaxFacts.IsConditionalAccessExpression(node)) + { + _syntaxFacts.GetPartsOfConditionalAccessExpression( + node, out var expression, out var questionToken, out var whenNotNull); + BreakIntoPieces(expression, pieces); + pieces.Add(questionToken); + BreakIntoPieces(whenNotNull, pieces); + } + else + { + pieces.Add(node); + } + } - // Recurse and see if we can pull out any more chunks prior to the - // first `.Name` member-chunk we found. - AddChunks(node, chunks); + //private bool IsCallExpression(SyntaxNode node) + // => IsCallExpression(node, out _, out _); - memberChunks.ReverseContents(); - argumentLists.ReverseContents(); + //private bool IsCallExpression( + // SyntaxNode node, out TExpressionSyntax expression, out TBaseArgumentListSyntax argumentList) + //{ + // if (IsCallExpressionWorker( + // node, out var expressionNode, out var argumentListNode)) + // { + // expression = (TExpressionSyntax)expressionNode; + // argumentList = (TBaseArgumentListSyntax)argumentListNode; + // return true; + // } - // now, create a call-chunk from the member-chunks and arg-lists we matched against. - chunks.Add(new CallChunk( - memberChunks.ToImmutableAndFree(), argumentLists.ToImmutableAndFree())); - } + // expression = null; + // argumentList = null; + // return false; + //} + + //private bool IsCallExpressionWorker( + // SyntaxNode node, out SyntaxNode expression, out SyntaxNode argumentList) + //{ + // if (node is TInvocationExpressionSyntax) + // { + // _syntaxFacts.GetPartsOfInvocationExpression(node, out expression, out argumentList); + // return true; + // } + + // if (node is TElementAccessExpressionSyntax) + // { + // _syntaxFacts.GetPartsOfElementAccessExpression(node, out expression, out argumentList); + // return true; + // } + + // expression = null; + // argumentList = null; + // return false; + //} + + //private ImmutableArray GetCallChunks(SyntaxNode node) + //{ + // var chunks = ArrayBuilder.GetInstance(); + // AddChunks(node, chunks); + // return chunks.ToImmutableAndFree(); + //} + + //private void AddChunks(SyntaxNode node, ArrayBuilder chunks) + //{ + // var argumentLists = ArrayBuilder.GetInstance(); + // var memberChunks = ArrayBuilder.GetInstance(); + + // // Walk downwards, consuming argument lists. + // // Note: because of how we walk down, the arg lists will be reverse order. + // // We take care of that below. + // while (IsCallExpression(node, out var expression, out var argumentList)) + // { + // argumentLists.Add(argumentList); + // node = expression; + // } + + // // Walk down the left side eating up `.Name` member-chunks. + // // Note: because of how we walk down, the member chunks will be reverse order. + // // We take care of that below. + // while (node is TMemberAccessExpressionSyntax memberAccess) + // { + // _syntaxFacts.GetPartsOfMemberAccessExpression( + // node, out var left, out var operatorToken, out var name); + + // // We could have a leading member without an 'left' side before + // // the dot. This happens in vb 'with' statements. In that case + // // we don't want to consider this part the one to align with. i.e. + // // we don't want to generate: + // // + // // with goo + // // .bar.baz() + // // .quux() + // // + // // We want to generate: + // // + // // with goo + // // .bar.baz() + // // .quux() + // if (left != null) + // { + // memberChunks.Add(new MemberChunk(operatorToken, (TNameSyntax)name)); + // } + + // node = left; + // } + + // // Had to have at least one argument list and at least one member chunk. + // if (argumentLists.Count == 0 || memberChunks.Count == 0) + // { + // argumentLists.Free(); + // memberChunks.Free(); + // return; + // } + + // // Recurse and see if we can pull out any more chunks prior to the + // // first `.Name` member-chunk we found. + // AddChunks(node, chunks); + + // memberChunks.ReverseContents(); + // argumentLists.ReverseContents(); + + // // now, create a call-chunk from the member-chunks and arg-lists we matched against. + // chunks.Add(new CallChunk( + // memberChunks.ToImmutableAndFree(), argumentLists.ToImmutableAndFree())); + //} } } diff --git a/src/EditorFeatures/Core/Wrapping/CallExpression/CallChunk.cs b/src/EditorFeatures/Core/Wrapping/CallExpression/CallChunk.cs index ffef39293a921bd3f7a4a7bef605fbab303de437..1c499622b71394d2004dc84f008c420b2a275828 100644 --- a/src/EditorFeatures/Core/Wrapping/CallExpression/CallChunk.cs +++ b/src/EditorFeatures/Core/Wrapping/CallExpression/CallChunk.cs @@ -1,68 +1,68 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +//// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; -using Microsoft.CodeAnalysis.Shared.Extensions; +//using System; +//using System.Collections.Immutable; +//using System.Diagnostics; +//using System.Linq; +//using Microsoft.CodeAnalysis.Shared.Extensions; -namespace Microsoft.CodeAnalysis.Editor.Wrapping.CallExpression -{ - internal abstract partial class AbstractCallExpressionWrapper< - TExpressionSyntax, - TNameSyntax, - TMemberAccessExpressionSyntax, - TInvocationExpressionSyntax, - TElementAccessExpressionSyntax, - TBaseArgumentListSyntax> - { - /// - /// A single `.Name` piece of a call-chunk like `.P1.P2.P3(...)` - /// - private readonly struct MemberChunk - { - public readonly SyntaxToken DotToken; - public readonly TNameSyntax Name; +//namespace Microsoft.CodeAnalysis.Editor.Wrapping.CallExpression +//{ +// internal abstract partial class AbstractCallExpressionWrapper< +// TExpressionSyntax, +// TNameSyntax, +// TMemberAccessExpressionSyntax, +// TInvocationExpressionSyntax, +// TElementAccessExpressionSyntax, +// TBaseArgumentListSyntax> +// { +// /// +// /// A single `.Name` piece of a call-chunk like `.P1.P2.P3(...)` +// /// +// private readonly struct MemberChunk +// { +// public readonly SyntaxToken DotToken; +// public readonly TNameSyntax Name; - public MemberChunk(SyntaxToken dotToken, TNameSyntax name) - { - DotToken = dotToken; - Name = name; - } +// public MemberChunk(SyntaxToken dotToken, TNameSyntax name) +// { +// DotToken = dotToken; +// Name = name; +// } - /// - /// The length this name chunk will be once all unnecessary whitespace has been - /// removed from it. - /// - public int NormalizedLength() - => DotToken.Width() + Name.Width(); - } +// /// +// /// The length this name chunk will be once all unnecessary whitespace has been +// /// removed from it. +// /// +// public int NormalizedLength() +// => DotToken.Width() + Name.Width(); +// } - /// - /// A full chunk of complex dotted call expression that we want to be - /// able to wrap as a single unit. It will have the form: `.P1.P2.P3(...)(...)` - /// - private readonly struct CallChunk - { - public readonly ImmutableArray MemberChunks; - public readonly ImmutableArray ArgumentLists; +// /// +// /// A full chunk of complex dotted call expression that we want to be +// /// able to wrap as a single unit. It will have the form: `.P1.P2.P3(...)(...)` +// /// +// private readonly struct CallChunk +// { +// public readonly ImmutableArray MemberChunks; +// public readonly ImmutableArray ArgumentLists; - public CallChunk( - ImmutableArray memberChunks, - ImmutableArray argumentLists) - { - Debug.Assert(memberChunks.Length > 0); - Debug.Assert(argumentLists.Length > 0); - MemberChunks = memberChunks; - ArgumentLists = argumentLists; - } +// public CallChunk( +// ImmutableArray memberChunks, +// ImmutableArray argumentLists) +// { +// Debug.Assert(memberChunks.Length > 0); +// Debug.Assert(argumentLists.Length > 0); +// MemberChunks = memberChunks; +// ArgumentLists = argumentLists; +// } - /// - /// The length this call chunk will be once all unnecessary whitespace has been - /// removed from it. - /// - public int NormalizedLength() - => MemberChunks.Sum(c => c.NormalizedLength()) + ArgumentLists.Sum(a => a.Width()); - } - } -} +// /// +// /// The length this call chunk will be once all unnecessary whitespace has been +// /// removed from it. +// /// +// public int NormalizedLength() +// => MemberChunks.Sum(c => c.NormalizedLength()) + ArgumentLists.Sum(a => a.Width()); +// } +// } +//} diff --git a/src/EditorFeatures/Core/Wrapping/CallExpression/CallExpressionCodeActionComputer.cs b/src/EditorFeatures/Core/Wrapping/CallExpression/CallExpressionCodeActionComputer.cs index 71929a6ef95b6407273d8fec2bc6b924806a7486..94672659c0fc316a1c7c8bdb2115b56579d35dc9 100644 --- a/src/EditorFeatures/Core/Wrapping/CallExpression/CallExpressionCodeActionComputer.cs +++ b/src/EditorFeatures/Core/Wrapping/CallExpression/CallExpressionCodeActionComputer.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Immutable; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editing; @@ -10,6 +12,7 @@ using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Utilities; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Editor.Wrapping.CallExpression { @@ -42,7 +45,7 @@ private class CallExpressionCodeActionComputer : /// but not wrapped. Successive chunks will be normalized and wrapped /// appropriately depending on if this is wrap-each or wrap-long. /// - private readonly ImmutableArray _callChunks; + private readonly ImmutableArray> _chunks; /// /// Trivia to place before a call-chunk when it is wrapped. @@ -61,18 +64,18 @@ private class CallExpressionCodeActionComputer : Document document, SourceText originalSourceText, DocumentOptionSet options, - ImmutableArray callChunks, + ImmutableArray> chunks, CancellationToken cancellationToken) : base(service, document, originalSourceText, options, cancellationToken) { - _callChunks = callChunks; + _chunks = chunks; var generator = SyntaxGenerator.GetGenerator(document); // Both [0] indices are safe here. We can only get here if we had more than // two call-chunks to wrap. And each call-chunk is required to have at least // one member-name-chunk. - var indentationString = OriginalSourceText.GetOffset(callChunks[0].MemberChunks[0].DotToken.SpanStart) + var indentationString = OriginalSourceText.GetOffset(chunks[0][0].SpanStart) .CreateIndentationString(UseTabs, TabSize); _indentationTrivia = new SyntaxTriviaList(generator.Whitespace(indentationString)); @@ -101,22 +104,16 @@ private ImmutableArray GetWrapEdits(int wrappingColumn) { var result = ArrayBuilder.GetInstance(); - // Deal with the first chunk (it has special behavior because we - // don't want to delete any trivia directly before it). - DeleteAllButLeadingSpacesInCallChunk(result, _callChunks[0]); + // First, normalize the first chunk. + var firstChunk = _chunks[0]; + DeleteAllSpacesInChunk(result, firstChunk); + var position = _indentationTrivia.FullSpan.Length + NormalizedWidth(firstChunk); - // The position we're currently at. If adding the next chunk would - // make us go past our preferred wrapping column, then we will wrap - // that chunk. - var position = _indentationTrivia.FullSpan.Length + _callChunks[0].NormalizedLength(); - - // Now go through all subsequence call chunks and normalize them all. - // Also, wrap any we encounter if we go past the specified wrapping - // column - for (var i = 1; i < _callChunks.Length; i++) + for (var i = 1; i < _chunks.Length; i++) { - var callChunk = _callChunks[i]; - var wrapChunk = position + callChunk.NormalizedLength() >= wrappingColumn; + var chunk = _chunks[i]; + var wrapChunk = position + NormalizedWidth(chunk) >= wrappingColumn; + if (wrapChunk) { // we're wrapping. So our position is reset to the indentation @@ -126,107 +123,166 @@ private ImmutableArray GetWrapEdits(int wrappingColumn) // First, add a newline at the end of the previous arglist, and then // indent the very first member chunk appropriately. result.Add(Edit.UpdateBetween( - _callChunks[i - 1].ArgumentLists.Last(), _newlineBeforeOperatorTrivia, - _indentationTrivia, callChunk.MemberChunks[0].DotToken)); - - // Now, delete all the remaining spaces in this call chunk. - DeleteAllButLeadingSpacesInCallChunk(result, callChunk); - } - else - { - // not wrapping. So just clean up this next call chunk by - // deleting all spaces. - DeleteAllSpacesInCallChunk(result, callChunk); + _chunks[i - 1].Last(), _newlineBeforeOperatorTrivia, + _indentationTrivia, chunk[0])); } + // Now, delete all the remaining spaces in this call chunk. + DeleteAllSpacesInChunk(result, chunk); + // Update position based on this chunk we just fixed up. - position += callChunk.NormalizedLength(); + position += NormalizedWidth(chunk); } + //// Deal with the first chunk (it has special behavior because we + //// don't want to delete any trivia directly before it). + //DeleteAllButLeadingSpacesInCallChunk(result, _callChunks[0]); + + //// The position we're currently at. If adding the next chunk would + //// make us go past our preferred wrapping column, then we will wrap + //// that chunk. + //var position = _indentationTrivia.FullSpan.Length + _callChunks[0].NormalizedLength(); + + //// Now go through all subsequence call chunks and normalize them all. + //// Also, wrap any we encounter if we go past the specified wrapping + //// column + //for (var i = 1; i < _callChunks.Length; i++) + //{ + // var callChunk = _callChunks[i]; + // var wrapChunk = position + callChunk.NormalizedLength() >= wrappingColumn; + // if (wrapChunk) + // { + // // we're wrapping. So our position is reset to the indentation + // // on the next line. + // position = _indentationTrivia.FullSpan.Length; + + // // First, add a newline at the end of the previous arglist, and then + // // indent the very first member chunk appropriately. + // result.Add(Edit.UpdateBetween( + // _callChunks[i - 1].ArgumentLists.Last(), _newlineBeforeOperatorTrivia, + // _indentationTrivia, callChunk.MemberChunks[0].DotToken)); + + // // Now, delete all the remaining spaces in this call chunk. + // DeleteAllButLeadingSpacesInCallChunk(result, callChunk); + // } + // else + // { + // // not wrapping. So just clean up this next call chunk by + // // deleting all spaces. + // DeleteAllSpacesInCallChunk(result, callChunk); + // } + + // // Update position based on this chunk we just fixed up. + // position += callChunk.NormalizedLength(); + //} + return result.ToImmutableAndFree(); } - private ImmutableArray GetUnwrapEdits() + private int NormalizedWidth(ImmutableArray chunk) { - var result = ArrayBuilder.GetInstance(); - - // Deal with the first chunk (it has special behavior because we - // don't want to delete any trivia directly before it). - DeleteAllButLeadingSpacesInCallChunk(result, _callChunks[0]); - - // Now, handle all successive call chunks. - for (var i = 1; i < _callChunks.Length; i++) + var width = 0; + foreach (var syntax in chunk) { - // In successive call chunks we want to delete all the spacing - // in the member chunks unilaterally. - DeleteAllSpacesInCallChunk(result, _callChunks[i]); + width += syntax.IsNode ? syntax.AsNode().Width() : syntax.AsToken().Width(); } - - return result.ToImmutableAndFree(); + return width; } - private static void DeleteAllSpacesInCallChunk(ArrayBuilder result, CallChunk callChunk) + private ImmutableArray GetUnwrapEdits() { - foreach (var memberChunk in callChunk.MemberChunks) - { - DeleteSpacesInMemberChunk(result, memberChunk); - } + var result = ArrayBuilder.GetInstance(); - // and then any whitespace before the first arg list and between the rest. - DeleteSpacesInArgumentLists(result, callChunk); - } + var flattened = _chunks.SelectMany(c => c).ToImmutableArray(); - /// - /// Removes all whitespace in the spaces between the elements of this chunk. - /// However no edits will be made before the the first dot in the first member - /// chunk of this call chunk. This is useful for the very first call chunk or - /// any callchunk we're explicitly wrapping. - /// - private void DeleteAllButLeadingSpacesInCallChunk(ArrayBuilder result, CallChunk callChunk) - { - // For the very first member chunk we have, don't make any edits prior - // to it. This is the chunk that contains the dot that we are aligning - // all wrapping to. It should never be touched. - // - // After that first member chunk, remove all whitespace between the - // other member chunks and between the arg list. - - // For the very first name chunk in .A.B.C (i.e. `.A` just remove the spaces - // between the dot and the name. - var firstMemberChunk = callChunk.MemberChunks[0]; - result.Add(Edit.DeleteBetween(firstMemberChunk.DotToken, firstMemberChunk.Name)); - - // For all subsequence name chunks in .A.B.C (i.e. `.B.C`) remove any spaces between - // the chunk and the last chunk, and between the dot and the name. - for (var i = 1; i < callChunk.MemberChunks.Length; i++) - { - var memberChunk = callChunk.MemberChunks[i]; - DeleteSpacesInMemberChunk(result, memberChunk); - } + DeleteAllSpacesInChunk(result, flattened); - // and then any whitespace before the first arg list and between the rest. - DeleteSpacesInArgumentLists(result, callChunk); - } + //// Deal with the first chunk (it has special behavior because we + //// don't want to delete any trivia directly before it). + //DeleteAllButLeadingSpacesInCallChunk(result, _callChunks[0]); - private static void DeleteSpacesInMemberChunk(ArrayBuilder result, MemberChunk memberChunk) - { - result.Add(Edit.DeleteBetween(memberChunk.DotToken.GetPreviousToken(), memberChunk.DotToken)); - result.Add(Edit.DeleteBetween(memberChunk.DotToken, memberChunk.Name)); + //// Now, handle all successive call chunks. + //for (var i = 1; i < _callChunks.Length; i++) + //{ + // // In successive call chunks we want to delete all the spacing + // // in the member chunks unilaterally. + // DeleteAllSpacesInCallChunk(result, _callChunks[i]); + //} + + return result.ToImmutableAndFree(); } - private static void DeleteSpacesInArgumentLists(ArrayBuilder result, CallChunk callChunk) + private static void DeleteAllSpacesInChunk( + ArrayBuilder result, ImmutableArray chunk) { - var argumentLists = callChunk.ArgumentLists; - - // Delete the whitespace between the last member name chunk and the first arg list. - result.Add(Edit.DeleteBetween(callChunk.MemberChunks.Last().Name, argumentLists[0])); - - // Now delete the whitespace between each arglist. - for (var i = 1; i < argumentLists.Length; i++) + for (int i = 1; i < chunk.Length; i++) { - result.Add(Edit.DeleteBetween(argumentLists[i - 1], argumentLists[i])); + result.Add(Edit.DeleteBetween(chunk[i - 1], chunk[i])); } } + + //private static void DeleteAllSpacesInCallChunk(ArrayBuilder result, CallChunk callChunk) + //{ + // foreach (var memberChunk in callChunk.MemberChunks) + // { + // DeleteSpacesInMemberChunk(result, memberChunk); + // } + + // // and then any whitespace before the first arg list and between the rest. + // DeleteSpacesInArgumentLists(result, callChunk); + //} + + ///// + ///// Removes all whitespace in the spaces between the elements of this chunk. + ///// However no edits will be made before the the first dot in the first member + ///// chunk of this call chunk. This is useful for the very first call chunk or + ///// any callchunk we're explicitly wrapping. + ///// + //private void DeleteAllButLeadingSpacesInCallChunk(ArrayBuilder result, CallChunk callChunk) + //{ + // // For the very first member chunk we have, don't make any edits prior + // // to it. This is the chunk that contains the dot that we are aligning + // // all wrapping to. It should never be touched. + // // + // // After that first member chunk, remove all whitespace between the + // // other member chunks and between the arg list. + + // // For the very first name chunk in .A.B.C (i.e. `.A` just remove the spaces + // // between the dot and the name. + // var firstMemberChunk = callChunk.MemberChunks[0]; + // result.Add(Edit.DeleteBetween(firstMemberChunk.DotToken, firstMemberChunk.Name)); + + // // For all subsequence name chunks in .A.B.C (i.e. `.B.C`) remove any spaces between + // // the chunk and the last chunk, and between the dot and the name. + // for (var i = 1; i < callChunk.MemberChunks.Length; i++) + // { + // var memberChunk = callChunk.MemberChunks[i]; + // DeleteSpacesInMemberChunk(result, memberChunk); + // } + + // // and then any whitespace before the first arg list and between the rest. + // DeleteSpacesInArgumentLists(result, callChunk); + //} + + //private static void DeleteSpacesInMemberChunk(ArrayBuilder result, MemberChunk memberChunk) + //{ + // result.Add(Edit.DeleteBetween(memberChunk.DotToken.GetPreviousToken(), memberChunk.DotToken)); + // result.Add(Edit.DeleteBetween(memberChunk.DotToken, memberChunk.Name)); + //} + + //private static void DeleteSpacesInArgumentLists(ArrayBuilder result, CallChunk callChunk) + //{ + // var argumentLists = callChunk.ArgumentLists; + + // // Delete the whitespace between the last member name chunk and the first arg list. + // result.Add(Edit.DeleteBetween(callChunk.MemberChunks.Last().Name, argumentLists[0])); + + // // Now delete the whitespace between each arglist. + // for (var i = 1; i < argumentLists.Length; i++) + // { + // result.Add(Edit.DeleteBetween(argumentLists[i - 1], argumentLists[i])); + // } + //} } } } diff --git a/src/EditorFeatures/VisualBasic/Wrapping/CallExpression/VisualBasicCallExpressionWrapper.vb b/src/EditorFeatures/VisualBasic/Wrapping/CallExpression/VisualBasicCallExpressionWrapper.vb index 516580c048a6c250b26dccf1fbfc80a989a9d2e6..6fc011f817498cd966559caaa2beb0c250c28d64 100644 --- a/src/EditorFeatures/VisualBasic/Wrapping/CallExpression/VisualBasicCallExpressionWrapper.vb +++ b/src/EditorFeatures/VisualBasic/Wrapping/CallExpression/VisualBasicCallExpressionWrapper.vb @@ -14,7 +14,7 @@ Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.Wrapping.CallExpression ArgumentListSyntax) Public Sub New() - MyBase.New(VisualBasicSyntaxFactsService.Instance) + MyBase.New(VisualBasicSyntaxFactsService.Instance, SyntaxKind.DotToken, SyntaxKind.QuestionToken) End Sub Public Overrides Function GetNewLineBeforeOperatorTrivia(newLine As SyntaxTriviaList) As SyntaxTriviaList diff --git a/src/EditorFeatures/VisualBasicTest/Wrapping/CallExpressionWrappingTests.vb b/src/EditorFeatures/VisualBasicTest/Wrapping/CallExpressionWrappingTests.vb index a1dc4f86f699274856844e553ce67a8c66ce6647..1cff043415406b36cbcb3004919b0980b1eec904 100644 --- a/src/EditorFeatures/VisualBasicTest/Wrapping/CallExpressionWrappingTests.vb +++ b/src/EditorFeatures/VisualBasicTest/Wrapping/CallExpressionWrappingTests.vb @@ -41,8 +41,8 @@ end class") end class", "class C sub Bar() - the.quick.brown() _ - .fox.jumped() + the.quick.brown().fox _ + .jumped() end sub end class") End Function @@ -57,8 +57,8 @@ end class") end class", "class C sub Bar() - the.quick.brown() _ - .fox.jumped(,) + the.quick.brown().fox _ + .jumped(,) end sub end class") End Function @@ -68,8 +68,8 @@ end class") Await TestAllWrappingCasesAsync( "class C sub Bar() - [||]the.quick.brown(1, 2, 3) _ - .fox.jumped(1)(2)(3) + [||]the.quick.brown(1, 2, 3).fox _ + .jumped(1)(2)(3) end sub end class", "class C @@ -91,8 +91,8 @@ end class") end class", "class C sub Bar() - the.quick.brown(1, 2, 3) _ - .fox.jumped(1)(2)(3) + the.quick.brown(1, 2, 3).fox _ + .jumped(1)(2)(3) end sub end class", "class C @@ -112,8 +112,8 @@ end class") end class", "class C sub Bar() - the().quick.brown(1, 2, 3) _ - .fox.jumped(1)(2)(3) + the().quick.brown(1, 2, 3).fox _ + .jumped(1)(2)(3) end sub end class") End Function @@ -128,8 +128,8 @@ end class") end class", "class C sub Bar() - dim y = the.quick.brown() _ - .fox.jumped().over + dim y = the.quick.brown().fox _ + .jumped().over end sub end class") End Function @@ -145,19 +145,19 @@ end class", GetIndentionColumn(35), "class C sub Bar() - the.quick.brown() _ - .fox.jumped() _ - .over.the() _ - .lazy() _ - .dog() + the.quick.brown().fox _ + .jumped().over _ + .the() _ + .lazy() _ + .dog() end sub end class", "class C sub Bar() - the.quick.brown() _ - .fox.jumped() _ - .over.the().lazy() _ - .dog() + the.quick.brown().fox _ + .jumped().over _ + .the().lazy() _ + .dog() end sub end class") End Function @@ -173,17 +173,18 @@ end class", GetIndentionColumn(40), "class C sub Bar() - the.quick.brown() _ - .fox.jumped() _ - .over.the() _ - .lazy() _ - .dog() + the.quick.brown().fox _ + .jumped().over _ + .the() _ + .lazy() _ + .dog() end sub end class", "class C sub Bar() - the.quick.brown().fox.jumped() _ - .over.the().lazy().dog() + the.quick.brown().fox _ + .jumped().over.the() _ + .lazy().dog() end sub end class") End Function @@ -199,17 +200,17 @@ end class", GetIndentionColumn(60), "class C sub Bar() - the.quick.brown() _ - .fox.jumped() _ - .over.the() _ - .lazy() _ - .dog() + the.quick.brown().fox _ + .jumped().over _ + .the() _ + .lazy() _ + .dog() end sub end class", "class C sub Bar() the.quick.brown().fox.jumped().over.the().lazy() _ - .dog() + .dog() end sub end class") End Function @@ -217,6 +218,26 @@ end class") Public Async Function TestAlignToSecondDotInWith() As Task Await TestAllWrappingCasesAsync( +"class C + sub Bar() + with goo + [||].the().quick.brown().fox.jumped(,) + end with + end sub +end class", +"class C + sub Bar() + with goo + .the().quick.brown().fox _ + .jumped(,) + end with + end sub +end class") + End Function + + + Public Async Function TestAlignToThirdDotInWith() As Task + Await TestAllWrappingCasesAsync( "class C sub Bar() with goo @@ -227,8 +248,8 @@ end class", "class C sub Bar() with goo - .the.quick.brown() _ - .fox.jumped(,) + .the.quick.brown().fox _ + .jumped(,) end with end sub end class") diff --git a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs index 57964bccfc786eafadbc2b669eafcfac344e0e35..0e77894bdbee1cb86b426ff682122ad3f752e18c 100644 --- a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs +++ b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs @@ -617,13 +617,34 @@ public bool IsConditionalAccessExpression(SyntaxNode node) => node is ConditionalAccessExpressionSyntax; public void GetPartsOfConditionalAccessExpression( - SyntaxNode node, out SyntaxNode expression, out SyntaxNode whenNotNull) + SyntaxNode node, out SyntaxNode expression, out SyntaxToken operatorToken, out SyntaxNode whenNotNull) { var conditionalAccess = (ConditionalAccessExpressionSyntax)node; expression = conditionalAccess.Expression; + operatorToken = conditionalAccess.OperatorToken; whenNotNull = conditionalAccess.WhenNotNull; } + public bool IsPostfixUnaryExpression(SyntaxNode node) + => node is PostfixUnaryExpressionSyntax; + + public void GetPartsOfPostfixUnaryExpression(SyntaxNode node, out SyntaxNode operand, out SyntaxToken operatorToken) + { + var expression = (PostfixUnaryExpressionSyntax)node; + operand = expression.Operand; + operatorToken = expression.OperatorToken; + } + + public bool IsMemberBindingExpression(SyntaxNode node) + => node is MemberBindingExpressionSyntax; + + public void GetPartsOfMemberBindingExpression(SyntaxNode node, out SyntaxToken operatorToken, out SyntaxNode name) + { + var memberBinding = (MemberBindingExpressionSyntax)node; + operatorToken = memberBinding.OperatorToken; + name = memberBinding.Name; + } + public bool IsPointerMemberAccessExpression(SyntaxNode node) => (node as MemberAccessExpressionSyntax)?.Kind() == SyntaxKind.PointerMemberAccessExpression; diff --git a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs index 148c40b53c8d4bcfca5a59f337fd963b96d584cc..b11927c9e35a7127474ce21af65ac98bb9b651a6 100644 --- a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs +++ b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs @@ -210,7 +210,13 @@ internal interface ISyntaxFactsService : ILanguageService SyntaxNode GetNameOfAttribute(SyntaxNode node); bool IsConditionalAccessExpression(SyntaxNode node); - void GetPartsOfConditionalAccessExpression(SyntaxNode node, out SyntaxNode expression, out SyntaxNode whenNotNull); + void GetPartsOfConditionalAccessExpression(SyntaxNode node, out SyntaxNode expression, out SyntaxToken operatorToken, out SyntaxNode whenNotNull); + + bool IsMemberBindingExpression(SyntaxNode node); + void GetPartsOfMemberBindingExpression(SyntaxNode node, out SyntaxToken operatorToken, out SyntaxNode name); + + bool IsPostfixUnaryExpression(SyntaxNode node); + void GetPartsOfPostfixUnaryExpression(SyntaxNode node, out SyntaxNode operand, out SyntaxToken operatorToken); bool IsParenthesizedExpression(SyntaxNode node); SyntaxNode GetExpressionOfParenthesizedExpression(SyntaxNode node); diff --git a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsServiceExtensions.cs b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsServiceExtensions.cs index 0acae7d80d13fce0225eb1fae17c711f840fd17e..5dba9cf8f9bd06dc195d864363ab54918b3815e2 100644 --- a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsServiceExtensions.cs +++ b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsServiceExtensions.cs @@ -173,8 +173,9 @@ public static SyntaxToken GetOperatorTokenOfMemberAccessExpression(this ISyntaxF } public static void GetPartsOfMemberAccessExpression(this ISyntaxFactsService syntaxFacts, SyntaxNode node, out SyntaxNode expression, out SyntaxNode name) - { - syntaxFacts.GetPartsOfMemberAccessExpression(node, out expression, out _, out name); - } + => syntaxFacts.GetPartsOfMemberAccessExpression(node, out expression, out _, out name); + + public static void GetPartsOfConditionalAccessExpression(this ISyntaxFactsService syntaxFacts, SyntaxNode node, out SyntaxNode expression, out SyntaxNode whenNotNull) + => syntaxFacts.GetPartsOfConditionalAccessExpression(node, out expression, out _, out whenNotNull); } } diff --git a/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb b/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb index fc65213352928ecfd9b981a79f08d29f5a5f1f2a..3496c766f959f093d2cab3f48c054436a2f04057 100644 --- a/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb +++ b/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb @@ -190,9 +190,10 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Return TypeOf node Is ConditionalAccessExpressionSyntax End Function - Public Sub GetPartsOfConditionalAccessExpression(node As SyntaxNode, ByRef expression As SyntaxNode, ByRef whenNotNull As SyntaxNode) Implements ISyntaxFactsService.GetPartsOfConditionalAccessExpression + Public Sub GetPartsOfConditionalAccessExpression(node As SyntaxNode, ByRef expression As SyntaxNode, ByRef operatorToken As SyntaxToken, ByRef whenNotNull As SyntaxNode) Implements ISyntaxFactsService.GetPartsOfConditionalAccessExpression Dim conditionalAccess = DirectCast(node, ConditionalAccessExpressionSyntax) expression = conditionalAccess.Expression + operatorToken = conditionalAccess.QuestionMarkToken whenNotNull = conditionalAccess.WhenNotNull End Sub @@ -1866,5 +1867,22 @@ Namespace Microsoft.CodeAnalysis.VisualBasic expression = invocation.Expression argumentList = invocation.ArgumentList End Sub + + Public Function IsPostfixUnaryExpression(node As SyntaxNode) As Boolean Implements ISyntaxFactsService.IsPostfixUnaryExpression + ' Does not exist in VB. + Return False + End Function + + Public Sub GetPartsOfPostfixUnaryExpression(node As SyntaxNode, ByRef operand As SyntaxNode, ByRef operatorToken As SyntaxToken) Implements ISyntaxFactsService.GetPartsOfPostfixUnaryExpression + Throw ExceptionUtilities.UnexpectedValue(node.Kind()) + End Sub + + Public Function IsMemberBindingExpression(node As SyntaxNode) As Boolean Implements ISyntaxFactsService.IsMemberBindingExpression + Return False + End Function + + Public Sub GetPartsOfMemberBindingExpression(node As SyntaxNode, ByRef dotToken As SyntaxToken, ByRef name As SyntaxNode) Implements ISyntaxFactsService.GetPartsOfMemberBindingExpression + Throw ExceptionUtilities.UnexpectedValue(node.Kind()) + End Sub End Class End Namespace