From 2aad1ced65eb9be4993d1cd604d8539e042852ed Mon Sep 17 00:00:00 2001 From: Arseniy Pendryak Date: Mon, 11 Jan 2021 11:13:25 +0300 Subject: [PATCH 1/2] T: refactor completion fixtures - moved all text-based methods from `RsCompletionTestFixtureBase` to `RsCompletionTestFixture` - provide a way to pass custom renderer for `checkContainsCompletion` methods --- .../core/completion/RsCompletionTestBase.kt | 22 ++++++-- .../completion/RsCompletionTestFixture.kt | 40 ++++++++++++- .../completion/RsCompletionTestFixtureBase.kt | 56 +++++++------------ 3 files changed, 74 insertions(+), 44 deletions(-) diff --git a/src/test/kotlin/org/rust/lang/core/completion/RsCompletionTestBase.kt b/src/test/kotlin/org/rust/lang/core/completion/RsCompletionTestBase.kt index 7b31719a4e4..d85bd7c7963 100644 --- a/src/test/kotlin/org/rust/lang/core/completion/RsCompletionTestBase.kt +++ b/src/test/kotlin/org/rust/lang/core/completion/RsCompletionTestBase.kt @@ -5,6 +5,7 @@ package org.rust.lang.core.completion +import com.intellij.codeInsight.lookup.LookupElement import com.intellij.openapiext.Testmark import com.intellij.psi.PsiElement import com.intellij.psi.util.PsiTreeUtil @@ -70,13 +71,21 @@ abstract class RsCompletionTestBase : RsTestBase() { protected fun checkContainsCompletion( variant: String, - @Language("Rust") code: String - ) = completionFixture.checkContainsCompletion(code, variant) + @Language("Rust") code: String, + render: LookupElement.() -> String = { lookupString } + ) = completionFixture.checkContainsCompletion(code, listOf(variant), render) protected fun checkContainsCompletion( variants: List, - @Language("Rust") code: String - ) = completionFixture.checkContainsCompletion(code, variants) + @Language("Rust") code: String, + render: LookupElement.() -> String = { lookupString } + ) = completionFixture.checkContainsCompletion(code, variants, render) + + protected fun checkContainsCompletionByFileTree( + variants: List, + @Language("Rust") code: String, + render: LookupElement.() -> String = { lookupString } + ) = completionFixture.checkContainsCompletionByFileTree(code, variants, render) protected fun checkCompletion( lookupString: String, @@ -88,8 +97,9 @@ abstract class RsCompletionTestBase : RsTestBase() { protected fun checkNotContainsCompletion( variant: String, - @Language("Rust") code: String - ) = completionFixture.checkNotContainsCompletion(code, variant) + @Language("Rust") code: String, + render: LookupElement.() -> String = { lookupString } + ) = completionFixture.checkNotContainsCompletion(code, variant, render) protected open fun checkNoCompletion(@Language("Rust") code: String) = completionFixture.checkNoCompletion(code) diff --git a/src/test/kotlin/org/rust/lang/core/completion/RsCompletionTestFixture.kt b/src/test/kotlin/org/rust/lang/core/completion/RsCompletionTestFixture.kt index a6859b84ed5..02a7c1ed036 100644 --- a/src/test/kotlin/org/rust/lang/core/completion/RsCompletionTestFixture.kt +++ b/src/test/kotlin/org/rust/lang/core/completion/RsCompletionTestFixture.kt @@ -5,8 +5,11 @@ package org.rust.lang.core.completion +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.openapi.vfs.VirtualFileFilter +import com.intellij.psi.impl.PsiManagerEx import com.intellij.testFramework.fixtures.CodeInsightTestFixture -import org.rust.InlineFile +import org.rust.* class RsCompletionTestFixture( fixture: CodeInsightTestFixture, @@ -16,4 +19,39 @@ class RsCompletionTestFixture( override fun prepare(code: String) { InlineFile(myFixture, code.trimIndent(), defaultFileName).withCaret() } + + fun doSingleCompletionByFileTree(before: String, after: String) = + doSingleCompletionByFileTree(fileTreeFromText(before), after) + + fun doSingleCompletionByFileTree(fileTree: FileTree, after: String, forbidAstLoading: Boolean = true) { + val testProject = fileTree.createAndOpenFileWithCaretMarker(myFixture) + if (forbidAstLoading) { + checkAstNotLoaded { file -> + !file.path.endsWith(testProject.fileWithCaret) + } + } + executeSoloCompletion() + myFixture.checkResult(replaceCaretMarker(after.trimIndent())) + } + + fun checkNoCompletionByFileTree(code: String) { + val testProject = fileTreeFromText(code).createAndOpenFileWithCaretMarker(myFixture) + checkAstNotLoaded { file -> + !file.path.endsWith(testProject.fileWithCaret) + } + noCompletionCheck() + } + + fun checkContainsCompletionByFileTree( + code: String, + variants: List, + render: LookupElement.() -> String = { lookupString } + ) { + fileTreeFromText(code).createAndOpenFileWithCaretMarker(myFixture) + doContainsCompletion(variants, render) + } + + private fun checkAstNotLoaded(fileFilter: VirtualFileFilter) { + PsiManagerEx.getInstanceEx(project).setAssertOnFileLoadingFilter(fileFilter, testRootDisposable) + } } diff --git a/src/test/kotlin/org/rust/lang/core/completion/RsCompletionTestFixtureBase.kt b/src/test/kotlin/org/rust/lang/core/completion/RsCompletionTestFixtureBase.kt index 092ea162f4c..6a8063981bb 100644 --- a/src/test/kotlin/org/rust/lang/core/completion/RsCompletionTestFixtureBase.kt +++ b/src/test/kotlin/org/rust/lang/core/completion/RsCompletionTestFixtureBase.kt @@ -7,9 +7,7 @@ package org.rust.lang.core.completion import com.intellij.codeInsight.lookup.LookupElement import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFileFilter import com.intellij.openapiext.Testmark -import com.intellij.psi.impl.PsiManagerEx import com.intellij.testFramework.fixtures.CodeInsightTestFixture import com.intellij.testFramework.fixtures.impl.BaseFixture import org.intellij.lang.annotations.Language @@ -19,7 +17,7 @@ abstract class RsCompletionTestFixtureBase( protected val myFixture: CodeInsightTestFixture ) : BaseFixture() { - private val project: Project get() = myFixture.project + protected val project: Project get() = myFixture.project fun executeSoloCompletion() { val lookups = myFixture.completeBasic() @@ -52,20 +50,6 @@ abstract class RsCompletionTestFixtureBase( checkByText(code, after.trimIndent()) { executeSoloCompletion() } } - fun doSingleCompletionByFileTree(before: String, after: String) = - doSingleCompletionByFileTree(fileTreeFromText(before), after) - - fun doSingleCompletionByFileTree(fileTree: FileTree, after: String, forbidAstLoading: Boolean = true) { - val testProject = fileTree.createAndOpenFileWithCaretMarker(myFixture) - if (forbidAstLoading) { - checkAstNotLoaded(VirtualFileFilter { file -> - !file.path.endsWith(testProject.fileWithCaret) - }) - } - executeSoloCompletion() - myFixture.checkResult(replaceCaretMarker(after.trimIndent())) - } - fun checkCompletion( lookupString: String, before: IN, @@ -90,14 +74,6 @@ abstract class RsCompletionTestFixtureBase( noCompletionCheck() } - fun checkNoCompletionByFileTree(code: String) { - val testProject = fileTreeFromText(code).createAndOpenFileWithCaretMarker(myFixture) - checkAstNotLoaded(VirtualFileFilter { file -> - !file.path.endsWith(testProject.fileWithCaret) - }) - noCompletionCheck() - } - protected fun noCompletionCheck() { val lookups = myFixture.completeBasic() checkNotNull(lookups) { @@ -109,30 +85,40 @@ abstract class RsCompletionTestFixtureBase( } } - fun checkContainsCompletion(code: IN, variant: String) = checkContainsCompletion(code, listOf(variant)) - - fun checkContainsCompletion(code: IN, variants: List) { + fun checkContainsCompletion( + code: IN, + variants: List, + render: LookupElement.() -> String = { lookupString } + ) { prepare(code) + doContainsCompletion(variants, render) + } + + fun doContainsCompletion(variants: List, render: LookupElement.() -> String) { val lookups = myFixture.completeBasic() checkNotNull(lookups) { "Expected completions that contain $variants, but no completions found" } for (variant in variants) { - if (lookups.all { it.lookupString != variant }) { - error("Expected completions that contain $variant, but got ${lookups.map { it.lookupString }}") + if (lookups.all { it.render() != variant }) { + error("Expected completions that contain $variant, but got ${lookups.map { it.render() }}") } } } - fun checkNotContainsCompletion(code: IN, variant: String) { + fun checkNotContainsCompletion( + code: IN, + variant: String, + render: LookupElement.() -> String = { lookupString } + ) { prepare(code) val lookups = myFixture.completeBasic() checkNotNull(lookups) { "Expected completions that contain $variant, but no completions found" } - if (lookups.any { it.lookupString == variant }) { - error("Expected completions that don't contain $variant, but got ${lookups.map { it.lookupString }}") + if (lookups.any { it.render() == variant }) { + error("Expected completions that don't contain $variant, but got ${lookups.map { it.render() }}") } } @@ -142,9 +128,5 @@ abstract class RsCompletionTestFixtureBase( myFixture.checkResult(replaceCaretMarker(after)) } - private fun checkAstNotLoaded(fileFilter: VirtualFileFilter) { - PsiManagerEx.getInstanceEx(project).setAssertOnFileLoadingFilter(fileFilter, testRootDisposable) - } - protected abstract fun prepare(code: IN) } From 49a19bc8aa5bfb2c6e286cad4e5470c5cecc66ce Mon Sep 17 00:00:00 2001 From: Arseniy Pendryak Date: Mon, 11 Jan 2021 11:40:58 +0300 Subject: [PATCH 2/2] #5415: show all re-exports of the same item in completion Previously, we filter out secondary import candidates for the same item from completion suggestions, as a result, if a library (e.g. async-std) re-exports some item from another library/stdlib, completion shows only single suggestion per item. Now, we show all suggestions that can be imported - the same items as in `Auto Import` quick-fix --- .../completion/RsCommonCompletionProvider.kt | 38 ++++++++++++++++++- .../RsPathCompletionFromIndexTest.kt | 36 +++++++++++++++++- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/org/rust/lang/core/completion/RsCommonCompletionProvider.kt b/src/main/kotlin/org/rust/lang/core/completion/RsCommonCompletionProvider.kt index ccb16ba304d..ef8a3b2645b 100644 --- a/src/main/kotlin/org/rust/lang/core/completion/RsCommonCompletionProvider.kt +++ b/src/main/kotlin/org/rust/lang/core/completion/RsCommonCompletionProvider.kt @@ -11,6 +11,7 @@ import com.intellij.codeInsight.completion.CompletionResultSet import com.intellij.codeInsight.completion.CompletionUtil import com.intellij.codeInsight.completion.InsertionContext import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementDecorator import com.intellij.openapiext.Testmark import com.intellij.patterns.ElementPattern import com.intellij.patterns.PlatformPatterns @@ -200,7 +201,6 @@ object RsCommonCompletionProvider : RsCompletionProvider() { } candidates - .distinctBy { it.qualifiedNamedItem.item } .map { candidate -> val item = candidate.qualifiedNamedItem.item createLookupElement( @@ -221,7 +221,7 @@ object RsCommonCompletionProvider : RsCompletionProvider() { } } } - ) + ).withImportCandidate(candidate) } .forEach(result::addElement) } @@ -377,3 +377,37 @@ private fun getExpectedTypeForEnclosingPathOrDotExpr(element: RsReferenceElement } return null } + +private fun LookupElement.withImportCandidate(candidate: ImportCandidate): RsImportLookupElement { + return RsImportLookupElement(this, candidate) +} + +/** + * Provides [equals] and [hashCode] that take into account the corresponding [ImportCandidate]. + * We need to distinguish lookup elements with the same psi element and the same lookup text + * but belong to different import candidates, otherwise the platform shows only one such item. + * + * See [#5415](https://github.com/intellij-rust/intellij-rust/issues/5415) + */ +private class RsImportLookupElement( + delegate: LookupElement, + private val candidate: ImportCandidate +) : LookupElementDecorator(delegate) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + if (!super.equals(other)) return false + + other as RsImportLookupElement + + if (candidate != other.candidate) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + candidate.hashCode() + return result + } +} diff --git a/src/test/kotlin/org/rust/lang/core/completion/RsPathCompletionFromIndexTest.kt b/src/test/kotlin/org/rust/lang/core/completion/RsPathCompletionFromIndexTest.kt index 7855dcdf823..e2c67ffe69b 100644 --- a/src/test/kotlin/org/rust/lang/core/completion/RsPathCompletionFromIndexTest.kt +++ b/src/test/kotlin/org/rust/lang/core/completion/RsPathCompletionFromIndexTest.kt @@ -5,10 +5,13 @@ package org.rust.lang.core.completion +import com.intellij.codeInsight.lookup.LookupElementPresentation import com.intellij.openapiext.Testmark import org.intellij.lang.annotations.Language +import org.rust.MockEdition import org.rust.ProjectDescriptor import org.rust.WithDependencyRustProjectDescriptor +import org.rust.cargo.project.workspace.CargoWorkspace import org.rust.hasCaretMarker import org.rust.ide.settings.RsCodeInsightSettings import org.rust.lang.core.completion.RsCommonCompletionProvider.Testmarks @@ -316,6 +319,31 @@ class RsPathCompletionFromIndexTest : RsCompletionTestBase() { fn foo(x: FooBar/*caret*/) {} """) + @MockEdition(CargoWorkspace.Edition.EDITION_2018) + @ProjectDescriptor(WithDependencyRustProjectDescriptor::class) + fun `test show all re-exports of single item`() { + withOutOfScopeSettings { + checkContainsCompletionByFileTree(listOf( + "Bar (crate::foo::Bar)", + "Bar (dep_lib_target::Bar)" + ), """ + //- dep-lib/lib.rs + pub struct Bar; + //- lib.rs + + pub mod foo { + pub use dep_lib_target::Bar; + } + fn foo(x: Ba/*caret*/) {} + """) { + val presentation = LookupElementPresentation() + renderElement(presentation) + + "${presentation.itemText}${presentation.tailText}" + } + } + } + private fun doTestByText( @Language("Rust") before: String, @Language("Rust") after: String, @@ -336,6 +364,12 @@ class RsPathCompletionFromIndexTest : RsCompletionTestBase() { suggestOutOfScopeItems: Boolean = true, importOutOfScopeItems: Boolean = true, check: (String, String) -> Unit + ) = withOutOfScopeSettings(suggestOutOfScopeItems, importOutOfScopeItems) { check(before, after) } + + private fun withOutOfScopeSettings( + suggestOutOfScopeItems: Boolean = true, + importOutOfScopeItems: Boolean = true, + action: () -> Unit ) { val settings = RsCodeInsightSettings.getInstance() val suggestInitialValue = settings.suggestOutOfScopeItems @@ -343,7 +377,7 @@ class RsPathCompletionFromIndexTest : RsCompletionTestBase() { settings.suggestOutOfScopeItems = suggestOutOfScopeItems settings.importOutOfScopeItems = importOutOfScopeItems try { - check(before, after) + action() } finally { settings.suggestOutOfScopeItems = suggestInitialValue settings.importOutOfScopeItems = importInitialValue