blob: 0ef1834f00bc0f6bd75d136756ef6a5349257be1 [file]
// Copyright 2000-2017 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.ide.util.TreeChooser
import com.intellij.ide.util.gotoByName.GotoSymbolModel2
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComponentWithBrowseButton
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFileSystemItem
import com.intellij.psi.PsiNamedElement
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.QualifiedName
import com.intellij.ui.TextAccessor
import com.intellij.util.ProcessingContext
import com.intellij.util.TextFieldCompletionProvider
import com.intellij.util.textCompletion.TextFieldWithCompletion
import com.jetbrains.extensions.getQName
import com.jetbrains.extensions.python.toPsi
import com.jetbrains.extensions.ContextAnchor
import com.jetbrains.extensions.QNameResolveContext
import com.jetbrains.extensions.resolveToElement
import com.jetbrains.python.PyGotoSymbolContributor
import com.jetbrains.python.PyNames
import com.jetbrains.python.PyTreeChooserDialog
import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.psi.PyQualifiedNameOwner
import com.jetbrains.python.psi.PyTypedElement
import com.jetbrains.python.psi.PyUtil
import com.jetbrains.python.psi.stubs.PyClassNameIndex
import com.jetbrains.python.psi.stubs.PyFunctionNameIndex
import com.jetbrains.python.psi.types.PyModuleType
import com.jetbrains.python.psi.types.PyType
import com.jetbrains.python.psi.types.TypeEvalContext
/**
* Python module is file or package with init.py.
* Only source based packages are supported.
* [PySymbolFieldWithBrowseButton] use this function to get list of root names
*/
fun isPythonModule(element: PsiElement): Boolean = element is PyFile || (element as? PsiDirectory)?.let {
it.findFile(PyNames.INIT_DOT_PY) != null
} ?: false
/**
* Text field to enter python symbols and browse button (from [PyGotoSymbolContributor]).
* Supports auto-completion for symbol fully qualified names inside of textbox
* @param filter lambda to filter symbols
* @param startFromDirectory symbols resolved against module, but may additionally be resolved against this folder if provided like in [QNameResolveContext.folderToStart]
*
*/
class PySymbolFieldWithBrowseButton(contextAnchor: ContextAnchor,
filter: ((PsiElement) -> Boolean)? = null,
startFromDirectory: (() -> VirtualFile)? = null) : TextAccessor, ComponentWithBrowseButton<TextFieldWithCompletion>(
TextFieldWithCompletion(contextAnchor.project, PyNameCompletionProvider(contextAnchor, filter, startFromDirectory), "", true, true, true),
null) {
init {
addActionListener {
val dialog = PySymbolChooserDialog(contextAnchor.project, contextAnchor.scope, filter)
dialog.showDialog()
val element = dialog.selected
if (element is PyQualifiedNameOwner) {
childComponent.setText(element.qualifiedName)
}
if (element is PyFile) {
childComponent.setText(element.getQName()?.toString())
}
}
}
override fun setText(text: String?) {
childComponent.setText(text)
}
override fun getText(): String = childComponent.text
}
private fun PyType.getVariants(element: PsiElement): Array<LookupElement> =
this.getCompletionVariants("", element, ProcessingContext()).filterIsInstance(LookupElement::class.java).toTypedArray()
private class PyNameCompletionProvider(private val contextAnchor: ContextAnchor,
private val filter: ((PsiElement) -> Boolean)?,
private val startFromDirectory: (() -> VirtualFile)? = null) : TextFieldCompletionProvider() {
override fun addCompletionVariants(text: String, offset: Int, prefix: String, result: CompletionResultSet) {
val lookups: Array<LookupElement>
var name: QualifiedName? = null
if ('.' !in text) {
lookups = contextAnchor.getRoots()
.map { rootFolder -> rootFolder.children.map { it.toPsi(contextAnchor.project) } }
.flatten()
.filterNotNull()
.filter { isPythonModule(it) }
.map { LookupElementBuilder.create(it, it.virtualFile.nameWithoutExtension) }
.distinctBy { it.lookupString }
.toTypedArray()
}
else {
val evalContext = TypeEvalContext.userInitiated(contextAnchor.project, null)
name = QualifiedName.fromDottedString(text)
val resolveContext = QNameResolveContext(contextAnchor, evalContext = evalContext, allowInaccurateResult = false,
folderToStart = startFromDirectory?.invoke())
var element = name.resolveToElement(resolveContext, stopOnFirstFail = true)
if (element == null && name.componentCount > 1) {
name = name.removeLastComponent()
element = name.resolveToElement(resolveContext, stopOnFirstFail = true)
}
if (element == null) {
return
}
lookups = when (element) {
is PyFile -> PyModuleType(element).getVariants(element)
is PsiDirectory -> {
val init = PyUtil.turnDirIntoInit(element) as? PyFile ?: return
PyModuleType(init).getVariants(element) +
element.children.filterIsInstance(PsiFileSystemItem::class.java)
// For package we need all symbols in initpy and all filesystem children of this folder except initpy itself
.filterNot { it.name == PyNames.INIT_DOT_PY }
.map { LookupElementBuilder.create(it, it.virtualFile.nameWithoutExtension) }
}
is PyTypedElement -> {
evalContext.getType(element)?.getVariants(element) ?: return
}
else -> return
}
}
result.addAllElements(lookups
.filter { it.psiElement != null }
.filter { filter?.invoke(it.psiElement!!) ?: true }
.map { if (name != null) LookupElementBuilder.create("$name.${it.lookupString}") else it })
}
}
private class PySymbolChooserDialog(project: Project, scope: GlobalSearchScope, private val filter: ((PsiElement) -> Boolean)?)
: PyTreeChooserDialog<PsiNamedElement>("Choose Symbol", PsiNamedElement::class.java,
project,
scope,
TreeChooser.Filter { filter?.invoke(it) ?: true }, null) {
override fun findElements(name: String, searchScope: GlobalSearchScope): Collection<PsiNamedElement> {
return PyClassNameIndex.find(name, project, searchScope) + PyFunctionNameIndex.find(name, project, searchScope)
}
override fun createChooseByNameModel() = GotoSymbolModel2(project, arrayOf(PyGotoSymbolContributor()))
}