-
Notifications
You must be signed in to change notification settings - Fork 4k
/
AutomaticLineEnderCommandHandler.cs
296 lines (253 loc) · 12.2 KB
/
AutomaticLineEnderCommandHandler.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
292
293
294
295
296
// 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.ComponentModel.Composition;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Utilities;
using Microsoft.CodeAnalysis.Editor.Implementation.AutomaticCompletion;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
using VSCommanding = Microsoft.VisualStudio.Commanding;
namespace Microsoft.CodeAnalysis.Editor.CSharp.AutomaticCompletion
{
/// <summary>
/// csharp automatic line ender command handler
/// </summary>
[Export(typeof(VSCommanding.ICommandHandler))]
[ContentType(ContentTypeNames.CSharpContentType)]
[Name(PredefinedCommandHandlerNames.AutomaticLineEnder)]
[Order(After = PredefinedCommandHandlerNames.Completion)]
[Order(After = PredefinedCompletionNames.CompletionCommandHandler)]
internal class AutomaticLineEnderCommandHandler : AbstractAutomaticLineEnderCommandHandler
{
[ImportingConstructor]
public AutomaticLineEnderCommandHandler(
ITextUndoHistoryRegistry undoRegistry,
IEditorOperationsFactoryService editorOperations,
IAsyncCompletionBroker asyncCompletionBroker)
: base(undoRegistry, editorOperations, asyncCompletionBroker)
{
}
protected override void NextAction(IEditorOperations editorOperation, Action nextAction)
{
editorOperation.InsertNewLine();
}
protected override bool TreatAsReturn(Document document, int position, CancellationToken cancellationToken)
{
var root = document.GetSyntaxRootSynchronously(cancellationToken);
var endToken = root.FindToken(position);
if (endToken.IsMissing)
{
return false;
}
var tokenToLeft = root.FindTokenOnLeftOfPosition(position);
var startToken = endToken.GetPreviousToken();
// case 1:
// Consider code like so: try {|}
// With auto brace completion on, user types `{` and `Return` in a hurry.
// During typing, it is possible that shift was still down and not released after typing `{`.
// So we've got an unintentional `shift + enter` and also we have nothing to complete this,
// so we put in a newline,
// which generates code like so : try { }
// |
// which is not useful as : try {
// |
// }
// To support this, we treat `shift + enter` like `enter` here.
var afterOpenBrace = startToken.Kind() == SyntaxKind.OpenBraceToken
&& endToken.Kind() == SyntaxKind.CloseBraceToken
&& tokenToLeft == startToken
&& endToken.Parent.IsKind(SyntaxKind.Block)
&& FormattingRangeHelper.AreTwoTokensOnSameLine(startToken, endToken);
return afterOpenBrace;
}
protected override void FormatAndApply(Document document, int position, CancellationToken cancellationToken)
{
var root = document.GetSyntaxRootSynchronously(cancellationToken);
var endToken = root.FindToken(position);
if (endToken.IsMissing)
{
return;
}
var ranges = FormattingRangeHelper.FindAppropriateRange(endToken, useDefaultRange: false);
if (ranges == null)
{
return;
}
var startToken = ranges.Value.Item1;
if (startToken.IsMissing || startToken.Kind() == SyntaxKind.None)
{
return;
}
var options = document.GetOptionsAsync(cancellationToken).WaitAndGetResult(cancellationToken);
var changes = Formatter.GetFormattedTextChanges(root, new TextSpan[] { TextSpan.FromBounds(startToken.SpanStart, endToken.Span.End) }, document.Project.Solution.Workspace, options,
rules: null, // use default
cancellationToken: cancellationToken);
document.ApplyTextChanges(changes.ToArray(), cancellationToken);
}
protected override string GetEndingString(Document document, int position, CancellationToken cancellationToken)
{
// prepare expansive information from document
var tree = document.GetSyntaxTreeSynchronously(cancellationToken);
var root = tree.GetRoot(cancellationToken);
var text = tree.GetText(cancellationToken);
var semicolon = SyntaxFacts.GetText(SyntaxKind.SemicolonToken);
// Go through the set of owning nodes in leaf to root chain.
foreach (var owningNode in GetOwningNodes(root, position))
{
if (!TryGetLastToken(text, position, owningNode, out var lastToken))
{
// If we can't get last token, there is nothing more to do, just skip
// the other owning nodes and return.
return null;
}
if (!CheckLocation(text, position, owningNode, lastToken))
{
// If we failed this check, we indeed got the intended owner node and
// inserting line ender here would introduce errors.
return null;
}
// so far so good. we only add semi-colon if it makes statement syntax error free
var textToParse = owningNode.NormalizeWhitespace().ToFullString() + semicolon;
// currently, Parsing a field is not supported. as a workaround, wrap the field in a type and parse
var node = ParseNode(tree, owningNode, textToParse);
// Insert line ender if we didn't introduce any diagnostics, if not try the next owning node.
if (node != null && !node.ContainsDiagnostics)
{
return semicolon;
}
}
return null;
}
private SyntaxNode ParseNode(SyntaxTree tree, SyntaxNode owningNode, string textToParse)
{
switch (owningNode)
{
case BaseFieldDeclarationSyntax n: return SyntaxFactory.ParseCompilationUnit(WrapInType(textToParse), options: (CSharpParseOptions)tree.Options);
case BaseMethodDeclarationSyntax n: return SyntaxFactory.ParseCompilationUnit(WrapInType(textToParse), options: (CSharpParseOptions)tree.Options);
case BasePropertyDeclarationSyntax n: return SyntaxFactory.ParseCompilationUnit(WrapInType(textToParse), options: (CSharpParseOptions)tree.Options);
case StatementSyntax n: return SyntaxFactory.ParseStatement(textToParse, options: (CSharpParseOptions)tree.Options);
case UsingDirectiveSyntax n: return SyntaxFactory.ParseCompilationUnit(textToParse, options: (CSharpParseOptions)tree.Options);
}
return null;
}
/// <summary>
/// wrap field in type
/// </summary>
private string WrapInType(string textToParse)
{
return "class C { " + textToParse + " }";
}
/// <summary>
/// make sure current location is okay to put semicolon
/// </summary>
private static bool CheckLocation(SourceText text, int position, SyntaxNode owningNode, SyntaxToken lastToken)
{
var line = text.Lines.GetLineFromPosition(position);
// if caret is at the end of the line and containing statement is expression statement
// don't do anything
if (position == line.End && owningNode is ExpressionStatementSyntax)
{
return false;
}
var locatedAtTheEndOfLine = LocatedAtTheEndOfLine(line, lastToken);
// make sure that there is no trailing text after last token on the line if it is not at the end of the line
if (!locatedAtTheEndOfLine)
{
var endingString = text.ToString(TextSpan.FromBounds(lastToken.Span.End, line.End));
if (!string.IsNullOrWhiteSpace(endingString))
{
return false;
}
}
// check whether using has contents
if (owningNode is UsingDirectiveSyntax u &&
(u.Name == null || u.Name.IsMissing))
{
return false;
}
// make sure there is no open string literals
var previousToken = lastToken.GetPreviousToken();
if (previousToken.Kind() == SyntaxKind.StringLiteralToken && previousToken.ToString().Last() != '"')
{
return false;
}
if (previousToken.Kind() == SyntaxKind.CharacterLiteralToken && previousToken.ToString().Last() != '\'')
{
return false;
}
// now, check embedded statement case
if (owningNode.IsEmbeddedStatementOwner())
{
var embeddedStatement = owningNode.GetEmbeddedStatement();
if (embeddedStatement == null || embeddedStatement.Span.IsEmpty)
{
return false;
}
}
return true;
}
/// <summary>
/// get last token of the given using/field/statement/expression bodied member if one exists
/// </summary>
private static bool TryGetLastToken(SourceText text, int position, SyntaxNode owningNode, out SyntaxToken lastToken)
{
lastToken = owningNode.GetLastToken(includeZeroWidth: true);
// last token must be on the same line as the caret
var line = text.Lines.GetLineFromPosition(position);
var locatedAtTheEndOfLine = LocatedAtTheEndOfLine(line, lastToken);
if (!locatedAtTheEndOfLine && text.Lines.IndexOf(lastToken.Span.End) != line.LineNumber)
{
return false;
}
// if we already have last semicolon, we don't need to do anything
if (!lastToken.IsMissing && lastToken.Kind() == SyntaxKind.SemicolonToken)
{
return false;
}
return true;
}
/// <summary>
/// check whether the line is located at the end of the line
/// </summary>
private static bool LocatedAtTheEndOfLine(TextLine line, SyntaxToken lastToken)
{
return lastToken.IsMissing && lastToken.Span.End == line.EndIncludingLineBreak;
}
/// <summary>
/// find owning usings/field/statement/expression-bodied member of the given position
/// </summary>
private static IEnumerable<SyntaxNode> GetOwningNodes(SyntaxNode root, int position)
{
// make sure caret position is somewhere we can find a token
var token = root.FindTokenFromEnd(position);
if (token.Kind() == SyntaxKind.None)
{
return SpecializedCollections.EmptyEnumerable<SyntaxNode>();
}
return token.GetAncestors<SyntaxNode>()
.Where(AllowedConstructs)
.Select(OwningNode);
}
private static bool AllowedConstructs(SyntaxNode n)
{
return n is StatementSyntax ||
n is BaseFieldDeclarationSyntax ||
n is UsingDirectiveSyntax ||
n is ArrowExpressionClauseSyntax;
}
private static SyntaxNode OwningNode(SyntaxNode n)
{
return n is ArrowExpressionClauseSyntax ? n.Parent : n;
}
}
}