diff --git a/src/main/kotlin/org/rust/ide/inspections/fixes/QualifyPathFix.kt b/src/main/kotlin/org/rust/ide/inspections/fixes/QualifyPathFix.kt index e092ff78ef3..fec4fda84ee 100644 --- a/src/main/kotlin/org/rust/ide/inspections/fixes/QualifyPathFix.kt +++ b/src/main/kotlin/org/rust/ide/inspections/fixes/QualifyPathFix.kt @@ -29,13 +29,19 @@ class QualifyPathFix( override fun invoke(project: Project, file: PsiFile, startElement: PsiElement, endElement: PsiElement) { val path = startElement as? RsPath ?: return - val qualifiedPath = importInfo.usePath - val fullPath = "$qualifiedPath${path.typeArgumentList?.text.orEmpty()}" - val newPath = RsPsiFactory(project).tryCreatePath(fullPath) ?: return + qualify(path, importInfo) + } + + companion object { + fun qualify(path: RsPath, importInfo: ImportInfo) { + val qualifiedPath = importInfo.usePath + val fullPath = "$qualifiedPath${path.typeArgumentList?.text.orEmpty()}" + val newPath = RsPsiFactory(path.project).tryCreatePath(fullPath) ?: return - if (!file.isIntentionPreviewElement) { - importInfo.insertExternCrateIfNeeded(path) + if (!path.isIntentionPreviewElement) { + importInfo.insertExternCrateIfNeeded(path) + } + path.replace(newPath) } - path.replace(newPath) } } diff --git a/src/main/kotlin/org/rust/ide/inspections/import/AutoImportFix.kt b/src/main/kotlin/org/rust/ide/inspections/import/AutoImportFix.kt index c58b6fe017b..4c142b99b66 100644 --- a/src/main/kotlin/org/rust/ide/inspections/import/AutoImportFix.kt +++ b/src/main/kotlin/org/rust/ide/inspections/import/AutoImportFix.kt @@ -130,7 +130,7 @@ class AutoImportFix(element: RsElement, private val context: Context) : companion object { const val NAME = "Import" - fun findApplicableContext(path: RsPath): Context? { + fun findApplicableContext(path: RsPath, type: ImportContext.Type = ImportContext.Type.AUTO_IMPORT): Context? { if (path.reference == null) return null // `impl Future` @@ -139,7 +139,7 @@ class AutoImportFix(element: RsElement, private val context: Context) : if (parent is RsAssocTypeBinding && parent.eq != null && parent.path == path) return null val basePath = path.basePath() - if (basePath.resolveStatus != PathResolveStatus.UNRESOLVED) return null + if (basePath.resolveStatus != PathResolveStatus.UNRESOLVED && type != ImportContext.Type.OTHER) return null if (path.ancestorStrict() != null) { // Don't try to import path in use item @@ -148,14 +148,14 @@ class AutoImportFix(element: RsElement, private val context: Context) : } val referenceName = basePath.referenceName ?: return null - val importContext = ImportContext.from(path, ImportContext.Type.AUTO_IMPORT) ?: return null + val importContext = ImportContext.from(path, type) ?: return null val candidates = ImportCandidatesCollector.getImportCandidates(importContext, referenceName) return Context(GENERAL_PATH, candidates) } fun findApplicableContext(pat: RsPatBinding): Context? { - val importContext = ImportContext.from(pat, ImportContext.Type.AUTO_IMPORT) ?: return null + val importContext = ImportContext.from(pat) ?: return null val candidates = ImportCandidatesCollector.getImportCandidates(importContext, pat.referenceName) if (candidates.isEmpty()) return null return Context(GENERAL_PATH, candidates) diff --git a/src/main/kotlin/org/rust/ide/typing/paste/RsImportCopyPasteProcessor.kt b/src/main/kotlin/org/rust/ide/typing/paste/RsImportCopyPasteProcessor.kt index 9b0849888fe..75c4c8e7f41 100644 --- a/src/main/kotlin/org/rust/ide/typing/paste/RsImportCopyPasteProcessor.kt +++ b/src/main/kotlin/org/rust/ide/typing/paste/RsImportCopyPasteProcessor.kt @@ -19,24 +19,42 @@ import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.impl.source.tree.injected.changesHandler.range +import org.rust.ide.inspections.fixes.QualifyPathFix import org.rust.ide.inspections.import.AutoImportFix import org.rust.ide.settings.RsCodeInsightSettings -import org.rust.ide.utils.import.ImportCandidate -import org.rust.ide.utils.import.import +import org.rust.ide.utils.import.* +import org.rust.lang.core.crate.CratePersistentId import org.rust.lang.core.psi.* -import org.rust.lang.core.psi.ext.RsElement -import org.rust.lang.core.psi.ext.RsQualifiedNamedElement -import org.rust.lang.core.psi.ext.qualifiedName -import org.rust.lang.core.psi.ext.startOffset +import org.rust.lang.core.psi.ext.* import org.rust.lang.core.types.inference import org.rust.openapiext.toPsiFile import java.awt.datatransfer.DataFlavor import java.awt.datatransfer.Transferable -data class ImportMap(private val offsetToFqnMap: Map) { - fun elementToFqn(element: PsiElement, range: TextRange): String? { - val offset = toRelativeOffset(element, range) - return offsetToFqnMap[offset] +/** + * Path of a single named element within the specified crate. + */ +data class QualifiedItemPath(val crateRelativePath: String, val crateId: CratePersistentId) { + fun matches(target: RsQualifiedNamedElement?): Boolean = + target != null + && crateRelativePath == target.crateRelativePath + && crateId == target.containingCrate.id +} + +/** + * Represents the end offset of an element that is a candidate for import after paste. + * The end offset is relative to the start of a range of elements that were copied. + */ +typealias RelativeEndOffset = Int + +/** + * Maps text ranges in a copy-pasted region to qualified paths that can be used to resolve proper imports. + * The range offsets are relative to the start of the copy-pasted region + */ +data class ImportMap(private val offsetToFqnMap: Map) { + fun elementToFqn(element: PsiElement, importOffset: Int): QualifiedItemPath? { + val relativeEndOffset = toRelativeEndOffset(element, importOffset) + return offsetToFqnMap[relativeEndOffset] } } @@ -94,12 +112,13 @@ class RsImportCopyPasteProcessor : CopyPastePostProcessor = mutableListOf() +private class ImportingVisitor(private val importOffset: Int, private val importMap: ImportMap) : RsRecursiveVisitor() { + private val importCandidatesInner: MutableList = mutableListOf() + private val qualifyCandidatesInner: MutableList> = mutableListOf() - val importCandidates: List = candidates + val importCandidates: List = importCandidatesInner + val qualifyCandidates: List> = qualifyCandidatesInner override fun visitPath(path: RsPath) { val ctx = AutoImportFix.findApplicableContext(path) - handleContext(path, ctx) + handleImport(path, ctx) super.visitPath(path) } override fun visitMethodCall(methodCall: RsMethodCall) { val ctx = AutoImportFix.findApplicableContext(methodCall) - handleContext(methodCall, ctx) + handleImport(methodCall, ctx) super.visitMethodCall(methodCall) } override fun visitPatBinding(binding: RsPatBinding) { - if (importMap.elementToFqn(binding, range) != null) { - val ctx = AutoImportFix.findApplicableContext(binding) - handleContext(binding, ctx) - } + val ctx = AutoImportFix.findApplicableContext(binding) + handleImport(binding, ctx) super.visitPatBinding(binding) } - private fun handleContext(element: PsiElement, ctx: AutoImportFix.Context?) { - if (ctx != null) { - val candidate = ctx.candidates.find { - val fqn = importMap.elementToFqn(element, range) - fqn == it.item.qualifiedName - } - if (candidate != null) { - candidates.add(candidate) + private fun handleImport(element: RsElement, ctx: AutoImportFix.Context?) { + val importMapCandidate = importMap.elementToFqn(element, importOffset) ?: return + + // Try to import with the "Auto import" context + val candidate = ctx.getCandidate(importMapCandidate) + if (candidate != null) { + importCandidatesInner.add(candidate) + return + } + + // If import was not successful, try to fully qualify the name + if (element is RsPath) { + val resolvedTargets = element.reference?.multiResolve() ?: return + if (resolvedTargets.isEmpty()) { + // No accessible path found, just fully qualify the path + if (importMapCandidate.crateId == element.containingCrate.id) { + val importInfo = ImportInfo.LocalImportInfo("crate${importMapCandidate.crateRelativePath}") + qualifyCandidatesInner.add(element to importInfo) + } + } else { + val resolvedTarget = resolvedTargets.singleOrNull() as? RsQualifiedNamedElement + if (importMapCandidate.matches(resolvedTarget)) return + + // Path resolves to something else than the original item + val otherCtx = AutoImportFix.findApplicableContext(element, ImportContext.Type.OTHER) + val otherCandidate = otherCtx.getCandidate(importMapCandidate) ?: return + qualifyCandidatesInner.add(element to otherCandidate.info) } } } } +private fun AutoImportFix.Context?.getCandidate(originalItem: QualifiedItemPath): ImportCandidate? = + this?.candidates?.find { originalItem.matches(it.item) } + /** * Records mapping between offsets (relative to copy/paste content range) and fully qualified names of resolved items * from paths and method calls. */ private fun createFqnMap(file: RsFile, range: TextRange): ImportMap { val elements = gatherElements(file, range) - val fqnMap = hashMapOf() + val fqnMap = hashMapOf() val visitor = object : RsRecursiveVisitor() { override fun visitPath(path: RsPath) { - val target = (path.reference?.resolve() as? RsQualifiedNamedElement)?.qualifiedName + super.visitPath(path) + + // We only want to record the start of the path that can be imported (e.g. `a` in `a::b::c`). + if (path.qualifier != null) return + + val target = path.reference?.resolve() as? RsQualifiedNamedElement if (target != null) { - fqnMap[toRelativeOffset(path, range)] = target + storeMapping(path, target) } - - super.visitPath(path) } override fun visitMethodCall(methodCall: RsMethodCall) { val methods = methodCall.inference?.getResolvedMethod(methodCall) val target = methods?.firstNotNullOfOrNull { - it.source.implementedTrait?.element?.qualifiedName + it.source.implementedTrait?.element } if (target != null) { - fqnMap[toRelativeOffset(methodCall, range)] = target + storeMapping(methodCall, target) } super.visitMethodCall(methodCall) } override fun visitPatBinding(binding: RsPatBinding) { - val target = (binding.reference.resolve() as? RsQualifiedNamedElement)?.qualifiedName + val target = binding.reference.resolve() as? RsQualifiedNamedElement if (target != null) { - fqnMap[toRelativeOffset(binding, range)] = target + storeMapping(binding, target) } super.visitPatBinding(binding) } + + fun storeMapping(element: RsElement, target: RsQualifiedNamedElement) { + fqnMap[toRelativeEndOffset(element, range.startOffset)] = QualifiedItemPath( + target.crateRelativePath ?: return, + target.containingCrate.id ?: return + ) + } } for (element in elements) { element.accept(visitor) @@ -219,5 +273,11 @@ private fun createFqnMap(file: RsFile, range: TextRange): ImportMap { private fun gatherElements(file: RsFile, range: TextRange): List = CollectHighlightsUtil.getElementsInRange(file, range.startOffset, range.endOffset) + .filter { elem -> elem !is PsiFile } -private fun toRelativeOffset(element: PsiElement, range: TextRange): Int = element.startOffset - range.startOffset +/** + * Converts an element to its relative end offset within some region. + * The start offset of the region is passed in `importOffset`. + */ +private fun toRelativeEndOffset(element: PsiElement, importOffset: Int): RelativeEndOffset = + element.endOffset - importOffset diff --git a/src/test/kotlin/org/rust/ide/typing/paste/RsAddImportOnCopyPasteTest.kt b/src/test/kotlin/org/rust/ide/typing/paste/RsAddImportOnCopyPasteTest.kt index c0c3c55eedc..39302ac2f26 100644 --- a/src/test/kotlin/org/rust/ide/typing/paste/RsAddImportOnCopyPasteTest.kt +++ b/src/test/kotlin/org/rust/ide/typing/paste/RsAddImportOnCopyPasteTest.kt @@ -430,7 +430,7 @@ class RsAddImportOnCopyPasteTest : RsTestBase() { } """) - fun `test do not import private item`() = doCopyPasteTest(""" + fun `test qualify private item`() = doCopyPasteTest(""" //- lib.rs mod a { pub struct S; @@ -455,7 +455,7 @@ class RsAddImportOnCopyPasteTest : RsTestBase() { fn foo2() -> S { S } } - fn foo2() -> S { S } + fn foo2() -> crate::b::S { crate::b::S } """) fun `test copy paste same location`() = doCopyPasteTest(""" @@ -610,6 +610,98 @@ class RsAddImportOnCopyPasteTest : RsTestBase() { } """) + fun `test qualify import if same name already exists in scope`() = doCopyPasteTest(""" + //- lib.rs + mod foo { + pub struct S; + + fn fun(_: S) {} + } + + mod bar { + struct S; + /*caret*/ + } + """, """ + //- lib.rs + mod foo { + pub struct S; + + fn fun(_: S) {} + } + + mod bar { + struct S; + fn fun(_: crate::foo::S) {} + } + """) + + fun `test import module`() = doCopyPasteTest(""" + //- lib.rs + mod foo { + pub mod bar { + pub struct S; + } + + fn fun(_: bar::S) {} + } + + mod baz { + /*caret*/ + } + """, """ + //- lib.rs + mod foo { + pub mod bar { + pub struct S; + } + + fn fun(_: bar::S) {} + } + + mod baz { + use crate::foo::bar; + + fn fun(_: bar::S) {} + } + """) + + fun `test qualify path with original re-export`() = doCopyPasteTest(""" + //- lib.rs + mod option { + pub use inner::*; + mod inner { + pub struct Option {} + } + } + + mod mod1 { + use crate::option::Option; + fn func(_: Option) {} + } + mod mod2 { + struct Option {} + /*caret*/ + } + """, """ + //- lib.rs + mod option { + pub use inner::*; + mod inner { + pub struct Option {} + } + } + + mod mod1 { + use crate::option::Option; + fn func(_: Option) {} + } + mod mod2 { + struct Option {} + fn func(_: crate::option::Option) {} + } + """) + private fun doCopyPasteTest( @Language("Rust") before: String, @Language("Rust") after: String