diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CommitManager.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CommitManager.cs index 192290e3d57bf..1059605bec8ed 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CommitManager.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CommitManager.cs @@ -105,14 +105,14 @@ public AsyncCompletionData.CommitResult TryCommit( return CommitResultUnhandled; } - if (!item.Properties.TryGetProperty(CompletionSource.RoslynItem, out RoslynCompletionItem roslynItem)) + if (!CompletionItemData.TryGetData(item, out var itemData)) { // 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)) + if (Helpers.IsFilterCharacter(itemData.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); @@ -124,14 +124,14 @@ public AsyncCompletionData.CommitResult TryCommit( // 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 character in the context of Rules that could change the list of commit characters. - if (!Helpers.IsStandardCommitCharacter(typeChar) && !IsCommitCharacter(serviceRules, roslynItem, typeChar)) + if (!Helpers.IsStandardCommitCharacter(typeChar) && !IsCommitCharacter(serviceRules, itemData.RoslynItem, typeChar)) { // 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 (!item.Properties.TryGetProperty(CompletionSource.TriggerLocation, out SnapshotPoint triggerLocation)) + if (!itemData.TriggerLocation.HasValue) { // Need the trigger snapshot to calculate the span when the commit changes to be applied. // They should always be available from items provided by Roslyn CompletionSource. @@ -139,19 +139,20 @@ public AsyncCompletionData.CommitResult TryCommit( return CommitResultUnhandled; } - if (!session.Properties.TryGetProperty(CompletionSource.CompletionListSpan, out TextSpan completionListSpan)) + var triggerDocument = itemData.TriggerLocation.Value.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (triggerDocument == null) { return CommitResultUnhandled; } - var triggerDocument = triggerLocation.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (triggerDocument == null) + var sessionData = CompletionSessionData.GetOrCreateSessionData(session); + if (!sessionData.CompletionListSpan.HasValue) { return CommitResultUnhandled; } // Telemetry - if (session.TextView.Properties.TryGetProperty(CompletionSource.TargetTypeFilterExperimentEnabled, out bool isExperimentEnabled) && isExperimentEnabled) + if (sessionData.TargetTypeFilterExperimentEnabled) { // 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). @@ -166,7 +167,7 @@ public AsyncCompletionData.CommitResult TryCommit( var commitChar = typeChar == '\0' ? null : (char?)typeChar; return Commit( session, triggerDocument, completionService, subjectBuffer, - roslynItem, completionListSpan, commitChar, triggerLocation.Snapshot, serviceRules, + itemData.RoslynItem, sessionData.CompletionListSpan.Value, commitChar, itemData.TriggerLocation.Value.Snapshot, serviceRules, filterText, cancellationToken); } diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionItemData.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionItemData.cs new file mode 100644 index 0000000000000..cba26a8302f54 --- /dev/null +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionItemData.cs @@ -0,0 +1,40 @@ +// 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 Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text; +using RoslynCompletionItem = Microsoft.CodeAnalysis.Completion.CompletionItem; + +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion +{ + internal sealed record class CompletionItemData(RoslynCompletionItem RoslynItem, SnapshotPoint? TriggerLocation) + { + private const string RoslynCompletionItemData = nameof(RoslynCompletionItemData); + + public static bool TryGetData(CompletionItem vsCompletionitem, out CompletionItemData data) + => vsCompletionitem.Properties.TryGetProperty(RoslynCompletionItemData, out data); + + public static RoslynCompletionItem GetOrAddDummyRoslynItem(CompletionItem vsItem) + { + if (TryGetData(vsItem, out var data)) + return data.RoslynItem; + + // TriggerLocation is null for items provided by non-roslyn completion source + var roslynItem = CreateDummyRoslynItem(vsItem); + AddData(vsItem, roslynItem, triggerLocation: null); + + return roslynItem; + } + + public static void AddData(CompletionItem vsCompletionitem, RoslynCompletionItem roslynItem, SnapshotPoint? triggerLocation) + => vsCompletionitem.Properties[RoslynCompletionItemData] = new CompletionItemData(roslynItem, triggerLocation); + + private static RoslynCompletionItem CreateDummyRoslynItem(CompletionItem vsItem) + => RoslynCompletionItem.Create( + displayText: vsItem.DisplayText, + filterText: vsItem.FilterText, + sortText: vsItem.SortText, + displayTextSuffix: vsItem.Suffix); + } +} diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSessionData.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSessionData.cs new file mode 100644 index 0000000000000..98729d3730fe5 --- /dev/null +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSessionData.cs @@ -0,0 +1,38 @@ +// 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.Collections.Immutable; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion; +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; +using Microsoft.VisualStudio.Text; +using RoslynCompletionList = Microsoft.CodeAnalysis.Completion.CompletionList; + +namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion +{ + /// + /// Contains data need to be tracked over an entire IAsyncCompletionSession completion + /// session for various operations. + /// + internal sealed class CompletionSessionData + { + private const string RoslynCompletionSessionData = nameof(RoslynCompletionSessionData); + public bool TargetTypeFilterExperimentEnabled { get; set; } + public bool TargetTypeFilterSelected { get; set; } + public bool HasSuggestionItemOptions { get; set; } + + public SnapshotPoint? ExpandedItemTriggerLocation { get; set; } + public TextSpan? CompletionListSpan { get; set; } + public ImmutableArray? CombinedSortedList { get; set; } + public Task<(CompletionContext, RoslynCompletionList)>? ExpandedItemsTask { get; set; } + + private CompletionSessionData() + { + } + + public static CompletionSessionData GetOrCreateSessionData(IAsyncCompletionSession session) + => session.Properties.GetOrCreateSingletonProperty(RoslynCompletionSessionData, static () => new CompletionSessionData()); + } +} diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSource.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSource.cs index 9244dd9679f20..5603b0d71f2be 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSource.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CompletionSource.cs @@ -10,9 +10,9 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Completion.Providers; -using Microsoft.CodeAnalysis.Classification; using Microsoft.CodeAnalysis.Editor.Host; using Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.QuickInfo; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; @@ -29,6 +29,7 @@ using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Adornments; using Microsoft.VisualStudio.Text.Editor; +using Roslyn.Utilities; using AsyncCompletionData = Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; using RoslynCompletionItem = Microsoft.CodeAnalysis.Completion.CompletionItem; using VSCompletionContext = Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data.CompletionContext; @@ -39,18 +40,11 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncComplet { internal sealed class CompletionSource : ForegroundThreadAffinitizedObject, IAsyncExpandingCompletionSource { - internal const string RoslynItem = nameof(RoslynItem); - internal const string TriggerLocation = nameof(TriggerLocation); - internal const string ExpandedItemTriggerLocation = nameof(ExpandedItemTriggerLocation); - internal const string CompletionListSpan = nameof(CompletionListSpan); - internal const string InsertionText = nameof(InsertionText); - internal const string HasSuggestionItemOptions = nameof(HasSuggestionItemOptions); - internal const string Description = nameof(Description); internal const string PotentialCommitCharacters = nameof(PotentialCommitCharacters); - internal const string ExcludedCommitCharacters = nameof(ExcludedCommitCharacters); internal const string NonBlockingCompletion = nameof(NonBlockingCompletion); - internal const string TargetTypeFilterExperimentEnabled = nameof(TargetTypeFilterExperimentEnabled); - internal const string ExpandedItemsTask = nameof(ExpandedItemsTask); + + // Don't change this property! Editor code currently has a dependency on it. + internal const string ExcludedCommitCharacters = nameof(ExcludedCommitCharacters); private static readonly ImmutableArray s_warningImageAttributeImagesArray = ImmutableArray.Create(new ImageElement(Glyph.CompletionWarning.GetImageId(), EditorFeaturesResources.Warning_image_element)); @@ -133,9 +127,6 @@ public AsyncCompletionData.CompletionStartData InitializeCompletion( // Set it later if met the condition. _snippetCompletionTriggeredIndirectly = false; - // For telemetry reporting purpose - _textView.Properties[TargetTypeFilterExperimentEnabled] = options.TargetTypedCompletionFilter; - var sourceText = document.GetTextSynchronously(cancellationToken); return ShouldTriggerCompletion(trigger, triggerLocation, sourceText, document, service, options) @@ -264,6 +255,10 @@ public async Task GetCompletionContextAsync( // to still provide those items later before they are truly required. var options = _globalOptions.GetCompletionOptions(document.Project.Language); + var sessionData = CompletionSessionData.GetOrCreateSessionData(session); + + // For telemetry reporting purpose + sessionData.TargetTypeFilterExperimentEnabled = options.TargetTypedCompletionFilter; if (!options.ShouldShowItemsFromUnimportNamspaces()) { @@ -271,7 +266,7 @@ public async Task GetCompletionContextAsync( var (context, list) = await GetCompletionContextWorkerAsync(document, trigger, triggerLocation, options with { ExpandedCompletionBehavior = ExpandedCompletionMode.NonExpandedItemsOnly }, cancellationToken).ConfigureAwait(false); - AddPropertiesToSession(session, list, triggerLocation); + UpdateSessionData(session, sessionData, list, triggerLocation); return context; } else if (!_responsiveCompletionEnabled) @@ -281,7 +276,7 @@ public async Task GetCompletionContextAsync( var (context, list) = await GetCompletionContextWorkerAsync(document, trigger, triggerLocation, options with { ExpandedCompletionBehavior = ExpandedCompletionMode.AllItems }, cancellationToken).ConfigureAwait(false); - AddPropertiesToSession(session, list, triggerLocation); + UpdateSessionData(session, sessionData, list, triggerLocation); AsyncCompletionLogger.LogImportCompletionGetContext(isBlocking: true, delayed: false); return context; } @@ -309,13 +304,13 @@ public async Task GetCompletionContextAsync( // Now trigger and wait for core providers to return; var (nonExpandedContext, nonExpandedCompletionList) = await GetCompletionContextWorkerAsync(document, trigger, triggerLocation, options with { ExpandedCompletionBehavior = ExpandedCompletionMode.NonExpandedItemsOnly }, cancellationToken).ConfigureAwait(false); - AddPropertiesToSession(session, nonExpandedCompletionList, triggerLocation); + UpdateSessionData(session, sessionData, nonExpandedCompletionList, triggerLocation); if (expandedItemsTask.IsCompleted) { // the task of expanded item is completed, get the result and combine it with result of non-expanded items. var (expandedContext, expandedCompletionList) = await expandedItemsTask.ConfigureAwait(false); - AddPropertiesToSession(session, expandedCompletionList, triggerLocation); + UpdateSessionData(session, sessionData, expandedCompletionList, triggerLocation); AsyncCompletionLogger.LogImportCompletionGetContext(isBlocking: false, delayed: false); return CombineCompletionContext(nonExpandedContext, expandedContext); @@ -327,7 +322,7 @@ public async Task GetCompletionContextAsync( // after core providers completed (instead of how long it takes end-to-end). stopwatch.Start(); - session.Properties[ExpandedItemsTask] = expandedItemsTask; + sessionData.ExpandedItemsTask = expandedItemsTask; AsyncCompletionLogger.LogImportCompletionGetContext(isBlocking: false, delayed: true); return nonExpandedContext; @@ -362,20 +357,25 @@ public async Task GetExpandedCompletionContextAsync( SnapshotSpan applicableToSpan, CancellationToken cancellationToken) { + var sessionData = CompletionSessionData.GetOrCreateSessionData(session); + // We only want to provide expanded items for Roslyn's expander. - if (expander == FilterSet.Expander && session.Properties.TryGetProperty(ExpandedItemTriggerLocation, out SnapshotPoint initialTriggerLocation)) + if (expander == FilterSet.Expander && sessionData.ExpandedItemTriggerLocation.HasValue) { + var initialTriggerLocation = sessionData.ExpandedItemTriggerLocation.Value; AsyncCompletionLogger.LogExpanderUsage(); // It's possible we didn't provide expanded items at the beginning of completion session because it was slow even if the feature is enabled. // ExpandedItemsTask would be available in this case, so we just need to return its result. - if (session.Properties.TryGetProperty(ExpandedItemsTask, out Task<(VSCompletionContext, CompletionList)> expandedItemsTask)) + if (sessionData.ExpandedItemsTask != null) { // Make sure the task is removed when returning expanded items, // so duplicated items won't be added in subsequent list updates. - session.Properties.RemoveProperty(ExpandedItemsTask); - var (expandedContext, expandedCompletionList) = await expandedItemsTask.ConfigureAwait(false); - AddPropertiesToSession(session, expandedCompletionList, initialTriggerLocation); + var task = sessionData.ExpandedItemsTask; + sessionData.ExpandedItemsTask = null; + + var (expandedContext, expandedCompletionList) = await task.ConfigureAwait(false); + UpdateSessionData(session, sessionData, expandedCompletionList, initialTriggerLocation); return expandedContext; } @@ -393,7 +393,7 @@ public async Task GetExpandedCompletionContextAsync( }; var (context, completionList) = await GetCompletionContextWorkerAsync(document, intialTrigger, initialTriggerLocation, options, cancellationToken).ConfigureAwait(false); - AddPropertiesToSession(session, completionList, initialTriggerLocation); + UpdateSessionData(session, sessionData, completionList, initialTriggerLocation); return context; } @@ -444,25 +444,22 @@ public async Task GetExpandedCompletionContextAsync( var suggestionItemOptions = new AsyncCompletionData.SuggestionItemOptions( completionList.SuggestionModeItem.DisplayText, - completionList.SuggestionModeItem.Properties.TryGetValue(Description, out var description) ? description : string.Empty); + completionList.SuggestionModeItem.Properties.TryGetValue(CommonCompletionItem.DescriptionProperty, out var description) ? description : string.Empty); return (new(items, suggestionItemOptions, selectionHint: AsyncCompletionData.InitialSelectionHint.SoftSelection, filters), completionList); } - private static void AddPropertiesToSession(IAsyncCompletionSession session, CompletionList completionList, SnapshotPoint triggerLocation) + private static void UpdateSessionData(IAsyncCompletionSession session, CompletionSessionData sessionData, CompletionList completionList, SnapshotPoint triggerLocation) { // Store around the span this completion list applies to. We'll use this later // to pass this value in when we're committing a completion list item. // It's OK to overwrite this value when expanded items are requested. - session.Properties[CompletionListSpan] = completionList.Span; + sessionData.CompletionListSpan = completionList.Span; // This is a code supporting original completion scenarios: // Controller.Session_ComputeModel: if completionList.SuggestionModeItem != null, then suggestionMode = true // If there are suggestionItemOptions, then later HandleNormalFiltering should set selection to SoftSelection. - if (!session.Properties.TryGetProperty(HasSuggestionItemOptions, out bool hasSuggestionItemOptionsBefore) || !hasSuggestionItemOptionsBefore) - { - session.Properties[HasSuggestionItemOptions] = completionList.SuggestionModeItem != null; - } + sessionData.HasSuggestionItemOptions |= completionList.SuggestionModeItem != null; var excludedCommitCharacters = GetExcludedCommitCharacters(completionList.Items); if (excludedCommitCharacters.Length > 0) @@ -480,9 +477,9 @@ private static void AddPropertiesToSession(IAsyncCompletionSession session, Comp // so when they are requested via expander later, we can retrieve it. // Technically we should save the trigger location for each individual service that made such claim, but in reality only Roslyn's // completion service uses expander, so we can get away with not making such distinction. - if (!session.Properties.ContainsProperty(ExpandedItemTriggerLocation)) + if (!sessionData.ExpandedItemTriggerLocation.HasValue) { - session.Properties[ExpandedItemTriggerLocation] = triggerLocation; + sessionData.ExpandedItemTriggerLocation = triggerLocation; } } @@ -493,13 +490,10 @@ private static void AddPropertiesToSession(IAsyncCompletionSession session, Comp if (item is null) throw new ArgumentNullException(nameof(item)); - if (!item.Properties.TryGetProperty(RoslynItem, out RoslynCompletionItem roslynItem) || - !item.Properties.TryGetProperty(TriggerLocation, out SnapshotPoint triggerLocation)) - { + if (!CompletionItemData.TryGetData(item, out var itemData) || !itemData.TriggerLocation.HasValue) return null; - } - var document = triggerLocation.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); + var document = itemData.TriggerLocation.Value.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); if (document == null) return null; @@ -509,7 +503,7 @@ private static void AddPropertiesToSession(IAsyncCompletionSession session, Comp var completionOptions = _globalOptions.GetCompletionOptions(document.Project.Language); var displayOptions = _globalOptions.GetSymbolDescriptionOptions(document.Project.Language); - var description = await service.GetDescriptionAsync(document, roslynItem, completionOptions, displayOptions, cancellationToken).ConfigureAwait(false); + var description = await service.GetDescriptionAsync(document, itemData.RoslynItem, completionOptions, displayOptions, cancellationToken).ConfigureAwait(false); if (description == null) return null; @@ -564,7 +558,7 @@ private VSCompletionItem Convert( // roslynItem generated by providers can contain an insertionText in a property bag. // We will not use it but other providers may need it. // We actually will calculate the insertion text once again when called TryCommit. - if (!roslynItem.Properties.TryGetValue(InsertionText, out var insertionText)) + if (!roslynItem.Properties.TryGetValue(CommonCompletionItem.InsertionTextProperty, out var insertionText)) { insertionText = roslynItem.DisplayText; } @@ -600,9 +594,7 @@ private VSCompletionItem Convert( automationText: roslynItem.AutomationText ?? roslynItem.DisplayText, attributeIcons: itemData.AttributeIcons); - item.Properties.AddProperty(RoslynItem, roslynItem); - item.Properties.AddProperty(TriggerLocation, initialTriggerLocation); - + CompletionItemData.AddData(item, roslynItem, initialTriggerLocation); return item; } diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/ItemManager.CompletionListUpdater.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/ItemManager.CompletionListUpdater.cs index 030692e4ad810..5bcbf0c447000 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/ItemManager.CompletionListUpdater.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/ItemManager.CompletionListUpdater.cs @@ -33,10 +33,11 @@ internal partial class ItemManager /// private sealed class CompletionListUpdater { - private readonly IAsyncCompletionSession _session; - private readonly AsyncCompletionSessionDataSnapshot _data; + private readonly CompletionSessionData _sessionData; + private readonly AsyncCompletionSessionDataSnapshot _snapshotData; private readonly RecentItemsManager _recentItemsManager; + private readonly ITrackingSpan _applicableToSpan; private readonly bool _hasSuggestedItemOptions; private readonly string _filterText; private readonly Document? _document; @@ -48,8 +49,8 @@ private sealed class CompletionListUpdater private readonly Func, string, ImmutableArray> _filterMethod; - private CompletionTriggerReason InitialTriggerReason => _data.InitialTrigger.Reason; - private CompletionTriggerReason UpdateTriggerReason => _data.Trigger.Reason; + private CompletionTriggerReason InitialTriggerReason => _snapshotData.InitialTrigger.Reason; + private CompletionTriggerReason UpdateTriggerReason => _snapshotData.Trigger.Reason; // We might need to handle large amount of items with import completion enabled, // so use a dedicated pool to minimize/avoid array allocations (especially in LOH) @@ -59,31 +60,26 @@ private sealed class CompletionListUpdater private static readonly ObjectPool>> s_listOfMatchResultPool = new(factory: () => new(), size: 1); public CompletionListUpdater( - IAsyncCompletionSession session, - AsyncCompletionSessionDataSnapshot data, + ITrackingSpan applicableToSpan, + CompletionSessionData sessionData, + AsyncCompletionSessionDataSnapshot snapshotData, RecentItemsManager recentItemsManager, IGlobalOptionService globalOptions) { - _session = session; - _data = data; + _sessionData = sessionData; + _snapshotData = snapshotData; _recentItemsManager = recentItemsManager; - _filterText = _session.ApplicableToSpan.GetText(_data.Snapshot); + _applicableToSpan = applicableToSpan; + _filterText = applicableToSpan.GetText(_snapshotData.Snapshot); - 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 = hasSuggestedItemOptions || _data.DisplaySuggestionItem; + _hasSuggestedItemOptions = _sessionData.HasSuggestionItemOptions || _snapshotData.DisplaySuggestionItem; // We prefer using the original snapshot, which should always be available from items provided by Roslyn's CompletionSource. // Only use data.Snapshot in the theoretically possible but rare case when all items we are handling are from some non-Roslyn CompletionSource. - var snapshotForDocument = TryGetInitialTriggerLocation(_data, out var intialTriggerLocation) + var snapshotForDocument = TryGetInitialTriggerLocation(_snapshotData, out var intialTriggerLocation) ? intialTriggerLocation.Snapshot - : _data.Snapshot; + : _snapshotData.Snapshot; _document = snapshotForDocument?.TextBuffer.AsTextContainer().GetOpenDocumentInCurrentContext(); if (_document != null) @@ -184,7 +180,7 @@ private bool ShouldDismissCompletionListImmediately() // // We'll bring up the completion list here (as VB has completion on ). // If the user then types '3', we don't want to match against Int32. - if (_filterText.Length > 0 && char.IsNumber(_filterText[0]) && !IsAfterDot(_data.Snapshot, _session.ApplicableToSpan)) + if (_filterText.Length > 0 && char.IsNumber(_filterText[0]) && !IsAfterDot(_snapshotData.Snapshot, _applicableToSpan)) { // Dismiss the session. return true; @@ -213,8 +209,8 @@ static bool IsAfterDot(ITextSnapshot snapshot, ITrackingSpan applicableToSpan) private void AddCompletionItems(List> list, CancellationToken cancellationToken) { // FilterStateHelper is used to decide whether a given item should be included in the list based on the state of filter/expander buttons. - var filterHelper = new FilterStateHelper(_data.SelectedFilters); - filterHelper.LogTargetTypeFilterTelemetry(_session); + var filterHelper = new FilterStateHelper(_snapshotData.SelectedFilters); + filterHelper.LogTargetTypeFilterTelemetry(_sessionData); // We want to sort the items by pattern matching results while preserving the original alphabetical order for items with // same pattern match score, but `List.Sort` isn't stable. Therefore we have to add a monotonically increasing integer @@ -227,20 +223,28 @@ private void AddCompletionItems(List> list, Cancel var roslynFilterReason = Helpers.GetFilterReason(UpdateTriggerReason); // Filter items based on the selected filters and matching. - foreach (var item in _data.InitialSortedList) + foreach (var item in _snapshotData.InitialSortedList) { cancellationToken.ThrowIfCancellationRequested(); if (filterHelper.ShouldBeFilteredOut(item)) continue; - var roslynItem = GetOrAddRoslynCompletionItem(item); - if (CompletionHelper.TryCreateMatchResult(_completionHelper, roslynItem, item, _filterText, + if (CompletionItemData.TryGetData(item, out var itemData)) + { + if (CompletionHelper.TryCreateMatchResult(_completionHelper, itemData.RoslynItem, item, _filterText, roslynInitialTriggerKind, roslynFilterReason, _recentItemsManager.RecentItems, _highlightMatchingPortions, currentIndex, out var matchResult)) + { + list.Add(matchResult); + currentIndex++; + } + } + else { - list.Add(matchResult); - currentIndex++; + // All items passed in should contain a CompletionItemData object in the property bag, + // which is guaranteed in `ItemManager.SortCompletionListAsync`. + throw ExceptionUtilities.Unreachable; } } @@ -314,7 +318,7 @@ private void AddCompletionItems(List> list, Cancel } } - var typedChar = _data.Trigger.Character; + var typedChar = _snapshotData.Trigger.Character; // Check that it is a filter symbol. We can be called for a non-filter symbol. // If inserting a non-filter character (neither IsPotentialFilterCharacter, nor Helpers.IsFilterCharacter), @@ -475,7 +479,7 @@ static Span GetOffsetSpan(TextSpan span, RoslynCompletionItem item) // selection. return new FilteredCompletionModel( items: ImmutableArray.Empty, selectedItemIndex: 0, - filters: _data.SelectedFilters, selectionHint: UpdateSelectionHint.SoftSelected, centerSelection: true, uniqueItem: null); + filters: _snapshotData.SelectedFilters, selectionHint: UpdateSelectionHint.SoftSelected, centerSelection: true, uniqueItem: null); } private ImmutableArray GetUpdatedFilters(IReadOnlyList> items, CancellationToken cancellationToken) @@ -493,7 +497,7 @@ private ImmutableArray GetUpdatedFilters(IReadOnlyLis // When no items are available for a given filter, it becomes unavailable. // Expanders always appear available as long as it's presented. - return _data.SelectedFilters.SelectAsArray(n => n.WithAvailability(n.Filter is CompletionExpander || filters.Contains(n.Filter))); + return _snapshotData.SelectedFilters.SelectAsArray(n => n.WithAvailability(n.Filter is CompletionExpander || filters.Contains(n.Filter))); } /// @@ -553,10 +557,13 @@ static int GetRecentItemIndex(ImmutableArray recentItems, RoslynCompleti private static bool TryGetInitialTriggerLocation(AsyncCompletionSessionDataSnapshot data, out SnapshotPoint intialTriggerLocation) { - var firstItem = data.InitialSortedList.FirstOrDefault(static item => item.Properties.ContainsProperty(CompletionSource.TriggerLocation)); - if (firstItem != null) + foreach (var item in data.InitialSortedList) { - return firstItem.Properties.TryGetProperty(CompletionSource.TriggerLocation, out intialTriggerLocation); + if (CompletionItemData.TryGetData(item, out var itemData) && itemData.TriggerLocation.HasValue) + { + intialTriggerLocation = itemData.TriggerLocation.Value; + return true; + } } intialTriggerLocation = default; @@ -656,7 +663,7 @@ private static bool IsPotentialFilterCharacter(char c) private ItemSelection UpdateSelectionBasedOnSuggestedDefaults(IReadOnlyList> items, ItemSelection itemSelection, CancellationToken cancellationToken) { // Editor doesn't provide us a list of "default" items. - if (_data.Defaults.IsDefaultOrEmpty) + if (_snapshotData.Defaults.IsDefaultOrEmpty) return itemSelection; // "Preselect" is only used when we have high confidence with the selection, so don't override it. @@ -713,7 +720,7 @@ private ItemSelection GetDefaultsMatch(IReadOnlyList filtersWithState) { // The filter state list contains two kinds of "filters": regular filter and expander. @@ -796,19 +800,19 @@ private bool ShouldBeFilteredOutOfExpandedCompletionList(VSCompletionItem item) return associatedWithUnselectedExpander; } - public void LogTargetTypeFilterTelemetry(IAsyncCompletionSession session) + public void LogTargetTypeFilterTelemetry(CompletionSessionData sessionData) { - if (session.TextView.Properties.TryGetProperty(CompletionSource.TargetTypeFilterExperimentEnabled, out bool isExperimentEnabled) && isExperimentEnabled) + if (sessionData.TargetTypeFilterExperimentEnabled) { // Telemetry: Want to know % of sessions with the "Target type matches" filter where that filter is actually enabled if (_needToFilter && - !session.Properties.ContainsProperty(_targetTypeCompletionFilterChosenMarker) && + !sessionData.TargetTypeFilterSelected && _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); + sessionData.TargetTypeFilterSelected = true; } } } diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/ItemManager.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/ItemManager.cs index 0bc14462de196..308c4b21fa1f8 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/ItemManager.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/ItemManager.cs @@ -7,12 +7,9 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion; using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; using Roslyn.Utilities; -using RoslynCompletionItem = Microsoft.CodeAnalysis.Completion.CompletionItem; -using RoslynCompletionList = Microsoft.CodeAnalysis.Completion.CompletionList; using VSCompletionItem = Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data.CompletionItem; namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion @@ -20,7 +17,6 @@ namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncComplet internal sealed partial class ItemManager : IAsyncCompletionItemManager { public const string AggressiveDefaultsMatchingOptionName = "AggressiveDefaultsMatchingOption"; - private const string CombinedSortedList = nameof(CombinedSortedList); private readonly RecentItemsManager _recentItemsManager; private readonly IGlobalOptionService _globalOptions; @@ -36,8 +32,9 @@ public Task> SortCompletionListAsync( AsyncCompletionSessionInitialDataSnapshot data, CancellationToken cancellationToken) { + var sessionData = CompletionSessionData.GetOrCreateSessionData(session); // This method is called exactly once, so use the opportunity to set a baseline for telemetry. - if (session.TextView.Properties.TryGetProperty(CompletionSource.TargetTypeFilterExperimentEnabled, out bool isTargetTypeFilterEnabled) && isTargetTypeFilterEnabled) + if (sessionData.TargetTypeFilterExperimentEnabled) { AsyncCompletionLogger.LogSessionHasTargetTypeFilterEnabled(); if (data.InitialList.Any(i => i.Filters.Any(f => f.DisplayText == FeaturesResources.Target_type_matches))) @@ -45,7 +42,7 @@ public Task> SortCompletionListAsync( } // Sort by default comparer of Roslyn CompletionItem - var sortedItems = data.InitialList.OrderBy(GetOrAddRoslynCompletionItem).ToImmutableArray(); + var sortedItems = data.InitialList.OrderBy(CompletionItemData.GetOrAddDummyRoslynItem).ToImmutableArray(); return Task.FromResult(sortedItems); } @@ -54,6 +51,8 @@ public Task> SortCompletionListAsync( AsyncCompletionSessionDataSnapshot data, CancellationToken cancellationToken) { + 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 @@ -64,57 +63,44 @@ public Task> SortCompletionListAsync( // 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 (session.Properties.TryGetProperty(CombinedSortedList, out ImmutableArray combinedSortedList)) + if (sessionData.CombinedSortedList.HasValue) { // Always use the previously saved combined list if available. - data = new AsyncCompletionSessionDataSnapshot(combinedSortedList, data.Snapshot, data.Trigger, data.InitialTrigger, data.SelectedFilters, + data = new AsyncCompletionSessionDataSnapshot(sessionData.CombinedSortedList.Value, data.Snapshot, data.Trigger, data.InitialTrigger, data.SelectedFilters, data.IsSoftSelected, data.DisplaySuggestionItem, data.Defaults); } - else if (session.Properties.TryGetProperty(CompletionSource.ExpandedItemsTask, out Task<(CompletionContext, RoslynCompletionList)> task) - && task.Status == TaskStatus.RanToCompletion) + else if (sessionData.ExpandedItemsTask != null) { - // Make sure the task is removed when Adding expanded items, - // so duplicated items won't be added in subsequent list updates. - session.Properties.RemoveProperty(CompletionSource.ExpandedItemsTask); - - var (expandedContext, _) = await task.ConfigureAwait(false); - if (expandedContext.Items.Length > 0) + var task = sessionData.ExpandedItemsTask; + if (task.Status == TaskStatus.RanToCompletion) { - // Here we rely on the implementation detail of `CompletionItem.CompareTo`, which always put expand items after regular ones. - var itemsBuilder = ImmutableArray.CreateBuilder(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. - session.Properties[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); + // 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(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(); } - - AsyncCompletionLogger.LogSessionWithDelayedImportCompletionIncludedInUpdate(); } - var updater = new CompletionListUpdater(session, data, _recentItemsManager, _globalOptions); + var updater = new CompletionListUpdater(session.ApplicableToSpan, sessionData, data, _recentItemsManager, _globalOptions); return updater.UpdateCompletionList(cancellationToken); } - - private static RoslynCompletionItem GetOrAddRoslynCompletionItem(VSCompletionItem vsItem) - { - if (!vsItem.Properties.TryGetProperty(CompletionSource.RoslynItem, out RoslynCompletionItem roslynItem)) - { - roslynItem = RoslynCompletionItem.Create( - displayText: vsItem.DisplayText, - filterText: vsItem.FilterText, - sortText: vsItem.SortText, - displayTextSuffix: vsItem.Suffix); - - vsItem.Properties.AddProperty(CompletionSource.RoslynItem, roslynItem); - } - - return roslynItem; - } } } diff --git a/src/EditorFeatures/Core/Implementation/LanguageServer/Handlers/Completion/CompletionHandler.cs b/src/EditorFeatures/Core/Implementation/LanguageServer/Handlers/Completion/CompletionHandler.cs index 1555f67c15d53..c4f674845709c 100644 --- a/src/EditorFeatures/Core/Implementation/LanguageServer/Handlers/Completion/CompletionHandler.cs +++ b/src/EditorFeatures/Core/Implementation/LanguageServer/Handlers/Completion/CompletionHandler.cs @@ -261,7 +261,9 @@ static async Task CreateCompletionItemAsync( // If the feature flag is off, return an InsertText. else { - completionItem.InsertText = item.Properties.ContainsKey("InsertionText") ? item.Properties["InsertionText"] : completeDisplayText; + completionItem.InsertText = item.Properties.ContainsKey(CommonCompletionItem.InsertionTextProperty) + ? item.Properties[CommonCompletionItem.InsertionTextProperty] + : completeDisplayText; } var commitCharacters = GetCommitCharacters(item, commitCharacterRulesCache, supportsVSExtensions); diff --git a/src/EditorFeatures/Test/Completion/FileSystemCompletionHelperTests.cs b/src/EditorFeatures/Test/Completion/FileSystemCompletionHelperTests.cs index 977908cdef448..dd635658652c4 100644 --- a/src/EditorFeatures/Test/Completion/FileSystemCompletionHelperTests.cs +++ b/src/EditorFeatures/Test/Completion/FileSystemCompletionHelperTests.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Collections.Immutable; using System.Linq; @@ -20,7 +18,7 @@ private static void AssertItemsEqual(ImmutableArray actual, para { AssertEx.Equal( expected, - actual.Select(c => $"'{c.DisplayText}', {string.Join(", ", c.Tags)}, '{c.Properties["Description"]}'"), + actual.Select(c => $"'{c.DisplayText}', {string.Join(", ", c.Tags)}, '{c.Properties[CommonCompletionItem.DescriptionProperty]}'"), itemInspector: c => $"@\"{c}\""); Assert.True(actual.All(i => i.Rules == TestFileSystemCompletionHelper.CompletionRules)); diff --git a/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests.vb b/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests.vb index d591d1bb73273..022364fd20aae 100644 --- a/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests.vb +++ b/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests.vb @@ -24,6 +24,7 @@ Imports Microsoft.VisualStudio.Text Imports Microsoft.VisualStudio.Text.Editor Imports Microsoft.VisualStudio.Text.Operations Imports Microsoft.VisualStudio.Text.Projection +Imports Roslyn.Utilities Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense <[UseExportProvider]> @@ -9700,8 +9701,10 @@ class C Await state.AssertCompletionItemsDoNotContainAny("TestUnimportedItem") Dim session = Await state.GetCompletionSession() - Dim expandTask As Task = Nothing - Assert.True(session.Properties.TryGetProperty(Of Task)(CompletionSource.ExpandedItemsTask, expandTask)) + Dim sessionData = CompletionSessionData.GetOrCreateSessionData(session) + Dim expandTask = sessionData.ExpandedItemsTask + + Assert.NotNull(expandTask) Assert.False(expandTask.IsCompleted) ' following up by typing a few more characters each triggers an list update @@ -9757,8 +9760,10 @@ class C Await state.AssertCompletionItemsDoNotContainAny("TestUnimportedItem") Dim session = Await state.GetCompletionSession() - Dim expandTask As Task = Nothing - Assert.True(session.Properties.TryGetProperty(Of Task)(CompletionSource.ExpandedItemsTask, expandTask)) + Dim sessionData = CompletionSessionData.GetOrCreateSessionData(session) + Dim expandTask = sessionData.ExpandedItemsTask + + Assert.NotNull(expandTask) Assert.False(expandTask.IsCompleted) ' following up by typing more characters each triggers an list update @@ -9815,8 +9820,10 @@ class C Await state.AssertCompletionItemsDoNotContainAny("TestUnimportedItem") Dim session = Await state.GetCompletionSession() - Dim expandTask As Task = Nothing - Assert.True(session.Properties.TryGetProperty(Of Task)(CompletionSource.ExpandedItemsTask, expandTask)) + Dim sessionData = CompletionSessionData.GetOrCreateSessionData(session) + Dim expandTask = sessionData.ExpandedItemsTask + + Assert.NotNull(expandTask) Assert.False(expandTask.IsCompleted) provider.Checkpoint.Release() diff --git a/src/EditorFeatures/TestUtilities2/Intellisense/TestState.vb b/src/EditorFeatures/TestUtilities2/Intellisense/TestState.vb index f7e9b9e8dde1d..81bf48edb6700 100644 --- a/src/EditorFeatures/TestUtilities2/Intellisense/TestState.vb +++ b/src/EditorFeatures/TestUtilities2/Intellisense/TestState.vb @@ -8,6 +8,7 @@ Imports Microsoft.CodeAnalysis.Completion Imports Microsoft.CodeAnalysis.Editor.CommandHandlers Imports Microsoft.CodeAnalysis.Editor.CSharp.CompleteStatement Imports Microsoft.CodeAnalysis.Editor.Implementation.Formatting +Imports Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion Imports Microsoft.CodeAnalysis.Editor.UnitTests.Extensions Imports Microsoft.CodeAnalysis.Editor.UnitTests.Utilities Imports Microsoft.CodeAnalysis.LanguageServices @@ -26,7 +27,6 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense Inherits AbstractCommandHandlerTestState Private Const timeoutMs = 60000 - Friend Const RoslynItem = "RoslynItem" Friend ReadOnly EditorCompletionCommandHandler As ICommandHandler Friend ReadOnly CompletionPresenterProvider As ICompletionPresenterProvider @@ -502,7 +502,15 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense End Function Private Shared Function GetRoslynCompletionItem(item As Data.CompletionItem) As CompletionItem - Return If(item IsNot Nothing, DirectCast(item.Properties(RoslynItem), CompletionItem), Nothing) + If (item Is Nothing) Then + Return Nothing + End If + + Dim roslynItemData As CompletionItemData = Nothing + If (CompletionItemData.TryGetData(item, roslynItemData) = False) Then + Return Nothing + End If + Return roslynItemData.RoslynItem End Function Public Sub RaiseFiltersChanged(args As ImmutableArray(Of Data.CompletionFilterWithState)) diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/CrefCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/CrefCompletionProvider.cs index b8e03bf4936ee..7d3964708e97e 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/CrefCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/CrefCompletionProvider.cs @@ -417,11 +417,9 @@ private static CompletionItemRules GetRules(string displayText) } } - private const string InsertionTextProperty = "insertionText"; - protected override Task GetTextChangeAsync(CompletionItem selectedItem, char? ch, CancellationToken cancellationToken) { - if (!selectedItem.Properties.TryGetValue(InsertionTextProperty, out var insertionText)) + if (!selectedItem.Properties.TryGetValue(CommonCompletionItem.InsertionTextProperty, out var insertionText)) { insertionText = selectedItem.DisplayText; } diff --git a/src/Features/Core/Portable/Completion/CommonCompletionItem.cs b/src/Features/Core/Portable/Completion/CommonCompletionItem.cs index 0b1442552014b..4a5a720b8ccb0 100644 --- a/src/Features/Core/Portable/Completion/CommonCompletionItem.cs +++ b/src/Features/Core/Portable/Completion/CommonCompletionItem.cs @@ -2,7 +2,6 @@ // 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; using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis.Tags; @@ -12,6 +11,9 @@ namespace Microsoft.CodeAnalysis.Completion { internal static class CommonCompletionItem { + public const string DescriptionProperty = nameof(DescriptionProperty); + public const string InsertionTextProperty = nameof(InsertionTextProperty); + public static CompletionItem Create( string displayText, string? displayTextSuffix, @@ -43,7 +45,7 @@ public static CompletionItem Create( properties ??= ImmutableDictionary.Empty; if (!description.IsDefault && description.Length > 0) { - properties = properties.Add("Description", EncodeDescription(description.ToTaggedText())); + properties = properties.Add(DescriptionProperty, EncodeDescription(description.ToTaggedText())); } return CompletionItem.Create( @@ -60,11 +62,11 @@ public static CompletionItem Create( } public static bool HasDescription(CompletionItem item) - => item.Properties.ContainsKey("Description"); + => item.Properties.ContainsKey(DescriptionProperty); public static CompletionDescription GetDescription(CompletionItem item) { - if (item.Properties.TryGetValue("Description", out var encodedDescription)) + if (item.Properties.TryGetValue(DescriptionProperty, out var encodedDescription)) { return DecodeDescription(encodedDescription); } diff --git a/src/Features/Core/Portable/Completion/Providers/SymbolCompletionItem.cs b/src/Features/Core/Portable/Completion/Providers/SymbolCompletionItem.cs index 3ec0e32e4fcea..c8f90f98d8176 100644 --- a/src/Features/Core/Portable/Completion/Providers/SymbolCompletionItem.cs +++ b/src/Features/Core/Portable/Completion/Providers/SymbolCompletionItem.cs @@ -43,7 +43,7 @@ private static CompletionItem CreateWorker( if (insertionText != null) { - props = props.Add("InsertionText", insertionText); + props = props.Add(CommonCompletionItem.InsertionTextProperty, insertionText); } props = props.Add("ContextPosition", contextPosition.ToString()); @@ -254,7 +254,7 @@ public static int GetDescriptionPosition(CompletionItem item) => GetContextPosition(item); public static string GetInsertionText(CompletionItem item) - => item.Properties["InsertionText"]; + => item.Properties[CommonCompletionItem.InsertionTextProperty]; // COMPAT OVERLOAD: This is used by IntelliCode. public static CompletionItem CreateWithSymbolId(