diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CommitManager.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CommitManager.cs index 0c7371df8377b..15defff55aa64 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CommitManager.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/CommitManager.cs @@ -172,7 +172,7 @@ public AsyncCompletionData.CommitResult TryCommit( roslynItem, completionListSpan, commitChar, triggerSnapshot, serviceRules, filterText, cancellationToken); - _recentItemsManager.MakeMostRecentItem(roslynItem.DisplayText); + _recentItemsManager.MakeMostRecentItem(roslynItem.FilterText); return new AsyncCompletionData.CommitResult(isHandled: true, commitBehavior); } diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/Helpers.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/Helpers.cs index 281a70e5f18dd..9b4ebd090f27f 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/Helpers.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/Helpers.cs @@ -4,6 +4,8 @@ using Microsoft.VisualStudio.Text; using AsyncCompletionData = Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data; using RoslynTrigger = Microsoft.CodeAnalysis.Completion.CompletionTrigger; +using RoslynCompletionItem = Microsoft.CodeAnalysis.Completion.CompletionItem; +using VSCompletionItem = Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data.CompletionItem; namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.AsyncCompletion { @@ -63,7 +65,7 @@ internal static CompletionFilterReason GetFilterReason(AsyncCompletionData.Compl } } - internal static bool IsFilterCharacter(CompletionItem item, char ch, string textTypedSoFar) + internal static bool IsFilterCharacter(RoslynCompletionItem item, char ch, string textTypedSoFar) { // First see if the item has any specific filter rules it wants followed. foreach (var rule in item.Rules.FilterCharacterRules) @@ -97,5 +99,15 @@ internal static bool IsFilterCharacter(CompletionItem item, char ch, string text return false; } + + // This is a temporarily method to support preference of IntelliCode items comparing to non-IntelliCode items. + // We expect that Editor will intorduce this support and we will get rid of relying on the "★" then. + internal static bool IsPreferredItem(this RoslynCompletionItem completionItem) + => completionItem.DisplayText.StartsWith("★"); + + // This is a temporarily method to support preference of IntelliCode items comparing to non-IntelliCode items. + // We expect that Editor will intorduce this support and we will get rid of relying on the "★" then. + internal static bool IsPreferredItem(this VSCompletionItem completionItem) + => completionItem.DisplayText.StartsWith("★"); } } diff --git a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/ItemManager.cs b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/ItemManager.cs index f9ec05eb32ee6..add944e45401a 100644 --- a/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/ItemManager.cs +++ b/src/EditorFeatures/Core/Implementation/IntelliSense/AsyncCompletion/ItemManager.cs @@ -293,12 +293,12 @@ private FilteredCompletionModel HandleNormalFiltering( if (bestItem != null) { selectedItemIndex = itemsInList.IndexOf(i => Equals(i.FilterResult.CompletionItem, bestItem)); - var deduplicatedList = matchingItems.Where(r => !r.DisplayText.StartsWith("★")); + var deduplicatedListCount = matchingItems.Where(r => !r.IsPreferredItem()).Count(); if (selectedItemIndex > -1 && - deduplicatedList.Count() == 1 && + deduplicatedListCount == 1 && filterText.Length > 0) { - var uniqueItemIndex = itemsInList.IndexOf(i => Equals(i.FilterResult.CompletionItem, deduplicatedList.First())); + var uniqueItemIndex = itemsInList.IndexOf(i => Equals(i.FilterResult.CompletionItem, bestItem)); uniqueItem = highlightedList[uniqueItemIndex].CompletionItem; } } @@ -391,7 +391,7 @@ private FilteredCompletionModel HandleDeletionTrigger( hardSelect = false; } - var deduplicatedListCount = matchingItems.Where(r => !r.VSCompletionItem.DisplayText.StartsWith("★")).Count(); + var deduplicatedListCount = matchingItems.Where(r => !r.VSCompletionItem.IsPreferredItem()).Count(); return new FilteredCompletionModel( highlightedList, index, filters, @@ -493,7 +493,8 @@ internal static RoslynCompletionItem GetBestCompletionItemBasedOnMRU( var mruIndex1 = GetRecentItemIndex(recentItems, bestItem); var mruIndex2 = GetRecentItemIndex(recentItems, chosenItem); - if (mruIndex2 < mruIndex1) + if ((mruIndex2 < mruIndex1) || + (mruIndex2 == mruIndex1 && !bestItem.IsPreferredItem() && chosenItem.IsPreferredItem())) { bestItem = chosenItem; } @@ -513,7 +514,8 @@ internal static RoslynCompletionItem GetBestCompletionItemBasedOnMRU( var bestItemPriority = bestItem.Rules.MatchPriority; var currentItemPriority = chosenItem.Rules.MatchPriority; - if (currentItemPriority > bestItemPriority) + if ((currentItemPriority > bestItemPriority) || + ((currentItemPriority == bestItemPriority) && !bestItem.IsPreferredItem() && chosenItem.IsPreferredItem())) { bestItem = chosenItem; } @@ -524,7 +526,7 @@ internal static RoslynCompletionItem GetBestCompletionItemBasedOnMRU( internal static int GetRecentItemIndex(ImmutableArray recentItems, RoslynCompletionItem item) { - var index = recentItems.IndexOf(item.DisplayText); + var index = recentItems.IndexOf(item.FilterText); return -index; } @@ -565,7 +567,13 @@ internal static bool IsBetterDeletionMatch(FilterResult result1, FilterResult re { return true; } + + if (result1.CompletionItem.IsPreferredItem() && !result2.CompletionItem.IsPreferredItem()) + { + return true; + } } + return false; } diff --git a/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests.vb b/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests.vb index fee24958f9316..6d021015678dc 100644 --- a/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests.vb +++ b/src/EditorFeatures/Test2/IntelliSense/CSharpCompletionCommandHandlerTests.vb @@ -5057,13 +5057,112 @@ class C state.SendTypeChars(".len") Await state.AssertCompletionSession() - state.AssertCompletionItemsContainAll({"Length", "★ Length3", "★ Length2"}) + state.AssertCompletionItemsContainAll({"Length", "★ Length", "★ Length2"}) state.SendCommitUniqueCompletionListItem() Await state.AssertNoCompletionSession() Assert.Contains("s.Length", state.GetLineTextFromCaretPosition(), StringComparison.Ordinal) End Using End Function + ' Implementation for the Modern completion only + + + Public Async Function IntelliCodeItemPreferredAfterCommitingIntelliCodeItem(completionImplementation As CompletionImplementation) As Task + Dim provider = New IntelliCodeMockProvider() + Using state = TestStateFactory.CreateCSharpTestState(completionImplementation, + +class C +{ + void Method() + { + var s = ""; + s$$ + } +} + , {provider}) + + state.Workspace.Options = state.Workspace.Options.WithChangedOption( + CompletionOptions.TriggerOnDeletion, LanguageNames.CSharp, True) + + state.SendTypeChars(".nor") + Await state.AssertCompletionSession() + state.AssertCompletionItemsContainAll({"Normalize", "★ Normalize"}) + Await state.AssertSelectedCompletionItem("★ Normalize", displayTextSuffix:="()") + state.SendTab() + Await state.AssertNoCompletionSession() + Assert.Contains("s.Normalize", state.GetLineTextFromCaretPosition(), StringComparison.Ordinal) + For i = 1 To "ze".Length + state.SendBackspace() + Next + Await state.AssertSelectedCompletionItem("★ Normalize", displayTextSuffix:="()") + + state.SendEscape() + For i = 1 To "Normali".Length + state.SendBackspace() + Next + state.SendEscape() + Assert.Contains("s.", state.GetLineTextFromCaretPosition(), StringComparison.Ordinal) + + state.SendInvokeCompletionList() + Await state.AssertSelectedCompletionItem("★ Normalize", displayTextSuffix:="()") + state.SendEscape() + + state.SendTypeChars("n") + Await state.AssertSelectedCompletionItem("★ Normalize", displayTextSuffix:="()") + End Using + End Function + + ' Implementation for the Modern completion only + + + Public Async Function IntelliCodeItemPreferredAfterCommitingNonIntelliCodeItem(completionImplementation As CompletionImplementation) As Task + Dim provider = New IntelliCodeMockProvider() + Using state = TestStateFactory.CreateCSharpTestState(completionImplementation, + +class C +{ + void Method() + { + var s = ""; + s$$ + } +} + , {provider}) + + state.Workspace.Options = state.Workspace.Options.WithChangedOption( + CompletionOptions.TriggerOnDeletion, LanguageNames.CSharp, True) + + state.SendTypeChars(".nor") + Await state.AssertCompletionSession() + state.AssertCompletionItemsContainAll({"Normalize", "★ Normalize"}) + Await state.AssertSelectedCompletionItem("★ Normalize", displayTextSuffix:="()") + + state.NavigateToDisplayText("Normalize") + state.SendTab() + + Await state.AssertNoCompletionSession() + Assert.Contains("s.Normalize", state.GetLineTextFromCaretPosition(), StringComparison.Ordinal) + For i = 1 To "ze".Length + state.SendBackspace() + Next + Await state.AssertSelectedCompletionItem("★ Normalize", displayTextSuffix:="()") + + state.SendEscape() + For i = 1 To "Normali".Length + state.SendBackspace() + Next + state.SendEscape() + Assert.Contains("s.", state.GetLineTextFromCaretPosition(), StringComparison.Ordinal) + + state.SendInvokeCompletionList() + Await state.AssertSelectedCompletionItem("★ Normalize", displayTextSuffix:="()") + state.SendEscape() + + state.SendTypeChars("n") + Await state.AssertSelectedCompletionItem("★ Normalize", displayTextSuffix:="()") + End Using + End Function + @@ -5451,33 +5550,37 @@ class C Public Overrides Function ProvideCompletionsAsync(context As CompletionContext) As Task context.AddItem(CompletionItem.Create(displayText:="★ Length", filterText:="Length")) - context.AddItem(CompletionItem.Create(displayText:="★ Normalize", filterText:="Normalize")) + context.AddItem(CompletionItem.Create(displayText:="★ Normalize", filterText:="Normalize", displayTextSuffix:="()")) context.AddItem(CompletionItem.Create(displayText:="Length", filterText:="Length")) - context.AddItem(CompletionItem.Create(displayText:="ToString()", filterText:="ToString")) - context.AddItem(CompletionItem.Create(displayText:="First()", filterText:="First")) + context.AddItem(CompletionItem.Create(displayText:="ToString", filterText:="ToString", displayTextSuffix:="()")) + context.AddItem(CompletionItem.Create(displayText:="First", filterText:="First", displayTextSuffix:="()")) Return Task.CompletedTask End Function Public Overrides Function ShouldTriggerCompletion(text As SourceText, caretPosition As Integer, trigger As CompletionTrigger, options As OptionSet) As Boolean Return True End Function + + Public Overrides Function GetChangeAsync(document As Document, item As CompletionItem, commitKey As Char?, cancellationToken As CancellationToken) As Task(Of CompletionChange) + Dim commitText = item.DisplayText + If commitText.StartsWith("★") Then + ' remove the star and the following space + commitText = commitText.Substring(2) + End If + + Return Task.FromResult(CompletionChange.Create(New TextChange(item.Span, commitText))) + End Function End Class ' Simulates a situation where IntelliCode provides items not included into the Rolsyn original list. ' We want to ignore these items in CommitIfUnique. ' This situation should not happen. Tests with this provider were added to cover protective scenarios. Private Class IntelliCodeMockWeirdProvider - Inherits CompletionProvider + Inherits IntelliCodeMockProvider - Public Overrides Function ProvideCompletionsAsync(context As CompletionContext) As Task - context.AddItem(CompletionItem.Create(displayText:="★ Length2", filterText:="Length2")) - context.AddItem(CompletionItem.Create(displayText:="Length", filterText:="Length")) - context.AddItem(CompletionItem.Create(displayText:="★ Length3", filterText:="Length3")) - Return Task.CompletedTask - End Function - - Public Overrides Function ShouldTriggerCompletion(text As SourceText, caretPosition As Integer, trigger As CompletionTrigger, options As OptionSet) As Boolean - Return True + Public Overrides Async Function ProvideCompletionsAsync(context As CompletionContext) As Task + Await MyBase.ProvideCompletionsAsync(context).ConfigureAwait(False) + context.AddItem(CompletionItem.Create(displayText:="★ Length2", filterText:="Length")) End Function End Class End Class diff --git a/src/EditorFeatures/TestUtilities2/Intellisense/ModernCompletionTestState.vb b/src/EditorFeatures/TestUtilities2/Intellisense/ModernCompletionTestState.vb index 920999606071b..f03353463cbe3 100644 --- a/src/EditorFeatures/TestUtilities2/Intellisense/ModernCompletionTestState.vb +++ b/src/EditorFeatures/TestUtilities2/Intellisense/ModernCompletionTestState.vb @@ -153,7 +153,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense Dim completionItems = session.GetComputedItems(CancellationToken.None) ' During the computation we can explicitly dismiss the session or we can return no items. ' Each of these conditions mean that there is no active completion. - Assert.True(session.IsDismissed OrElse completionItems.Items.Count() = 0) + Assert.True(session.IsDismissed OrElse completionItems.Items.Count() = 0, "AssertNoCompletionSession") End Function Public Overrides Sub AssertNoCompletionSessionWithNoBlock() @@ -188,7 +188,7 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense Await WaitForAsynchronousOperationsAsync() Dim session = GetExportedValue(Of IAsyncCompletionBroker)().GetSession(view) - Assert.NotNull(session) + Assert.True(session IsNot Nothing, "AssertCompletionSession") End Function Public Overrides Async Function AssertCompletionSessionAfterTypingHash() As Task @@ -291,15 +291,6 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense End If End Sub - Private Function GetRoslynCompletionItemOpt(editorCompletionItem As Data.CompletionItem) As CompletionItem - Dim roslynCompletionItem As CompletionItem = Nothing - If editorCompletionItem?.Properties.TryGetProperty(RoslynItem, roslynCompletionItem) Then - Return roslynCompletionItem - End If - - Return Nothing - End Function - Public Overrides Function GetCompletionItems() As IList(Of CompletionItem) WaitForAsynchronousOperationsAsync() Dim session = GetExportedValue(Of IAsyncCompletionBroker)().GetSession(TextView) diff --git a/src/EditorFeatures/TestUtilities2/Intellisense/TestStateBase.vb b/src/EditorFeatures/TestUtilities2/Intellisense/TestStateBase.vb index 3a5de6732f9f0..c3a044b114dcc 100644 --- a/src/EditorFeatures/TestUtilities2/Intellisense/TestStateBase.vb +++ b/src/EditorFeatures/TestUtilities2/Intellisense/TestStateBase.vb @@ -259,6 +259,31 @@ Namespace Microsoft.CodeAnalysis.Editor.UnitTests.IntelliSense Public MustOverride Function WaitForUIRenderedAsync() As Task + Public Sub NavigateToDisplayText(targetText As String) + Dim currentText = GetSelectedItem().DisplayText + + ' GetComputedItems provided by the Editor for tests does not guarantee that + ' the order of items match the order of items actually displayed in the completion popup. + ' For example, they put starred items (intellicode) below non-starred ones. + ' And the order they display those items in the UI is opposite. + ' Therefore, we do the full traverse: down to the bottom and if not found up to the top. + Do While currentText <> targetText + SendDownKey() + Dim newText = GetSelectedItem().DisplayText + If currentText = newText Then + ' Nothing found on going down. Try going up + Do While currentText <> targetText + SendUpKey() + newText = GetSelectedItem().DisplayText + Assert.True(newText <> currentText, "Reached the bottom, then the top and didn't find the match") + currentText = newText + Loop + End If + + currentText = newText + Loop + End Sub + #End Region #Region "Signature Help Operations"