-
Notifications
You must be signed in to change notification settings - Fork 4k
/
GoToDefinitionCommandHandler.cs
199 lines (174 loc) · 9.65 KB
/
GoToDefinitionCommandHandler.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
// 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.ComponentModel.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.BackgroundWorkIndicator;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Options;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Navigation;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.GoToDefinition
{
[Export(typeof(ICommandHandler))]
[ContentType(ContentTypeNames.RoslynContentType)]
[Name(PredefinedCommandHandlerNames.GoToDefinition)]
[method: ImportingConstructor]
[method: SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
internal class GoToDefinitionCommandHandler(
IGlobalOptionService globalOptionService,
IThreadingContext threadingContext,
IUIThreadOperationExecutor executor,
IAsynchronousOperationListenerProvider listenerProvider) :
ICommandHandler<GoToDefinitionCommandArgs>
{
private readonly IGlobalOptionService _globalOptionService = globalOptionService;
private readonly IThreadingContext _threadingContext = threadingContext;
private readonly IUIThreadOperationExecutor _executor = executor;
private readonly IAsynchronousOperationListener _listener = listenerProvider.GetListener(FeatureAttribute.GoToDefinition);
public string DisplayName => EditorFeaturesResources.Go_to_Definition;
private static (Document?, IGoToDefinitionService?, IAsyncGoToDefinitionService?) GetDocumentAndService(ITextSnapshot snapshot)
{
var document = snapshot.GetOpenDocumentInCurrentContextWithChanges();
return (document, document?.GetLanguageService<IGoToDefinitionService>(), document?.GetLanguageService<IAsyncGoToDefinitionService>());
}
public CommandState GetCommandState(GoToDefinitionCommandArgs args)
{
var (_, service, asyncService) = GetDocumentAndService(args.SubjectBuffer.CurrentSnapshot);
return service != null || asyncService != null
? CommandState.Available
: CommandState.Unspecified;
}
public bool ExecuteCommand(GoToDefinitionCommandArgs args, CommandExecutionContext context)
{
var subjectBuffer = args.SubjectBuffer;
var (document, service, asyncService) = GetDocumentAndService(subjectBuffer.CurrentSnapshot);
if (service == null && asyncService == null)
return false;
// In Live Share, typescript exports a gotodefinition service that returns no results and prevents the LSP
// client from handling the request. So prevent the local service from handling goto def commands in the
// remote workspace. This can be removed once typescript implements LSP support for goto def.
if (subjectBuffer.IsInLspEditorContext())
return false;
// If the file is empty, there's nothing to be on that we can goto-def on. This also ensures that we can
// create an appropriate non-empty tracking span later on.
var currentSnapshot = subjectBuffer.CurrentSnapshot;
if (currentSnapshot.Length == 0)
return false;
Contract.ThrowIfNull(document);
// If there's a selection, use the starting point of the selection as the invocation point. Otherwise, just
// pick wherever the caret is exactly at.
var caretPos =
args.TextView.Selection.GetSnapshotSpansOnBuffer(subjectBuffer).FirstOrNull()?.Start ??
args.TextView.GetCaretPoint(subjectBuffer);
if (!caretPos.HasValue)
return false;
if (asyncService != null && _globalOptionService.GetOption(FeatureOnOffOptions.NavigateAsynchronously))
{
// We're showing our own UI, ensure the editor doesn't show anything itself.
context.OperationContext.TakeOwnership();
var token = _listener.BeginAsyncOperation(nameof(ExecuteCommand));
ExecuteAsynchronouslyAsync(args, document, asyncService, caretPos.Value)
.ReportNonFatalErrorAsync()
.CompletesAsyncOperation(token);
}
else
{
// The language either doesn't support async goto-def, or the option is disabled to navigate
// asynchronously. So fall back to normal synchronous navigation.
var succeeded = ExecuteSynchronously(document, service, asyncService, caretPos.Value, context);
if (!succeeded)
{
// Dismiss any context dialog that is up before showing our own notification.
context.OperationContext.TakeOwnership();
ReportFailure(document);
}
}
return true;
}
private bool ExecuteSynchronously(
Document document,
IGoToDefinitionService? service,
IAsyncGoToDefinitionService? asyncService,
int position,
CommandExecutionContext context)
{
using (context.OperationContext.AddScope(allowCancellation: true, EditorFeaturesResources.Navigating_to_definition))
{
var cancellationToken = context.OperationContext.UserCancellationToken;
if (asyncService != null)
{
return _threadingContext.JoinableTaskFactory.Run(async () =>
{
// determine the location first.
var (location, _) = await asyncService.FindDefinitionLocationAsync(
document, position, includeType: true, cancellationToken).ConfigureAwait(false);
return await location.TryNavigateToAsync(
_threadingContext, NavigationOptions.Default, cancellationToken).ConfigureAwait(false);
});
}
else if (service != null)
{
return service.TryGoToDefinition(document, position, cancellationToken);
}
else
{
throw ExceptionUtilities.Unreachable();
}
}
}
private static void ReportFailure(Document document)
{
var notificationService = document.Project.Solution.Services.GetRequiredService<INotificationService>();
notificationService.SendNotification(
FeaturesResources.Cannot_navigate_to_the_symbol_under_the_caret, EditorFeaturesResources.Go_to_Definition, NotificationSeverity.Information);
}
private async Task ExecuteAsynchronouslyAsync(
GoToDefinitionCommandArgs args, Document document, IAsyncGoToDefinitionService service, SnapshotPoint position)
{
bool succeeded;
var indicatorFactory = document.Project.Solution.Services.GetRequiredService<IBackgroundWorkIndicatorFactory>();
// TODO: prior logic was to get a tracking span of length 1 here. Preserving that, though it's unclear if
// that is necessary for the BWI to work properly.
Contract.ThrowIfTrue(position.Snapshot.Length == 0);
var applicableToSpan = position < position.Snapshot.Length
? new SnapshotSpan(position, position + 1)
: new SnapshotSpan(position - 1, position);
using (var backgroundIndicator = indicatorFactory.Create(
args.TextView, applicableToSpan,
EditorFeaturesResources.Navigating_to_definition))
{
var cancellationToken = backgroundIndicator.UserCancellationToken;
// determine the location first.
var (location, _) = await service.FindDefinitionLocationAsync(
document, position, includeType: true, cancellationToken).ConfigureAwait(false);
// make sure that if our background indicator got canceled, that we do not still perform the navigation.
if (backgroundIndicator.UserCancellationToken.IsCancellationRequested)
return;
// we're about to navigate. so disable cancellation on focus-lost in our indicator so we don't end up
// causing ourselves to self-cancel.
backgroundIndicator.CancelOnFocusLost = false;
succeeded = await location.TryNavigateToAsync(
_threadingContext, new NavigationOptions(PreferProvisionalTab: true, ActivateTab: true), cancellationToken).ConfigureAwait(false);
}
if (!succeeded)
{
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(CancellationToken.None);
ReportFailure(document);
}
}
}
}