Skip to content

Commit

Permalink
Merge #9441
Browse files Browse the repository at this point in the history
9441: IDE: qualify unimportable elements in import after paste r=dima74 a=Kobzol

This is an improvement of #7597. Changes:
- Fixes proper import of non-trivial paths (like `a::b`).
- Unimportable paths are now fully qualified (if the items are in the same crate).

changelog: Fully qualify paths from the local crate that cannot be imported after paste.

Co-authored-by: Jakub Beránek <berykubik@gmail.com>
  • Loading branch information
bors[bot] and Kobzol committed Jan 7, 2023
2 parents cbeb923 + e19e6f1 commit 2cec227
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 52 deletions.
18 changes: 12 additions & 6 deletions src/main/kotlin/org/rust/ide/inspections/fixes/QualifyPathFix.kt
Expand Up @@ -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)
}
}
Expand Up @@ -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<Output=i32>`
Expand All @@ -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<RsUseSpeck>() != null) {
// Don't try to import path in use item
Expand All @@ -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)
Expand Down
140 changes: 100 additions & 40 deletions src/main/kotlin/org/rust/ide/typing/paste/RsImportCopyPasteProcessor.kt
Expand Up @@ -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<Int, String>) {
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<RelativeEndOffset, QualifiedItemPath>) {
fun elementToFqn(element: PsiElement, importOffset: Int): QualifiedItemPath? {
val relativeEndOffset = toRelativeEndOffset(element, importOffset)
return offsetToFqnMap[relativeEndOffset]
}
}

Expand Down Expand Up @@ -94,12 +112,13 @@ class RsImportCopyPasteProcessor : CopyPastePostProcessor<RsTextBlockTransferabl

val data = values.getOrNull(0) ?: return
val file = editor.document.toPsiFile(project) as? RsFile ?: return
val range = bounds.range

val elements = gatherElements(file, range)
val elements = gatherElements(file, bounds.range)
val importCtx = elements.firstOrNull { it is RsElement } as? RsElement ?: return

val visitor = ImportingVisitor(range, data.importMap)
val importOffset = bounds.range.startOffset

val visitor = ImportingVisitor(importOffset, data.importMap)

runWriteAction {
for (element in elements) {
Expand All @@ -110,6 +129,9 @@ class RsImportCopyPasteProcessor : CopyPastePostProcessor<RsTextBlockTransferabl
for (candidate in visitor.importCandidates) {
candidate.import(importCtx)
}
for ((element, importInfo) in visitor.qualifyCandidates) {
QualifyPathFix.qualify(element, importInfo)
}
}
}

Expand All @@ -133,82 +155,114 @@ class RsImportCopyPasteProcessor : CopyPastePostProcessor<RsTextBlockTransferabl

private class RsReferenceData

private class ImportingVisitor(private val range: TextRange, private val importMap: ImportMap) : RsRecursiveVisitor() {
private val candidates: MutableList<ImportCandidate> = mutableListOf()
private class ImportingVisitor(private val importOffset: Int, private val importMap: ImportMap) : RsRecursiveVisitor() {
private val importCandidatesInner: MutableList<ImportCandidate> = mutableListOf()
private val qualifyCandidatesInner: MutableList<Pair<RsPath, ImportInfo>> = mutableListOf()

val importCandidates: List<ImportCandidate> = candidates
val importCandidates: List<ImportCandidate> = importCandidatesInner
val qualifyCandidates: List<Pair<RsPath, ImportInfo>> = 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<Int, String>()
val fqnMap = hashMapOf<RelativeEndOffset, QualifiedItemPath>()

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)
Expand All @@ -219,5 +273,11 @@ private fun createFqnMap(file: RsFile, range: TextRange): ImportMap {

private fun gatherElements(file: RsFile, range: TextRange): List<PsiElement> =
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

0 comments on commit 2cec227

Please sign in to comment.