forked from dotnet/roslyn
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathCommitManager.cs
394 lines (339 loc) · 18.6 KB
/
CommitManager.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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
// 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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Completion.Providers.Snippets;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Text.Shared.Extensions;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;
using AsyncCompletionData = Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data;
using RoslynCompletionItem = Microsoft.CodeAnalysis.Completion.CompletionItem;
using VSCompletionItem = Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data.CompletionItem;
namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion
{
internal sealed class CommitManager : IAsyncCompletionCommitManager
{
private static readonly AsyncCompletionData.CommitResult CommitResultUnhandled =
new(isHandled: false, AsyncCompletionData.CommitBehavior.None);
private readonly RecentItemsManager _recentItemsManager;
private readonly ITextView _textView;
private readonly IGlobalOptionService _globalOptions;
private readonly IThreadingContext _threadingContext;
private readonly ILanguageServerSnippetExpander? _languageServerSnippetExpander;
public IEnumerable<char> PotentialCommitCharacters
{
get
{
if (_textView.Properties.TryGetProperty(CompletionSource.PotentialCommitCharacters, out ImmutableArray<char> potentialCommitCharacters))
{
return potentialCommitCharacters;
}
else
{
// If we were not initialized with a CompletionService or are called for a wrong textView, we should not make a commit.
return ImmutableArray<char>.Empty;
}
}
}
internal CommitManager(
ITextView textView,
RecentItemsManager recentItemsManager,
IGlobalOptionService globalOptions,
IThreadingContext threadingContext,
ILanguageServerSnippetExpander? languageServerSnippetExpander)
{
_globalOptions = globalOptions;
_threadingContext = threadingContext;
_recentItemsManager = recentItemsManager;
_textView = textView;
_languageServerSnippetExpander = languageServerSnippetExpander;
}
/// <summary>
/// The method performs a preliminarily filtering of commit availability.
/// In case of a doubt, it should respond with true.
/// We will be able to cancel later in
/// <see cref="TryCommit(IAsyncCompletionSession, ITextBuffer, VSCompletionItem, char, CancellationToken)"/>
/// based on <see cref="VSCompletionItem"/> item, e.g. based on <see cref="CompletionItemRules"/>.
/// </summary>
public bool ShouldCommitCompletion(
IAsyncCompletionSession session,
SnapshotPoint location,
char typedChar,
CancellationToken cancellationToken)
{
if (!PotentialCommitCharacters.Contains(typedChar))
{
return false;
}
return !(session.Properties.TryGetProperty(CompletionSource.ExcludedCommitCharacters, out ImmutableArray<char> excludedCommitCharacter)
&& excludedCommitCharacter.Contains(typedChar));
}
public AsyncCompletionData.CommitResult TryCommit(
IAsyncCompletionSession session,
ITextBuffer subjectBuffer,
VSCompletionItem item,
char typeChar,
CancellationToken cancellationToken)
{
// We can make changes to buffers. We would like to be sure nobody can change them at the same time.
_threadingContext.ThrowIfNotOnUIThread();
var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
if (document == null)
{
return CommitResultUnhandled;
}
var completionService = document.GetLanguageService<CompletionService>();
if (completionService == null)
{
return CommitResultUnhandled;
}
if (!CompletionItemData.TryGetData(item, out var itemData))
{
// Roslyn should not be called if the item committing was not provided by Roslyn.
return CommitResultUnhandled;
}
var filterText = session.ApplicableToSpan.GetText(session.ApplicableToSpan.TextBuffer.CurrentSnapshot) + typeChar;
if (Helpers.IsFilterCharacter(itemData.RoslynItem, typeChar, filterText))
{
// Returning Cancel means we keep the current session and consider the character for further filtering.
return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.CancelCommit);
}
var options = _globalOptions.GetCompletionOptions(document.Project.Language);
var serviceRules = completionService.GetRules(options);
// We can be called before for ShouldCommitCompletion. However, that call does not provide rules applied for the completion item.
// Now we check for the commit character in the context of Rules that could change the list of commit characters.
if (!Helpers.IsStandardCommitCharacter(typeChar) && !IsCommitCharacter(serviceRules, itemData.RoslynItem, typeChar))
{
// Returning None means we complete the current session with a void commit.
// The Editor then will try to trigger a new completion session for the character.
return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None);
}
if (!itemData.TriggerLocation.HasValue)
{
// Need the trigger snapshot to calculate the span when the commit changes to be applied.
// They should always be available from items provided by Roslyn CompletionSource.
// Just to be defensive, if it's not found here, Roslyn should not make a commit.
return CommitResultUnhandled;
}
var triggerDocument = itemData.TriggerLocation.Value.Snapshot.GetOpenDocumentInCurrentContextWithChanges();
if (triggerDocument == null)
{
return CommitResultUnhandled;
}
var sessionData = CompletionSessionData.GetOrCreateSessionData(session);
if (!sessionData.CompletionListSpan.HasValue)
{
return CommitResultUnhandled;
}
// Commit with completion service assumes that null is provided is case of invoke. VS provides '\0' in the case.
var commitChar = typeChar == '\0' ? null : (char?)typeChar;
return Commit(
session, triggerDocument, completionService, subjectBuffer,
itemData.RoslynItem, sessionData.CompletionListSpan.Value, commitChar, itemData.TriggerLocation.Value.Snapshot, serviceRules,
filterText, cancellationToken);
}
private AsyncCompletionData.CommitResult Commit(
IAsyncCompletionSession session,
Document document,
CompletionService completionService,
ITextBuffer subjectBuffer,
RoslynCompletionItem roslynItem,
TextSpan completionListSpan,
char? commitCharacter,
ITextSnapshot triggerSnapshot,
CompletionRules rules,
string filterText,
CancellationToken cancellationToken)
{
_threadingContext.ThrowIfNotOnUIThread();
bool includesCommitCharacter;
if (!subjectBuffer.CheckEditAccess())
{
// We are on the wrong thread.
FatalError.ReportAndCatch(new InvalidOperationException("Subject buffer did not provide Edit Access"), ErrorSeverity.Critical);
return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None);
}
if (subjectBuffer.EditInProgress)
{
FatalError.ReportAndCatch(new InvalidOperationException("Subject buffer is editing by someone else."), ErrorSeverity.Critical);
return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None);
}
CompletionChange change;
// We met an issue when external code threw an OperationCanceledException and the cancellationToken is not canceled.
// Catching this scenario for further investigations.
// See https://github.com/dotnet/roslyn/issues/38455.
try
{
// Cached items have a span computed at the point they were created. This span may no
// longer be valid when used again. In that case, override the span with the latest span
// for the completion list itself.
if (roslynItem.Flags.IsCached())
roslynItem.Span = completionListSpan;
change = completionService.GetChangeAsync(document, roslynItem, commitCharacter, cancellationToken).WaitAndGetResult(cancellationToken);
}
catch (OperationCanceledException e) when (e.CancellationToken != cancellationToken && FatalError.ReportAndCatch(e))
{
return CommitResultUnhandled;
}
cancellationToken.ThrowIfCancellationRequested();
var view = session.TextView;
var provider = completionService.GetProvider(roslynItem);
if (provider is ICustomCommitCompletionProvider customCommitProvider)
{
customCommitProvider.Commit(roslynItem, view, subjectBuffer, triggerSnapshot, commitCharacter);
return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None);
}
var textChange = change.TextChange;
var triggerSnapshotSpan = new SnapshotSpan(triggerSnapshot, textChange.Span.ToSpan());
var mappedSpan = triggerSnapshotSpan.TranslateTo(subjectBuffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive);
// Specifically for snippets, we check to see if the associated completion item is a snippet,
// and if so, we call upon the LanguageServerSnippetExpander's TryExpand to insert the snippet.
if (SnippetCompletionItem.IsSnippet(roslynItem))
{
Contract.ThrowIfNull(_languageServerSnippetExpander);
var lspSnippetText = change.Properties[SnippetCompletionItem.LSPSnippetKey];
Contract.ThrowIfNull(lspSnippetText);
if (!_languageServerSnippetExpander.TryExpand(lspSnippetText, mappedSpan, _textView))
{
FatalError.ReportAndCatch(new InvalidOperationException("The invoked LSP snippet expander came back as false."), ErrorSeverity.Critical);
}
return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None);
}
ITextSnapshot updatedCurrentSnapshot;
using (var edit = subjectBuffer.CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null))
{
edit.Replace(mappedSpan.Span, change.TextChange.NewText);
// edit.Apply() may trigger changes made by extensions.
// updatedCurrentSnapshot will contain changes made by Roslyn but not by other extensions.
updatedCurrentSnapshot = edit.Apply();
}
if (change.NewPosition.HasValue)
{
// Roslyn knows how to position the caret in the snapshot we just created.
// If there were more edits made by extensions, TryMoveCaretToAndEnsureVisible maps the snapshot point to the most recent one.
view.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(updatedCurrentSnapshot, change.NewPosition.Value));
}
else
{
// Or, If we're doing a minimal change, then the edit that we make to the
// buffer may not make the total text change that places the caret where we
// would expect it to go based on the requested change. In this case,
// determine where the item should go and set the care manually.
// Note: we only want to move the caret if the caret would have been moved
// by the edit. i.e. if the caret was actually in the mapped span that
// we're replacing.
var caretPositionInBuffer = view.GetCaretPoint(subjectBuffer);
if (caretPositionInBuffer.HasValue && mappedSpan.IntersectsWith(caretPositionInBuffer.Value))
{
view.TryMoveCaretToAndEnsureVisible(new SnapshotPoint(subjectBuffer.CurrentSnapshot, mappedSpan.Start.Position + textChange.NewText?.Length ?? 0));
}
else
{
view.Caret.EnsureVisible();
}
}
includesCommitCharacter = change.IncludesCommitCharacter;
if (roslynItem.Rules.FormatOnCommit)
{
// The edit updates the snapshot however other extensions may make changes there.
// Therefore, it is required to use subjectBuffer.CurrentSnapshot for further calculations rather than the updated current snapshot defined above.
var currentDocument = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
var formattingService = currentDocument?.GetRequiredLanguageService<IFormattingInteractionService>();
if (currentDocument != null && formattingService != null)
{
var spanToFormat = triggerSnapshotSpan.TranslateTo(subjectBuffer.CurrentSnapshot, SpanTrackingMode.EdgeInclusive);
// Note: C# always completes synchronously, TypeScript is async
var changes = formattingService.GetFormattingChangesAsync(currentDocument, subjectBuffer, spanToFormat.Span.ToTextSpan(), cancellationToken).WaitAndGetResult(cancellationToken);
subjectBuffer.ApplyChanges(changes);
}
}
_recentItemsManager.MakeMostRecentItem(roslynItem.FilterText);
if (provider is INotifyCommittingItemCompletionProvider notifyProvider)
{
_ = _threadingContext.JoinableTaskFactory.RunAsync(async () =>
{
// Make sure the notification isn't sent on UI thread.
await TaskScheduler.Default;
_ = notifyProvider.NotifyCommittingItemAsync(document, roslynItem, commitCharacter, cancellationToken).ReportNonFatalErrorAsync();
});
}
if (includesCommitCharacter)
{
return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.SuppressFurtherTypeCharCommandHandlers);
}
if (commitCharacter == '\n' && SendEnterThroughToEditor(rules, roslynItem, filterText))
{
return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.RaiseFurtherReturnKeyAndTabKeyCommandHandlers);
}
return new AsyncCompletionData.CommitResult(isHandled: true, AsyncCompletionData.CommitBehavior.None);
}
internal static bool IsCommitCharacter(CompletionRules completionRules, CompletionItem item, char ch)
{
// First see if the item has any specific commit rules it wants followed.
foreach (var rule in item.Rules.CommitCharacterRules)
{
switch (rule.Kind)
{
case CharacterSetModificationKind.Add:
if (rule.Characters.Contains(ch))
{
return true;
}
continue;
case CharacterSetModificationKind.Remove:
if (rule.Characters.Contains(ch))
{
return false;
}
continue;
case CharacterSetModificationKind.Replace:
return rule.Characters.Contains(ch);
}
}
// Fall back to the default rules for this language's completion service.
return completionRules.DefaultCommitCharacters.IndexOf(ch) >= 0;
}
internal static bool SendEnterThroughToEditor(CompletionRules rules, RoslynCompletionItem item, string textTypedSoFar)
{
var rule = item.Rules.EnterKeyRule;
if (rule == EnterKeyRule.Default)
{
rule = rules.DefaultEnterKeyRule;
}
switch (rule)
{
default:
case EnterKeyRule.Default:
case EnterKeyRule.Never:
return false;
case EnterKeyRule.Always:
return true;
case EnterKeyRule.AfterFullyTypedWord:
// textTypedSoFar is concatenated from individual chars typed.
// '\n' is the enter char.
// That is why, there is no need to check for '\r\n'.
if (textTypedSoFar.LastOrDefault() == '\n')
{
textTypedSoFar = textTypedSoFar.Substring(0, textTypedSoFar.Length - 1);
}
return item.GetEntireDisplayText() == textTypedSoFar;
}
}
}
}