Skip to content

Commit a13d7dc

Browse files
Update the Copilot powered Inline Rename UX (#76001)
2 parents c8fba92 + 7dffaa5 commit a13d7dc

File tree

1 file changed

+103
-48
lines changed

1 file changed

+103
-48
lines changed

src/EditorFeatures/Core.Wpf/InlineRename/UI/SmartRename/SmartRenameViewModel.cs

Lines changed: 103 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,27 @@ internal sealed partial class SmartRenameViewModel : INotifyPropertyChanged, IDi
3434
private readonly IGlobalOptionService _globalOptionService;
3535
private readonly IThreadingContext _threadingContext;
3636
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();
3847
private bool _isDisposed;
3948
private TimeSpan AutomaticFetchDelay => _smartRenameSession.AutomaticFetchDelay;
40-
private Task _getSuggestionsTask = Task.CompletedTask;
4149
private TimeSpan _semanticContextDelay;
4250
private bool _semanticContextError;
4351
private bool _semanticContextUsed;
4452

53+
/// <summary>
54+
/// Backing field for <see cref="IsInProgress"/>.
55+
/// </summary>
56+
private bool _isInProgress = false;
57+
4558
public event PropertyChangedEventHandler? PropertyChanged;
4659

4760
public RenameFlyoutViewModel BaseViewModel { get; }
@@ -52,7 +65,26 @@ internal sealed partial class SmartRenameViewModel : INotifyPropertyChanged, IDi
5265

5366
public bool HasSuggestions => _smartRenameSession.HasSuggestions;
5467

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+
}
5688

5789
public string StatusMessage => _smartRenameSession.StatusMessage;
5890

@@ -150,64 +182,80 @@ public SmartRenameViewModel(
150182
private void FetchSuggestions(bool isAutomaticOnInitialization)
151183
{
152184
_threadingContext.ThrowIfNotOnUIThread();
153-
if (this.SuggestedNames.Count > 0 || _isDisposed)
185+
if (this.SuggestedNames.Count > 0 || _isDisposed || this.IsInProgress)
154186
{
155187
// Don't get suggestions again
156188
return;
157189
}
158190

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);
166195
}
167196

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>
168203
private async Task GetSuggestionsTaskAsync(bool isAutomaticOnInitialization, CancellationToken cancellationToken)
169204
{
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);
180208

181-
if (IsUsingSemanticContext)
209+
try
182210
{
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)
188212
{
189-
var editorRenameService = document.GetRequiredLanguageService<IEditorInlineRenameService>();
190-
var renameLocations = await this.BaseViewModel.Session.AllRenameLocationsTask.JoinAsync(cancellationToken)
213+
await Task.Delay(_smartRenameSession.AutomaticFetchDelay, cancellationToken)
191214
.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)
193246
.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;
198247
}
199-
catch (Exception e) when (FatalError.ReportAndCatch(e, ErrorSeverity.Diagnostic))
248+
else
200249
{
201-
_semanticContextError = true;
202-
// use empty smartRenameContext
250+
_ = await _smartRenameSession.GetSuggestionsAsync(cancellationToken)
251+
.ConfigureAwait(false);
203252
}
204-
_ = await _smartRenameSession.GetSuggestionsAsync(smartRenameContext, cancellationToken)
205-
.ConfigureAwait(false);
206253
}
207-
else
254+
finally
208255
{
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;
211259
}
212260
}
213261

@@ -263,7 +311,7 @@ private async Task SessionPropertyChangedAsync(object sender, PropertyChangedEve
263311

264312
public void Cancel()
265313
{
266-
_cancellationTokenSource?.Cancel();
314+
_cancellationTokenSource.Cancel();
267315
// It's needed by editor-side telemetry.
268316
_smartRenameSession.OnCancel();
269317
PostTelemetry(isCommit: false);
@@ -278,28 +326,33 @@ public void Commit(string finalIdentifierName)
278326

279327
public void Dispose()
280328
{
329+
_threadingContext.ThrowIfNotOnUIThread();
281330
_isDisposed = true;
282331
_smartRenameSession.PropertyChanged -= SessionPropertyChanged;
283332
BaseViewModel.PropertyChanged -= BaseViewModelPropertyChanged;
284333
_smartRenameSession.Dispose();
285-
_cancellationTokenSource?.Cancel();
286-
_cancellationTokenSource?.Dispose();
334+
_cancellationTokenSource.Cancel();
287335
}
288336

289337
/// <summary>
290338
/// 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.
293341
/// </summary>
294342
public void ToggleOrTriggerSuggestions()
295343
{
344+
_threadingContext.ThrowIfNotOnUIThread();
296345
if (this.SupportsAutomaticSuggestions)
297346
{
298347
this.IsAutomaticSuggestionsEnabled = !this.IsAutomaticSuggestionsEnabled;
299348
if (this.IsAutomaticSuggestionsEnabled)
300349
{
301350
this.FetchSuggestions(isAutomaticOnInitialization: false);
302351
}
352+
else
353+
{
354+
_cancellationTokenSource.Cancel();
355+
}
303356
NotifyPropertyChanged(nameof(IsSuggestionsPanelExpanded));
304357
NotifyPropertyChanged(nameof(IsAutomaticSuggestionsEnabled));
305358
// 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)
316369

317370
private void BaseViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
318371
{
372+
_threadingContext.ThrowIfNotOnUIThread();
319373
if (e.PropertyName == nameof(BaseViewModel.IdentifierText))
320374
{
321-
_cancellationTokenSource?.Cancel();
375+
// User is typing the new identifier name, cancel the ongoing request to get suggestions.
376+
_cancellationTokenSource.Cancel();
322377
}
323378
}
324379
}

0 commit comments

Comments
 (0)