public Task <ImmutableArray <VSCompletionItem> > SortCompletionListAsync( IAsyncCompletionSession session, AsyncCompletionSessionInitialDataSnapshot data, CancellationToken cancellationToken) { if (session.TextView.Properties.TryGetProperty(CompletionSource.TargetTypeFilterExperimentEnabled, out bool isTargetTypeFilterEnabled) && isTargetTypeFilterEnabled) { AsyncCompletionLogger.LogSessionHasTargetTypeFilterEnabled(); // This method is called exactly once, so use the opportunity to set a baseline for telemetry. if (data.InitialList.Any(i => i.Filters.Any(f => f.DisplayText == FeaturesResources.Target_type_matches))) { AsyncCompletionLogger.LogSessionContainsTargetTypeFilter(); } } if (session.TextView.Properties.TryGetProperty(CompletionSource.TypeImportCompletionEnabled, out bool isTypeImportCompletionEnabled) && isTypeImportCompletionEnabled) { AsyncCompletionLogger.LogSessionWithTypeImportCompletionEnabled(); } // Sort by default comparer of Roslyn CompletionItem var sortedItems = data.InitialList.OrderBy(GetOrAddRoslynCompletionItem).ToImmutableArray(); return(Task.FromResult(sortedItems)); }
public Task <ImmutableArray <VSCompletionItem> > SortCompletionListAsync( IAsyncCompletionSession session, AsyncCompletionSessionInitialDataSnapshot data, CancellationToken cancellationToken) { var stopwatch = SharedStopwatch.StartNew(); // Sort by default comparer of Roslyn CompletionItem var sortedItems = data.InitialList.OrderBy(CompletionItemData.GetOrAddDummyRoslynItem).ToImmutableArray(); AsyncCompletionLogger.LogItemManagerSortTicksDataPoint((int)stopwatch.Elapsed.TotalMilliseconds); return(Task.FromResult(sortedItems)); }
public Task <ImmutableArray <VSCompletionItem> > SortCompletionListAsync( IAsyncCompletionSession session, AsyncCompletionSessionInitialDataSnapshot data, CancellationToken cancellationToken) { var stopwatch = SharedStopwatch.StartNew(); var sessionData = CompletionSessionData.GetOrCreateSessionData(session); // This method is called exactly once, so use the opportunity to set a baseline for telemetry. if (sessionData.TargetTypeFilterExperimentEnabled) { AsyncCompletionLogger.LogSessionHasTargetTypeFilterEnabled(); if (data.InitialList.Any(i => i.Filters.Any(f => f.DisplayText == FeaturesResources.Target_type_matches))) { AsyncCompletionLogger.LogSessionContainsTargetTypeFilter(); } } // Sort by default comparer of Roslyn CompletionItem var sortedItems = data.InitialList.OrderBy(CompletionItemData.GetOrAddDummyRoslynItem).ToImmutableArray(); AsyncCompletionLogger.LogItemManagerSortTicksDataPoint((int)stopwatch.Elapsed.TotalMilliseconds); return(Task.FromResult(sortedItems)); }
public async Task <FilteredCompletionModel?> UpdateCompletionListAsync( IAsyncCompletionSession session, AsyncCompletionSessionDataSnapshot data, CancellationToken cancellationToken) { var stopwatch = SharedStopwatch.StartNew(); try { var sessionData = CompletionSessionData.GetOrCreateSessionData(session); // As explained in more details in the comments for `CompletionSource.GetCompletionContextAsync`, expanded items might // not be provided upon initial trigger of completion to reduce typing delays, even if they are supposed to be included by default. // While we do not expect to run in to this scenario very often, we'd still want to minimize the impact on user experience of this feature // as best as we could when it does occur. So the solution we came up with is this: if we decided to not include expanded items (because the // computation is running too long,) we will let it run in the background as long as the completion session is still active. Then whenever // any user input that would cause the completion list to refresh, we will check the state of this background task and add expanded items as part // of the update if they are available. // There is a `CompletionContext.IsIncomplete` flag, which is only supported in LSP mode at the moment. Therefore we opt to handle the checking // and combining the items in Roslyn until the `IsIncomplete` flag is fully supported in classic mode. if (sessionData.CombinedSortedList.HasValue) { // Always use the previously saved combined list if available. data = new AsyncCompletionSessionDataSnapshot(sessionData.CombinedSortedList.Value, data.Snapshot, data.Trigger, data.InitialTrigger, data.SelectedFilters, data.IsSoftSelected, data.DisplaySuggestionItem, data.Defaults); } else if (sessionData.ExpandedItemsTask != null) { var task = sessionData.ExpandedItemsTask; if (task.Status == TaskStatus.RanToCompletion) { // Make sure the task is removed when Adding expanded items, // so duplicated items won't be added in subsequent list updates. sessionData.ExpandedItemsTask = null; var(expandedContext, _) = await task.ConfigureAwait(false); if (expandedContext.Items.Length > 0) { // Here we rely on the implementation detail of `CompletionItem.CompareTo`, which always put expand items after regular ones. var itemsBuilder = ImmutableArray.CreateBuilder <VSCompletionItem>(expandedContext.Items.Length + data.InitialSortedList.Length); itemsBuilder.AddRange(data.InitialSortedList); itemsBuilder.AddRange(expandedContext.Items); var combinedList = itemsBuilder.MoveToImmutable(); // Add expanded items into a combined list, and save it to be used for future updates during the same session. sessionData.CombinedSortedList = combinedList; var combinedFilterStates = FilterSet.CombineFilterStates(expandedContext.Filters, data.SelectedFilters); data = new AsyncCompletionSessionDataSnapshot(combinedList, data.Snapshot, data.Trigger, data.InitialTrigger, combinedFilterStates, data.IsSoftSelected, data.DisplaySuggestionItem, data.Defaults); } AsyncCompletionLogger.LogSessionWithDelayedImportCompletionIncludedInUpdate(); } } var updater = new CompletionListUpdater(session.ApplicableToSpan, sessionData, data, _recentItemsManager, _globalOptions); return(updater.UpdateCompletionList(cancellationToken)); } finally { AsyncCompletionLogger.LogItemManagerUpdateDataPoint((int)stopwatch.Elapsed.TotalMilliseconds, isCanceled: cancellationToken.IsCancellationRequested); } }
private FilteredCompletionModel UpdateCompletionList( IAsyncCompletionSession session, AsyncCompletionSessionDataSnapshot data, CancellationToken cancellationToken) { if (!session.Properties.TryGetProperty(CompletionSource.HasSuggestionItemOptions, out bool hasSuggestedItemOptions)) { // This is the scenario when the session is created out of Roslyn, in some other provider, e.g. in Debugger. // For now, the default hasSuggestedItemOptions is false. hasSuggestedItemOptions = false; } hasSuggestedItemOptions |= data.DisplaySuggestionItem; var filterText = session.ApplicableToSpan.GetText(data.Snapshot); var reason = data.Trigger.Reason; var initialRoslynTriggerKind = Helpers.GetRoslynTriggerKind(data.InitialTrigger); // Check if the user is typing a number. If so, only proceed if it's a number // directly after a <dot>. That's because it is actually reasonable for completion // to be brought up after a <dot> and for the user to want to filter completion // items based on a number that exists in the name of the item. However, when // we are not after a dot (i.e. we're being brought up after <space> is typed) // then we don't want to filter things. Consider the user writing: // // dim i =<space> // // We'll bring up the completion list here (as VB has completion on <space>). // If the user then types '3', we don't want to match against Int32. if (filterText.Length > 0 && char.IsNumber(filterText[0])) { if (!IsAfterDot(data.Snapshot, session.ApplicableToSpan)) { // Dismiss the session. return(null); } } // We need to filter if // 1. a non-empty strict subset of filters are selected // 2. a non-empty set of expanders are unselected var nonExpanderFilterStates = data.SelectedFilters.WhereAsArray(f => !(f.Filter is CompletionExpander)); var selectedNonExpanderFilters = nonExpanderFilterStates.Where(f => f.IsSelected).SelectAsArray(f => f.Filter); var needToFilter = selectedNonExpanderFilters.Length > 0 && selectedNonExpanderFilters.Length < nonExpanderFilterStates.Length; var unselectedExpanders = data.SelectedFilters.Where(f => !f.IsSelected && f.Filter is CompletionExpander).SelectAsArray(f => f.Filter); var needToFilterExpanded = unselectedExpanders.Length > 0; if (session.TextView.Properties.TryGetProperty(CompletionSource.TargetTypeFilterExperimentEnabled, out bool isExperimentEnabled) && isExperimentEnabled) { // Telemetry: Want to know % of sessions with the "Target type matches" filter where that filter is actually enabled if (needToFilter && !session.Properties.ContainsProperty(_targetTypeCompletionFilterChosenMarker) && selectedNonExpanderFilters.Any(f => f.DisplayText == FeaturesResources.Target_type_matches)) { AsyncCompletionLogger.LogTargetTypeFilterChosenInSession(); // Make sure we only record one enabling of the filter per session session.Properties.AddProperty(_targetTypeCompletionFilterChosenMarker, _targetTypeCompletionFilterChosenMarker); } } var filterReason = Helpers.GetFilterReason(data.Trigger); // If the session was created/maintained out of Roslyn, e.g. in debugger; no properties are set and we should use data.Snapshot. // However, we prefer using the original snapshot in some projection scenarios. var snapshotForDocument = Helpers.TryGetInitialTriggerLocation(session, out var triggerLocation) ? triggerLocation.Snapshot : data.Snapshot; var document = snapshotForDocument.TextBuffer.AsTextContainer().GetOpenDocumentInCurrentContext(); var completionService = document?.GetLanguageService <CompletionService>(); var completionRules = completionService?.GetRules() ?? CompletionRules.Default; var completionHelper = document != null?CompletionHelper.GetHelper(document) : _defaultCompletionHelper; // DismissIfLastCharacterDeleted should be applied only when started with Insertion, and then Deleted all characters typed. // This conforms with the original VS 2010 behavior. if (initialRoslynTriggerKind == CompletionTriggerKind.Insertion && data.Trigger.Reason == CompletionTriggerReason.Backspace && completionRules.DismissIfLastCharacterDeleted && session.ApplicableToSpan.GetText(data.Snapshot).Length == 0) { // Dismiss the session return(null); } var options = document?.Project.Solution.Options; var highlightMatchingPortions = options?.GetOption(CompletionOptions.HighlightMatchingPortionsOfCompletionListItems, document.Project.Language) ?? false; // Nothing to highlight if user hasn't typed anything yet. highlightMatchingPortions = highlightMatchingPortions && filterText.Length > 0; // Use a monotonically increasing integer to keep track the original alphabetical order of each item. var currentIndex = 0; var builder = ArrayBuilder <MatchResult> .GetInstance(); foreach (var item in data.InitialSortedList) { cancellationToken.ThrowIfCancellationRequested(); if (needToFilter && ShouldBeFilteredOutOfCompletionList(item, selectedNonExpanderFilters)) { continue; } if (needToFilterExpanded && ShouldBeFilteredOutOfExpandedCompletionList(item, unselectedExpanders)) { continue; } if (TryCreateMatchResult( completionHelper, item, filterText, initialRoslynTriggerKind, filterReason, _recentItemsManager.RecentItems, highlightMatchingPortions: highlightMatchingPortions, ref currentIndex, out var matchResult)) { builder.Add(matchResult); } } if (builder.Count == 0) { return(HandleAllItemsFilteredOut(reason, data.SelectedFilters, completionRules)); } // Sort the items by pattern matching results. // Note that we want to preserve the original alphabetical order for items with same pattern match score, // but `ArrayBuilder.Sort` isn't stable. Therefore we have to add a monotonically increasing integer // to `MatchResult` to archieve this. builder.Sort(MatchResult.SortingComparer); var initialListOfItemsToBeIncluded = builder.ToImmutableAndFree(); var showCompletionItemFilters = options?.GetOption(CompletionOptions.ShowCompletionItemFilters, document.Project.Language) ?? true; var updatedFilters = showCompletionItemFilters ? GetUpdatedFilters(initialListOfItemsToBeIncluded, data.SelectedFilters) : ImmutableArray <CompletionFilterWithState> .Empty; // If this was deletion, then we control the entire behavior of deletion ourselves. if (initialRoslynTriggerKind == CompletionTriggerKind.Deletion) { return(HandleDeletionTrigger(data.Trigger.Reason, initialListOfItemsToBeIncluded, filterText, updatedFilters)); } Func <ImmutableArray <(RoslynCompletionItem, PatternMatch?)>, string, ImmutableArray <RoslynCompletionItem> > filterMethod; if (completionService == null) { filterMethod = (itemsWithPatternMatches, text) => CompletionService.FilterItems(completionHelper, itemsWithPatternMatches); } else { filterMethod = (itemsWithPatternMatches, text) => completionService.FilterItems(document, itemsWithPatternMatches, text); } return(HandleNormalFiltering( filterMethod, filterText, updatedFilters, filterReason, data.Trigger.Character, initialListOfItemsToBeIncluded, hasSuggestedItemOptions));
private FilteredCompletionModel UpdateCompletionList( IAsyncCompletionSession session, AsyncCompletionSessionDataSnapshot data, CancellationToken cancellationToken) { if (!session.Properties.TryGetProperty(CompletionSource.HasSuggestionItemOptions, out bool hasSuggestedItemOptions)) { // This is the scenario when the session is created out of Roslyn, in some other provider, e.g. in Debugger. // For now, the default hasSuggestedItemOptions is false. hasSuggestedItemOptions = false; } hasSuggestedItemOptions |= data.DisplaySuggestionItem; var filterText = session.ApplicableToSpan.GetText(data.Snapshot); var reason = data.Trigger.Reason; if (!session.Properties.TryGetProperty(CompletionSource.InitialTriggerKind, out CompletionTriggerKind initialRoslynTriggerKind)) { initialRoslynTriggerKind = CompletionTriggerKind.Invoke; } // Check if the user is typing a number. If so, only proceed if it's a number // directly after a <dot>. That's because it is actually reasonable for completion // to be brought up after a <dot> and for the user to want to filter completion // items based on a number that exists in the name of the item. However, when // we are not after a dot (i.e. we're being brought up after <space> is typed) // then we don't want to filter things. Consider the user writing: // // dim i =<space> // // We'll bring up the completion list here (as VB has completion on <space>). // If the user then types '3', we don't want to match against Int32. if (filterText.Length > 0 && char.IsNumber(filterText[0])) { if (!IsAfterDot(data.Snapshot, session.ApplicableToSpan)) { // Dismiss the session. return(null); } } // We need to filter if a non-empty strict subset of filters are selected var selectedFilters = data.SelectedFilters.Where(f => f.IsSelected).Select(f => f.Filter).ToImmutableArray(); var needToFilter = selectedFilters.Length > 0 && selectedFilters.Length < data.SelectedFilters.Length; if (session.TextView.Properties.TryGetProperty(CompletionSource.TargetTypeFilterExperimentEnabled, out bool isExperimentEnabled) && isExperimentEnabled) { // Telemetry: Want to know % of sessions with the "Target type matches" filter where that filter is actually enabled if (needToFilter && !session.Properties.ContainsProperty(_targetTypeCompletionFilterChosenMarker) && selectedFilters.Any(f => f.DisplayText == FeaturesResources.Target_type_matches)) { AsyncCompletionLogger.LogTargetTypeFilterChosenInSession(); // Make sure we only record one enabling of the filter per session session.Properties.AddProperty(_targetTypeCompletionFilterChosenMarker, _targetTypeCompletionFilterChosenMarker); } } var filterReason = Helpers.GetFilterReason(data.Trigger); // If the session was created/maintained out of Roslyn, e.g. in debugger; no properties are set and we should use data.Snapshot. // However, we prefer using the original snapshot in some projection scenarios. if (!session.Properties.TryGetProperty(CompletionSource.TriggerSnapshot, out ITextSnapshot snapshotForDocument)) { snapshotForDocument = data.Snapshot; } var document = snapshotForDocument.TextBuffer.AsTextContainer().GetOpenDocumentInCurrentContext(); var completionService = document?.GetLanguageService <CompletionService>(); var completionRules = completionService?.GetRules() ?? CompletionRules.Default; var completionHelper = document != null?CompletionHelper.GetHelper(document) : _defaultCompletionHelper; var initialListOfItemsToBeIncluded = new List <ExtendedFilterResult>(); foreach (var item in data.InitialSortedList) { cancellationToken.ThrowIfCancellationRequested(); if (needToFilter && ShouldBeFilteredOutOfCompletionList(item, selectedFilters)) { continue; } if (!item.Properties.TryGetProperty(CompletionSource.RoslynItem, out RoslynCompletionItem roslynItem)) { roslynItem = RoslynCompletionItem.Create( displayText: item.DisplayText, filterText: item.FilterText, sortText: item.SortText, displayTextSuffix: item.Suffix); } if (MatchesFilterText(completionHelper, roslynItem, filterText, initialRoslynTriggerKind, filterReason, _recentItemsManager.RecentItems)) { initialListOfItemsToBeIncluded.Add(new ExtendedFilterResult(item, new FilterResult(roslynItem, filterText, matchedFilterText: true))); } else { // The item didn't match the filter text. We'll still keep it in the list // if one of two things is true: // // 1. The user has only typed a single character. In this case they might // have just typed the character to get completion. Filtering out items // here is not desirable. // // 2. They brough up completion with ctrl-j or through deletion. In these // cases we just always keep all the items in the list. if (initialRoslynTriggerKind == CompletionTriggerKind.Deletion || initialRoslynTriggerKind == CompletionTriggerKind.Invoke || filterText.Length <= 1) { initialListOfItemsToBeIncluded.Add(new ExtendedFilterResult(item, new FilterResult(roslynItem, filterText, matchedFilterText: false))); } } } // DismissIfLastCharacterDeleted should be applied only when started with Insertion, and then Deleted all characters typed. // This confirms with the original VS 2010 behavior. if (initialRoslynTriggerKind == CompletionTriggerKind.Insertion && data.Trigger.Reason == CompletionTriggerReason.Backspace && completionRules.DismissIfLastCharacterDeleted && session.ApplicableToSpan.GetText(data.Snapshot).Length == 0) { // Dismiss the session return(null); } if (initialListOfItemsToBeIncluded.Count == 0) { return(HandleAllItemsFilteredOut(reason, data.SelectedFilters, completionRules)); } var options = document?.Project.Solution.Options; var highlightMatchingPortions = options?.GetOption(CompletionOptions.HighlightMatchingPortionsOfCompletionListItems, document.Project.Language) ?? true; var showCompletionItemFilters = options?.GetOption(CompletionOptions.ShowCompletionItemFilters, document.Project.Language) ?? true; var updatedFilters = showCompletionItemFilters ? GetUpdatedFilters(initialListOfItemsToBeIncluded, data.SelectedFilters) : ImmutableArray <CompletionFilterWithState> .Empty; var highlightedList = GetHighlightedList(initialListOfItemsToBeIncluded, filterText, completionHelper, highlightMatchingPortions).ToImmutableArray(); // If this was deletion, then we control the entire behavior of deletion ourselves. if (initialRoslynTriggerKind == CompletionTriggerKind.Deletion) { return(HandleDeletionTrigger(data.Trigger.Reason, initialListOfItemsToBeIncluded, filterText, updatedFilters, highlightedList)); } Func <ImmutableArray <RoslynCompletionItem>, string, ImmutableArray <RoslynCompletionItem> > filterMethod; if (completionService == null) { filterMethod = (items, text) => CompletionService.FilterItems(completionHelper, items, text); } else { filterMethod = (items, text) => completionService.FilterItems(document, items, text); } return(HandleNormalFiltering( filterMethod, filterText, updatedFilters, initialRoslynTriggerKind, filterReason, data.Trigger.Character, initialListOfItemsToBeIncluded, highlightedList, completionHelper, hasSuggestedItemOptions)); }
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. AssertIsForeground(); var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); if (document == null) { return(CommitResultUnhandled); } var completionService = document.GetLanguageService <CompletionService>(); if (completionService == null) { return(CommitResultUnhandled); } if (!item.Properties.TryGetProperty(CompletionSource.RoslynItem, out RoslynCompletionItem roslynItem)) { // 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(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 serviceRules = completionService.GetRules(); // 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 charcter in the context of Rules that could change the list of commit characters. // Tab, Enter and Null (call invoke commit) are always commit characters. if (typeChar != '\t' && typeChar != '\n' && typeChar != '\0' && !IsCommitCharacter(serviceRules, roslynItem, typeChar, filterText)) { // 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 (!session.Properties.TryGetProperty(CompletionSource.TriggerSnapshot, out ITextSnapshot triggerSnapshot)) { // Need the trigger snapshot to calculate the span when the commit changes to be applied. // It should be inserted into a property bag within GetCompletionContextAsync for each item created by Roslyn. // If not found here, Roslyn should not make a commit. return(CommitResultUnhandled); } if (!session.Properties.TryGetProperty(CompletionSource.CompletionListSpan, out TextSpan completionListSpan)) { return(CommitResultUnhandled); } var triggerDocument = triggerSnapshot.GetOpenDocumentInCurrentContextWithChanges(); if (triggerDocument == null) { return(CommitResultUnhandled); } // Telemetry if (session.TextView.Properties.TryGetProperty(CompletionSource.TypeImportCompletionEnabled, out bool isTyperImportCompletionEnabled) && isTyperImportCompletionEnabled) { AsyncCompletionLogger.LogCommitWithTypeImportCompletionEnabled(); if (roslynItem.IsCached) { AsyncCompletionLogger.LogCommitOfTypeImportCompletionItem(); } } if (session.TextView.Properties.TryGetProperty(CompletionSource.TargetTypeFilterExperimentEnabled, out bool isExperimentEnabled) && isExperimentEnabled) { // Capture the % of committed completion items that would have appeared in the "Target type matches" filter // (regardless of whether that filter button was active at the time of commit). AsyncCompletionLogger.LogCommitWithTargetTypeCompletionExperimentEnabled(); if (item.Filters.Any(f => f.DisplayText == FeaturesResources.Target_type_matches)) { AsyncCompletionLogger.LogCommitItemWithTargetTypeFilter(); } } // Commit with completion service assumes that null is provided is case of invoke. VS provides '\0' in the case. char?commitChar = typeChar == '\0' ? null : (char?)typeChar; var commitBehavior = Commit( triggerDocument, completionService, session.TextView, subjectBuffer, roslynItem, completionListSpan, commitChar, triggerSnapshot, serviceRules, filterText, cancellationToken); _recentItemsManager.MakeMostRecentItem(roslynItem.DisplayText); return(new AsyncCompletionData.CommitResult(isHandled: true, commitBehavior)); }