-
Notifications
You must be signed in to change notification settings - Fork 4k
/
CodeRefactoringHelpers.cs
131 lines (116 loc) · 5.21 KB
/
CodeRefactoringHelpers.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// 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.GetRelevantNodesAsync{TSyntaxNode}(Document, TextSpan, bool,
/// CancellationToken)"/> 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 async Task<TextSpan> GetTrimmedTextSpanAsync(Document document, TextSpan span, CancellationToken cancellationToken)
{
if (span.IsEmpty)
{
return span;
}
var sourceText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false);
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);
}
}