in src/Editor/Language/Impl/Language/AsyncCompletion/AsyncCompletionSession.cs [1205:1429]
private async Task<CompletionModel> UpdateSnapshot(CompletionModel model, CompletionTrigger trigger, SnapshotPoint updateLocation, ITextSnapshot rootSnapshot, int thisId, CancellationToken token)
{
// Always record keystrokes, even if filtering is preempted
_telemetry.RecordKeystroke();
// Completion got cancelled
if (token.IsCancellationRequested || model == null)
return default;
// Dismiss if we are outside of the applicable span
var instantaneousSnapshot = updateLocation.Snapshot;
var currentlyApplicableToSpan = ApplicableToSpan.GetSpan(instantaneousSnapshot);
if (updateLocation < currentlyApplicableToSpan.Start
|| updateLocation > currentlyApplicableToSpan.End)
{
((IAsyncCompletionSession)this).Dismiss();
return model;
}
// If the applicable to span was empty, is empty again, and user is deleting, then dismiss
if (currentlyApplicableToSpan.IsEmpty
&& model.ApplicableToSpanWasEmpty
&& (trigger.Reason == CompletionTriggerReason.Deletion || trigger.Reason == CompletionTriggerReason.Backspace))
{
_finalSessionState = CompletionSessionState.DismissedDueToBackspace;
((IAsyncCompletionSession)this).Dismiss();
return model;
}
// If user is backspacing at the beginning of a span, dismiss
if (updateLocation == currentlyApplicableToSpan.Start && trigger.Reason == CompletionTriggerReason.Backspace)
{
if (_inCaretLocationFallback)
{
// If user was previously at the beginning of the span, this backspace will dismiss completion
_finalSessionState = CompletionSessionState.DismissedDueToBackspace;
((IAsyncCompletionSession)this).Dismiss();
return model;
}
else
{
// Caret just moved to the beginning of the span, enter soft selection
_selectionModeBeforeCaretLocationFallback = model.UseSoftSelection;
model = model.WithSoftSelection(true);
_inCaretLocationFallback = true;
}
}
// Record whether the applicable to span is empty
model = model.WithApplicableToSpanStatus(currentlyApplicableToSpan.IsEmpty);
// The model previously received no items, but we are called again because user typed something.
// There is a chance that language service will provide items this time.
// Due to timing issues, if we dismiss and start another session, we would miss some user actions.
// Instead, attempt to get items again within this session.
if (model.Uninitialized && thisId > 1) // Don't attempt to get items on the very first UpdateSnapshot
{
// Attempt to get new completion items
model = await GetInitialModel(trigger, updateLocation, rootSnapshot, token).ConfigureAwait(true);
if (model == null) // This happens when computation has been cancelled
{
_finalSessionState = CompletionSessionState.DismissedDueToCancellation;
((IAsyncCompletionSession)this).Dismiss();
return model;
}
}
// If we still have no items, dismiss, unless there is another task queued (because user has typed).
if (model.Uninitialized)
{
_finalSessionState = CompletionSessionState.DismissedUninitialized;
var dismissed = await TryDismissSafely(thisId).ConfigureAwait(true);
return model;
}
// There is another taks queued: We are preempted, store the most recent snapshot for the upcoming invocation of UpdateSnapshot
if (thisId != _lastFilteringTaskId)
return model.WithSnapshot(instantaneousSnapshot);
_telemetry.ComputationStopwatch.Restart();
var filteredCompletion = await _guardedOperations.CallExtensionPointAsync(
errorSource: _completionItemManager,
asyncCall: () => _completionItemManager.UpdateCompletionListAsync(
session: this,
data: new AsyncCompletionSessionDataSnapshot(
model.InitialItems,
instantaneousSnapshot,
trigger,
InitialTrigger,
model.Filters,
model.UseSoftSelection,
model.DisplaySuggestionItem),
token: token),
valueOnThrow: null).ConfigureAwait(true);
// Error cases are handled by logging them above and dismissing the session.
if (token.IsCancellationRequested)
{
_telemetry.RecordBlockingExtension(_completionItemManager);
_finalSessionState = CompletionSessionState.DismissedDueToCancellation;
((IAsyncCompletionSession)this).Dismiss();
return model;
}
if (filteredCompletion == null)
{
_finalSessionState = CompletionSessionState.DismissedDuringFiltering;
((IAsyncCompletionSession)this).Dismiss();
return model;
}
// Other error cases that we attribute to the IAsyncCompletionItemManager
if (filteredCompletion.SelectedItemIndex == -1 && !model.DisplaySuggestionItem)
{
_guardedOperations.HandleException(errorSource: _completionItemManager,
e: new InvalidOperationException($"{nameof(IAsyncCompletionItemManager)} recommended selecting suggestion item when there is no suggestion item."));
_finalSessionState = CompletionSessionState.DismissedDuringFiltering;
((IAsyncCompletionSession)this).Dismiss();
return model;
}
int selectedIndex = filteredCompletion.SelectedItemIndex;
bool selectedIndexOverridden = false; // Used when ApplicableToSpan is empty
// Special experience when there are no returned items:
ImmutableArray<CompletionItemWithHighlight> returnedItems;
if (filteredCompletion.Items.IsDefault)
{
// Prevent null references when service returns default(ImmutableArray)
returnedItems = ImmutableArray<CompletionItemWithHighlight>.Empty;
}
else if (filteredCompletion.Items.IsEmpty)
{
if (model.PresentedItems.IsDefaultOrEmpty)
{
// There were no previously visible results. Return a valid empty array
returnedItems = ImmutableArray<CompletionItemWithHighlight>.Empty;
}
else
{
// Show previously visible results, without highlighting
returnedItems = model.PresentedItems.Select(n => new CompletionItemWithHighlight(n.CompletionItem)).ToImmutableArray();
selectedIndex = model.SelectedIndex;
if (!_inNoResultFallback)
{
// Enter the no results mode to preserve the selection state
_selectionModeBeforeNoResultFallback = model.UseSoftSelection;
_inNoResultFallback = true;
model = model.WithSoftSelection(true);
}
}
}
else
{
// Default behavior, we received completion items
returnedItems = filteredCompletion.Items;
if (_inNoResultFallback)
{
// we were in the no result mode and just received no items. Restore the selection mode.
model = model.WithSoftSelection(_selectionModeBeforeNoResultFallback);
_inNoResultFallback = false;
}
// Special experience when ApplicableToSpan is empty: attempt to select last selected item
if (currentlyApplicableToSpan.IsEmpty && !string.IsNullOrEmpty(_previouslySelectedItemText))
{
int indexOfPreviouslySelectedItem = -1;
for (int i = 0; i < filteredCompletion.Items.Length; i++)
{
if (filteredCompletion.Items[i].CompletionItem.DisplayText.Equals(_previouslySelectedItemText, StringComparison.Ordinal))
{
indexOfPreviouslySelectedItem = i;
break;
}
}
if (indexOfPreviouslySelectedItem != -1)
{
// We found a matching item
model = model.WithSelectedIndex(indexOfPreviouslySelectedItem, preserveSoftSelection: true).WithSoftSelection(true);
selectedIndexOverridden = true;
}
}
// Leave the caret location fallback if user just typed something
if (_inCaretLocationFallback && trigger.Reason == CompletionTriggerReason.Insertion)
{
// User just typed something, so we can't be at the beginning of applicable to span. Revert the selection mode.
model = model.WithSoftSelection(_selectionModeBeforeCaretLocationFallback);
_inCaretLocationFallback = false;
}
}
_telemetry.ComputationStopwatch.Stop();
_telemetry.RecordProcessing(_telemetry.ComputationStopwatch.ElapsedMilliseconds, returnedItems.Length);
// Allow the item manager to control the selection of the suggestion item
if (model.DisplaySuggestionItem)
{
if (filteredCompletion.SelectedItemIndex == -1)
model = model.WithSuggestionItemSelected();
else if (!selectedIndexOverridden)
model = model.WithSelectedIndex(selectedIndex, preserveSoftSelection: true);
// If suggestion item is present, we default to soft selection.
model = model.WithSoftSelection(true);
_previouslySelectedItemText = string.Empty;
}
else if (!selectedIndexOverridden && !returnedItems.IsDefaultOrEmpty)
{
model = model.WithSelectedIndex(selectedIndex, preserveSoftSelection: true);
_previouslySelectedItemText = returnedItems[selectedIndex].CompletionItem.DisplayText;
}
// Allow the item manager to override the selection style.
// Our recommendation for extenders is to use UpdateSelectionHint.NoChange whenever possible
if (filteredCompletion.SelectionHint == UpdateSelectionHint.SoftSelected)
model = model.WithSoftSelection(true);
else if (filteredCompletion.SelectionHint == UpdateSelectionHint.Selected)
model = model.WithSoftSelection(false);
// Prepare the suggestionItem if user ever activates suggestion mode
var enteredText = currentlyApplicableToSpan.GetText();
var suggestionItem = new CompletionItem(enteredText, SuggestionModeCompletionItemSource);
return model.WithSnapshotItemsAndFilters(updateLocation.Snapshot, returnedItems, filteredCompletion.UniqueItem, suggestionItem, filteredCompletion.Filters);
}