@@ -34,14 +34,27 @@ internal sealed partial class SmartRenameViewModel : INotifyPropertyChanged, IDi
34
34
private readonly IGlobalOptionService _globalOptionService ;
35
35
private readonly IThreadingContext _threadingContext ;
36
36
private readonly IAsynchronousOperationListener _asyncListener ;
37
- private CancellationTokenSource ? _cancellationTokenSource ;
37
+
38
+ /// <summary>
39
+ /// Cancellation token source for <see cref="ISmartRenameSessionWrapper.GetSuggestionsAsync(ImmutableDictionary{string, ImmutableArray{ValueTuple{string, string}}}, CancellationToken)"/>.
40
+ /// Each call uses a new instance. Mutliple calls are allowed only if previous call failed or was canceled.
41
+ /// The request is canceled on UI thread through one of the following user interactions:
42
+ /// 1. <see cref="BaseViewModelPropertyChanged"/> when user types in the text box.
43
+ /// 2. <see cref="ToggleOrTriggerSuggestions"/> when user toggles the automatic suggestions.
44
+ /// 3. <see cref="Dispose"/> when the dialog is closed.
45
+ /// </summary>
46
+ private CancellationTokenSource _cancellationTokenSource = new ( ) ;
38
47
private bool _isDisposed ;
39
48
private TimeSpan AutomaticFetchDelay => _smartRenameSession . AutomaticFetchDelay ;
40
- private Task _getSuggestionsTask = Task . CompletedTask ;
41
49
private TimeSpan _semanticContextDelay ;
42
50
private bool _semanticContextError ;
43
51
private bool _semanticContextUsed ;
44
52
53
+ /// <summary>
54
+ /// Backing field for <see cref="IsInProgress"/>.
55
+ /// </summary>
56
+ private bool _isInProgress = false ;
57
+
45
58
public event PropertyChangedEventHandler ? PropertyChanged ;
46
59
47
60
public RenameFlyoutViewModel BaseViewModel { get ; }
@@ -52,7 +65,26 @@ internal sealed partial class SmartRenameViewModel : INotifyPropertyChanged, IDi
52
65
53
66
public bool HasSuggestions => _smartRenameSession . HasSuggestions ;
54
67
55
- public bool IsInProgress => _smartRenameSession . IsInProgress ;
68
+ /// <summary>
69
+ /// Indicates whether a request to get suggestions is in progress.
70
+ /// The request to get suggestions is comprised of initial short delay, <see cref="AutomaticFetchDelay"/>
71
+ /// and call to <see cref="ISmartRenameSessionWrapper.GetSuggestionsAsync(ImmutableDictionary{string, ImmutableArray{ValueTuple{string, string}}}, CancellationToken)"/>.
72
+ /// When <c>true</c>, the UI shows the progress bar, and prevents <see cref="FetchSuggestions(bool)"/> from making parallel request.
73
+ /// </summary>
74
+ public bool IsInProgress
75
+ {
76
+ get
77
+ {
78
+ _threadingContext . ThrowIfNotOnUIThread ( ) ;
79
+ return _isInProgress ;
80
+ }
81
+ set
82
+ {
83
+ _threadingContext . ThrowIfNotOnUIThread ( ) ;
84
+ _isInProgress = value ;
85
+ NotifyPropertyChanged ( nameof ( IsInProgress ) ) ;
86
+ }
87
+ }
56
88
57
89
public string StatusMessage => _smartRenameSession . StatusMessage ;
58
90
@@ -150,64 +182,80 @@ public SmartRenameViewModel(
150
182
private void FetchSuggestions( bool isAutomaticOnInitialization )
151
183
{
152
184
_threadingContext . ThrowIfNotOnUIThread ( ) ;
153
- if ( this . SuggestedNames . Count > 0 || _isDisposed )
185
+ if ( this . SuggestedNames . Count > 0 || _isDisposed || this . IsInProgress )
154
186
{
155
187
// Don't get suggestions again
156
188
return ;
157
189
}
158
190
159
- if ( _getSuggestionsTask . Status is TaskStatus. RanToCompletion or TaskStatus. Faulted or TaskStatus. Canceled)
160
- {
161
- var listenerToken = _asyncListener . BeginAsyncOperation ( nameof ( _smartRenameSession . GetSuggestionsAsync ) ) ;
162
- _cancellationTokenSource ? . Dispose ( ) ;
163
- _cancellationTokenSource = new CancellationTokenSource ( ) ;
164
- _getSuggestionsTask = GetSuggestionsTaskAsync ( isAutomaticOnInitialization , _cancellationTokenSource . Token ) . CompletesAsyncOperation ( listenerToken ) ;
165
- }
191
+ var listenerToken = _asyncListener. BeginAsyncOperation( nameof ( _smartRenameSession . GetSuggestionsAsync ) ) ;
192
+ _cancellationTokenSource . Cancel ( ) ;
193
+ _cancellationTokenSource = new CancellationTokenSource ( ) ;
194
+ GetSuggestionsTaskAsync ( isAutomaticOnInitialization , _cancellationTokenSource . Token ) . CompletesAsyncOperation ( listenerToken ) ;
166
195
}
167
196
197
+ /// <summary>
198
+ /// The request for rename suggestions. It's made of three parts:
199
+ /// 1. Short delay of duration <see cref="AutomaticFetchDelay"/>.
200
+ /// 2. Get definition and references if <see cref="IsUsingSemanticContext"/> is set.
201
+ /// 3. Call to <see cref="ISmartRenameSessionWrapper.GetSuggestionsAsync(ImmutableDictionary{string, ImmutableArray{ValueTuple{string, string}}}, CancellationToken)"/>.
202
+ /// </summary>
168
203
private async Task GetSuggestionsTaskAsync ( bool isAutomaticOnInitialization , CancellationToken cancellationToken )
169
204
{
170
- if ( isAutomaticOnInitialization )
171
- {
172
- await Task. Delay( _smartRenameSession . AutomaticFetchDelay , cancellationToken )
173
- . ConfigureAwait ( false ) ;
174
- }
175
-
176
- if ( cancellationToken . IsCancellationRequested || _isDisposed )
177
- {
178
- return ;
179
- }
205
+ RoslynDebug . Assert ( ! this . IsInProgress ) ;
206
+ this . IsInProgress = true;
207
+ await _threadingContext . JoinableTaskFactory . SwitchToMainThreadAsync ( cancellationToken ) ;
180
208
181
- if ( IsUsingSemanticContext )
209
+ try
182
210
{
183
- var stopwatch = SharedStopwatch. StartNew( ) ;
184
- _semanticContextUsed = true;
185
- var document = this . BaseViewModel. Session. TriggerDocument;
186
- var smartRenameContext = ImmutableDictionary < string , ImmutableArray < ( string filePath , string content ) > > . Empty ;
187
- try
211
+ if ( isAutomaticOnInitialization )
188
212
{
189
- var editorRenameService = document . GetRequiredLanguageService < IEditorInlineRenameService > ( ) ;
190
- var renameLocations = await this . BaseViewModel . Session . AllRenameLocationsTask . JoinAsync ( cancellationToken )
213
+ await Task. Delay( _smartRenameSession . AutomaticFetchDelay , cancellationToken )
191
214
. ConfigureAwait ( false ) ;
192
- var context = await editorRenameService. GetRenameContextAsync( this . BaseViewModel . Session . RenameInfo , renameLocations , cancellationToken )
215
+ }
216
+
217
+ if ( cancellationToken . IsCancellationRequested || _isDisposed )
218
+ {
219
+ return ;
220
+ }
221
+
222
+ if ( IsUsingSemanticContext)
223
+ {
224
+ var stopwatch = SharedStopwatch. StartNew( ) ;
225
+ _semanticContextUsed = true;
226
+ var document = this . BaseViewModel . Session . TriggerDocument ;
227
+ var smartRenameContext = ImmutableDictionary < string , ImmutableArray < ( string filePath , string content ) > > . Empty ;
228
+ try
229
+ {
230
+ var editorRenameService = document . GetRequiredLanguageService < IEditorInlineRenameService > ( ) ;
231
+ var renameLocations = await this . BaseViewModel . Session . AllRenameLocationsTask . JoinAsync ( cancellationToken )
232
+ . ConfigureAwait( false) ;
233
+ var context = await editorRenameService. GetRenameContextAsync( this . BaseViewModel . Session . RenameInfo , renameLocations , cancellationToken )
234
+ . ConfigureAwait ( false ) ;
235
+ smartRenameContext = ImmutableDictionary . CreateRange < string , ImmutableArray < ( string filePath , string content ) > > (
236
+ context
237
+ . Select ( n => new KeyValuePair < string , ImmutableArray < ( string filePath , string content ) > > ( n . Key , n . Value ) ) ) ;
238
+ _semanticContextDelay = stopwatch . Elapsed ;
239
+ }
240
+ catch ( Exception e ) when ( FatalError . ReportAndCatch ( e , ErrorSeverity . Diagnostic ) )
241
+ {
242
+ _semanticContextError = true;
243
+ // use empty smartRenameContext
244
+ }
245
+ _ = await _smartRenameSession . GetSuggestionsAsync ( smartRenameContext , cancellationToken )
193
246
. ConfigureAwait ( false ) ;
194
- smartRenameContext = ImmutableDictionary . CreateRange < string , ImmutableArray < ( string filePath , string content ) > > (
195
- context
196
- . Select ( n => new KeyValuePair < string , ImmutableArray < ( string filePath , string content ) > > ( n . Key , n . Value ) ) ) ;
197
- _semanticContextDelay = stopwatch . Elapsed ;
198
247
}
199
- catch ( Exception e ) when ( FatalError . ReportAndCatch ( e , ErrorSeverity . Diagnostic ) )
248
+ else
200
249
{
201
- _semanticContextError = true ;
202
- // use empty smartRenameContext
250
+ _ = await _smartRenameSession . GetSuggestionsAsync ( cancellationToken )
251
+ . ConfigureAwait ( false ) ;
203
252
}
204
- _ = await _smartRenameSession . GetSuggestionsAsync ( smartRenameContext , cancellationToken )
205
- . ConfigureAwait ( false ) ;
206
253
}
207
- else
254
+ finally
208
255
{
209
- _ = await _smartRenameSession . GetSuggestionsAsync ( cancellationToken )
210
- . ConfigureAwait( false ) ;
256
+ // cancellationToken might be already canceled. Fallback to the disposal token.
257
+ await _threadingContext . JoinableTaskFactory . SwitchToMainThreadAsync ( _threadingContext . DisposalToken ) ;
258
+ this . IsInProgress = false;
211
259
}
212
260
}
213
261
@@ -263,7 +311,7 @@ private async Task SessionPropertyChangedAsync(object sender, PropertyChangedEve
263
311
264
312
public void Cancel ( )
265
313
{
266
- _cancellationTokenSource? . Cancel( ) ;
314
+ _cancellationTokenSource. Cancel( ) ;
267
315
// It's needed by editor-side telemetry.
268
316
_smartRenameSession . OnCancel ( ) ;
269
317
PostTelemetry ( isCommit : false) ;
@@ -278,28 +326,33 @@ public void Commit(string finalIdentifierName)
278
326
279
327
public void Dispose( )
280
328
{
329
+ _threadingContext . ThrowIfNotOnUIThread ( ) ;
281
330
_isDisposed = true;
282
331
_smartRenameSession . PropertyChanged -= SessionPropertyChanged ;
283
332
BaseViewModel . PropertyChanged -= BaseViewModelPropertyChanged ;
284
333
_smartRenameSession . Dispose ( ) ;
285
- _cancellationTokenSource ? . Cancel ( ) ;
286
- _cancellationTokenSource ? . Dispose ( ) ;
334
+ _cancellationTokenSource . Cancel ( ) ;
287
335
}
288
336
289
337
/// <summary>
290
338
/// When smart rename operates in explicit mode, this method gets the suggestions.
291
- /// When smart rename operates in automatic mode, this method toggles the automatic suggestions,
292
- /// and gets the suggestions if it was just enabled.
339
+ /// When smart rename operates in automatic mode, this method toggles the automatic suggestions:
340
+ /// gets the suggestions if it was just enabled, and cancels the ongoing request if it was just disabled .
293
341
/// </summary>
294
342
public void ToggleOrTriggerSuggestions ( )
295
343
{
344
+ _threadingContext. ThrowIfNotOnUIThread( ) ;
296
345
if ( this . SupportsAutomaticSuggestions )
297
346
{
298
347
this . IsAutomaticSuggestionsEnabled = ! this . IsAutomaticSuggestionsEnabled;
299
348
if ( this . IsAutomaticSuggestionsEnabled )
300
349
{
301
350
this . FetchSuggestions( isAutomaticOnInitialization: false) ;
302
351
}
352
+ else
353
+ {
354
+ _cancellationTokenSource . Cancel ( ) ;
355
+ }
303
356
NotifyPropertyChanged ( nameof ( IsSuggestionsPanelExpanded ) ) ;
304
357
NotifyPropertyChanged ( nameof ( IsAutomaticSuggestionsEnabled ) ) ;
305
358
// Use existing "CollapseSuggestionsPanel" option (true if user does not wish to get suggestions automatically) to honor user's choice.
@@ -316,9 +369,11 @@ private void NotifyPropertyChanged([CallerMemberName] string? name = null)
316
369
317
370
private void BaseViewModelPropertyChanged ( object sender , PropertyChangedEventArgs e )
318
371
{
372
+ _threadingContext . ThrowIfNotOnUIThread ( ) ;
319
373
if ( e . PropertyName = = nameof ( BaseViewModel . IdentifierText ) )
320
374
{
321
- _cancellationTokenSource ? . Cancel ( ) ;
375
+ // User is typing the new identifier name, cancel the ongoing request to get suggestions.
376
+ _cancellationTokenSource . Cancel ( ) ;
322
377
}
323
378
}
324
379
}
0 commit comments