Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

COMP: show all re-exports of the same item in completion #6650

Merged
merged 2 commits into from Jan 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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
Expand Down Expand Up @@ -200,7 +201,6 @@ object RsCommonCompletionProvider : RsCompletionProvider() {
}

candidates
.distinctBy { it.qualifiedNamedItem.item }
.map { candidate ->
val item = candidate.qualifiedNamedItem.item
createLookupElement(
Expand All @@ -221,7 +221,7 @@ object RsCommonCompletionProvider : RsCompletionProvider() {
}
}
}
)
).withImportCandidate(candidate)
}
.forEach(result::addElement)
}
Expand Down Expand Up @@ -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<LookupElement>(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
}
}
Expand Up @@ -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
Expand Down Expand Up @@ -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<String>,
@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<String>,
@Language("Rust") code: String,
render: LookupElement.() -> String = { lookupString }
) = completionFixture.checkContainsCompletionByFileTree(code, variants, render)

protected fun checkCompletion(
lookupString: String,
Expand All @@ -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)

Expand Down
Expand Up @@ -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,
Expand All @@ -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<String>,
render: LookupElement.() -> String = { lookupString }
) {
fileTreeFromText(code).createAndOpenFileWithCaretMarker(myFixture)
doContainsCompletion(variants, render)
}

private fun checkAstNotLoaded(fileFilter: VirtualFileFilter) {
PsiManagerEx.getInstanceEx(project).setAssertOnFileLoadingFilter(fileFilter, testRootDisposable)
}
}
Expand Up @@ -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
Expand All @@ -19,7 +17,7 @@ abstract class RsCompletionTestFixtureBase<IN>(
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()
Expand Down Expand Up @@ -52,20 +50,6 @@ abstract class RsCompletionTestFixtureBase<IN>(
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,
Expand All @@ -90,14 +74,6 @@ abstract class RsCompletionTestFixtureBase<IN>(
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) {
Expand All @@ -109,30 +85,40 @@ abstract class RsCompletionTestFixtureBase<IN>(
}
}

fun checkContainsCompletion(code: IN, variant: String) = checkContainsCompletion(code, listOf(variant))

fun checkContainsCompletion(code: IN, variants: List<String>) {
fun checkContainsCompletion(
code: IN,
variants: List<String>,
render: LookupElement.() -> String = { lookupString }
) {
prepare(code)
doContainsCompletion(variants, render)
}

fun doContainsCompletion(variants: List<String>, 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() }}")
}
}

Expand All @@ -142,9 +128,5 @@ abstract class RsCompletionTestFixtureBase<IN>(
myFixture.checkResult(replaceCaretMarker(after))
}

private fun checkAstNotLoaded(fileFilter: VirtualFileFilter) {
PsiManagerEx.getInstanceEx(project).setAssertOnFileLoadingFilter(fileFilter, testRootDisposable)
}

protected abstract fun prepare(code: IN)
}
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -336,14 +364,20 @@ 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
val importInitialValue = settings.importOutOfScopeItems
settings.suggestOutOfScopeItems = suggestOutOfScopeItems
settings.importOutOfScopeItems = importOutOfScopeItems
try {
check(before, after)
action()
} finally {
settings.suggestOutOfScopeItems = suggestInitialValue
settings.importOutOfScopeItems = importInitialValue
Expand Down