-
Notifications
You must be signed in to change notification settings - Fork 4k
/
AbstractCodeActionComputer.cs
291 lines (250 loc) · 14 KB
/
AbstractCodeActionComputer.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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
using static Microsoft.CodeAnalysis.CodeActions.CodeAction;
namespace Microsoft.CodeAnalysis.Editor.Wrapping
{
internal abstract partial class AbstractSyntaxWrapper
{
/// <summary>
/// Class responsible for actually computing the entire set of code actions to offer the
/// user. Contains lots of helper functionality used by all the different Wrapper
/// implementations.
///
/// Specifically subclasses of this type can simply provide a list of code-actions to
/// perform. This type will then take those code actions and will ensure there aren't
/// multiple code actions that end up having the same effect on the document. For example,
/// a "wrap all" action may produce the same results as a "wrap long" action. In that case
/// this type will only keep around the first of those actions to prevent showing the user
/// something that will be unclear.
/// </summary>
protected abstract class AbstractCodeActionComputer<TWrapper> : ICodeActionComputer
where TWrapper : AbstractSyntaxWrapper
{
/// <summary>
/// Annotation used so that we can track the top-most node we want to format after
/// performing all our edits.
/// </summary>
private static readonly SyntaxAnnotation s_toFormatAnnotation = new SyntaxAnnotation();
protected readonly TWrapper Wrapper;
protected readonly Document OriginalDocument;
protected readonly SourceText OriginalSourceText;
protected readonly CancellationToken CancellationToken;
protected readonly bool UseTabs;
protected readonly int TabSize;
protected readonly string NewLine;
protected readonly int WrappingColumn;
protected readonly SyntaxTriviaList NewLineTrivia;
protected readonly SyntaxTriviaList SingleWhitespaceTrivia;
protected readonly SyntaxTriviaList NoTrivia;
/// <summary>
/// The contents of the documents we've created code-actions for. This is used so that
/// we can prevent creating multiple code actions that produce the same results.
/// </summary>
private readonly List<SyntaxNode> _seenDocumentRoots = new List<SyntaxNode>();
public AbstractCodeActionComputer(
TWrapper service,
Document document,
SourceText originalSourceText,
DocumentOptionSet options,
CancellationToken cancellationToken)
{
Wrapper = service;
OriginalDocument = document;
OriginalSourceText = originalSourceText;
CancellationToken = cancellationToken;
UseTabs = options.GetOption(FormattingOptions.UseTabs);
TabSize = options.GetOption(FormattingOptions.TabSize);
NewLine = options.GetOption(FormattingOptions.NewLine);
WrappingColumn = options.GetOption(FormattingOptions.PreferredWrappingColumn);
var generator = SyntaxGenerator.GetGenerator(document);
NewLineTrivia = new SyntaxTriviaList(generator.EndOfLine(NewLine));
SingleWhitespaceTrivia = new SyntaxTriviaList(generator.Whitespace(" "));
}
protected abstract Task<ImmutableArray<WrappingGroup>> ComputeWrappingGroupsAsync();
/// <summary>
/// Try to create a CodeAction representing these edits. Can return <see langword="null"/> in several
/// cases, including:
///
/// 1. No edits.
/// 2. Edits would change more than whitespace.
/// 3. A previous code action was created that already had the same effect.
/// </summary>
protected async Task<WrapItemsAction> TryCreateCodeActionAsync(
ImmutableArray<Edit> edits, string parentTitle, string title)
{
// First, rewrite the tree with the edits provided.
var (root, rewrittenRoot, spanToFormat) = await RewriteTreeAsync(edits).ConfigureAwait(false);
if (rewrittenRoot == null)
{
// Couldn't rewrite for some reason. No code action to create.
return null;
}
// Now, format the part of the tree that we edited. This will ensure we properly
// respect the user preferences around things like comma/operator spacing.
var formattedDocument = await FormatDocumentAsync(rewrittenRoot, spanToFormat).ConfigureAwait(false);
var formattedRoot = await formattedDocument.GetSyntaxRootAsync(CancellationToken).ConfigureAwait(false);
// Now, check if this new formatted tree matches our starting tree, or any of the
// trees we've already created for our other code actions. If so, we don't want to
// add this duplicative code action. Note: this check will actually run quickly.
// 'IsEquivalentTo' can return quickly when comparing equivalent green nodes. So
// all that we need to check is the spine of the change which will happen very
// quickly.
if (root.IsEquivalentTo(formattedRoot))
{
return null;
}
foreach (var seenRoot in _seenDocumentRoots)
{
if (seenRoot.IsEquivalentTo(formattedRoot))
{
return null;
}
}
// This is a genuinely different code action from all previous ones we've created.
// Store the root so we don't just end up creating this code action again.
_seenDocumentRoots.Add(formattedRoot);
return new WrapItemsAction(title, parentTitle, _ => Task.FromResult(formattedDocument));
}
private async Task<Document> FormatDocumentAsync(SyntaxNode rewrittenRoot, TextSpan spanToFormat)
{
var newDocument = OriginalDocument.WithSyntaxRoot(rewrittenRoot);
var formattedDocument = await Formatter.FormatAsync(
newDocument, spanToFormat, cancellationToken: CancellationToken).ConfigureAwait(false);
return formattedDocument;
}
private async Task<(SyntaxNode root, SyntaxNode rewrittenRoot, TextSpan spanToFormat)> RewriteTreeAsync(ImmutableArray<Edit> edits)
{
var leftTokenToTrailingTrivia = PooledDictionary<SyntaxToken, SyntaxTriviaList>.GetInstance();
var rightTokenToLeadingTrivia = PooledDictionary<SyntaxToken, SyntaxTriviaList>.GetInstance();
try
{
foreach (var edit in edits)
{
var span = TextSpan.FromBounds(edit.Left.Span.End, edit.Right.Span.Start);
var text = OriginalSourceText.ToString(span);
if (!IsSafeToRemove(text))
{
// editing some piece of non-whitespace trivia. We don't support this.
return default;
}
// Make sure we're not about to make an edit that just changes the code to what
// is already there.
if (text != edit.GetNewTrivia())
{
leftTokenToTrailingTrivia.Add(edit.Left, edit.NewLeftTrailingTrivia);
rightTokenToLeadingTrivia.Add(edit.Right, edit.NewRightLeadingTrivia);
}
}
if (leftTokenToTrailingTrivia.Count == 0)
{
// No actual edits that would change anything. Nothing to do.
return default;
}
return await RewriteTreeAsync(
leftTokenToTrailingTrivia, rightTokenToLeadingTrivia).ConfigureAwait(false);
}
finally
{
leftTokenToTrailingTrivia.Free();
rightTokenToLeadingTrivia.Free();
}
}
private static bool IsSafeToRemove(string text)
{
foreach (var ch in text)
{
// It's safe to remove whitespace between tokens, or the VB line-continuation character.
if (!char.IsWhiteSpace(ch) && ch != '_')
{
return false;
}
}
return true;
}
private async Task<(SyntaxNode root, SyntaxNode rewrittenRoot, TextSpan spanToFormat)> RewriteTreeAsync(
Dictionary<SyntaxToken, SyntaxTriviaList> leftTokenToTrailingTrivia,
Dictionary<SyntaxToken, SyntaxTriviaList> rightTokenToLeadingTrivia)
{
var root = await OriginalDocument.GetSyntaxRootAsync(CancellationToken).ConfigureAwait(false);
var tokens = leftTokenToTrailingTrivia.Keys.Concat(rightTokenToLeadingTrivia.Keys).Distinct().ToImmutableArray();
// Find the closest node that contains all the tokens we're editing. That's the
// node we'll format at the end. This will ensure that all formattin respects
// user settings for things like spacing around commas/operators/etc.
var nodeToFormat = tokens.SelectAsArray(t => t.Parent).FindInnermostCommonNode<SyntaxNode>();
// Rewrite the tree performing the following actions:
//
// 1. Add an annotation to nodeToFormat so that we can find that node again after
// updating all tokens.
//
// 2. Hit all tokens in the two passed in maps, and update their leading/trailing
// trivia accordingly.
var rewrittenRoot = root.ReplaceSyntax(
nodes: new[] { nodeToFormat },
computeReplacementNode: (oldNode, newNode) => newNode.WithAdditionalAnnotations(s_toFormatAnnotation),
tokens: leftTokenToTrailingTrivia.Keys.Concat(rightTokenToLeadingTrivia.Keys).Distinct(),
computeReplacementToken: (oldToken, newToken) =>
{
if (leftTokenToTrailingTrivia.TryGetValue(oldToken, out var trailingTrivia))
{
newToken = newToken.WithTrailingTrivia(trailingTrivia);
}
if (rightTokenToLeadingTrivia.TryGetValue(oldToken, out var leadingTrivia))
{
newToken = newToken.WithLeadingTrivia(leadingTrivia);
}
return newToken;
},
trivia: null,
computeReplacementTrivia: null);
var trackedNode = rewrittenRoot.GetAnnotatedNodes(s_toFormatAnnotation).Single();
return (root, rewrittenRoot, trackedNode.Span);
}
public async Task<ImmutableArray<CodeAction>> GetTopLevelCodeActionsAsync()
{
// Ask subclass to produce whole nested list of wrapping code actions
var wrappingGroups = await ComputeWrappingGroupsAsync().ConfigureAwait(false);
var result = ArrayBuilder<CodeAction>.GetInstance();
foreach (var group in wrappingGroups)
{
// if a group is empty just ignore it.
var wrappingActions = group.WrappingActions.WhereNotNull().ToImmutableArray();
if (wrappingActions.Length == 0)
{
continue;
}
// If a group only has one item, and subclass says the item is inlinable,
// then just directly return that nested item as a top level item.
if (wrappingActions.Length == 1 && group.IsInlinable)
{
result.Add(wrappingActions[0]);
continue;
}
// Otherwise, sort items and add to the resultant list
var sorted = WrapItemsAction.SortActionsByMostRecentlyUsed(ImmutableArray<CodeAction>.CastUp(wrappingActions));
// Make our code action low priority. This option will be offered *a lot*, and
// much of the time will not be something the user particularly wants to do.
// It should be offered after all other normal refactorings.
result.Add(new CodeActionWithNestedActions(
wrappingActions[0].ParentTitle, sorted,
group.IsInlinable, CodeActionPriority.Low));
}
// Finally, sort the topmost list we're building and return that. This ensures that
// both the top level items and the nested items are ordered appropriate.
return WrapItemsAction.SortActionsByMostRecentlyUsed(result.ToImmutableAndFree());
}
}
}
}