|
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis;
internal static class CodeRefactoringHelpers
{
/// <summary>
/// <para>
/// Determines if a <paramref name="node"/> is under-selected given <paramref name="selection"/>.
/// </para>
/// <para>
/// Under-selection is defined as omitting whole nodes from either the beginning or the end. It can be used e.g. to
/// detect that following selection `1 + [|2 + 3|]` is under-selecting the whole expression node tree.
/// </para>
/// <para>
/// Returns false if only and precisely one <see cref="SyntaxToken"/> is selected. In that case the <paramref
/// name="selection"/> is treated more as a caret location.
/// </para>
/// <para>
/// It's intended to be used in conjunction with <see cref="IRefactoringHelpersService.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.
/// </para>
/// <para>
/// When <paramref name="selection"/> doesn't intersect the node in any way it's not considered to be
/// under-selected.
/// </para>
/// <para>
/// Null node is always considered under-selected.
/// </para>
/// </summary>
public static bool IsNodeUnderselected(SyntaxNode? node, TextSpan selection)
{
// Selection is null -> it's always under-selected
// REASON: Easier API use -> under-selected node, don't work on it further
if (node == null)
{
return true;
}
// Selection or node is empty -> can't be under-selected
if (selection.IsEmpty || node.Span.IsEmpty)
{
return false;
}
// Selection is larger than node.Span -> can't be under-selecting
if (selection.Contains(node.Span))
{
return false;
}
// Selection doesn't intersect node -> can't be under-selecting.
// RATIONALE: If there's no intersection then we got the node in some other way, e.g.
// extracting it after user selected `;` at the end of an expression statement
// `goo()[|;|]` for `goo()` node.
if (!node.FullSpan.OverlapsWith(selection))
{
return false;
}
// Only precisely one token of the node is selected -> treat is as empty selection -> not
// under-selected. The rationale is that if only one Token is selected then the selection
// wasn't about precisely getting the one node and nothing else & therefore we should treat
// it as empty selection.
if (node.FullSpan.Contains(selection.Start))
{
var selectionStartToken = node.FindToken(selection.Start);
if (selection.IsAround(selectionStartToken))
{
return false;
}
}
var beginningNode = node.FindToken(node.Span.Start).Parent;
var endNode = node.FindToken(node.Span.End - 1).Parent;
RoslynDebug.Assert(beginningNode is object);
RoslynDebug.Assert(endNode is object);
// Node is under-selected if either the first (lowest) child doesn't contain start of selection
// of the last child doesn't intersect with the end.
// Node is under-selected if either the first (lowest) child ends before the selection has started
// or the last child starts after the selection ends (i.e. one of them is completely on the outside of selection).
// It's a crude heuristic but it allows omitting parts of nodes or trivial tokens from the beginning/end
// but fires up e.g.: `1 + [|2 + 3|]`.
return beginningNode.Span.End <= selection.Start || endNode.Span.Start >= selection.End;
}
/// <summary>
/// Trims leading and trailing whitespace from <paramref name="span"/>.
/// </summary>
/// <remarks>
/// Returns unchanged <paramref name="span"/> in case <see cref="TextSpan.IsEmpty"/>.
/// Returns empty Span with original <see cref="TextSpan.Start"/> in case it contains only whitespace.
/// </remarks>
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);
}
}
|