DS-4868 Wrong attribute-style completion items for polar dataframes

GitOrigin-RevId: d377d740c358ee22490c891862ad1eb7fbf0f2d8
This commit is contained in:
Olga.Lavrichenko
2023-05-19 11:31:48 +00:00
committed by intellij-monorepo-bot
parent b221598456
commit 830dc346e3
2 changed files with 101 additions and 51 deletions

View File

@@ -13,6 +13,7 @@ import com.intellij.openapi.project.Project
import com.intellij.patterns.PlatformPatterns import com.intellij.patterns.PlatformPatterns
import com.intellij.util.ProcessingContext import com.intellij.util.ProcessingContext
import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl
import com.jetbrains.python.PyTokenTypes
import com.jetbrains.python.debugger.PyDebugValue import com.jetbrains.python.debugger.PyDebugValue
import com.jetbrains.python.debugger.state.PyRuntime import com.jetbrains.python.debugger.state.PyRuntime
import com.jetbrains.python.debugger.values.DataFrameDebugValue import com.jetbrains.python.debugger.values.DataFrameDebugValue
@@ -34,9 +35,10 @@ private fun postProcessingChildren(completionResultData: CompletionResultData,
return when (completionResultData.completionType) { return when (completionResultData.completionType) {
PyRuntimeCompletionType.DATA_FRAME_COLUMNS -> { PyRuntimeCompletionType.DATA_FRAME_COLUMNS -> {
val project = parameters.editor.project ?: return emptyList() val project = parameters.editor.project ?: return emptyList()
processDataFrameColumns(candidate.psiName, val needValidatorCheck = (candidate.pyQualifiedExpressionList.lastOrNull()?.delimiter ?: candidate.psiName.delimiter) != PyTokenTypes.LBRACKET
processDataFrameColumns(candidate.psiName.pyQualifiedName,
completionResultData.setOfCompletionItems, completionResultData.setOfCompletionItems,
candidate.needValidatorCheck, needValidatorCheck,
parameters.position, parameters.position,
project, project,
true) true)
@@ -52,12 +54,12 @@ interface PyRuntimeCompletionRetrievalService {
*/ */
fun canComplete(parameters: CompletionParameters): Boolean fun canComplete(parameters: CompletionParameters): Boolean
fun extractItemsForCompletion(result: Pair<XValueNodeImpl, List<String>>?, fun extractItemsForCompletion(result: Pair<XValueNodeImpl, List<PyQualifiedExpressionItem>>?,
candidate: PyObjectCandidate): CompletionResultData? { candidate: PyObjectCandidate): CompletionResultData? {
val (node, listOfCalls) = result ?: return null val (node, listOfCalls) = result ?: return null
val debugValue = node.valueContainer val debugValue = node.valueContainer
if (debugValue is DataFrameDebugValue) { if (debugValue is DataFrameDebugValue) {
val dfColumns = completePandasDataFrameColumns(debugValue.treeColumns, listOfCalls) ?: return null val dfColumns = completePandasDataFrameColumns(debugValue.treeColumns, listOfCalls.map { it.pyQualifiedName }) ?: return null
return CompletionResultData(dfColumns, PyRuntimeCompletionType.DATA_FRAME_COLUMNS) return CompletionResultData(dfColumns, PyRuntimeCompletionType.DATA_FRAME_COLUMNS)
} }
computeChildrenIfNeeded(node) computeChildrenIfNeeded(node)
@@ -119,11 +121,10 @@ abstract class AbstractRuntimeCompletionContributor : CompletionContributor(), D
abstract fun getCompletionRetrievalService(project: Project): PyRuntimeCompletionRetrievalService abstract fun getCompletionRetrievalService(project: Project): PyRuntimeCompletionRetrievalService
} }
private fun createCompletionResultSet(
retrievalService: PyRuntimeCompletionRetrievalService, fun createCompletionResultSet(retrievalService: PyRuntimeCompletionRetrievalService,
runtimeService: PyRuntime, runtimeService: PyRuntime,
parameters: CompletionParameters, parameters: CompletionParameters): List<LookupElement> {
): List<LookupElement> {
if (!retrievalService.canComplete(parameters)) return emptyList() if (!retrievalService.canComplete(parameters)) return emptyList()
val project = parameters.editor.project ?: return emptyList() val project = parameters.editor.project ?: return emptyList()
val treeNodeList = runtimeService.getGlobalPythonVariables(parameters.originalFile.virtualFile, project, parameters.editor) val treeNodeList = runtimeService.getGlobalPythonVariables(parameters.originalFile.virtualFile, project, parameters.editor)
@@ -132,15 +133,17 @@ private fun createCompletionResultSet(
return ApplicationUtil.runWithCheckCanceled(Callable { return ApplicationUtil.runWithCheckCanceled(Callable {
return@Callable pyObjectCandidates.flatMap { candidate -> return@Callable pyObjectCandidates.flatMap { candidate ->
val parentNode = getParentNodeByName(treeNodeList, candidate.psiName) val parentNode = getParentNodeByName(treeNodeList, candidate.psiName.pyQualifiedName)
val valueContainer = parentNode?.valueContainer val valueContainer = parentNode?.valueContainer
/** if (valueContainer is PyDebugValue) {
* Don't need to send requests to jupyter server about Python's module, /**
* because LegacyCompletionContributor provide completion items for Python's modules * Don't need to send requests to jupyter server about Python's module,
* @see com.intellij.codeInsight.completion.LegacyCompletionContributor * because LegacyCompletionContributor provide completion items for Python's modules
*/ * @see com.intellij.codeInsight.completion.LegacyCompletionContributor
if (valueContainer is PyDebugValue && valueContainer.type == "module") return@flatMap emptyList() */
if (valueContainer.type == "module") return@flatMap emptyList()
if (checkDelimiterByType(valueContainer.qualifiedType, candidate.psiName.delimiter)) return@flatMap emptyList()
}
getSetOfChildrenByListOfCall(parentNode, candidate.pyQualifiedExpressionList) getSetOfChildrenByListOfCall(parentNode, candidate.pyQualifiedExpressionList)
.let { retrievalService.extractItemsForCompletion(it, candidate) } .let { retrievalService.extractItemsForCompletion(it, candidate) }
?.let { postProcessingChildren(it, candidate, parameters) } ?.let { postProcessingChildren(it, candidate, parameters) }

View File

@@ -14,6 +14,7 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.util.TextRange import com.intellij.openapi.util.TextRange
import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiElement import com.intellij.psi.PsiElement
import com.intellij.psi.tree.IElementType
import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.elementType import com.intellij.psi.util.elementType
import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreeListener import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreeListener
@@ -24,6 +25,7 @@ import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl
import com.jetbrains.python.PyBundle import com.jetbrains.python.PyBundle
import com.jetbrains.python.PyTokenTypes import com.jetbrains.python.PyTokenTypes
import com.jetbrains.python.PythonLanguage import com.jetbrains.python.PythonLanguage
import com.jetbrains.python.debugger.PyDebugValue
import com.jetbrains.python.debugger.PyXValueGroup import com.jetbrains.python.debugger.PyXValueGroup
import com.jetbrains.python.debugger.pydev.ProcessDebugger import com.jetbrains.python.debugger.pydev.ProcessDebugger
import com.jetbrains.python.debugger.values.DataFrameDebugValue import com.jetbrains.python.debugger.values.DataFrameDebugValue
@@ -33,16 +35,21 @@ import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import javax.swing.tree.TreeNode import javax.swing.tree.TreeNode
data class PyQualifiedExpressionItem(val pyQualifiedName: String, val delimiter: IElementType)
/** /**
* This data class stores information about a possible python object. * This data class stores information about a possible python object.
* @param psiName - name of PsiElement, that could be a python object * @param psiName - name of PsiElement, that could be a python object
* @param needValidatorCheck - boolean flag to switch on check for validating completion items name
* @param pyQualifiedExpressionList - represents a qualified expression in the list
* LanguageNamesValidation.INSTANCE.forLanguage(PythonLanguage.getInstance()).isIdentifier() * LanguageNamesValidation.INSTANCE.forLanguage(PythonLanguage.getInstance()).isIdentifier()
* @param pyQualifiedExpressionList - represents a qualified expression in the list
* @see PyQualifiedExpressionItem
* *
* An example "a.b.c": psiName = "a", needValidatorCheck = false, pyQualifiedExpressionList = ["b","c"] * An example "a.b.c.":
* psiName = PyQualifiedExpressionItem("a",PyTokenTypes.DOT),
* pyQualifiedExpressionList = [PyQualifiedExpressionItem("b",PyTokenTypes.DOT),PyQualifiedExpressionItem("c",PyTokenTypes.DOT)]
*/ */
data class PyObjectCandidate(val psiName: String, val needValidatorCheck: Boolean, val pyQualifiedExpressionList: List<String>) data class PyObjectCandidate(val psiName: PyQualifiedExpressionItem,
val pyQualifiedExpressionList: List<PyQualifiedExpressionItem>)
// Temporary priority value to control order in CompletionResultSet (DS-3746) // Temporary priority value to control order in CompletionResultSet (DS-3746)
@@ -51,9 +58,8 @@ private const val RUNTIME_COMPLETION_PRIORITY = 100.0
fun getCompleteAttribute(parameters: CompletionParameters): List<PyObjectCandidate> { fun getCompleteAttribute(parameters: CompletionParameters): List<PyObjectCandidate> {
val callInnerReferenceExpression = getCallInnerReferenceExpression(parameters) val callInnerReferenceExpression = getCallInnerReferenceExpression(parameters)
val (currentElement, needValidatorCheck) = val (currentElement, lastDelimiter) = findCompleteAttribute(parameters) ?: return getPossibleObjectsDataFrame(parameters,
findCompleteAttribute(parameters) callInnerReferenceExpression)
?: return getPossibleObjectsDataFrame(parameters, callInnerReferenceExpression)
return when (currentElement) { return when (currentElement) {
is PyCallExpression, is PyParenthesizedExpression -> { is PyCallExpression, is PyParenthesizedExpression -> {
@@ -61,10 +67,10 @@ fun getCompleteAttribute(parameters: CompletionParameters): List<PyObjectCandida
} }
is PyExpression -> buildList { is PyExpression -> buildList {
val parentLambdaExpression = collectParentOfLambdaExpression(currentElement, callInnerReferenceExpression) val parentLambdaExpression = collectParentOfLambdaExpression(currentElement, callInnerReferenceExpression)
parentLambdaExpression?.let { parentLambdaExpression?.let { expression ->
add(createPandasDataFrameCandidate(it, needValidatorCheck)) createPyObjectCandidate(expression, lastDelimiter)?.let { add(it) }
} }
add(createPandasDataFrameCandidate(currentElement, needValidatorCheck)) createPyObjectCandidate(currentElement, lastDelimiter)?.let { add(it) }
} }
else -> { else -> {
emptyList() emptyList()
@@ -72,36 +78,56 @@ fun getCompleteAttribute(parameters: CompletionParameters): List<PyObjectCandida
} }
} }
private fun createPandasDataFrameCandidate(psiElement: PsiElement, needValidatorCheck: Boolean): PyObjectCandidate { private fun getPyElementText(child: PsiElement?): String? {
val columns = mutableListOf<String>() child ?: return null
if (child.elementType != PyTokenTypes.DOT && child.elementType != PyTokenTypes.LBRACKET && child.elementType != PyTokenTypes.RBRACKET) {
return when (child) {
is PyStringLiteralExpression -> {
return (child as PyStringLiteralExpressionImpl).stringValue
}
is PyStringElement -> {
child.content
}
else -> {
child.text
}
}
}
return null
}
private fun createPyObjectCandidate(psiElement: PsiElement, lastDelimiter: IElementType): PyObjectCandidate? {
val names = mutableListOf<String>()
val delimiter = mutableListOf<IElementType>()
val firstChild: PsiElement = PsiTreeUtil.getDeepestFirst(psiElement) val firstChild: PsiElement = PsiTreeUtil.getDeepestFirst(psiElement)
val lastChild = PsiTreeUtil.getDeepestLast(psiElement) val lastChild = PsiTreeUtil.getDeepestLast(psiElement)
if (firstChild != lastChild) { if (firstChild != lastChild) {
var child = PsiTreeUtil.nextLeaf(firstChild) var child = PsiTreeUtil.nextLeaf(firstChild)
while (child != null) { while (child != null) {
if (child.elementType != PyTokenTypes.DOT && child.elementType != PyTokenTypes.LBRACKET && child.elementType != PyTokenTypes.RBRACKET) { if (child.elementType == PyTokenTypes.DOT || child.elementType == PyTokenTypes.LBRACKET) {
when (child) { child.elementType?.let { delimiter.add(it) }
is PyStringLiteralExpression -> { child = PsiTreeUtil.nextLeaf(child)
columns.add((child as PyStringLiteralExpressionImpl).stringValue) continue
}
is PyStringElement -> {
columns.add(child.content)
}
else -> {
columns.add(child.text)
}
}
} }
getPyElementText(child)?.let { names.add(it) }
if (child == lastChild) { if (child == lastChild) {
break break
} }
child = PsiTreeUtil.nextLeaf(child) child = PsiTreeUtil.nextLeaf(child)
} }
}
return PyObjectCandidate(firstChild.text, needValidatorCheck, columns) if (delimiter.size == names.size) {
val firstDelimiter = delimiter.removeAt(0)
delimiter.add(lastDelimiter)
return PyObjectCandidate(PyQualifiedExpressionItem(firstChild.text, firstDelimiter),
names.zip(delimiter).map { pair ->
PyQualifiedExpressionItem(pair.first, pair.second)
})
}
return null
}
return PyObjectCandidate(PyQualifiedExpressionItem(firstChild.text, lastDelimiter), emptyList())
} }
private fun getPossibleObjectsDataFrame(parameters: CompletionParameters, private fun getPossibleObjectsDataFrame(parameters: CompletionParameters,
@@ -109,22 +135,25 @@ private fun getPossibleObjectsDataFrame(parameters: CompletionParameters,
return setOfNotNull(callInnerReferenceExpression?.text, getSliceSubscriptionReferenceExpression(parameters)?.text, return setOfNotNull(callInnerReferenceExpression?.text, getSliceSubscriptionReferenceExpression(parameters)?.text,
getAttributeReferenceExpression(parameters)?.text).map { getAttributeReferenceExpression(parameters)?.text).map {
PyObjectCandidate(it, false, emptyList()) PyObjectCandidate(PyQualifiedExpressionItem(it, PyTokenTypes.LBRACKET), emptyList())
} }
} }
private fun findCompleteAttribute(parameters: CompletionParameters): Pair<PsiElement, Boolean>? { private fun findCompleteAttribute(parameters: CompletionParameters): Pair<PsiElement, IElementType>? {
var needCheck = true
var element = parameters.position var element = parameters.position
val delimiter: IElementType?
if (element.prevSibling?.elementType != PyTokenTypes.DOT && element.parent?.prevSibling?.elementType == PyTokenTypes.LBRACKET) { if (element.prevSibling?.elementType != PyTokenTypes.DOT && element.parent?.prevSibling?.elementType == PyTokenTypes.LBRACKET) {
needCheck = false delimiter = element.parent?.prevSibling?.elementType
element = element.parent element = element.parent
} }
else {
delimiter = element.prevSibling?.elementType
}
val exactElement = element.prevSibling?.prevSibling val exactElement = element.prevSibling?.prevSibling
return when { return when {
exactElement != null -> Pair(exactElement, needCheck) exactElement != null && delimiter != null -> Pair(exactElement, delimiter)
else -> null else -> null
} }
@@ -296,7 +325,21 @@ fun getParentNodeByName(children: List<TreeNode>, psiName: String): XValueNodeIm
return null return null
} }
fun getSetOfChildrenByListOfCall(valueNode: XValueNodeImpl?, listOfCall: List<String>): Pair<XValueNodeImpl, List<String>>? { val typeToDelimiter = mapOf(
"polars.internals.dataframe.frame.DataFrame" to setOf(PyTokenTypes.LBRACKET),
"polars.dataframe.frame.DataFrame" to setOf(PyTokenTypes.LBRACKET),
"pandas.core.frame.DataFrame" to setOf(PyTokenTypes.LBRACKET, PyTokenTypes.DOT)
)
fun checkDelimiterByType(qualifiedType: String?, delimiter: IElementType): Boolean {
qualifiedType ?: return false
val delimiters = typeToDelimiter[qualifiedType]
if (delimiters != null && delimiter !in delimiters) return true
return false
}
fun getSetOfChildrenByListOfCall(valueNode: XValueNodeImpl?,
listOfCall: List<PyQualifiedExpressionItem>): Pair<XValueNodeImpl, List<PyQualifiedExpressionItem>>? {
var currentNode = valueNode ?: return null var currentNode = valueNode ?: return null
listOfCall.forEachIndexed { index, call -> listOfCall.forEachIndexed { index, call ->
@@ -306,9 +349,13 @@ fun getSetOfChildrenByListOfCall(valueNode: XValueNodeImpl?, listOfCall: List<St
} }
else -> { else -> {
computeChildrenIfNeeded(currentNode) computeChildrenIfNeeded(currentNode)
currentNode = extractChildByName(currentNode.children, call) ?: return null currentNode = extractChildByName(currentNode.children, call.pyQualifiedName) ?: return null
} }
} }
val valueContainer = currentNode.valueContainer
if (valueContainer is PyDebugValue) {
if (checkDelimiterByType(valueContainer.qualifiedType, call.delimiter)) return null
}
} }
return Pair(currentNode, emptyList()) return Pair(currentNode, emptyList())
} }