private async Task <bool> TryInitializeAsync( SemanticDocument document, TextSpan textSpan, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); Expression = await document.Document.TryGetRelevantNodeAsync <TExpressionSyntax>(textSpan, cancellationToken).ConfigureAwait(false); if (Expression == null || CodeRefactoringHelpers.IsNodeUnderselected(Expression, textSpan)) { return(false); } // Don't introduce constant for another constant. Doesn't apply to sub-expression of constant. if (IsInitializerOfConstant(document, Expression)) { return(false); } var expressionType = Document.SemanticModel.GetTypeInfo(Expression, cancellationToken).Type; if (expressionType is IErrorTypeSymbol) { return(false); } var containingType = Expression.AncestorsAndSelf() .Select(n => Document.SemanticModel.GetDeclaredSymbol(n, cancellationToken)) .OfType <INamedTypeSymbol>() .FirstOrDefault(); containingType ??= Document.SemanticModel.Compilation.ScriptClass; if (containingType == null || containingType.TypeKind == TypeKind.Interface) { return(false); } if (!CanIntroduceVariable(textSpan.IsEmpty, cancellationToken)) { return(false); } IsConstant = IsExpressionConstant(Document, Expression, _service, cancellationToken); // Note: the ordering of these clauses are important. They go, generally, from // innermost to outermost order. if (IsInQueryContext(cancellationToken)) { if (CanGenerateInto <TQueryExpressionSyntax>(cancellationToken)) { InQueryContext = true; return(true); } return(false); } if (IsInConstructorInitializerContext(cancellationToken)) { if (CanGenerateInto <TTypeDeclarationSyntax>(cancellationToken)) { InConstructorInitializerContext = true; return(true); } return(false); } var enclosingBlocks = _service.GetContainingExecutableBlocks(Expression); if (enclosingBlocks.Any()) { // If we're inside a block, then don't even try the other options (like field, // constructor initializer, etc.). This is desirable behavior. If we're in a // block in a field, then we're in a lambda, and we want to offer to generate // a local, and not a field. if (IsInBlockContext(cancellationToken)) { InBlockContext = true; return(true); } return(false); } // NOTE: All checks from this point forward are intentionally ordered to be AFTER the check for Block Context. // If we are inside a block within an Expression bodied member we should generate inside the block, // instead of rewriting a concise expression bodied member to its equivalent that has a body with a block. if (_service.IsInExpressionBodiedMember(Expression)) { if (CanGenerateInto <TTypeDeclarationSyntax>(cancellationToken)) { InExpressionBodiedMemberContext = true; return(true); } return(false); } if (_service.IsInAutoPropertyInitializer(Expression)) { if (CanGenerateInto <TTypeDeclarationSyntax>(cancellationToken)) { InAutoPropertyInitializerContext = true; return(true); } return(false); } if (CanGenerateInto <TTypeDeclarationSyntax>(cancellationToken)) { if (IsInParameterContext(cancellationToken)) { InParameterContext = true; return(true); } else if (IsInFieldContext(cancellationToken)) { InFieldContext = true; return(true); } else if (IsInAttributeContext()) { InAttributeContext = true; return(true); } } return(false);
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { var(document, textSpan, cancellationToken) = context; var possibleExpressions = await context.GetRelevantNodesAsync <TExpressionSyntax>().ConfigureAwait(false); var syntaxFacts = document.GetLanguageService <ISyntaxFactsService>(); var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); // let's take the largest (last) StringConcat we can given current textSpan var top = possibleExpressions .Where(expr => IsStringConcat(syntaxFacts, expr, semanticModel, cancellationToken)) .LastOrDefault(); if (top == null) { return; } // if there is a const keyword, the refactoring shouldn't show because interpolated string is not const string var declarator = top.FirstAncestorOrSelf <SyntaxNode>(syntaxFacts.IsVariableDeclarator); if (declarator != null) { var generator = SyntaxGenerator.GetGenerator(document); if (generator.GetModifiers(declarator).IsConst) { return; } } // Currently we can concatenate only full subtrees. Therefore we can't support arbitrary selection. We could // theoretically support selecting the selections that correspond to full sub-trees (e.g. prefixes of // correct length but from UX point of view that it would feel arbitrary). // Thus, we only support selection that takes the whole topmost expression. It breaks some leniency around under-selection // but it's the best solution so far. if (CodeRefactoringHelpers.IsNodeUnderselected(top, textSpan) || IsStringConcat(syntaxFacts, top.Parent, semanticModel, cancellationToken)) { return; } // Now walk down the concatenation collecting all the pieces that we are // concatenating. using var _ = ArrayBuilder <SyntaxNode> .GetInstance(out var pieces); CollectPiecesDown(syntaxFacts, pieces, top, semanticModel, cancellationToken); var stringLiterals = pieces .Where(x => syntaxFacts.IsStringLiteralExpression(x) || syntaxFacts.IsCharacterLiteralExpression(x)) .ToImmutableArray(); // If the entire expression is just concatenated strings, then don't offer to // make an interpolated string. The user likely manually split this for // readability. if (stringLiterals.Length == pieces.Count) { return; } var isVerbatimStringLiteral = false; if (stringLiterals.Length > 0) { // Make sure that all the string tokens we're concatenating are the same type // of string literal. i.e. if we have an expression like: @" "" " + " \r\n " // then we don't merge this. We don't want to be munging different types of // escape sequences in these strings, so we only support combining the string // tokens if they're all the same type. var firstStringToken = stringLiterals[0].GetFirstToken(); isVerbatimStringLiteral = syntaxFacts.IsVerbatimStringLiteral(firstStringToken); if (stringLiterals.Any( lit => isVerbatimStringLiteral != syntaxFacts.IsVerbatimStringLiteral(lit.GetFirstToken()))) { return; } } var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var interpolatedString = CreateInterpolatedString(document, isVerbatimStringLiteral, pieces); context.RegisterRefactoring( new MyCodeAction( _ => UpdateDocumentAsync(document, root, top, interpolatedString)), top.Span); }
public async Task <ImmutableArray <TSyntaxNode> > GetRelevantNodesAsync <TSyntaxNode>( Document document, TextSpan selectionRaw, 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 = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); if (root == null) { return(ImmutableArray <TSyntaxNode> .Empty); } var syntaxFacts = document.Project.LanguageServices.GetRequiredService <ISyntaxFactsService>(); var selectionTrimmed = await CodeRefactoringHelpers.GetTrimmedTextSpan(document, selectionRaw, cancellationToken).ConfigureAwait(false); // 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(ImmutableArray <TSyntaxNode> .Empty); } using var relevantNodesBuilderDisposer = ArrayBuilder <TSyntaxNode> .GetInstance(out var relevantNodesBuilder); // 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, relevantNodesBuilder, cancellationToken); } else { // 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). // First we need to get tokens we might potentially be touching, tokenToRightOrIn and tokenToLeft. var(tokenToRightOrIn, tokenToLeft, location) = await GetTokensToRightOrInToLeftAndUpdatedLocation( document, root, selectionTrimmed, cancellationToken).ConfigureAwait(false); // 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, syntaxFacts), relevantNodesBuilder, cancellationToken); // Add Nodes for touching tokens as described above. AddNodesForTokenToRightOrIn(syntaxFacts, root, relevantNodesBuilder, location, tokenToRightOrIn, cancellationToken); AddNodesForTokenToLeft(syntaxFacts, relevantNodesBuilder, location, tokenToLeft, cancellationToken); // 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 <TSyntaxNode>()) { // Reason to treat Arguments (and potentially others) as Expression-like: // https://github.com/dotnet/roslyn/pull/37295#issuecomment-516145904 await AddNodesDeepIn(document, location, relevantNodesBuilder, cancellationToken).ConfigureAwait(false); } } return(relevantNodesBuilder.ToImmutable()); }
protected async Task <SyntaxNode> TryGetSelectedNodeAsync( Document document, TextSpan selectionRaw, Func <SyntaxNode, bool> predicate, Func <SyntaxNode, ISyntaxFactsService, IEnumerable <SyntaxNode> > extractNodes, Func <SyntaxNode, int, ISyntaxFactsService, IEnumerable <SyntaxNode> > extracNodestIfInHeader, CancellationToken cancellationToken) { // 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 = document.GetLanguageService <ISyntaxFactsService>(); var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var selectionTrimmed = await CodeRefactoringHelpers.GetTrimmedTextSpan(document, selectionRaw, cancellationToken).ConfigureAwait(false); // 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(null); } // Every time a Node is considered by an extractNodes method is called to check & potentially return 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;` // // In addition to per-node extractions we 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. // Handle selections: // - 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 and testing predicate // - Handles situations where: // - Token with wanted Node as direct parent is selected (e.g. IdentifierToken for LocalFunctionStatement: `C [|Fun|]() {}`) // - Most/the whole wanted Node is selected (e.g. `C [|Fun() {}|]` var selectionNode = root.FindNode(selectionTrimmed, getInnermostNodeForTie: true); var prevNode = selectionNode; do { var acceptedNode = extractNodes(selectionNode, syntaxFacts).FirstOrDefault(predicate); if (acceptedNode != null) { // 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 = GetSpanWithoutAttributes(acceptedNode, root, syntaxFacts); if (!selectionTrimmed.IntersectsWith(spanWithoutAttributes)) { break; } return(acceptedNode); } prevNode = selectionNode; selectionNode = selectionNode.Parent; }while (selectionNode != null && prevNode.FullWidth() == selectionNode.FullWidth()); // 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. // // Whether we have selection of 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`. // // 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. if (!selectionRaw.IsEmpty) { return(null); } // get Token for current location var location = selectionTrimmed.Start; var tokenOnLocation = root.FindToken(location); // Gets a token that is directly to the right of current location or that encompasses current location (`[||]tokenToRightOrIn` or `tok[||]enToRightOrIn`) var tokenToRightOrIn = 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 (tokenToRightOrIn == default || tokenToRightOrIn.FullSpan.Start == location) { var tokenPreLocation = (tokenOnLocation.Span.End == location) ? tokenOnLocation : tokenOnLocation.GetPreviousToken(); tokenToLeft = (tokenPreLocation.Span.End == location) ? tokenPreLocation : 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 && tokenToRightOrIn == default) { var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); var sourceText = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); // 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 if (Math.Abs(tokenLine.LineNumber - locationLine.LineNumber) <= 1) { // 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. // tokenOnLocation: token in whose trivia location is at if (tokenOnLocation.Span.Start >= location) { tokenToRightOrIn = tokenOnLocation; location = tokenToRightOrIn.Span.Start; } else { tokenToLeft = tokenOnLocation; location = tokenToLeft.Span.End; } } } // First check if we're in a header of some higher-level node what would pass predicate & if we are -> return it // We can't check any sooner because the code above (that figures out tokenToLeft, ...) can change current // `location`. var acceptedHeaderNode = extracNodestIfInHeader(root, location, syntaxFacts).FirstOrDefault(predicate); if (acceptedHeaderNode != null) { return(acceptedHeaderNode); } if (tokenToRightOrIn != default) { var rightNode = tokenOnLocation.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) var acceptedNode = extractNodes(rightNode, syntaxFacts).FirstOrDefault(predicate); if (acceptedNode != null) { return(acceptedNode); } 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 = GetSpanWithoutAttributes(rightNode, root, syntaxFacts); if (rightNodeSpanWithoutAttributes.Start != location) { break; } } }while (true); } if (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) var acceptedNode = extractNodes(leftNode, syntaxFacts).FirstOrDefault(predicate); if (acceptedNode != null) { return(acceptedNode); } leftNode = leftNode.Parent; if (leftNode == null || leftNode.GetLastToken().Span.End != location) { break; } }while (true); } // nothing found -> return null return(null); }
public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context) { var(document, textSpan, cancellationToken) = context; if (document.Project.Solution.Workspace.Kind == WorkspaceKind.MiscellaneousFiles) { return; } var expression = await document.TryGetRelevantNodeAsync <TExpressionSyntax>(textSpan, cancellationToken).ConfigureAwait(false); if (expression == null || CodeRefactoringHelpers.IsNodeUnderselected(expression, textSpan)) { return; } var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); var expressionType = semanticModel.GetTypeInfo(expression, cancellationToken).Type; if (expressionType is null or IErrorTypeSymbol) { return; } var syntaxFacts = document.GetRequiredLanguageService <ISyntaxFactsService>(); // Need to special case for expressions that are contained within a parameter // because it is technically "contained" within a method, but an expression in a parameter does not make // sense to introduce. var parameterNode = expression.FirstAncestorOrSelf <SyntaxNode>(node => syntaxFacts.IsParameter(node)); if (parameterNode is not null) { return; } // Need to special case for highlighting of method types because they are also "contained" within a method, // but it does not make sense to introduce a parameter in that case. if (syntaxFacts.IsInNamespaceOrTypeContext(expression)) { return; } // Need to special case for expressions whose direct parent is a MemberAccessExpression since they will // never introduce a parameter that makes sense in that case. if (syntaxFacts.IsNameOfAnyMemberAccessExpression(expression)) { return; } var generator = SyntaxGenerator.GetGenerator(document); var containingMethod = expression.FirstAncestorOrSelf <SyntaxNode>(node => generator.GetParameterListNode(node) is not null); if (containingMethod is null) { return; } var containingSymbol = semanticModel.GetDeclaredSymbol(containingMethod, cancellationToken); if (containingSymbol is not IMethodSymbol methodSymbol) { return; } var expressionSymbol = semanticModel.GetSymbolInfo(expression, cancellationToken).Symbol; if (expressionSymbol is IParameterSymbol parameterSymbol && parameterSymbol.ContainingSymbol.Equals(containingSymbol)) { return; } // Code actions for trampoline and overloads will not be offered if the method is a constructor. // Code actions for overloads will not be offered if the method if the method is a local function. var methodKind = methodSymbol.MethodKind; if (methodKind is not(MethodKind.Ordinary or MethodKind.LocalFunction or MethodKind.Constructor)) { return; } if (IsDestructor(methodSymbol)) { return; } var actions = await GetActionsAsync(document, expression, methodSymbol, containingMethod, context.Options, cancellationToken).ConfigureAwait(false); if (actions is null) { return; } var singleLineExpression = syntaxFacts.ConvertToSingleLine(expression); var nodeString = singleLineExpression.ToString(); if (actions.Value.actions.Length > 0) { context.RegisterRefactoring(CodeActionWithNestedActions.Create( string.Format(FeaturesResources.Introduce_parameter_for_0, nodeString), actions.Value.actions, isInlinable: false, priority: CodeActionPriority.Low), textSpan); } if (actions.Value.actionsAllOccurrences.Length > 0) { context.RegisterRefactoring(CodeActionWithNestedActions.Create( string.Format(FeaturesResources.Introduce_parameter_for_all_occurrences_of_0, nodeString), actions.Value.actionsAllOccurrences, isInlinable: false, priority: CodeActionPriority.Low), textSpan); } }
/// <summary> /// <para> /// Returns a Node for refactoring given specified selection that passes <paramref name="predicate"/> /// or null if no such instance exists. /// </para> /// <para> /// A node instance is return if: /// - Selection is zero-width and inside/touching a Token with direct parent passing <paramref name="predicate"/>. /// - Selection is zero-width and touching a Token whose ancestor Node passing <paramref name="predicate"/> ends/starts precisely on current selection. /// - Token whose direct parent passing <paramref name="predicate"/> is selected. /// - Whole node passing <paramref name="predicate"/> is selected. /// </para> /// <para> /// The <paramref name="extractNode"/> enables testing with <paramref name="predicate"/> and potentially returning Nodes /// that are under those that might be selected / considered (as described above). It is a <see cref="Func{SyntaxNode, SyntaxNode}"/> that /// should always return either given Node or a Node somewhere below it that should be tested with <paramref name="predicate"/> and /// potentially returned instead of current Node. /// </para> /// <para> /// Note: this function trims all whitespace from both the beginning and the end of given <paramref name="selection"/>. /// The trimmed version is then used to determine relevant <see cref="SyntaxNode"/>. It also handles incomplete selections /// of tokens gracefully. /// </para> /// </summary> protected async Task <SyntaxNode> TryGetSelectedNodeAsync(Document document, TextSpan selection, Predicate <SyntaxNode> predicate, Func <SyntaxNode, ISyntaxFactsService, SyntaxNode> extractNode, CancellationToken cancellationToken) { // Given selection is trimmed first to enable overselection that spans multiple lines. Since trailing whitespace ends // at newline boundary overselection 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 = document.GetLanguageService <ISyntaxFactsService>(); var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var selectionTrimmed = await CodeRefactoringHelpers.GetTrimmedTextSpan(document, selection, cancellationToken).ConfigureAwait(false); // Everytime a Node is considered by following algorithm (and tested with predicate) and the predicate fails // extractNode is called on the node and the result is tested with predicate again. If any of those succeed // a respective Node gets returned. // // That enables us to e.g. return node `b` when Node `var a = b;` is being considered without a complex (and potentially // lang. & situation dependant) into Children descending code here. We can't just try extracted Node because we might // want the whole node `var a = b;` // // See local function TryGetAcceptedNodeOrExtracted DefaultNodeExtractor for more info. // Handle selections: // - The smallest node whose FullSpan inlcudes the whole (trimmed) selection // - Travels upwards through same-sized (FullSpan) nodes, extracting and testing predicate // - Handles situations where: // - Token with wanted Node as direct parent is selected (e.g. IdentifierToken for LocalFunctionStatement: `C [|Fun|]() {}`) // - Most/the whole wanted Node is seleted (e.g. `C [|Fun() {}|]` var node = root.FindNode(selectionTrimmed, getInnermostNodeForTie: true); SyntaxNode prevNode; do { var wantedNode = TryGetAcceptedNodeOrExtracted(node, predicate, extractNode, syntaxFacts); if (wantedNode != null) { return(wantedNode); } prevNode = node; node = node.Parent; }while (node != null && prevNode.FullWidth() == node.FullWidth()); // 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 resonable 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 travelling 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) if (!selection.IsEmpty) { return(null); } // get Token for current selection (empty) location var tokenOnSelection = root.FindToken(selectionTrimmed.Start); // Gets a token that is directly to the right of current (empty) selection or that encompases current selection (`[||]tokenITORightOrIn` or `tok[||]enITORightOrIn`) var tokenToRightOrIn = tokenOnSelection.Span.Contains(selectionTrimmed.Start) ? tokenOnSelection : default; if (tokenToRightOrIn != default) { var rightNode = tokenOnSelection.Parent; do { // Consider either a Node that is: // - Parent of touched Token (selection can be within) // - Ancestor Node of such Token as long as their their span starts on selection (it's still on the edge) var wantedNode = TryGetAcceptedNodeOrExtracted(rightNode, predicate, extractNode, syntaxFacts); if (wantedNode != null) { return(wantedNode); } rightNode = rightNode?.Parent; }while (rightNode != null && rightNode.Span.Start == selection.Start); } // if the selection is inside tokenToRightOrIn -> no Token can be to Left (tokenToRightOrIn is also left from selection, e.g: `tok[||]enITORightOrIn`) if (tokenToRightOrIn != default && tokenToRightOrIn.Span.Start != selectionTrimmed.Start) { return(null); } // Token to left: a token whose span ends on current (empty) selection var tokenPreSelection = (tokenOnSelection.Span.End == selectionTrimmed.Start) ? tokenOnSelection : tokenOnSelection.GetPreviousToken(); var tokenToLeft = (tokenPreSelection.Span.End == selectionTrimmed.Start) ? tokenPreSelection : default; if (tokenToLeft != default) { var leftNode = tokenToLeft.Parent; do { // Consider either a Node that is: // - Ancestor Node of such Token as long as their their span ends on selection (it's still on the edge) var wantedNode = TryGetAcceptedNodeOrExtracted(leftNode, predicate, extractNode, syntaxFacts); if (wantedNode != null) { return(wantedNode); } leftNode = leftNode?.Parent; }while (leftNode != null && leftNode.Span.End == selection.Start); } // nothing found -> return null return(null);