From efe88dde84807eadea6f444bfa303fe84ed2bfa5 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Tue, 17 Jun 2025 11:19:15 -0700 Subject: [PATCH 1/8] Breaking out refactoring helpers into core api vs service --- Roslyn.sln | 2 +- .../CSharp/CSharpCompilerExtensions.projitems | 4 + .../CSharpRefactoringHelpers.cs | 113 ++++ .../Core/CompilerExtensions.projitems | 3 + .../AbstractRefactoringHelpers.cs | 612 ++++++++++++++++++ .../RefactoringHelpers/IRefactoringHelpers.cs | 56 ++ .../CSharpRefactoringHelpersService.cs | 95 +-- .../AbstractRefactoringHelpersService.cs | 576 +---------------- .../CodeRefactoringHelpers.cs | 25 - .../IRefactoringHelpersService.cs | 52 +- 10 files changed, 813 insertions(+), 725 deletions(-) create mode 100644 src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/RefactoringHelpers/CSharpRefactoringHelpers.cs create mode 100644 src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/RefactoringHelpers/AbstractRefactoringHelpers.cs create mode 100644 src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/RefactoringHelpers/IRefactoringHelpers.cs diff --git a/Roslyn.sln b/Roslyn.sln index 980009f1a1d57..83e930c9a0463 100644 --- a/Roslyn.sln +++ b/Roslyn.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 -VisualStudioVersion = 18.0.10623.112 main +VisualStudioVersion = 18.0.10623.112 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RoslynDeployment", "src\Deployment\RoslynDeployment.csproj", "{600AF682-E097-407B-AD85-EE3CED37E680}" EndProject diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems index a567cd03b6aca..340a732367ac5 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems @@ -97,6 +97,7 @@ + @@ -131,4 +132,7 @@ + + + \ No newline at end of file diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/RefactoringHelpers/CSharpRefactoringHelpers.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/RefactoringHelpers/CSharpRefactoringHelpers.cs new file mode 100644 index 0000000000000..9170d1ee2fa9c --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Services/RefactoringHelpers/CSharpRefactoringHelpers.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.CodeAnalysis.CodeRefactorings; +using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.LanguageService; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.LanguageService; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings; + +internal sealed class CSharpRefactoringHelpers : AbstractRefactoringHelpers +{ + public static readonly CSharpRefactoringHelpers Instance = new(); + + private CSharpRefactoringHelpers() + { + } + + protected override IHeaderFacts HeaderFacts => CSharpHeaderFacts.Instance; + protected override ISyntaxFacts SyntaxFacts => CSharpSyntaxFacts.Instance; + + public override bool IsBetweenTypeMembers(SourceText sourceText, SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? typeDeclaration) + { + var token = root.FindToken(position); + var typeDecl = token.GetAncestor(); + typeDeclaration = typeDecl; + + if (typeDecl == null) + return false; + + RoslynDebug.AssertNotNull(typeDeclaration); + if (position < typeDecl.OpenBraceToken.Span.End || + position > typeDecl.CloseBraceToken.Span.Start) + { + return false; + } + + var line = sourceText.Lines.GetLineFromPosition(position); + if (!line.IsEmptyOrWhitespace()) + return false; + + var member = typeDecl.Members.FirstOrDefault(d => d.FullSpan.Contains(position)); + if (member == null) + { + // There are no members, or we're after the last member. + return true; + } + else + { + // We're within a member. Make sure we're in the leading whitespace of + // the member. + if (position < member.SpanStart) + { + foreach (var trivia in member.GetLeadingTrivia()) + { + if (!trivia.IsWhitespaceOrEndOfLine()) + return false; + + if (trivia.FullSpan.Contains(position)) + return true; + } + } + } + + return false; + } + + protected override IEnumerable ExtractNodesSimple(SyntaxNode? node, ISyntaxFacts syntaxFacts) + { + if (node == null) + { + yield break; + } + + foreach (var extractedNode in base.ExtractNodesSimple(node, syntaxFacts)) + { + yield return extractedNode; + } + + // `var a = b;` + // -> `var a = b`; + if (node is LocalDeclarationStatementSyntax localDeclaration) + { + yield return localDeclaration.Declaration; + } + + // var `a = b`; + if (node is VariableDeclaratorSyntax declarator) + { + var declaration = declarator.Parent; + if (declaration?.Parent is LocalDeclarationStatementSyntax localDeclarationStatement) + { + var variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement); + if (variables.Count == 1) + { + // -> `var a = b`; + yield return declaration; + + // -> `var a = b;` + yield return localDeclarationStatement; + } + } + } + } +} diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems index e1f2f2e635b9a..29564fc49f112 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems @@ -367,6 +367,8 @@ + + @@ -548,5 +550,6 @@ + \ No newline at end of file diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/RefactoringHelpers/AbstractRefactoringHelpers.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/RefactoringHelpers/AbstractRefactoringHelpers.cs new file mode 100644 index 0000000000000..b2774254b459b --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/RefactoringHelpers/AbstractRefactoringHelpers.cs @@ -0,0 +1,612 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis.LanguageService; +using Microsoft.CodeAnalysis.Shared.Collections; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.CodeRefactorings; + +internal abstract class AbstractRefactoringHelpers : IRefactoringHelpers + where TExpressionSyntax : SyntaxNode + where TArgumentSyntax : SyntaxNode + where TExpressionStatementSyntax : SyntaxNode +{ + protected abstract ISyntaxFacts SyntaxFacts { get; } + protected abstract IHeaderFacts HeaderFacts { get; } + + public abstract bool IsBetweenTypeMembers(SourceText sourceText, SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? typeDeclaration); + + private static void AddNode(bool allowEmptyNodes, ref TemporaryArray result, TSyntaxNode node) where TSyntaxNode : SyntaxNode + { + if (!allowEmptyNodes && node.Span.IsEmpty) + return; + + result.Add(node); + } + + /// + /// Trims leading and trailing whitespace from . + /// + /// + /// Returns unchanged in case . + /// Returns empty Span with original in case it contains only whitespace. + /// + private static TextSpan GetTrimmedTextSpan(SourceText sourceText, TextSpan span) + { + if (span.IsEmpty) + return span; + + var start = span.Start; + var end = span.End; + + while (start < end && char.IsWhiteSpace(sourceText[end - 1])) + end--; + + while (start < end && char.IsWhiteSpace(sourceText[start])) + start++; + + return TextSpan.FromBounds(start, end); + } + + public void AddRelevantNodes( + SourceText sourceText, SyntaxNode root, TextSpan selectionRaw, bool allowEmptyNodes, int maxCount, ref TemporaryArray result, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + { + // Given selection is trimmed first to enable over-selection that spans multiple lines. Since trailing whitespace ends + // at newline boundary over-selection to e.g. a line after LocalFunctionStatement would cause FindNode to find enclosing + // block's Node. That is because in addition to LocalFunctionStatement the selection would also contain trailing trivia + // (whitespace) of following statement. + + var syntaxFacts = this.SyntaxFacts; + var headerFacts = this.HeaderFacts; + var selectionTrimmed = GetTrimmedTextSpan(sourceText, selectionRaw); + + // If user selected only whitespace we don't want to return anything. We could do following: + // 1) Consider token that owns (as its trivia) the whitespace. + // 2) Consider start/beginning of whitespace as location (empty selection) + // Option 1) can't be used all the time and 2) can be confusing for users. Therefore bailing out is the + // most consistent option. + if (selectionTrimmed.IsEmpty && !selectionRaw.IsEmpty) + return; + + // Every time a Node is considered an extractNodes method is called to add all nodes around the original one + // that should also be considered. + // + // That enables us to e.g. return node `b` when Node `var a = b;` is being considered without a complex (and potentially + // lang. & situation dependent) into Children descending code here. We can't just try extracted Node because we might + // want the whole node `var a = b;` + + // Handle selections: + // - Most/the whole wanted Node is selected (e.g. `C [|Fun() {}|]` + // - The smallest node whose FullSpan includes the whole (trimmed) selection + // - Using FullSpan is important because it handles over-selection with comments + // - Travels upwards through same-sized (FullSpan) nodes, extracting + // - Token with wanted Node as direct parent is selected (e.g. IdentifierToken for LocalFunctionStatement: `C [|Fun|]() {}`) + // Note: Whether we have selection or location has to be checked against original selection because selecting just + // whitespace could collapse selectionTrimmed into and empty Location. But we don't want `[| |]token` + // registering as ` [||]token`. + if (!selectionTrimmed.IsEmpty) + { + AddRelevantNodesForSelection(syntaxFacts, root, selectionTrimmed, allowEmptyNodes, maxCount, ref result, cancellationToken); + } + else + { + var location = selectionTrimmed.Start; + + // No more selection -> Handle what current selection is touching: + // + // Consider touching only for empty selections. Otherwise `[|C|] methodName(){}` would be considered as + // touching the Method's Node (through the left edge, see below) which is something the user probably + // didn't want since they specifically selected only the return type. + // + // What the selection is touching is used in two ways. + // - Firstly, it is used to handle situation where it touches a Token whose direct ancestor is wanted + // Node. While having the (even empty) selection inside such token or to left of such Token is already + // handle by code above touching it from right `C methodName[||](){}` isn't (the FindNode for that + // returns Args node). + // + // - Secondly, it is used for left/right edge climbing. E.g. `[||]C methodName(){}` the touching token's + // direct ancestor is TypeNode for the return type but it is still reasonable to expect that the user + // might want to be given refactorings for the whole method (as he has caret on the edge of it). + // Therefore we travel the Node tree upwards and as long as we're on the left edge of a Node's span we + // consider such node & potentially continue traveling upwards. The situation for right edge (`C + // methodName(){}[||]`) is analogical. E.g. for right edge `C methodName(){}[||]`: CloseBraceToken -> + // BlockSyntax -> LocalFunctionStatement -> null (higher node doesn't end on position anymore) Note: + // left-edge climbing needs to handle AttributeLists explicitly, see below for more information. + // + // - Thirdly, if location isn't touching anything, we move the location to the token in whose trivia + // location is in. more about that below. + // + // - Fourthly, if we're in an expression / argument we consider touching a parent expression whenever + // we're within it as long as it is on the first line of such expression (arbitrary heuristic). + + // In addition to per-node extr also check if current location (if selection is empty) is in a header of + // higher level desired node once. We do that only for locations because otherwise `[|int|] A { get; + // set; }) would trigger all refactorings for Property Decl. We cannot check this any sooner because the + // above code could've changed current location. + AddNonHiddenCorrectTypeNodes(ExtractNodesInHeader(root, location, headerFacts), allowEmptyNodes, maxCount, ref result, cancellationToken); + if (result.Count >= maxCount) + return; + + var (tokenToLeft, tokenToRight) = GetTokensToLeftAndRight(sourceText, root, location); + + // Add Nodes for touching tokens as described above. + AddNodesForTokenToRight(syntaxFacts, root, allowEmptyNodes, maxCount, ref result, tokenToRight, cancellationToken); + if (result.Count >= maxCount) + return; + + AddNodesForTokenToLeft(syntaxFacts, allowEmptyNodes, maxCount, ref result, tokenToLeft, cancellationToken); + if (result.Count >= maxCount) + return; + + // If the wanted node is an expression syntax -> traverse upwards even if location is deep within a SyntaxNode. + // We want to treat more types like expressions, e.g.: ArgumentSyntax should still trigger even if deep-in. + if (IsWantedTypeExpressionLike()) + { + // Reason to treat Arguments (and potentially others) as Expression-like: + // https://github.com/dotnet/roslyn/pull/37295#issuecomment-516145904 + AddNodesDeepIn(sourceText, root, location, allowEmptyNodes, maxCount, ref result, cancellationToken); + } + } + } + + private static bool IsWantedTypeExpressionLike() where TSyntaxNode : SyntaxNode + { + var wantedType = typeof(TSyntaxNode); + + var expressionType = typeof(TExpressionSyntax); + var argumentType = typeof(TArgumentSyntax); + var expressionStatementType = typeof(TExpressionStatementSyntax); + + return IsAEqualOrSubclassOfB(wantedType, expressionType) || + IsAEqualOrSubclassOfB(wantedType, argumentType) || + IsAEqualOrSubclassOfB(wantedType, expressionStatementType); + + static bool IsAEqualOrSubclassOfB(Type a, Type b) + { + return a == b || a.IsSubclassOf(b); + } + } + + private (SyntaxToken tokenToLeft, SyntaxToken tokenToRight) GetTokensToLeftAndRight( + SourceText sourceText, SyntaxNode root, int location) + { + // get Token for current location + var tokenOnLocation = root.FindToken(location); + + var syntaxKinds = this.SyntaxFacts.SyntaxKinds; + if (tokenOnLocation.RawKind == syntaxKinds.CommaToken && location >= tokenOnLocation.Span.End) + { + var commaToken = tokenOnLocation; + + // A couple of scenarios to care about: + // + // X,$$ Y + // + // In this case, consider the user on the Y node. + // + // X,$$ + // Y + // + // In this case, consider the user on the X node. + var nextToken = commaToken.GetNextToken(); + var previousToken = commaToken.GetPreviousToken(); + if (nextToken != default && !commaToken.TrailingTrivia.Any(t => t.RawKind == syntaxKinds.EndOfLineTrivia)) + { + return (tokenToLeft: default, tokenToRight: nextToken); + } + else if (previousToken != default && previousToken.Span.End == commaToken.Span.Start) + { + return (tokenToLeft: previousToken, tokenToRight: default); + } + } + + // Gets a token that is directly to the right of current location or that encompasses current location (`[||]tokenToRightOrIn` or `tok[||]enToRightOrIn`) + var tokenToRight = tokenOnLocation.Span.Contains(location) + ? tokenOnLocation + : default; + + // A token can be to the left only when there's either no tokenDirectlyToRightOrIn or there's one directly starting at current location. + // Otherwise (otherwise tokenToRightOrIn is also left from location, e.g: `tok[||]enToRightOrIn`) + var tokenToLeft = default(SyntaxToken); + if (tokenToRight == default || tokenToRight.FullSpan.Start == location) + { + var previousToken = tokenOnLocation.Span.End == location + ? tokenOnLocation + : tokenOnLocation.GetPreviousToken(includeZeroWidth: true); + + tokenToLeft = previousToken.Span.End == location + ? previousToken + : default; + } + + // If both tokens directly to left & right are empty -> we're somewhere in the middle of whitespace. + // Since there wouldn't be (m)any other refactorings we can try to offer at least the ones for (semantically) + // closest token/Node. Thus, we move the location to the token in whose `.FullSpan` the original location was. + if (tokenToLeft == default && + tokenToRight == default && + IsAcceptableLineDistanceAway(sourceText, tokenOnLocation, location)) + { + // tokenOnLocation: token in whose trivia location is at + if (tokenOnLocation.Span.Start >= location) + { + tokenToRight = tokenOnLocation; + } + else + { + tokenToLeft = tokenOnLocation; + } + } + + return (tokenToLeft, tokenToRight); + + static bool IsAcceptableLineDistanceAway( + SourceText sourceText, SyntaxToken tokenOnLocation, int location) + { + // assume non-trivia token can't span multiple lines + var tokenLine = sourceText.Lines.GetLineFromPosition(tokenOnLocation.Span.Start); + var locationLine = sourceText.Lines.GetLineFromPosition(location); + + // Change location to nearest token only if the token is off by one line or less + var lineDistance = tokenLine.LineNumber - locationLine.LineNumber; + if (lineDistance is not 0 and not 1) + return false; + + // Note: being a line below a tokenOnLocation is impossible in current model as whitespace + // trailing trivia ends on new line. Which is fine because if you're a line _after_ some node + // you usually don't want refactorings for what's above you. + + if (lineDistance == 1) + { + // position is one line above the node of interest. This is fine if that + // line is blank. Otherwise, if it isn't (i.e. it contains comments, + // directives, or other trivia), then it's not likely the user is selecting + // this entry. + return locationLine.IsEmptyOrWhitespace(); + } + + // On hte same line. This position is acceptable. + return true; + } + } + + private void AddNodesForTokenToLeft( + ISyntaxFacts syntaxFacts, + bool allowEmptyNodes, + int maxCount, + ref TemporaryArray result, + SyntaxToken tokenToLeft, + CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + { + var location = tokenToLeft.Span.End; + + // there could be multiple (n) tokens to the left if first n-1 are Empty -> iterate over all of them + while (tokenToLeft != default) + { + var leftNode = tokenToLeft.Parent; + do + { + // Consider either a Node that is: + // - Ancestor Node of such Token as long as their span ends on location (it's still on the edge) + AddNonHiddenCorrectTypeNodes(ExtractNodesSimple(leftNode, syntaxFacts), allowEmptyNodes, maxCount, ref result, cancellationToken); + if (result.Count >= maxCount) + return; + + leftNode = leftNode?.Parent; + if (leftNode is null) + break; + + if (leftNode.GetLastToken().Span.End != location && leftNode.Span.End != location) + break; + } + while (true); + + // as long as current tokenToLeft is empty -> its previous token is also tokenToLeft + tokenToLeft = tokenToLeft.Span.IsEmpty + ? tokenToLeft.GetPreviousToken(includeZeroWidth: true) + : default; + } + } + + private void AddNodesForTokenToRight( + ISyntaxFacts syntaxFacts, + SyntaxNode root, + bool allowEmptyNodes, + int maxCount, + ref TemporaryArray result, + SyntaxToken tokenToRightOrIn, + CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + { + var location = tokenToRightOrIn.Span.Start; + + if (tokenToRightOrIn != default) + { + var rightNode = tokenToRightOrIn.Parent; + do + { + // Consider either a Node that is: + // - Parent of touched Token (location can be within) + // - Ancestor Node of such Token as long as their span starts on location (it's still on the edge) + AddNonHiddenCorrectTypeNodes(ExtractNodesSimple(rightNode, syntaxFacts), allowEmptyNodes, maxCount, ref result, cancellationToken); + if (result.Count >= maxCount) + return; + + rightNode = rightNode?.Parent; + if (rightNode == null) + break; + + // The edge climbing for node to the right needs to handle Attributes e.g.: + // [Test1] + // //Comment1 + // [||]object Property1 { get; set; } + // In essence: + // - On the left edge of the node (-> left edge of first AttributeLists) + // - On the left edge of the node sans AttributeLists (& as everywhere comments) + if (rightNode.Span.Start != location) + { + var rightNodeSpanWithoutAttributes = syntaxFacts.GetSpanWithoutAttributes(root, rightNode); + if (rightNodeSpanWithoutAttributes.Start != location) + break; + } + } + while (true); + } + } + + private void AddRelevantNodesForSelection( + ISyntaxFacts syntaxFacts, + SyntaxNode root, + TextSpan selectionTrimmed, + bool allowEmptyNodes, + int maxCount, + ref TemporaryArray result, + CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + { + var selectionNode = root.FindNode(selectionTrimmed, getInnermostNodeForTie: true); + var prevNode = selectionNode; + do + { + var nonHiddenExtractedSelectedNodes = ExtractNodesSimple(selectionNode, syntaxFacts).OfType().Where(n => !n.OverlapsHiddenPosition(cancellationToken)); + foreach (var nonHiddenExtractedNode in nonHiddenExtractedSelectedNodes) + { + // For selections we need to handle an edge case where only AttributeLists are within selection (e.g. `Func([|[in][out]|] arg1);`). + // In that case the smallest encompassing node is still the whole argument node but it's hard to justify showing refactorings for it + // if user selected only its attributes. + + // Selection contains only AttributeLists -> don't consider current Node + var spanWithoutAttributes = syntaxFacts.GetSpanWithoutAttributes(root, nonHiddenExtractedNode); + if (!selectionTrimmed.IntersectsWith(spanWithoutAttributes)) + { + break; + } + + AddNode(allowEmptyNodes, ref result, nonHiddenExtractedNode); + if (result.Count >= maxCount) + return; + } + + prevNode = selectionNode; + selectionNode = selectionNode.Parent; + } + while (selectionNode != null && prevNode.FullWidth() == selectionNode.FullWidth()); + } + + /// + /// Extractor function that retrieves all nodes that should be considered for extraction of given current node. + /// + /// The rationale is that when user selects e.g. entire local declaration statement [|var a = b;|] it is reasonable + /// to provide refactoring for `b` node. Similarly for other types of refactorings. + /// + /// + /// + /// Should also return given node. + /// + protected virtual IEnumerable ExtractNodesSimple(SyntaxNode? node, ISyntaxFacts syntaxFacts) + { + if (node == null) + { + yield break; + } + + // First return the node itself so that it is considered + yield return node; + + // REMARKS: + // The set of currently attempted extractions is in no way exhaustive and covers only cases + // that were found to be relevant for refactorings that were moved to `TryGetSelectedNodeAsync`. + // Feel free to extend it / refine current heuristics. + + // `var a = b;` | `var a = b`; + if (syntaxFacts.IsLocalDeclarationStatement(node) || syntaxFacts.IsLocalDeclarationStatement(node.Parent)) + { + var localDeclarationStatement = syntaxFacts.IsLocalDeclarationStatement(node) ? node : node.Parent!; + + // Check if there's only one variable being declared, otherwise following transformation + // would go through which isn't reasonable since we can't say the first one specifically + // is wanted. + // `var a = 1, `c = 2, d = 3`; + // -> `var a = 1`, c = 2, d = 3; + var variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement); + if (variables.Count == 1) + { + var declaredVariable = variables.First(); + + // -> `a = b` + yield return declaredVariable; + + // -> `b` + var initializer = syntaxFacts.GetInitializerOfVariableDeclarator(declaredVariable); + if (initializer != null) + { + var value = syntaxFacts.GetValueOfEqualsValueClause(initializer); + if (value != null) + { + yield return value; + } + } + } + } + + // var `a = b`; + if (syntaxFacts.IsVariableDeclarator(node)) + { + // -> `b` + var initializer = syntaxFacts.GetInitializerOfVariableDeclarator(node); + if (initializer != null) + { + var value = syntaxFacts.GetValueOfEqualsValueClause(initializer); + if (value != null) + { + yield return value; + } + } + } + + // `a = b;` + // -> `b` + if (syntaxFacts.IsSimpleAssignmentStatement(node)) + { + syntaxFacts.GetPartsOfAssignmentExpressionOrStatement(node, out _, out _, out var rightSide); + yield return rightSide; + } + + // `a();` + // -> a() + if (syntaxFacts.IsExpressionStatement(node)) + { + yield return syntaxFacts.GetExpressionOfExpressionStatement(node); + } + + // `a()`; + // -> `a();` + if (syntaxFacts.IsExpressionStatement(node.Parent)) + { + yield return node.Parent; + } + } + + /// + /// Extractor function that checks and retrieves all nodes current location is in a header. + /// + protected virtual IEnumerable ExtractNodesInHeader(SyntaxNode root, int location, IHeaderFacts headerFacts) + { + // Header: [Test] `public int a` { get; set; } + if (headerFacts.IsOnPropertyDeclarationHeader(root, location, out var propertyDeclaration)) + yield return propertyDeclaration; + + // Header: public C([Test]`int a = 42`) {} + if (headerFacts.IsOnParameterHeader(root, location, out var parameter)) + yield return parameter; + + // Header: `public I.C([Test]int a = 42)` {} + if (headerFacts.IsOnMethodHeader(root, location, out var method)) + yield return method; + + // Header: `static C([Test]int a = 42)` {} + if (headerFacts.IsOnLocalFunctionHeader(root, location, out var localFunction)) + yield return localFunction; + + // Header: `var a = `3,` b = `5,` c = `7 + 3``; + if (headerFacts.IsOnLocalDeclarationHeader(root, location, out var localDeclaration)) + yield return localDeclaration; + + // Header: `if(...)`{ }; + if (headerFacts.IsOnIfStatementHeader(root, location, out var ifStatement)) + yield return ifStatement; + + // Header: `foreach (var a in b)` { } + if (headerFacts.IsOnForeachHeader(root, location, out var foreachStatement)) + yield return foreachStatement; + + if (headerFacts.IsOnTypeHeader(root, location, out var typeDeclaration)) + yield return typeDeclaration; + } + + private static void AddNodesDeepIn( + SourceText sourceText, + SyntaxNode root, + int position, + bool allowEmptyNodes, + int maxCount, + ref TemporaryArray result, + CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + { + // If we're deep inside we don't have to deal with being on edges (that gets dealt by TryGetSelectedNodeAsync) + // -> can simply FindToken -> proceed testing its ancestors + var token = root.FindTokenOnRightOfPosition(position, true); + + // traverse upwards and add all parents if of correct type + var ancestor = token.Parent; + while (ancestor != null) + { + if (ancestor is TSyntaxNode typedAncestor) + { + var argumentStartLine = sourceText.Lines.GetLineFromPosition(typedAncestor.Span.Start).LineNumber; + var caretLine = sourceText.Lines.GetLineFromPosition(position).LineNumber; + + if (argumentStartLine == caretLine && !typedAncestor.OverlapsHiddenPosition(cancellationToken)) + { + AddNode(allowEmptyNodes, ref result, typedAncestor); + if (result.Count >= maxCount) + return; + } + else if (argumentStartLine < caretLine) + { + // higher level nodes will have Span starting at least on the same line -> can bail out + return; + } + } + + ancestor = ancestor.Parent; + } + } + + private static void AddNonHiddenCorrectTypeNodes( + IEnumerable nodes, bool allowEmptyNodes, int maxCount, ref TemporaryArray result, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + { + foreach (var node in nodes) + { + if (node is TSyntaxNode typedNode && + !node.OverlapsHiddenPosition(cancellationToken)) + { + AddNode(allowEmptyNodes, ref result, typedNode); + if (result.Count >= maxCount) + return; + } + } + } + + public bool IsOnTypeHeader(SyntaxNode root, int position, bool fullHeader, [NotNullWhen(true)] out SyntaxNode? typeDeclaration) + => HeaderFacts.IsOnTypeHeader(root, position, fullHeader, out typeDeclaration); + + public bool IsOnPropertyDeclarationHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? propertyDeclaration) + => HeaderFacts.IsOnPropertyDeclarationHeader(root, position, out propertyDeclaration); + + public bool IsOnParameterHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? parameter) + => HeaderFacts.IsOnParameterHeader(root, position, out parameter); + + public bool IsOnMethodHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? method) + => HeaderFacts.IsOnMethodHeader(root, position, out method); + + public bool IsOnLocalFunctionHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? localFunction) + => HeaderFacts.IsOnLocalFunctionHeader(root, position, out localFunction); + + public bool IsOnLocalDeclarationHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? localDeclaration) + => HeaderFacts.IsOnLocalDeclarationHeader(root, position, out localDeclaration); + + public bool IsOnIfStatementHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? ifStatement) + => HeaderFacts.IsOnIfStatementHeader(root, position, out ifStatement); + + public bool IsOnWhileStatementHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? whileStatement) + => HeaderFacts.IsOnWhileStatementHeader(root, position, out whileStatement); + + public bool IsOnForeachHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? foreachStatement) + => HeaderFacts.IsOnForeachHeader(root, position, out foreachStatement); +} diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/RefactoringHelpers/IRefactoringHelpers.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/RefactoringHelpers/IRefactoringHelpers.cs new file mode 100644 index 0000000000000..cab3fdfaa207f --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/RefactoringHelpers/IRefactoringHelpers.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.CodeAnalysis.LanguageService; +using Microsoft.CodeAnalysis.Shared.Collections; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.CodeRefactorings; + +/// +/// Contains helpers related to asking intuitive semantic questions about a users intent +/// based on the position of their caret or span of their selection. +/// +internal interface IRefactoringHelpers : IHeaderFacts +{ + /// + /// True if the user is on a blank line where a member could go inside a type declaration. + /// This will be between members and not ever inside a member. + /// + bool IsBetweenTypeMembers(SourceText sourceText, SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? typeDeclaration); + + /// + /// + /// Returns an array of instances for refactoring given specified selection + /// in document. determines if the returned nodes will can have empty spans + /// or not. + /// + /// + /// A instance is returned if: - Selection is zero-width and inside/touching + /// a Token with direct parent of type . - Selection is zero-width and + /// touching a Token whose ancestor of type ends/starts precisely on current + /// selection. - Selection is zero-width and in whitespace that corresponds to a Token whose direct ancestor is + /// of type of type . - Selection is zero-width and in a header (defined by + /// ISyntaxFacts helpers) of an node of type of type . - Token whose direct + /// parent of type is selected. - Selection is zero-width and wanted node is + /// an expression / argument with selection within such syntax node (arbitrarily deep) on its first line. - + /// Whole node of a type is selected. + /// + /// + /// Attempts extracting a Node of type for each Node it considers (see + /// above). E.g. extracts initializer expressions from declarations and assignments, Property declaration from + /// any header node, etc. + /// + /// + /// Note: this function trims all whitespace from both the beginning and the end of given . The trimmed version is then used to determine relevant . It also + /// handles incomplete selections of tokens gracefully. Over-selection containing leading comments is also + /// handled correctly. + /// + /// + void AddRelevantNodes( + SourceText sourceText, SyntaxNode root, TextSpan selection, bool allowEmptyNodes, int maxCount, ref TemporaryArray result, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode; +} diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/CodeRefactorings/CSharpRefactoringHelpersService.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/CodeRefactorings/CSharpRefactoringHelpersService.cs index d6f17829e39d6..ee2123b3ea092 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/CodeRefactorings/CSharpRefactoringHelpersService.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/CodeRefactorings/CSharpRefactoringHelpersService.cs @@ -3,19 +3,10 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using System.Composition; -using System.Diagnostics.CodeAnalysis; -using System.Linq; using Microsoft.CodeAnalysis.CodeRefactorings; -using Microsoft.CodeAnalysis.CSharp.Extensions; -using Microsoft.CodeAnalysis.CSharp.LanguageService; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.LanguageService; -using Microsoft.CodeAnalysis.Shared.Extensions; -using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings; @@ -24,89 +15,5 @@ namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings; [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal sealed class CSharpRefactoringHelpersService() : AbstractRefactoringHelpersService { - protected override IHeaderFacts HeaderFacts => CSharpHeaderFacts.Instance; - - public override bool IsBetweenTypeMembers(SourceText sourceText, SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? typeDeclaration) - { - var token = root.FindToken(position); - var typeDecl = token.GetAncestor(); - typeDeclaration = typeDecl; - - if (typeDecl == null) - return false; - - RoslynDebug.AssertNotNull(typeDeclaration); - if (position < typeDecl.OpenBraceToken.Span.End || - position > typeDecl.CloseBraceToken.Span.Start) - { - return false; - } - - var line = sourceText.Lines.GetLineFromPosition(position); - if (!line.IsEmptyOrWhitespace()) - return false; - - var member = typeDecl.Members.FirstOrDefault(d => d.FullSpan.Contains(position)); - if (member == null) - { - // There are no members, or we're after the last member. - return true; - } - else - { - // We're within a member. Make sure we're in the leading whitespace of - // the member. - if (position < member.SpanStart) - { - foreach (var trivia in member.GetLeadingTrivia()) - { - if (!trivia.IsWhitespaceOrEndOfLine()) - return false; - - if (trivia.FullSpan.Contains(position)) - return true; - } - } - } - - return false; - } - - protected override IEnumerable ExtractNodesSimple(SyntaxNode? node, ISyntaxFactsService syntaxFacts) - { - if (node == null) - { - yield break; - } - - foreach (var extractedNode in base.ExtractNodesSimple(node, syntaxFacts)) - { - yield return extractedNode; - } - - // `var a = b;` - // -> `var a = b`; - if (node is LocalDeclarationStatementSyntax localDeclaration) - { - yield return localDeclaration.Declaration; - } - - // var `a = b`; - if (node is VariableDeclaratorSyntax declarator) - { - var declaration = declarator.Parent; - if (declaration?.Parent is LocalDeclarationStatementSyntax localDeclarationStatement) - { - var variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement); - if (variables.Count == 1) - { - // -> `var a = b`; - yield return declaration; - - // -> `var a = b;` - yield return localDeclarationStatement; - } - } - } - } + protected override IRefactoringHelpers RefactoringHelpers { get; } = CSharpRefactoringHelpers.Instance; } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/AbstractRefactoringHelpersService.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/AbstractRefactoringHelpersService.cs index 2372d41935977..e6a76f41847da 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/AbstractRefactoringHelpersService.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/AbstractRefactoringHelpersService.cs @@ -2,14 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Threading; -using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Collections; -using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.CodeRefactorings; @@ -19,581 +14,38 @@ internal abstract class AbstractRefactoringHelpersService this.RefactoringHelpers.IsBetweenTypeMembers(sourceText, root, position, out typeDeclaration); - private static void AddNode(bool allowEmptyNodes, ref TemporaryArray result, TSyntaxNode node) where TSyntaxNode : SyntaxNode - { - if (!allowEmptyNodes && node.Span.IsEmpty) - return; - - result.Add(node); - } - - public void AddRelevantNodes( - ParsedDocument document, TextSpan selectionRaw, bool allowEmptyNodes, int maxCount, ref TemporaryArray result, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode - { - // Given selection is trimmed first to enable over-selection that spans multiple lines. Since trailing whitespace ends - // at newline boundary over-selection to e.g. a line after LocalFunctionStatement would cause FindNode to find enclosing - // block's Node. That is because in addition to LocalFunctionStatement the selection would also contain trailing trivia - // (whitespace) of following statement. - - var root = document.Root; - - var syntaxFacts = document.LanguageServices.GetRequiredService(); - var headerFacts = document.LanguageServices.GetRequiredService(); - var selectionTrimmed = CodeRefactoringHelpers.GetTrimmedTextSpan(document, selectionRaw); - - // If user selected only whitespace we don't want to return anything. We could do following: - // 1) Consider token that owns (as its trivia) the whitespace. - // 2) Consider start/beginning of whitespace as location (empty selection) - // Option 1) can't be used all the time and 2) can be confusing for users. Therefore bailing out is the - // most consistent option. - if (selectionTrimmed.IsEmpty && !selectionRaw.IsEmpty) - return; - - // Every time a Node is considered an extractNodes method is called to add all nodes around the original one - // that should also be considered. - // - // That enables us to e.g. return node `b` when Node `var a = b;` is being considered without a complex (and potentially - // lang. & situation dependent) into Children descending code here. We can't just try extracted Node because we might - // want the whole node `var a = b;` - - // Handle selections: - // - Most/the whole wanted Node is selected (e.g. `C [|Fun() {}|]` - // - The smallest node whose FullSpan includes the whole (trimmed) selection - // - Using FullSpan is important because it handles over-selection with comments - // - Travels upwards through same-sized (FullSpan) nodes, extracting - // - Token with wanted Node as direct parent is selected (e.g. IdentifierToken for LocalFunctionStatement: `C [|Fun|]() {}`) - // Note: Whether we have selection or location has to be checked against original selection because selecting just - // whitespace could collapse selectionTrimmed into and empty Location. But we don't want `[| |]token` - // registering as ` [||]token`. - if (!selectionTrimmed.IsEmpty) - { - AddRelevantNodesForSelection(syntaxFacts, root, selectionTrimmed, allowEmptyNodes, maxCount, ref result, cancellationToken); - } - else - { - var location = selectionTrimmed.Start; - - // No more selection -> Handle what current selection is touching: - // - // Consider touching only for empty selections. Otherwise `[|C|] methodName(){}` would be considered as - // touching the Method's Node (through the left edge, see below) which is something the user probably - // didn't want since they specifically selected only the return type. - // - // What the selection is touching is used in two ways. - // - Firstly, it is used to handle situation where it touches a Token whose direct ancestor is wanted - // Node. While having the (even empty) selection inside such token or to left of such Token is already - // handle by code above touching it from right `C methodName[||](){}` isn't (the FindNode for that - // returns Args node). - // - // - Secondly, it is used for left/right edge climbing. E.g. `[||]C methodName(){}` the touching token's - // direct ancestor is TypeNode for the return type but it is still reasonable to expect that the user - // might want to be given refactorings for the whole method (as he has caret on the edge of it). - // Therefore we travel the Node tree upwards and as long as we're on the left edge of a Node's span we - // consider such node & potentially continue traveling upwards. The situation for right edge (`C - // methodName(){}[||]`) is analogical. E.g. for right edge `C methodName(){}[||]`: CloseBraceToken -> - // BlockSyntax -> LocalFunctionStatement -> null (higher node doesn't end on position anymore) Note: - // left-edge climbing needs to handle AttributeLists explicitly, see below for more information. - // - // - Thirdly, if location isn't touching anything, we move the location to the token in whose trivia - // location is in. more about that below. - // - // - Fourthly, if we're in an expression / argument we consider touching a parent expression whenever - // we're within it as long as it is on the first line of such expression (arbitrary heuristic). - - // In addition to per-node extr also check if current location (if selection is empty) is in a header of - // higher level desired node once. We do that only for locations because otherwise `[|int|] A { get; - // set; }) would trigger all refactorings for Property Decl. We cannot check this any sooner because the - // above code could've changed current location. - AddNonHiddenCorrectTypeNodes(ExtractNodesInHeader(root, location, headerFacts), allowEmptyNodes, maxCount, ref result, cancellationToken); - if (result.Count >= maxCount) - return; - - var (tokenToLeft, tokenToRight) = GetTokensToLeftAndRight(document, root, location); - - // Add Nodes for touching tokens as described above. - AddNodesForTokenToRight(syntaxFacts, root, allowEmptyNodes, maxCount, ref result, tokenToRight, cancellationToken); - if (result.Count >= maxCount) - return; - - AddNodesForTokenToLeft(syntaxFacts, allowEmptyNodes, maxCount, ref result, tokenToLeft, cancellationToken); - if (result.Count >= maxCount) - return; - - // If the wanted node is an expression syntax -> traverse upwards even if location is deep within a SyntaxNode. - // We want to treat more types like expressions, e.g.: ArgumentSyntax should still trigger even if deep-in. - if (IsWantedTypeExpressionLike()) - { - // Reason to treat Arguments (and potentially others) as Expression-like: - // https://github.com/dotnet/roslyn/pull/37295#issuecomment-516145904 - AddNodesDeepIn(document, location, allowEmptyNodes, maxCount, ref result, cancellationToken); - } - } - } - - private static bool IsWantedTypeExpressionLike() where TSyntaxNode : SyntaxNode - { - var wantedType = typeof(TSyntaxNode); - - var expressionType = typeof(TExpressionSyntax); - var argumentType = typeof(TArgumentSyntax); - var expressionStatementType = typeof(TExpressionStatementSyntax); - - return IsAEqualOrSubclassOfB(wantedType, expressionType) || - IsAEqualOrSubclassOfB(wantedType, argumentType) || - IsAEqualOrSubclassOfB(wantedType, expressionStatementType); - - static bool IsAEqualOrSubclassOfB(Type a, Type b) - { - return a == b || a.IsSubclassOf(b); - } - } - - private static (SyntaxToken tokenToLeft, SyntaxToken tokenToRight) GetTokensToLeftAndRight( - ParsedDocument document, - SyntaxNode root, - int location) - { - // get Token for current location - var tokenOnLocation = root.FindToken(location); - - var syntaxKinds = document.LanguageServices.GetRequiredService(); - if (tokenOnLocation.RawKind == syntaxKinds.CommaToken && location >= tokenOnLocation.Span.End) - { - var commaToken = tokenOnLocation; - - // A couple of scenarios to care about: - // - // X,$$ Y - // - // In this case, consider the user on the Y node. - // - // X,$$ - // Y - // - // In this case, consider the user on the X node. - var nextToken = commaToken.GetNextToken(); - var previousToken = commaToken.GetPreviousToken(); - if (nextToken != default && !commaToken.TrailingTrivia.Any(t => t.RawKind == syntaxKinds.EndOfLineTrivia)) - { - return (tokenToLeft: default, tokenToRight: nextToken); - } - else if (previousToken != default && previousToken.Span.End == commaToken.Span.Start) - { - return (tokenToLeft: previousToken, tokenToRight: default); - } - } - - // Gets a token that is directly to the right of current location or that encompasses current location (`[||]tokenToRightOrIn` or `tok[||]enToRightOrIn`) - var tokenToRight = tokenOnLocation.Span.Contains(location) - ? tokenOnLocation - : default; - - // A token can be to the left only when there's either no tokenDirectlyToRightOrIn or there's one directly starting at current location. - // Otherwise (otherwise tokenToRightOrIn is also left from location, e.g: `tok[||]enToRightOrIn`) - var tokenToLeft = default(SyntaxToken); - if (tokenToRight == default || tokenToRight.FullSpan.Start == location) - { - var previousToken = tokenOnLocation.Span.End == location - ? tokenOnLocation - : tokenOnLocation.GetPreviousToken(includeZeroWidth: true); - - tokenToLeft = previousToken.Span.End == location - ? previousToken - : default; - } - - // If both tokens directly to left & right are empty -> we're somewhere in the middle of whitespace. - // Since there wouldn't be (m)any other refactorings we can try to offer at least the ones for (semantically) - // closest token/Node. Thus, we move the location to the token in whose `.FullSpan` the original location was. - if (tokenToLeft == default && tokenToRight == default) - { - var sourceText = document.Text; - - if (IsAcceptableLineDistanceAway(sourceText, tokenOnLocation, location)) - { - // tokenOnLocation: token in whose trivia location is at - if (tokenOnLocation.Span.Start >= location) - { - tokenToRight = tokenOnLocation; - } - else - { - tokenToLeft = tokenOnLocation; - } - } - } - - return (tokenToLeft, tokenToRight); - - static bool IsAcceptableLineDistanceAway( - SourceText sourceText, SyntaxToken tokenOnLocation, int location) - { - // assume non-trivia token can't span multiple lines - var tokenLine = sourceText.Lines.GetLineFromPosition(tokenOnLocation.Span.Start); - var locationLine = sourceText.Lines.GetLineFromPosition(location); - - // Change location to nearest token only if the token is off by one line or less - var lineDistance = tokenLine.LineNumber - locationLine.LineNumber; - if (lineDistance is not 0 and not 1) - return false; - - // Note: being a line below a tokenOnLocation is impossible in current model as whitespace - // trailing trivia ends on new line. Which is fine because if you're a line _after_ some node - // you usually don't want refactorings for what's above you. - - if (lineDistance == 1) - { - // position is one line above the node of interest. This is fine if that - // line is blank. Otherwise, if it isn't (i.e. it contains comments, - // directives, or other trivia), then it's not likely the user is selecting - // this entry. - return locationLine.IsEmptyOrWhitespace(); - } - - // On hte same line. This position is acceptable. - return true; - } - } - - private void AddNodesForTokenToLeft( - ISyntaxFactsService syntaxFacts, - bool allowEmptyNodes, - int maxCount, - ref TemporaryArray result, - SyntaxToken tokenToLeft, - CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode - { - var location = tokenToLeft.Span.End; - - // there could be multiple (n) tokens to the left if first n-1 are Empty -> iterate over all of them - while (tokenToLeft != default) - { - var leftNode = tokenToLeft.Parent; - do - { - // Consider either a Node that is: - // - Ancestor Node of such Token as long as their span ends on location (it's still on the edge) - AddNonHiddenCorrectTypeNodes(ExtractNodesSimple(leftNode, syntaxFacts), allowEmptyNodes, maxCount, ref result, cancellationToken); - if (result.Count >= maxCount) - return; - - leftNode = leftNode?.Parent; - if (leftNode is null) - break; - - if (leftNode.GetLastToken().Span.End != location && leftNode.Span.End != location) - break; - } - while (true); - - // as long as current tokenToLeft is empty -> its previous token is also tokenToLeft - tokenToLeft = tokenToLeft.Span.IsEmpty - ? tokenToLeft.GetPreviousToken(includeZeroWidth: true) - : default; - } - } - - private void AddNodesForTokenToRight( - ISyntaxFactsService syntaxFacts, - SyntaxNode root, - bool allowEmptyNodes, - int maxCount, - ref TemporaryArray result, - SyntaxToken tokenToRightOrIn, - CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode - { - var location = tokenToRightOrIn.Span.Start; - - if (tokenToRightOrIn != default) - { - var rightNode = tokenToRightOrIn.Parent; - do - { - // Consider either a Node that is: - // - Parent of touched Token (location can be within) - // - Ancestor Node of such Token as long as their span starts on location (it's still on the edge) - AddNonHiddenCorrectTypeNodes(ExtractNodesSimple(rightNode, syntaxFacts), allowEmptyNodes, maxCount, ref result, cancellationToken); - if (result.Count >= maxCount) - return; - - rightNode = rightNode?.Parent; - if (rightNode == null) - break; - - // The edge climbing for node to the right needs to handle Attributes e.g.: - // [Test1] - // //Comment1 - // [||]object Property1 { get; set; } - // In essence: - // - On the left edge of the node (-> left edge of first AttributeLists) - // - On the left edge of the node sans AttributeLists (& as everywhere comments) - if (rightNode.Span.Start != location) - { - var rightNodeSpanWithoutAttributes = syntaxFacts.GetSpanWithoutAttributes(root, rightNode); - if (rightNodeSpanWithoutAttributes.Start != location) - break; - } - } - while (true); - } - } - - private void AddRelevantNodesForSelection( - ISyntaxFactsService syntaxFacts, - SyntaxNode root, - TextSpan selectionTrimmed, - bool allowEmptyNodes, - int maxCount, - ref TemporaryArray result, - CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode - { - var selectionNode = root.FindNode(selectionTrimmed, getInnermostNodeForTie: true); - var prevNode = selectionNode; - do - { - var nonHiddenExtractedSelectedNodes = ExtractNodesSimple(selectionNode, syntaxFacts).OfType().Where(n => !n.OverlapsHiddenPosition(cancellationToken)); - foreach (var nonHiddenExtractedNode in nonHiddenExtractedSelectedNodes) - { - // For selections we need to handle an edge case where only AttributeLists are within selection (e.g. `Func([|[in][out]|] arg1);`). - // In that case the smallest encompassing node is still the whole argument node but it's hard to justify showing refactorings for it - // if user selected only its attributes. - - // Selection contains only AttributeLists -> don't consider current Node - var spanWithoutAttributes = syntaxFacts.GetSpanWithoutAttributes(root, nonHiddenExtractedNode); - if (!selectionTrimmed.IntersectsWith(spanWithoutAttributes)) - { - break; - } - - AddNode(allowEmptyNodes, ref result, nonHiddenExtractedNode); - if (result.Count >= maxCount) - return; - } - - prevNode = selectionNode; - selectionNode = selectionNode.Parent; - } - while (selectionNode != null && prevNode.FullWidth() == selectionNode.FullWidth()); - } - - /// - /// Extractor function that retrieves all nodes that should be considered for extraction of given current node. - /// - /// The rationale is that when user selects e.g. entire local declaration statement [|var a = b;|] it is reasonable - /// to provide refactoring for `b` node. Similarly for other types of refactorings. - /// - /// - /// - /// Should also return given node. - /// - protected virtual IEnumerable ExtractNodesSimple(SyntaxNode? node, ISyntaxFactsService syntaxFacts) - { - if (node == null) - { - yield break; - } - - // First return the node itself so that it is considered - yield return node; - - // REMARKS: - // The set of currently attempted extractions is in no way exhaustive and covers only cases - // that were found to be relevant for refactorings that were moved to `TryGetSelectedNodeAsync`. - // Feel free to extend it / refine current heuristics. - - // `var a = b;` | `var a = b`; - if (syntaxFacts.IsLocalDeclarationStatement(node) || syntaxFacts.IsLocalDeclarationStatement(node.Parent)) - { - var localDeclarationStatement = syntaxFacts.IsLocalDeclarationStatement(node) ? node : node.Parent!; - - // Check if there's only one variable being declared, otherwise following transformation - // would go through which isn't reasonable since we can't say the first one specifically - // is wanted. - // `var a = 1, `c = 2, d = 3`; - // -> `var a = 1`, c = 2, d = 3; - var variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement); - if (variables.Count == 1) - { - var declaredVariable = variables.First(); - - // -> `a = b` - yield return declaredVariable; - - // -> `b` - var initializer = syntaxFacts.GetInitializerOfVariableDeclarator(declaredVariable); - if (initializer != null) - { - var value = syntaxFacts.GetValueOfEqualsValueClause(initializer); - if (value != null) - { - yield return value; - } - } - } - } - - // var `a = b`; - if (syntaxFacts.IsVariableDeclarator(node)) - { - // -> `b` - var initializer = syntaxFacts.GetInitializerOfVariableDeclarator(node); - if (initializer != null) - { - var value = syntaxFacts.GetValueOfEqualsValueClause(initializer); - if (value != null) - { - yield return value; - } - } - } - - // `a = b;` - // -> `b` - if (syntaxFacts.IsSimpleAssignmentStatement(node)) - { - syntaxFacts.GetPartsOfAssignmentExpressionOrStatement(node, out _, out _, out var rightSide); - yield return rightSide; - } - - // `a();` - // -> a() - if (syntaxFacts.IsExpressionStatement(node)) - { - yield return syntaxFacts.GetExpressionOfExpressionStatement(node); - } - - // `a()`; - // -> `a();` - if (syntaxFacts.IsExpressionStatement(node.Parent)) - { - yield return node.Parent; - } - } - - /// - /// Extractor function that checks and retrieves all nodes current location is in a header. - /// - protected virtual IEnumerable ExtractNodesInHeader(SyntaxNode root, int location, IHeaderFactsService headerFacts) - { - // Header: [Test] `public int a` { get; set; } - if (headerFacts.IsOnPropertyDeclarationHeader(root, location, out var propertyDeclaration)) - yield return propertyDeclaration; - - // Header: public C([Test]`int a = 42`) {} - if (headerFacts.IsOnParameterHeader(root, location, out var parameter)) - yield return parameter; - - // Header: `public I.C([Test]int a = 42)` {} - if (headerFacts.IsOnMethodHeader(root, location, out var method)) - yield return method; - - // Header: `static C([Test]int a = 42)` {} - if (headerFacts.IsOnLocalFunctionHeader(root, location, out var localFunction)) - yield return localFunction; - - // Header: `var a = `3,` b = `5,` c = `7 + 3``; - if (headerFacts.IsOnLocalDeclarationHeader(root, location, out var localDeclaration)) - yield return localDeclaration; - - // Header: `if(...)`{ }; - if (headerFacts.IsOnIfStatementHeader(root, location, out var ifStatement)) - yield return ifStatement; - - // Header: `foreach (var a in b)` { } - if (headerFacts.IsOnForeachHeader(root, location, out var foreachStatement)) - yield return foreachStatement; - - if (headerFacts.IsOnTypeHeader(root, location, out var typeDeclaration)) - yield return typeDeclaration; - } - - private static void AddNodesDeepIn( - ParsedDocument document, - int position, - bool allowEmptyNodes, - int maxCount, - ref TemporaryArray result, - CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode - { - // If we're deep inside we don't have to deal with being on edges (that gets dealt by TryGetSelectedNodeAsync) - // -> can simply FindToken -> proceed testing its ancestors - var root = document.Root; - if (root is null) - throw new NotSupportedException(WorkspaceExtensionsResources.Document_does_not_support_syntax_trees); - - var token = root.FindTokenOnRightOfPosition(position, true); - - // traverse upwards and add all parents if of correct type - var ancestor = token.Parent; - while (ancestor != null) - { - if (ancestor is TSyntaxNode typedAncestor) - { - var sourceText = document.Text; - - var argumentStartLine = sourceText.Lines.GetLineFromPosition(typedAncestor.Span.Start).LineNumber; - var caretLine = sourceText.Lines.GetLineFromPosition(position).LineNumber; - - if (argumentStartLine == caretLine && !typedAncestor.OverlapsHiddenPosition(cancellationToken)) - { - AddNode(allowEmptyNodes, ref result, typedAncestor); - if (result.Count >= maxCount) - return; - } - else if (argumentStartLine < caretLine) - { - // higher level nodes will have Span starting at least on the same line -> can bail out - return; - } - } - - ancestor = ancestor.Parent; - } - } - - private static void AddNonHiddenCorrectTypeNodes( - IEnumerable nodes, bool allowEmptyNodes, int maxCount, ref TemporaryArray result, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode - { - foreach (var node in nodes) - { - if (node is TSyntaxNode typedNode && - !node.OverlapsHiddenPosition(cancellationToken)) - { - AddNode(allowEmptyNodes, ref result, typedNode); - if (result.Count >= maxCount) - return; - } - } - } + public void AddRelevantNodes(SourceText sourceText, SyntaxNode root, TextSpan selection, bool allowEmptyNodes, int maxCount, ref TemporaryArray result, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + => this.RefactoringHelpers.AddRelevantNodes(sourceText, root, selection, allowEmptyNodes, maxCount, ref result, cancellationToken); public bool IsOnTypeHeader(SyntaxNode root, int position, bool fullHeader, [NotNullWhen(true)] out SyntaxNode? typeDeclaration) - => HeaderFacts.IsOnTypeHeader(root, position, fullHeader, out typeDeclaration); + => this.RefactoringHelpers.IsOnTypeHeader(root, position, fullHeader, out typeDeclaration); public bool IsOnPropertyDeclarationHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? propertyDeclaration) - => HeaderFacts.IsOnPropertyDeclarationHeader(root, position, out propertyDeclaration); + => this.RefactoringHelpers.IsOnPropertyDeclarationHeader(root, position, out propertyDeclaration); public bool IsOnParameterHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? parameter) - => HeaderFacts.IsOnParameterHeader(root, position, out parameter); + => this.RefactoringHelpers.IsOnParameterHeader(root, position, out parameter); public bool IsOnMethodHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? method) - => HeaderFacts.IsOnMethodHeader(root, position, out method); + => this.RefactoringHelpers.IsOnMethodHeader(root, position, out method); public bool IsOnLocalFunctionHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? localFunction) - => HeaderFacts.IsOnLocalFunctionHeader(root, position, out localFunction); + => this.RefactoringHelpers.IsOnLocalFunctionHeader(root, position, out localFunction); public bool IsOnLocalDeclarationHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? localDeclaration) - => HeaderFacts.IsOnLocalDeclarationHeader(root, position, out localDeclaration); + => this.RefactoringHelpers.IsOnLocalDeclarationHeader(root, position, out localDeclaration); public bool IsOnIfStatementHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? ifStatement) - => HeaderFacts.IsOnIfStatementHeader(root, position, out ifStatement); + => this.RefactoringHelpers.IsOnIfStatementHeader(root, position, out ifStatement); public bool IsOnWhileStatementHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? whileStatement) - => HeaderFacts.IsOnWhileStatementHeader(root, position, out whileStatement); + => this.RefactoringHelpers.IsOnWhileStatementHeader(root, position, out whileStatement); public bool IsOnForeachHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? foreachStatement) - => HeaderFacts.IsOnForeachHeader(root, position, out foreachStatement); + => this.RefactoringHelpers.IsOnForeachHeader(root, position, out foreachStatement); } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/CodeRefactoringHelpers.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/CodeRefactoringHelpers.cs index 54a227a9b3410..89c6875f20d71 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/CodeRefactoringHelpers.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/CodeRefactoringHelpers.cs @@ -93,29 +93,4 @@ public static bool IsNodeUnderselected(SyntaxNode? node, TextSpan selection) // but fires up e.g.: `1 + [|2 + 3|]`. return beginningNode.Span.End <= selection.Start || endNode.Span.Start >= selection.End; } - - /// - /// Trims leading and trailing whitespace from . - /// - /// - /// Returns unchanged in case . - /// Returns empty Span with original in case it contains only whitespace. - /// - public static TextSpan GetTrimmedTextSpan(ParsedDocument document, TextSpan span) - { - if (span.IsEmpty) - return span; - - var sourceText = document.Text; - var start = span.Start; - var end = span.End; - - while (start < end && char.IsWhiteSpace(sourceText[end - 1])) - end--; - - while (start < end && char.IsWhiteSpace(sourceText[start])) - start++; - - return TextSpan.FromBounds(start, end); - } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/IRefactoringHelpersService.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/IRefactoringHelpersService.cs index abdf86630343b..5b959319b46dd 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/IRefactoringHelpersService.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/IRefactoringHelpersService.cs @@ -2,56 +2,22 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Diagnostics.CodeAnalysis; using System.Threading; using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.CodeRefactorings; -/// -/// Contains helpers related to asking intuitive semantic questions about a users intent -/// based on the position of their caret or span of their selection. -/// -internal interface IRefactoringHelpersService : IHeaderFactsService, ILanguageService +internal interface IRefactoringHelpersService : IRefactoringHelpers, ILanguageService { - /// - /// True if the user is on a blank line where a member could go inside a type declaration. - /// This will be between members and not ever inside a member. - /// - bool IsBetweenTypeMembers(SourceText sourceText, SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? typeDeclaration); +} - /// - /// - /// Returns an array of instances for refactoring given specified selection - /// in document. determines if the returned nodes will can have empty spans - /// or not. - /// - /// - /// A instance is returned if: - Selection is zero-width and inside/touching - /// a Token with direct parent of type . - Selection is zero-width and - /// touching a Token whose ancestor of type ends/starts precisely on current - /// selection. - Selection is zero-width and in whitespace that corresponds to a Token whose direct ancestor is - /// of type of type . - Selection is zero-width and in a header (defined by - /// ISyntaxFacts helpers) of an node of type of type . - Token whose direct - /// parent of type is selected. - Selection is zero-width and wanted node is - /// an expression / argument with selection within such syntax node (arbitrarily deep) on its first line. - - /// Whole node of a type is selected. - /// - /// - /// Attempts extracting a Node of type for each Node it considers (see - /// above). E.g. extracts initializer expressions from declarations and assignments, Property declaration from - /// any header node, etc. - /// - /// - /// Note: this function trims all whitespace from both the beginning and the end of given . The trimmed version is then used to determine relevant . It also - /// handles incomplete selections of tokens gracefully. Over-selection containing leading comments is also - /// handled correctly. - /// - /// - void AddRelevantNodes( - ParsedDocument document, TextSpan selection, bool allowEmptyNodes, int maxCount, ref TemporaryArray result, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode; +internal static class IRefactoringHelpersServiceExtensions +{ + public static void AddRelevantNodes( + this IRefactoringHelpersService service, ParsedDocument document, TextSpan selection, bool allowEmptyNodes, int maxCount, ref TemporaryArray result, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode + { + service.AddRelevantNodes(document.Text, document.Root, selection, allowEmptyNodes, maxCount, ref result, cancellationToken); + } } From 3edd6ceaf6053833dc30488591c36c8ffd8ea845 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Tue, 17 Jun 2025 11:22:13 -0700 Subject: [PATCH 2/8] VB side --- .../VisualBasicRefactoringHelpers.vb | 106 ++++++++++++++++++ .../VisualBasicCompilerExtensions.projitems | 4 + .../CodeRefactoringHelpers.cs | 2 +- .../VisualBasicRefactoringHelpersService.vb | 90 +-------------- 4 files changed, 113 insertions(+), 89 deletions(-) create mode 100644 src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/RefactoringHelpers/VisualBasicRefactoringHelpers.vb diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/RefactoringHelpers/VisualBasicRefactoringHelpers.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/RefactoringHelpers/VisualBasicRefactoringHelpers.vb new file mode 100644 index 0000000000000..db77a9665a326 --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/Services/RefactoringHelpers/VisualBasicRefactoringHelpers.vb @@ -0,0 +1,106 @@ +' Licensed to the .NET Foundation under one or more agreements. +' The .NET Foundation licenses this file to you under the MIT license. +' See the LICENSE file in the project root for more information. + +Imports Microsoft.CodeAnalysis.CodeRefactorings +Imports Microsoft.CodeAnalysis.LanguageService +Imports Microsoft.CodeAnalysis.Text +Imports Microsoft.CodeAnalysis.VisualBasic.LanguageService +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax + +Namespace Microsoft.CodeAnalysis.VisualBasic.CodeRefactorings + Friend NotInheritable Class VisualBasicRefactoringHelpers + Inherits AbstractRefactoringHelpers(Of ExpressionSyntax, ArgumentSyntax, ExpressionStatementSyntax) + + Public Shared ReadOnly Instance As New VisualBasicRefactoringHelpers() + + Private Sub New() + End Sub + + Protected Overrides ReadOnly Property HeaderFacts As IHeaderFacts = VisualBasicHeaderFacts.Instance + Protected Overrides ReadOnly Property SyntaxFacts As ISyntaxFacts = VisualBasicSyntaxFacts.Instance + + Public Overrides Function IsBetweenTypeMembers(sourceText As SourceText, root As SyntaxNode, position As Integer, ByRef typeDeclaration As SyntaxNode) As Boolean + Dim token = root.FindToken(position) + Dim typeDecl = token.GetAncestor(Of TypeBlockSyntax) + typeDeclaration = typeDecl + + If typeDecl IsNot Nothing Then + Dim start = If(typeDecl.Implements.LastOrDefault()?.Span.End, + If(typeDecl.Inherits.LastOrDefault()?.Span.End, + typeDecl.BlockStatement.Span.End)) + + If position >= start AndAlso + position <= typeDecl.EndBlockStatement.Span.Start Then + + Dim line = sourceText.Lines.GetLineFromPosition(position) + If Not line.IsEmptyOrWhitespace() Then + Return False + End If + + Dim member = typeDecl.Members.FirstOrDefault(Function(d) d.FullSpan.Contains(position)) + If member Is Nothing Then + ' There are no members, Or we're after the last member. + Return True + Else + ' We're within a member. Make sure we're in the leading whitespace of + ' the member. + If position < member.SpanStart Then + For Each trivia In member.GetLeadingTrivia() + If Not trivia.IsWhitespaceOrEndOfLine() Then + Return False + End If + + If trivia.FullSpan.Contains(position) Then + Return True + End If + Next + End If + End If + End If + End If + + Return False + End Function + + Protected Overrides Iterator Function ExtractNodesSimple(node As SyntaxNode, syntaxFacts As ISyntaxFacts) As IEnumerable(Of SyntaxNode) + For Each baseExtraction In MyBase.ExtractNodesSimple(node, syntaxFacts) + Yield baseExtraction + Next + + ' VB's arguments can have identifiers nested in ModifiedArgument -> we want + ' identifiers to represent parent node -> need to extract. + If IsIdentifierOfParameter(node) Then + Yield node.Parent + End If + + ' In VB Statement both for/foreach are split into Statement (header) and the rest + ' selecting the header should still count for the whole blockSyntax + If TypeOf node Is ForEachStatementSyntax And TypeOf node.Parent Is ForEachBlockSyntax Then + Dim foreachStatement = CType(node, ForEachStatementSyntax) + Yield foreachStatement.Parent + End If + + If TypeOf node Is ForStatementSyntax And TypeOf node.Parent Is ForBlockSyntax Then + Dim forStatement = CType(node, ForStatementSyntax) + Yield forStatement.Parent + End If + + If TypeOf node Is VariableDeclaratorSyntax Then + Dim declarator = CType(node, VariableDeclaratorSyntax) + If TypeOf declarator.Parent Is LocalDeclarationStatementSyntax Then + Dim localDeclarationStatement = CType(declarator.Parent, LocalDeclarationStatementSyntax) + ' Only return the whole localDeclarationStatement if there's just one declarator with just one name + If localDeclarationStatement.Declarators.Count = 1 And localDeclarationStatement.Declarators.First().Names.Count = 1 Then + Yield localDeclarationStatement + End If + End If + End If + + End Function + + Public Shared Function IsIdentifierOfParameter(node As SyntaxNode) As Boolean + Return (TypeOf node Is ModifiedIdentifierSyntax) AndAlso (TypeOf node.Parent Is ParameterSyntax) AndAlso (CType(node.Parent, ParameterSyntax).Identifier Is node) + End Function + End Class +End Namespace diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/VisualBasicCompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/VisualBasicCompilerExtensions.projitems index b927e9d48a300..7b009805d3f93 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/VisualBasicCompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/VisualBasicCompilerExtensions.projitems @@ -30,6 +30,7 @@ + @@ -60,4 +61,7 @@ + + + \ No newline at end of file diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/CodeRefactoringHelpers.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/CodeRefactoringHelpers.cs index 89c6875f20d71..3caf656e1fbde 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/CodeRefactoringHelpers.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/CodeRefactoringHelpers.cs @@ -24,7 +24,7 @@ internal static class CodeRefactoringHelpers /// name="selection"/> is treated more as a caret location. /// /// - /// It's intended to be used in conjunction with that, for + /// It's intended to be used in conjunction with that, for /// non-empty selections, returns the smallest encompassing node. A node that can, for certain refactorings, be too /// large given user-selection even though it is the smallest that can be retrieved. /// diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/CodeRefactorings/VisualBasicRefactoringHelpersService.vb b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/CodeRefactorings/VisualBasicRefactoringHelpersService.vb index 0539a9d7f2ad4..113c5ea4e6e56 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/CodeRefactorings/VisualBasicRefactoringHelpersService.vb +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/VisualBasic/CodeRefactorings/VisualBasicRefactoringHelpersService.vb @@ -4,11 +4,8 @@ Imports System.Composition Imports Microsoft.CodeAnalysis.CodeRefactorings -Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Imports Microsoft.CodeAnalysis.Host.Mef -Imports Microsoft.CodeAnalysis.LanguageService -Imports Microsoft.CodeAnalysis.VisualBasic.LanguageService -Imports Microsoft.CodeAnalysis.Text +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax Namespace Microsoft.CodeAnalysis.VisualBasic.CodeRefactorings @@ -20,89 +17,6 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.CodeRefactorings Public Sub New() End Sub - Protected Overrides ReadOnly Property HeaderFacts As IHeaderFacts = VisualBasicHeaderFacts.Instance - - Public Overrides Function IsBetweenTypeMembers(sourceText As SourceText, root As SyntaxNode, position As Integer, ByRef typeDeclaration As SyntaxNode) As Boolean - Dim token = root.FindToken(position) - Dim typeDecl = token.GetAncestor(Of TypeBlockSyntax) - typeDeclaration = typeDecl - - If typeDecl IsNot Nothing Then - Dim start = If(typeDecl.Implements.LastOrDefault()?.Span.End, - If(typeDecl.Inherits.LastOrDefault()?.Span.End, - typeDecl.BlockStatement.Span.End)) - - If position >= start AndAlso - position <= typeDecl.EndBlockStatement.Span.Start Then - - Dim line = sourceText.Lines.GetLineFromPosition(position) - If Not line.IsEmptyOrWhitespace() Then - Return False - End If - - Dim member = typeDecl.Members.FirstOrDefault(Function(d) d.FullSpan.Contains(position)) - If member Is Nothing Then - ' There are no members, Or we're after the last member. - Return True - Else - ' We're within a member. Make sure we're in the leading whitespace of - ' the member. - If position < member.SpanStart Then - For Each trivia In member.GetLeadingTrivia() - If Not trivia.IsWhitespaceOrEndOfLine() Then - Return False - End If - - If trivia.FullSpan.Contains(position) Then - Return True - End If - Next - End If - End If - End If - End If - - Return False - End Function - - Protected Overrides Iterator Function ExtractNodesSimple(node As SyntaxNode, syntaxFacts As ISyntaxFactsService) As IEnumerable(Of SyntaxNode) - For Each baseExtraction In MyBase.ExtractNodesSimple(node, syntaxFacts) - Yield baseExtraction - Next - - ' VB's arguments can have identifiers nested in ModifiedArgument -> we want - ' identifiers to represent parent node -> need to extract. - If IsIdentifierOfParameter(node) Then - Yield node.Parent - End If - - ' In VB Statement both for/foreach are split into Statement (header) and the rest - ' selecting the header should still count for the whole blockSyntax - If TypeOf node Is ForEachStatementSyntax And TypeOf node.Parent Is ForEachBlockSyntax Then - Dim foreachStatement = CType(node, ForEachStatementSyntax) - Yield foreachStatement.Parent - End If - - If TypeOf node Is ForStatementSyntax And TypeOf node.Parent Is ForBlockSyntax Then - Dim forStatement = CType(node, ForStatementSyntax) - Yield forStatement.Parent - End If - - If TypeOf node Is VariableDeclaratorSyntax Then - Dim declarator = CType(node, VariableDeclaratorSyntax) - If TypeOf declarator.Parent Is LocalDeclarationStatementSyntax Then - Dim localDeclarationStatement = CType(declarator.Parent, LocalDeclarationStatementSyntax) - ' Only return the whole localDeclarationStatement if there's just one declarator with just one name - If localDeclarationStatement.Declarators.Count = 1 And localDeclarationStatement.Declarators.First().Names.Count = 1 Then - Yield localDeclarationStatement - End If - End If - End If - - End Function - - Public Shared Function IsIdentifierOfParameter(node As SyntaxNode) As Boolean - Return (TypeOf node Is ModifiedIdentifierSyntax) AndAlso (TypeOf node.Parent Is ParameterSyntax) AndAlso (CType(node.Parent, ParameterSyntax).Identifier Is node) - End Function + Protected Overrides ReadOnly Property RefactoringHelpers As IRefactoringHelpers = VisualBasicRefactoringHelpers.Instance End Class End Namespace From 9a26edcc53654243dcbcd758465c051ce2f03abf Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Tue, 17 Jun 2025 11:39:57 -0700 Subject: [PATCH 3/8] Move type --- .../CodeRefactorings/CodeRefactoringContextExtensions.cs | 9 +++++---- .../Workspace/Core/WorkspaceExtensions.projitems | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) rename src/{Features/Core/Portable => Workspaces/SharedUtilitiesAndExtensions/Workspace/Core}/CodeRefactorings/CodeRefactoringContextExtensions.cs (98%) diff --git a/src/Features/Core/Portable/CodeRefactorings/CodeRefactoringContextExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/CodeRefactoringContextExtensions.cs similarity index 98% rename from src/Features/Core/Portable/CodeRefactorings/CodeRefactoringContextExtensions.cs rename to src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/CodeRefactoringContextExtensions.cs index 5cd4ae09ae54d..c26aff2d22b36 100644 --- a/src/Features/Core/Portable/CodeRefactorings/CodeRefactoringContextExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/CodeRefactoringContextExtensions.cs @@ -25,14 +25,15 @@ public static void RegisterRefactorings( { foreach (var action in actions) { +#if WORKSPACE if (applicableToSpan != null) { context.RegisterRefactoring(action, applicableToSpan.Value); + continue; } - else - { - context.RegisterRefactoring(action); - } +#endif + + context.RegisterRefactoring(action); } } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/WorkspaceExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/WorkspaceExtensions.projitems index f0502fd1b4058..c97473fcaf65a 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/WorkspaceExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/WorkspaceExtensions.projitems @@ -14,6 +14,7 @@ + From bbe7943087268e33ea7c895935618fec13894990 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Tue, 17 Jun 2025 11:41:53 -0700 Subject: [PATCH 4/8] Update src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems --- .../Compiler/CSharp/CSharpCompilerExtensions.projitems | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems index 340a732367ac5..e177d0d482783 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems @@ -97,7 +97,7 @@ - + From 5341b77907beaff87f8b62571641e626d9a01998 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Tue, 17 Jun 2025 11:46:05 -0700 Subject: [PATCH 5/8] workaround --- .../Workspace/Core/CodeRefactorings/CodeRefactoringHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/CodeRefactoringHelpers.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/CodeRefactoringHelpers.cs index 3caf656e1fbde..03dc9682bd290 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/CodeRefactoringHelpers.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeRefactorings/CodeRefactoringHelpers.cs @@ -24,7 +24,7 @@ internal static class CodeRefactoringHelpers /// name="selection"/> is treated more as a caret location. /// /// - /// It's intended to be used in conjunction with that, for + /// It's intended to be used in conjunction with IRefactoringHelpers.AddRelevantNodes that, for /// non-empty selections, returns the smallest encompassing node. A node that can, for certain refactorings, be too /// large given user-selection even though it is the smallest that can be retrieved. /// From 177a46ca5ff1a47e40a8036b2429d64082c4a9ec Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Tue, 17 Jun 2025 11:46:43 -0700 Subject: [PATCH 6/8] Update src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems --- .../Compiler/CSharp/CSharpCompilerExtensions.projitems | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems index e177d0d482783..fd9f9aba7d1f9 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/CSharpCompilerExtensions.projitems @@ -132,7 +132,4 @@ - - - \ No newline at end of file From bc1e7515ba38cc7d72a5d7c97ace8a3e76a29de6 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Tue, 17 Jun 2025 12:08:44 -0700 Subject: [PATCH 7/8] Update src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems --- .../Compiler/Core/CompilerExtensions.projitems | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems index 29564fc49f112..322711f9dab5a 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems @@ -367,8 +367,8 @@ - - + + From 91ff4b4390b8a461673a68dbe90f6ba52a4106ed Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Tue, 17 Jun 2025 12:09:07 -0700 Subject: [PATCH 8/8] Update src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/VisualBasicCompilerExtensions.projitems --- .../VisualBasic/VisualBasicCompilerExtensions.projitems | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/VisualBasicCompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/VisualBasicCompilerExtensions.projitems index 7b009805d3f93..3eff7ff7534c1 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/VisualBasicCompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/VisualBasic/VisualBasicCompilerExtensions.projitems @@ -30,7 +30,7 @@ - +