From 23cfe9dea34cd51d688b1d3d064c514d03d56bfc Mon Sep 17 00:00:00 2001 From: "Nikita.Ashihmin" Date: Tue, 14 Oct 2025 20:37:10 +0400 Subject: [PATCH] PY-84790 Jupyter: Fix review and small refactors (cherry picked from commit 83d42335c6e6a0ee8014b5b0e4ac61f01538ab50) GitOrigin-RevId: aca283d1a38dd54877a42e3bb7d948d4f26dde8d --- .../intellij.python.community.impl.xml | 6 +- .../AbstractRuntimeCompletionContributor.kt | 86 ++ .../completion/CompletionResultData.kt | 11 + .../completion/PyObjectCandidate.kt | 20 + .../completion/PyQualifiedExpressionItem.kt | 6 + .../PyRuntimeCompletionRetrievalService.kt | 267 +----- .../completion/PyRuntimeCompletionType.kt | 6 + .../completion/PyRuntimeCompletionUtils.kt | 838 ++++++++++-------- .../completion/RuntimeCompletionMethods.kt | 8 + .../completion/RuntimeLookupElement.kt | 11 + .../DummyRemoteFilePathRetrievalService.kt | 9 + .../RemoteFilePathRetrievalService.kt | 13 + 12 files changed, 668 insertions(+), 613 deletions(-) create mode 100644 python/src/com/jetbrains/python/codeInsight/completion/AbstractRuntimeCompletionContributor.kt create mode 100644 python/src/com/jetbrains/python/codeInsight/completion/CompletionResultData.kt create mode 100644 python/src/com/jetbrains/python/codeInsight/completion/PyObjectCandidate.kt create mode 100644 python/src/com/jetbrains/python/codeInsight/completion/PyQualifiedExpressionItem.kt create mode 100644 python/src/com/jetbrains/python/codeInsight/completion/PyRuntimeCompletionType.kt create mode 100644 python/src/com/jetbrains/python/codeInsight/completion/RuntimeCompletionMethods.kt create mode 100644 python/src/com/jetbrains/python/codeInsight/completion/RuntimeLookupElement.kt create mode 100644 python/src/com/jetbrains/python/codeInsight/completion/runtime/patProvider/DummyRemoteFilePathRetrievalService.kt create mode 100644 python/src/com/jetbrains/python/codeInsight/completion/runtime/patProvider/RemoteFilePathRetrievalService.kt diff --git a/python/pluginResources/intellij.python.community.impl.xml b/python/pluginResources/intellij.python.community.impl.xml index 655174bad64d..b932f62f9dfe 100644 --- a/python/pluginResources/intellij.python.community.impl.xml +++ b/python/pluginResources/intellij.python.community.impl.xml @@ -142,9 +142,9 @@ - + diff --git a/python/src/com/jetbrains/python/codeInsight/completion/AbstractRuntimeCompletionContributor.kt b/python/src/com/jetbrains/python/codeInsight/completion/AbstractRuntimeCompletionContributor.kt new file mode 100644 index 000000000000..2d9db3bebbd3 --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/completion/AbstractRuntimeCompletionContributor.kt @@ -0,0 +1,86 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.codeInsight.completion + +import com.intellij.codeInsight.completion.* +import com.intellij.openapi.components.service +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.TextRange +import com.intellij.patterns.PlatformPatterns +import com.intellij.util.ProcessingContext +import com.jetbrains.python.codeInsight.completion.runtime.patProvider.RemoteFilePathRetrievalService +import com.jetbrains.python.debugger.state.PyRuntime +import com.jetbrains.python.psi.PyStringElement + +abstract class AbstractRuntimeCompletionContributor : CompletionContributor(), DumbAware { + override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) { + val project = parameters.editor.project ?: return + if (parameters.completionType == CompletionType.CLASS_NAME) return + + val context = ProcessingContext() + if (!PlatformPatterns.psiElement().accepts(parameters.position, context)) return + + ProgressManager.checkCanceled() + + val service: PyRuntimeCompletionRetrievalService = getCompletionRetrievalService(project) + if (!service.canComplete(parameters)) return + + fillCompletionVariantsFromRuntime(project, service, parameters, result) + } + + abstract fun getRuntimeEnvService(project: Project): PyRuntime + + abstract fun getCompletionRetrievalService(project: Project): PyRuntimeCompletionRetrievalService + + private fun fillCompletionVariantsFromRuntime( + project: Project, + service: PyRuntimeCompletionRetrievalService, + parameters: CompletionParameters, + result: CompletionResultSet, + ) { + + val runtimeResults: MutableMap = + PyRuntimeCompletionUtils.createCompletionResultSet(service, getRuntimeEnvService(project), parameters) + .associateByTo(hashMapOf(), { it.lookupString }, + { RuntimeLookupElement(it, createCustomMatcher(parameters, result)) }) + + if (runtimeResults.isEmpty()) { + val remoteFileResults = project.service().retrieveRemoteFileLookupElements(parameters) + runtimeResults.putAll(remoteFileResults) + } + + // In general, [createCompletionResultSet] returns an empty list in two cases: + // * If there is no runtime. In that case, it's better to return early to not waste CPU on runRemainingContributors and + // hash table access, even though these operations are fast. + // * If there is nothing found. In that case, it's better to return early again, because there is nothing to add to the result. + // * Other very improbable cases like the absence of the project assigned to the editor, which are handled in a defensive manner. + if (runtimeResults.isEmpty()) return + + if (!result.isStopped) { + result.runRemainingContributors(parameters) { item -> + if (runtimeResults.remove(item.lookupElement.lookupString) != null) { + val prioritizedCompletionResult = item.withLookupElement(PyRuntimeCompletionUtils.createPrioritizedLookupElement(item.lookupElement, true)) + result.withPrefixMatcher(item.prefixMatcher).passResult(prioritizedCompletionResult) + } + else { + result.passResult(item) + } + } + } + + runtimeResults.values.forEach { + result.withPrefixMatcher(it.prefix).addElement(it.lookupElement) + } + } + + private fun createCustomMatcher(parameters: CompletionParameters, result: CompletionResultSet): PrefixMatcher { + val currentElement = parameters.position + if (currentElement is PyStringElement) { + val newPrefix = TextRange.create(currentElement.contentRange.startOffset, + parameters.offset - currentElement.textRange.startOffset).substring(currentElement.text) + return PlainPrefixMatcher(newPrefix) + } + return PlainPrefixMatcher(result.prefixMatcher.prefix) + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/codeInsight/completion/CompletionResultData.kt b/python/src/com/jetbrains/python/codeInsight/completion/CompletionResultData.kt new file mode 100644 index 000000000000..92a53e9d0cd2 --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/completion/CompletionResultData.kt @@ -0,0 +1,11 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.codeInsight.completion + +/** + * @param completionType - type of completion items to choose post-processing function in the future + */ +data class CompletionResultData( + val setOfCompletionItems: Set, + val completionType: PyRuntimeCompletionType, + val referenceString: String, +) \ No newline at end of file diff --git a/python/src/com/jetbrains/python/codeInsight/completion/PyObjectCandidate.kt b/python/src/com/jetbrains/python/codeInsight/completion/PyObjectCandidate.kt new file mode 100644 index 000000000000..76c8997cff8e --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/completion/PyObjectCandidate.kt @@ -0,0 +1,20 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.codeInsight.completion + +/** + * This data class stores information about a possible python object. + * @param psiName - name of PsiElement, that could be a python object + * LanguageNamesValidation.INSTANCE.forLanguage(PythonLanguage.getInstance()).isIdentifier() + * @param pyQualifiedExpressionList - represents a qualified expression in the list + * @param requiredTypes - list of the required objects type for additional check + * @see PyQualifiedExpressionItem + * + * An example "a.b.c.": + * psiName = PyQualifiedExpressionItem("a",PyTokenTypes.DOT), + * pyQualifiedExpressionList = [PyQualifiedExpressionItem("b",PyTokenTypes.DOT),PyQualifiedExpressionItem("c",PyTokenTypes.DOT)] + */ +data class PyObjectCandidate( + val psiName: PyQualifiedExpressionItem, + val pyQualifiedExpressionList: List, + val requiredTypes: List? = null, +) \ No newline at end of file diff --git a/python/src/com/jetbrains/python/codeInsight/completion/PyQualifiedExpressionItem.kt b/python/src/com/jetbrains/python/codeInsight/completion/PyQualifiedExpressionItem.kt new file mode 100644 index 000000000000..2c3dc2612a7b --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/completion/PyQualifiedExpressionItem.kt @@ -0,0 +1,6 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.codeInsight.completion + +import com.intellij.psi.tree.IElementType + +data class PyQualifiedExpressionItem(val pyQualifiedName: String, val delimiter: IElementType?) \ No newline at end of file diff --git a/python/src/com/jetbrains/python/codeInsight/completion/PyRuntimeCompletionRetrievalService.kt b/python/src/com/jetbrains/python/codeInsight/completion/PyRuntimeCompletionRetrievalService.kt index 58aac7174b3e..b63ef4de5a62 100644 --- a/python/src/com/jetbrains/python/codeInsight/completion/PyRuntimeCompletionRetrievalService.kt +++ b/python/src/com/jetbrains/python/codeInsight/completion/PyRuntimeCompletionRetrievalService.kt @@ -1,150 +1,13 @@ // Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.codeInsight.completion -import com.intellij.codeInsight.completion.* -import com.intellij.codeInsight.lookup.LookupElement -import com.intellij.codeInsight.lookup.LookupElementBuilder -import com.intellij.lang.LanguageNamesValidation -import com.intellij.openapi.application.ex.ApplicationUtil -import com.intellij.openapi.components.service -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.project.DumbAware -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.text.StringUtil -import com.intellij.patterns.PlatformPatterns -import com.intellij.psi.PsiElement -import com.intellij.ui.IconManager -import com.intellij.ui.PlatformIcons -import com.intellij.util.ProcessingContext +import com.intellij.codeInsight.completion.CompletionParameters +import com.intellij.codeInsight.completion.CompletionType import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl -import com.jetbrains.python.PyBundle -import com.jetbrains.python.PyTokenTypes -import com.jetbrains.python.PythonLanguage import com.jetbrains.python.debugger.PyDebugValue -import com.jetbrains.python.debugger.state.PyRuntime import com.jetbrains.python.debugger.values.DataFrameDebugValue import com.jetbrains.python.debugger.values.completeDataFrameColumns -import com.jetbrains.python.psi.PyStringElement -import java.util.concurrent.Callable -enum class PyRuntimeCompletionType { - DYNAMIC_CLASS, DATA_FRAME_COLUMNS, DICT_KEYS -} - - -/** - * @param completionType - type of completion items to choose post-processing function in the future - */ -data class CompletionResultData(val setOfCompletionItems: Set, - val completionType: PyRuntimeCompletionType, - val referenceString: String) - -private fun processDataFrameColumns(columns: Set, - needValidatorCheck: Boolean, - elementOnPosition: PsiElement, - project: Project, - stringPresentation: String, - ignoreML: Boolean = true): List { - val validator = LanguageNamesValidation.INSTANCE.forLanguage(PythonLanguage.getInstance()) - return columns.mapNotNull { column -> - when { - !needValidatorCheck && elementOnPosition !is PyStringElement -> { - "'${StringUtil.escapeStringCharacters(column.length, column, "'", false, StringBuilder())}'" - } - !needValidatorCheck -> StringUtil.escapeStringCharacters(column.length, column, "'\"", false, StringBuilder()) - validator.isIdentifier(column, project) -> column - else -> null - }?.let { - val lookupElement = LookupElementBuilder.create(it).withTypeText(stringPresentation).withIcon( - IconManager.getInstance().getPlatformIcon(PlatformIcons.Parameter)) - createPrioritizedLookupElement(lookupElement, ignoreML) - } - } -} - -/** - * @param lookupElement - completion item - * @param prefix - prefix for prefix matcher corresponding to lookupElement - */ -data class RuntimeLookupElement(val lookupElement: LookupElement, val prefix: PrefixMatcher) - -interface RemoteFilePathRetrievalService { - /** - * This function returns a map where the key is file name and value - RuntimeLookupElement. - * @see RuntimeLookupElement - */ - fun retrieveRemoteFileLookupElements(parameters: CompletionParameters): Map -} - -class DummyRemoteFilePathRetrievalService : RemoteFilePathRetrievalService { - override fun retrieveRemoteFileLookupElements(parameters: CompletionParameters): Map = emptyMap() -} - -private fun postProcessingChildren(completionResultData: CompletionResultData, - candidate: PyObjectCandidate, - parameters: CompletionParameters): List { - return when (completionResultData.completionType) { - PyRuntimeCompletionType.DATA_FRAME_COLUMNS -> { - val project = parameters.editor.project ?: return emptyList() - val needValidatorCheck = (candidate.pyQualifiedExpressionList.lastOrNull()?.delimiter - ?: candidate.psiName.delimiter) != PyTokenTypes.LBRACKET - processDataFrameColumns(completionResultData.setOfCompletionItems, - needValidatorCheck, - parameters.position, - project, - PyBundle.message("pandas.completion.type.text", completionResultData.referenceString)) - } - PyRuntimeCompletionType.DICT_KEYS -> { - val setOfCompletionItems = completionResultData.setOfCompletionItems.filter { it != "__len__" }.map { - it.removeSurrounding("\'") - }.toSet() - val project = parameters.editor.project ?: return emptyList() - processDataFrameColumns(setOfCompletionItems, - false, - parameters.position, - project, - PyBundle.message("dict.completion.type.text")) - } - PyRuntimeCompletionType.DYNAMIC_CLASS -> proceedPyValueChildrenNames(completionResultData.setOfCompletionItems, - candidate.psiName.pyQualifiedName) - } -} - -private fun proceedPyValueChildrenNames(childrenNodes: Set, - stringPresentation: String?, - ignoreML: Boolean = true): List { - return childrenNodes.map { - val lookupElement = LookupElementBuilder.create(it).withTypeText(stringPresentation).withIcon( - IconManager.getInstance().getPlatformIcon(PlatformIcons.Parameter)) - createPrioritizedLookupElement(lookupElement, ignoreML) - } -} - -/** - * This function returns string presentation of reference expression. - * - * An example: - * ``` - * class B: - * d = { - * "key1" : df - * } - * ``` - * For completion inside `B.d['key1'].` that corresponding to `df` PyDebugValue returns "B.d['key1']" - */ -private fun getReferenceExpression(pyDebugValue: PyDebugValue, nodeName: String?): String { - nodeName ?: return "" - val parent = pyDebugValue.parent ?: return pyDebugValue.name - var referenceName: String = nodeName - - for (parentValue in generateSequence(parent, PyDebugValue::getParent)) { - if (parentValue.qualifiedType in typeToDelimiter.keys) { - referenceName = "${parentValue.name}[$referenceName]" - } - referenceName = "${parentValue.name}.$referenceName" - } - return referenceName -} interface PyRuntimeCompletionRetrievalService { /** @@ -153,8 +16,10 @@ interface PyRuntimeCompletionRetrievalService { */ fun canComplete(parameters: CompletionParameters): Boolean - fun extractItemsForCompletion(result: Pair>?, - candidate: PyObjectCandidate, completionType: CompletionType): CompletionResultData? { + fun extractItemsForCompletion( + result: Pair>?, + candidate: PyObjectCandidate, completionType: CompletionType, + ): CompletionResultData? { val (node, listOfCalls) = result ?: return null val debugValue = node.valueContainer if (debugValue is DataFrameDebugValue) { @@ -162,7 +27,7 @@ interface PyRuntimeCompletionRetrievalService { return CompletionResultData(dfColumns, PyRuntimeCompletionType.DATA_FRAME_COLUMNS, getReferenceExpression(debugValue, node.name)) } if (completionType == CompletionType.BASIC) return null - computeChildrenIfNeeded(node) + PyRuntimeCompletionUtils.computeChildrenIfNeeded(node) if ((debugValue as PyDebugValue).qualifiedType == "builtins.dict") { return CompletionResultData(node.loadedChildren.mapNotNull { (it as? XValueNodeImpl)?.name }.toSet(), PyRuntimeCompletionType.DICT_KEYS, getReferenceExpression(debugValue, node.name)) @@ -170,102 +35,30 @@ interface PyRuntimeCompletionRetrievalService { return CompletionResultData(node.loadedChildren.mapNotNull { (it as? XValueNodeImpl)?.name }.toSet(), PyRuntimeCompletionType.DYNAMIC_CLASS, getReferenceExpression(debugValue, node.name)) } -} + /** + * This function returns string presentation of reference expression. + * + * An example: + * ``` + * class B: + * d = { + * "key1" : df + * } + * ``` + * For completion inside `B.d['key1'].` that corresponding to `df` PyDebugValue returns "B.d['key1']" + */ + private fun getReferenceExpression(pyDebugValue: PyDebugValue, nodeName: String?): String { + nodeName ?: return "" + val parent = pyDebugValue.parent ?: return pyDebugValue.name + var referenceName: String = nodeName -abstract class AbstractRuntimeCompletionContributor : CompletionContributor(), DumbAware { - override fun fillCompletionVariants(parameters: CompletionParameters, result: CompletionResultSet) { - val project = parameters.editor.project ?: return - if (parameters.completionType == CompletionType.CLASS_NAME) return - - val context = ProcessingContext() - if (!PlatformPatterns.psiElement().accepts(parameters.position, context)) return - - ProgressManager.checkCanceled() - - val service: PyRuntimeCompletionRetrievalService = getCompletionRetrievalService(project) - if (!service.canComplete(parameters)) return - - fillCompletionVariantsFromRuntime(project, service, parameters, result) + for (parentValue in generateSequence(parent, PyDebugValue::getParent)) { + if (parentValue.qualifiedType in PyRuntimeCompletionUtils.typeToDelimiter.keys) { + referenceName = "${parentValue.name}[$referenceName]" + } + referenceName = "${parentValue.name}.$referenceName" + } + return referenceName } - - private fun fillCompletionVariantsFromRuntime( - project: Project, - service: PyRuntimeCompletionRetrievalService, - parameters: CompletionParameters, - result: CompletionResultSet, - ) { - - val runtimeResults: MutableMap = - createCompletionResultSet(service, getRuntimeEnvService(project), parameters) - .associateByTo(hashMapOf(), { it.lookupString }, - { RuntimeLookupElement(it, createCustomMatcher(parameters, result)) }) - - if (runtimeResults.isEmpty()) { - val remoteFileResults = project.service().retrieveRemoteFileLookupElements(parameters) - runtimeResults.putAll(remoteFileResults) - } - - // In general, [createCompletionResultSet] returns an empty list in two cases: - // * If there is no runtime. In that case, it's better to return early to not waste CPU on runRemainingContributors and - // hash table access, even though these operations are fast. - // * If there is nothing found. In that case, it's better to return early again, because there is nothing to add to the result. - // * Other very improbable cases like the absence of the project assigned to the editor, which are handled in a defensive manner. - if (runtimeResults.isEmpty()) return - - if (!result.isStopped) { - result.runRemainingContributors(parameters) { item -> - if (runtimeResults.remove(item.lookupElement.lookupString) != null) { - val prioritizedCompletionResult = item.withLookupElement(createPrioritizedLookupElement(item.lookupElement, true)) - result.withPrefixMatcher(item.prefixMatcher).passResult(prioritizedCompletionResult) - } - else { - result.passResult(item) - } - } - } - - runtimeResults.values.forEach { - result.withPrefixMatcher(it.prefix).addElement(it.lookupElement) - } - } - - abstract fun getRuntimeEnvService(project: Project): PyRuntime - - abstract fun getCompletionRetrievalService(project: Project): PyRuntimeCompletionRetrievalService -} - - -fun createCompletionResultSet(retrievalService: PyRuntimeCompletionRetrievalService, - runtimeService: PyRuntime, - parameters: CompletionParameters): List { - if (!retrievalService.canComplete(parameters)) return emptyList() - val project = parameters.editor.project ?: return emptyList() - val treeNodeList = runtimeService.getGlobalPythonVariables(parameters.originalFile.virtualFile, project) - ?: return emptyList() - val pyObjectCandidates = getCompleteAttribute(parameters) - - return ApplicationUtil.runWithCheckCanceled(Callable { - return@Callable pyObjectCandidates.flatMap { candidate -> - if (candidate.psiName.delimiter == null) { - return@flatMap getNodesByPrefix(treeNodeList, candidate.psiName.pyQualifiedName, - parameters.completionType).flatMap { proceedPyValueChildrenNames(setOf(it), null) } - } - val parentNode = getParentNodeByName(treeNodeList, candidate.psiName.pyQualifiedName, parameters.completionType) - 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 - * @see com.intellij.codeInsight.completion.LegacyCompletionContributor - */ - if (valueContainer.type == "module") return@flatMap emptyList() - if (checkDelimiterByType(valueContainer.qualifiedType, candidate.psiName.delimiter)) return@flatMap emptyList() - } - getSetOfChildrenByListOfCall(parentNode, candidate, parameters.completionType) - .let { retrievalService.extractItemsForCompletion(it, candidate, parameters.completionType) } - ?.let { postProcessingChildren(it, candidate, parameters) } - ?: emptyList() - } - }, ProgressManager.getInstance().progressIndicator) } \ No newline at end of file diff --git a/python/src/com/jetbrains/python/codeInsight/completion/PyRuntimeCompletionType.kt b/python/src/com/jetbrains/python/codeInsight/completion/PyRuntimeCompletionType.kt new file mode 100644 index 000000000000..50785de31cca --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/completion/PyRuntimeCompletionType.kt @@ -0,0 +1,6 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.codeInsight.completion + +enum class PyRuntimeCompletionType { + DYNAMIC_CLASS, DATA_FRAME_COLUMNS, DICT_KEYS +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/codeInsight/completion/PyRuntimeCompletionUtils.kt b/python/src/com/jetbrains/python/codeInsight/completion/PyRuntimeCompletionUtils.kt index 5dd21cb10792..2ce10e09683f 100644 --- a/python/src/com/jetbrains/python/codeInsight/completion/PyRuntimeCompletionUtils.kt +++ b/python/src/com/jetbrains/python/codeInsight/completion/PyRuntimeCompletionUtils.kt @@ -1,453 +1,545 @@ // Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.codeInsight.completion -import com.intellij.codeInsight.completion.* +import com.intellij.codeInsight.completion.CompletionParameters +import com.intellij.codeInsight.completion.CompletionType +import com.intellij.codeInsight.completion.LegacyCompletionContributor +import com.intellij.codeInsight.completion.PrioritizedLookupElement import com.intellij.codeInsight.completion.ml.MLRankingIgnorable import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.codeInsight.lookup.LookupElementBuilder import com.intellij.codeInsight.lookup.LookupElementDecorator +import com.intellij.lang.LanguageNamesValidation +import com.intellij.openapi.application.ex.ApplicationUtil import com.intellij.openapi.application.invokeLater -import com.intellij.openapi.util.TextRange +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil import com.intellij.psi.PsiElement import com.intellij.psi.tree.IElementType import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.elementType import com.intellij.psi.util.startOffset +import com.intellij.ui.IconManager +import com.intellij.ui.PlatformIcons import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreeListener import com.intellij.xdebugger.impl.ui.tree.nodes.XDebuggerTreeNode import com.intellij.xdebugger.impl.ui.tree.nodes.XValueContainerNode import com.intellij.xdebugger.impl.ui.tree.nodes.XValueGroupNodeImpl import com.intellij.xdebugger.impl.ui.tree.nodes.XValueNodeImpl +import com.jetbrains.python.PyBundle import com.jetbrains.python.PyTokenTypes +import com.jetbrains.python.PythonLanguage import com.jetbrains.python.debugger.PyDebugValue import com.jetbrains.python.debugger.PyXValueGroup import com.jetbrains.python.debugger.pydev.ProcessDebugger +import com.jetbrains.python.debugger.state.PyRuntime import com.jetbrains.python.debugger.values.DataFrameDebugValue import com.jetbrains.python.psi.* import com.jetbrains.python.psi.impl.PyPsiUtils import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl +import java.util.concurrent.Callable import java.util.concurrent.CompletableFuture import javax.swing.tree.TreeNode -data class PyQualifiedExpressionItem(val pyQualifiedName: String, val delimiter: IElementType?) -/** - * This data class stores information about a possible python object. - * @param psiName - name of PsiElement, that could be a python object - * LanguageNamesValidation.INSTANCE.forLanguage(PythonLanguage.getInstance()).isIdentifier() - * @param pyQualifiedExpressionList - represents a qualified expression in the list - * @param requiredTypes - list of the required objects type for additional check - * @see PyQualifiedExpressionItem - * - * An example "a.b.c.": - * psiName = PyQualifiedExpressionItem("a",PyTokenTypes.DOT), - * pyQualifiedExpressionList = [PyQualifiedExpressionItem("b",PyTokenTypes.DOT),PyQualifiedExpressionItem("c",PyTokenTypes.DOT)] - */ -data class PyObjectCandidate(val psiName: PyQualifiedExpressionItem, - val pyQualifiedExpressionList: List, - val requiredTypes: List? = null) +internal object PyRuntimeCompletionUtils { + // Temporary priority value to control order in CompletionResultSet (DS-3746) + private const val RUNTIME_COMPLETION_PRIORITY = 100.0 -// Temporary priority value to control order in CompletionResultSet (DS-3746) -private const val RUNTIME_COMPLETION_PRIORITY = 100.0 + internal fun getCompleteAttribute(parameters: CompletionParameters): List { -internal fun getCompleteAttribute(parameters: CompletionParameters): List { + val callInnerReferenceExpression = getCallInnerReferenceExpression(parameters) + val (currentElement, lastDelimiter) = findCompleteAttribute(parameters) ?: return getPossibleObjectsDataFrame(parameters, + callInnerReferenceExpression) - val callInnerReferenceExpression = getCallInnerReferenceExpression(parameters) - val (currentElement, lastDelimiter) = findCompleteAttribute(parameters) ?: return getPossibleObjectsDataFrame(parameters, - callInnerReferenceExpression) - - return when (currentElement) { - is PyCallExpression, is PyParenthesizedExpression -> { - if (lastDelimiter == PyTokenTypes.DOT) { - val qualifiedElement = PyQualifiedExpressionItem(currentElement.firstChild.text, CALL_DOT) - return listOf(PyObjectCandidate(qualifiedElement, emptyList())) + return when (currentElement) { + is PyCallExpression, is PyParenthesizedExpression -> { + if (lastDelimiter == PyTokenTypes.DOT) { + val qualifiedElement = PyQualifiedExpressionItem(currentElement.firstChild.text, CALL_DOT) + return listOf(PyObjectCandidate(qualifiedElement, emptyList())) + } + emptyList() } - emptyList() - } - is PyExpression -> buildList { - val parentLambdaExpression = collectParentOfLambdaExpression(currentElement, callInnerReferenceExpression) - parentLambdaExpression?.let { expression -> - createPyObjectCandidate(expression, lastDelimiter)?.let { add(it) } - } - createPyObjectCandidate(currentElement, lastDelimiter)?.let { add(it) } - } - else -> { - emptyList() - } - } -} - -private fun getPyElementText(child: PsiElement?): 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 + is PyExpression -> buildList { + val parentLambdaExpression = collectParentOfLambdaExpression(currentElement, callInnerReferenceExpression) + parentLambdaExpression?.let { expression -> + createPyObjectCandidate(expression, lastDelimiter)?.let { add(it) } + } + createPyObjectCandidate(currentElement, lastDelimiter)?.let { add(it) } } else -> { - child.text + emptyList() } } } - return null -} -private val CALL_DOT = PyElementType("CALL_DOT") -val pattern = listOf(PyTokenTypes.LPAR, PyTokenTypes.RPAR, PyTokenTypes.DOT) - -private fun createPyObjectCandidate(psiElement: PsiElement, lastDelimiter: IElementType): PyObjectCandidate? { - val names = mutableListOf() - val delimiter = mutableListOf() - - val firstChild: PsiElement = PsiTreeUtil.getDeepestFirst(psiElement) - val lastChild = PsiTreeUtil.getDeepestLast(psiElement) - - val allChildren = generateSequence(firstChild) { child -> - if (child == lastChild) null else PsiTreeUtil.nextLeaf(child) - }.toList() - - var index = 0 - while (index < allChildren.size) { - val elementType = allChildren[index].elementType ?: continue - when (elementType) { - PyTokenTypes.LPAR -> { - if (allChildren.size > index + pattern.size && allChildren.subList(index, index + pattern.size).map { it.elementType } == pattern) { - delimiter.add(CALL_DOT) - index += 3 + private fun getPyElementText(child: PsiElement?): 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 } - else { + is PyStringElement -> { + child.content + } + else -> { + child.text + } + } + } + return null + } + + private val CALL_DOT = PyElementType("CALL_DOT") + val pattern = listOf(PyTokenTypes.LPAR, PyTokenTypes.RPAR, PyTokenTypes.DOT) + + private fun createPyObjectCandidate(psiElement: PsiElement, lastDelimiter: IElementType): PyObjectCandidate? { + val names = mutableListOf() + val delimiter = mutableListOf() + + val firstChild: PsiElement = PsiTreeUtil.getDeepestFirst(psiElement) + val lastChild = PsiTreeUtil.getDeepestLast(psiElement) + + val allChildren = generateSequence(firstChild) { child -> + if (child == lastChild) null else PsiTreeUtil.nextLeaf(child) + }.toList() + + var index = 0 + while (index < allChildren.size) { + val elementType = allChildren[index].elementType ?: continue + when (elementType) { + PyTokenTypes.LPAR -> { + if (allChildren.size > index + pattern.size && allChildren.subList(index, index + pattern.size).map { it.elementType } == pattern) { + delimiter.add(CALL_DOT) + index += 3 + } + else { + delimiter.add(elementType) + index += 1 + } + } + PyTokenTypes.RBRACKET -> { + if (delimiter.last() != PyTokenTypes.LBRACKET) { + delimiter.add(elementType) + } + index += 1 + } + PyTokenTypes.DOT, PyTokenTypes.LBRACKET -> { delimiter.add(elementType) index += 1 } - } - PyTokenTypes.RBRACKET -> { - if (delimiter.last() != PyTokenTypes.LBRACKET) { - delimiter.add(elementType) + else -> { + val elementText = getPyElementText(allChildren[index]) ?: return null + names.add(elementText) + index += 1 } - index += 1 } - PyTokenTypes.DOT, PyTokenTypes.LBRACKET -> { - delimiter.add(elementType) - index += 1 + } + delimiter.add(lastDelimiter) + + if (names.isNotEmpty() && delimiter.size == names.size) { + val firstDelimiter = delimiter.removeFirst() + val firstName = names.removeFirst() + return PyObjectCandidate(PyQualifiedExpressionItem(firstName, firstDelimiter), names.zip(delimiter).map { pair -> + PyQualifiedExpressionItem(pair.first, pair.second) + }) + } + + return PyObjectCandidate(PyQualifiedExpressionItem(firstChild.text, lastDelimiter), emptyList()) + } + + private fun getPossibleObjectsDataFrame( + parameters: CompletionParameters, + callInnerReferenceExpression: PyExpression?, + ): List { + val callExpression = PsiTreeUtil.getParentOfType(parameters.position, PyCallExpression::class.java) + parseMethodsWithArguments(callExpression)?.let { + return it + } + + return setOfNotNull( + parameters.position, + callInnerReferenceExpression, + getSliceSubscriptionReferenceExpression(parameters), + getAttributeReferenceExpression(parameters) + ).map { + if (it.elementType == PyTokenTypes.IDENTIFIER) { + PyObjectCandidate( + PyQualifiedExpressionItem(it.text.substring(0, parameters.offset - it.startOffset), null), emptyList()) } - else -> { - val elementText = getPyElementText(allChildren[index]) ?: return null - names.add(elementText) - index += 1 + else { + PyObjectCandidate(PyQualifiedExpressionItem(it.text, PyTokenTypes.LBRACKET), emptyList()) } } } - delimiter.add(lastDelimiter) - if (names.isNotEmpty() && delimiter.size == names.size) { - val firstDelimiter = delimiter.removeFirst() - val firstName = names.removeFirst() - return PyObjectCandidate(PyQualifiedExpressionItem(firstName, firstDelimiter), names.zip(delimiter).map { pair -> - PyQualifiedExpressionItem(pair.first, pair.second) - }) - } + private fun findCompleteAttribute(parameters: CompletionParameters): Pair? { + var element = parameters.position + val delimiter: IElementType? - return PyObjectCandidate(PyQualifiedExpressionItem(firstChild.text, lastDelimiter), emptyList()) -} - -private fun getPossibleObjectsDataFrame(parameters: CompletionParameters, - callInnerReferenceExpression: PyExpression?): List { - val callExpression = PsiTreeUtil.getParentOfType(parameters.position, PyCallExpression::class.java) - parseMethodsWithArguments(callExpression)?.let { - return it - } - - return setOfNotNull( - parameters.position, - callInnerReferenceExpression, - getSliceSubscriptionReferenceExpression(parameters), - getAttributeReferenceExpression(parameters) - ).map { - if (it.elementType == PyTokenTypes.IDENTIFIER) { - PyObjectCandidate( - PyQualifiedExpressionItem(it.text.substring(0, parameters.offset - it.startOffset), null), emptyList()) + if (element.prevSibling?.elementType != PyTokenTypes.DOT && element.parent?.prevSibling?.elementType == PyTokenTypes.LBRACKET) { + delimiter = element.parent?.prevSibling?.elementType + element = element.parent } else { - PyObjectCandidate(PyQualifiedExpressionItem(it.text, PyTokenTypes.LBRACKET), emptyList()) + delimiter = element.prevSibling?.elementType + } + + val exactElement = element.prevSibling?.prevSibling + return when { + exactElement != null && delimiter != null -> Pair(exactElement, delimiter) + + else -> null } } -} -private fun findCompleteAttribute(parameters: CompletionParameters): Pair? { - var element = parameters.position - val delimiter: IElementType? - if (element.prevSibling?.elementType != PyTokenTypes.DOT && element.parent?.prevSibling?.elementType == PyTokenTypes.LBRACKET) { - delimiter = element.parent?.prevSibling?.elementType - element = element.parent - } - else { - delimiter = element.prevSibling?.elementType + fun createCompletionResultSet( + retrievalService: PyRuntimeCompletionRetrievalService, + runtimeService: PyRuntime, + parameters: CompletionParameters, + ): List { + if (!retrievalService.canComplete(parameters)) return emptyList() + val project = parameters.editor.project ?: return emptyList() + val treeNodeList = runtimeService.getGlobalPythonVariables(parameters.originalFile.virtualFile, project) + ?: return emptyList() + val pyObjectCandidates = getCompleteAttribute(parameters) + + return ApplicationUtil.runWithCheckCanceled(Callable { + return@Callable pyObjectCandidates.flatMap { candidate -> + if (candidate.psiName.delimiter == null) { + return@flatMap getNodesByPrefix(treeNodeList, candidate.psiName.pyQualifiedName, + parameters.completionType).flatMap { proceedPyValueChildrenNames(setOf(it), null) } + } + val parentNode = getParentNodeByName(treeNodeList, candidate.psiName.pyQualifiedName, parameters.completionType) + 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 + * @see LegacyCompletionContributor + */ + if (valueContainer.type == "module") return@flatMap emptyList() + if (checkDelimiterByType(valueContainer.qualifiedType, candidate.psiName.delimiter)) return@flatMap emptyList() + } + getSetOfChildrenByListOfCall(parentNode, candidate, parameters.completionType) + .let { retrievalService.extractItemsForCompletion(it, candidate, parameters.completionType) } + ?.let { postProcessingChildren(it, candidate, parameters) } + ?: emptyList() + } + }, ProgressManager.getInstance().progressIndicator) } - val exactElement = element.prevSibling?.prevSibling - return when { - exactElement != null && delimiter != null -> Pair(exactElement, delimiter) - - else -> null + private fun postProcessingChildren( + completionResultData: CompletionResultData, + candidate: PyObjectCandidate, + parameters: CompletionParameters, + ): List { + return when (completionResultData.completionType) { + PyRuntimeCompletionType.DATA_FRAME_COLUMNS -> { + val project = parameters.editor.project ?: return emptyList() + val needValidatorCheck = (candidate.pyQualifiedExpressionList.lastOrNull()?.delimiter + ?: candidate.psiName.delimiter) != PyTokenTypes.LBRACKET + processDataFrameColumns(completionResultData.setOfCompletionItems, + needValidatorCheck, + parameters.position, + project, + PyBundle.message("pandas.completion.type.text", completionResultData.referenceString)) + } + PyRuntimeCompletionType.DICT_KEYS -> { + val setOfCompletionItems = completionResultData.setOfCompletionItems.filter { it != "__len__" }.map { + it.removeSurrounding("\'") + }.toSet() + val project = parameters.editor.project ?: return emptyList() + processDataFrameColumns(setOfCompletionItems, + false, + parameters.position, + project, + PyBundle.message("dict.completion.type.text")) + } + PyRuntimeCompletionType.DYNAMIC_CLASS -> proceedPyValueChildrenNames(completionResultData.setOfCompletionItems, + candidate.psiName.pyQualifiedName) + } } -} -// DS-4870 -/** - * This data class retains types and methods about particular expressions associated with certain module usages. - * @see moduleToMethods - */ -private data class RuntimeCompletionMethods(val requiredTypes: List?, val methodNames: List) + private fun processDataFrameColumns( + columns: Set, + needValidatorCheck: Boolean, + elementOnPosition: PsiElement, + project: Project, + stringPresentation: String, + ignoreML: Boolean = true, + ): List { + val validator = LanguageNamesValidation.INSTANCE.forLanguage(PythonLanguage.getInstance()) + return columns.mapNotNull { column -> + when { + !needValidatorCheck && elementOnPosition !is PyStringElement -> { + "'${StringUtil.escapeStringCharacters(column.length, column, "'", false, StringBuilder())}'" + } + !needValidatorCheck -> StringUtil.escapeStringCharacters(column.length, column, "'\"", false, StringBuilder()) + validator.isIdentifier(column, project) -> column + else -> null + }?.let { + val lookupElement = LookupElementBuilder.create(it).withTypeText(stringPresentation).withIcon( + IconManager.getInstance().getPlatformIcon(PlatformIcons.Parameter)) + createPrioritizedLookupElement(lookupElement, ignoreML) + } + } + } + + private fun proceedPyValueChildrenNames( + childrenNodes: Set, + stringPresentation: String?, + ignoreML: Boolean = true, + ): List { + return childrenNodes.map { + val lookupElement = LookupElementBuilder.create(it).withTypeText(stringPresentation).withIcon( + IconManager.getInstance().getPlatformIcon(PlatformIcons.Parameter)) + createPrioritizedLookupElement(lookupElement, ignoreML) + } + } -private val moduleToMethods = mapOf( - "polars" to RuntimeCompletionMethods(listOf("polars.dataframe.frame.DataFrame", "polars.internals.dataframe.frame.DataFrame"), - listOf("any", "approx_unique", "avg", "arg_sort_by", "by_name", "col", "count", "cumsum", "exclude", - "first", "from_epoch", "groups", "head", "implode", "last", "mean", "median", "min", "max", - "n_unique", "quantile", "std", "tail", "sum")), -) + private val moduleToMethods = mapOf( + "polars" to RuntimeCompletionMethods(listOf("polars.dataframe.frame.DataFrame", "polars.internals.dataframe.frame.DataFrame"), + listOf("any", "approx_unique", "avg", "arg_sort_by", "by_name", "col", "count", "cumsum", "exclude", + "first", "from_epoch", "groups", "head", "implode", "last", "mean", "median", "min", "max", + "n_unique", "quantile", "std", "tail", "sum")), + ) -fun parseMethodsWithArguments(callExpression: PyCallExpression?): List? { - val calleeFqn = (callExpression?.callee?.reference?.resolve() as? PyFunction)?.qualifiedName?.split(".") ?: return null - val moduleMethods = moduleToMethods[calleeFqn.firstOrNull()] ?: return null - if (calleeFqn.lastOrNull() in moduleMethods.methodNames) { - var parentExpression = PsiTreeUtil.getParentOfType(callExpression.parent, PyCallExpression::class.java) - val result = mutableListOf() - while (parentExpression != null) { - val psiElement = PsiTreeUtil.getChildOfType(parentExpression, PyReferenceExpression::class.java)?.navigationElement ?: break - val possibleDataFrame = createPyObjectCandidate(psiElement, PyTokenTypes.LPAR) ?: return null - result.add(PyObjectCandidate(PyQualifiedExpressionItem(possibleDataFrame.psiName.pyQualifiedName, PyTokenTypes.LBRACKET), - emptyList(), - moduleMethods.requiredTypes)) - parentExpression = PsiTreeUtil.getParentOfType(parentExpression, PyCallExpression::class.java) + fun parseMethodsWithArguments(callExpression: PyCallExpression?): List? { + val calleeFqn = (callExpression?.callee?.reference?.resolve() as? PyFunction)?.qualifiedName?.split(".") ?: return null + val moduleMethods = moduleToMethods[calleeFqn.firstOrNull()] ?: return null + if (calleeFqn.lastOrNull() in moduleMethods.methodNames) { + var parentExpression = PsiTreeUtil.getParentOfType(callExpression.parent, PyCallExpression::class.java) + val result = mutableListOf() + while (parentExpression != null) { + val psiElement = PsiTreeUtil.getChildOfType(parentExpression, PyReferenceExpression::class.java)?.navigationElement ?: break + val possibleDataFrame = createPyObjectCandidate(psiElement, PyTokenTypes.LPAR) ?: return null + result.add(PyObjectCandidate(PyQualifiedExpressionItem(possibleDataFrame.psiName.pyQualifiedName, PyTokenTypes.LBRACKET), + emptyList(), + moduleMethods.requiredTypes)) + parentExpression = PsiTreeUtil.getParentOfType(parentExpression, PyCallExpression::class.java) + } + return result + } + return null + } + + internal fun getCallInnerReferenceExpression(parameters: CompletionParameters): PyExpression? { + var callExpression: PyCallExpression? = PsiTreeUtil.getParentOfType(parameters.position, PyCallExpression::class.java) ?: return null + var referenceExpression: PyReferenceExpression? = null + + while (true) { + val tmp = PsiTreeUtil.findChildOfType(callExpression, PyReferenceExpression::class.java) + if (tmp == null) break + referenceExpression = tmp + callExpression = PsiTreeUtil.getChildOfType(referenceExpression, PyCallExpression::class.java) + } + if (referenceExpression != null) { + return PyPsiUtils.getFirstQualifier(referenceExpression) + } + return null + } + + private fun getSliceSubscriptionReferenceExpression(parameters: CompletionParameters): PyExpression? { + val expression = PsiTreeUtil.getParentOfType(parameters.position, PySubscriptionExpression::class.java) + val result = PsiTreeUtil.getChildOfType(expression, PyReferenceExpression::class.java) + if (result != null) { + return PyPsiUtils.getFirstQualifier(result) + } + if (expression != null) { + return getSliceSubscriptionReferenceExpressionWithMultiIndex(expression) + } + return null + } + + private fun getSliceSubscriptionReferenceExpressionWithMultiIndex(expression: PyExpression): PyExpression? { + var count = 0 + var child = expression.copy() + + while (PsiTreeUtil.getChildOfType(child, PyReferenceExpression::class.java) == null && child != null) { + count++ + child = PsiTreeUtil.getChildOfType(child, PySubscriptionExpression::class.java) + } + + val result = PsiTreeUtil.getChildOfType(child, PyReferenceExpression::class.java) + if (result != null) { + return PyPsiUtils.getFirstQualifier(result) + } + return null + } + + private fun getAttributeReferenceExpression(parameters: CompletionParameters): PyExpression? { + val result = PsiTreeUtil.getParentOfType(parameters.position, PyReferenceExpression::class.java) + + if (result != null) { + val child = PsiTreeUtil.getChildrenOfType(result, PyExpression::class.java) ?: emptyArray() + if (!child.isEmpty()) { + return child[0] + } + return PyPsiUtils.getFirstQualifier(result) + } + + return null + } + + private fun collectParentOfLambdaExpression(element: PyExpression, callInnerReferenceExpression: PyExpression?): PyExpression? { + if (callInnerReferenceExpression != null) { + val lambdaExpression = PsiTreeUtil.getParentOfType(element, PyLambdaExpression::class.java) + + if (lambdaExpression != null && callInnerReferenceExpression is PyQualifiedExpression) { + if (lambdaExpression.parameterList.parameters.any { it.name == element.name }) { + val callInnerReferencePyExpression = callInnerReferenceExpression as? PyQualifiedExpression + return callInnerReferencePyExpression?.let { PyPsiUtils.getFirstQualifier(it) } + } + } + } + return null + } + + internal fun createPrioritizedLookupElement(lookupElement: LookupElement, ignoreML: Boolean): LookupElement { + val prioritizedElement = PrioritizedLookupElement.withPriority(lookupElement, RUNTIME_COMPLETION_PRIORITY) + if (ignoreML) { + return prioritizedElement.asMLIgnorable() + } + return prioritizedElement + } + + private fun LookupElement.asMLIgnorable(): LookupElement { + return object : LookupElementDecorator(this), MLRankingIgnorable {} + } + + + internal fun computeChildrenIfNeeded(valueNode: XValueContainerNode<*>) { + if (!valueNode.isLeaf && valueNode.loadedChildren.isEmpty()) { + val futureChildrenReady = CompletableFuture() + val listener = object : XDebuggerTreeListener { + override fun childrenLoaded(node: XDebuggerTreeNode, children: MutableList>, last: Boolean) { + if (node == valueNode) { + futureChildrenReady.complete(true) + } + } + } + // Start to listen to tree changes. + valueNode.tree.addTreeListener(listener) + invokeLater { + valueNode.startComputingChildren() + } + // Additional check (children did not empty yet) to prevent race condition. + if (valueNode.loadedChildren.isEmpty()) { + // Wait until children will be added to the tree. + futureChildrenReady.get() + } + // Stop to listen to tree changes. + valueNode.tree.removeTreeListener(listener) + } + } + + private fun extractChildByName(currentNode: XValueContainerNode<*>, childrenNodes: List, name: String): XValueNodeImpl? { + if ((currentNode.valueContainer as? PyDebugValue)?.qualifiedType == "builtins.dict") { + return childrenNodes.firstOrNull { it is XValueNodeImpl && it.name == "'${name}'" } as XValueNodeImpl? + } + return childrenNodes.firstOrNull { it is XValueNodeImpl && it.name == name } as XValueNodeImpl? + } + + internal fun getParentNodeByName(children: List, psiName: String, completionType: CompletionType): XValueNodeImpl? { + /** + * For preventing an extra load of "Special variables". + * Firstly, looking through loaded variables and if not found - load values inside the group (make a request to jupyter server). + */ + val globalVariables = children.filterIsInstance() + globalVariables.firstOrNull { it.name == psiName }?.let { return it } + + if (completionType == CompletionType.BASIC) return null + val specialVariables = children.filterIsInstance().filter { node -> + (node.valueContainer as PyXValueGroup).groupType == ProcessDebugger.GROUP_TYPE.SPECIAL + } + specialVariables.forEach { node -> + computeChildrenIfNeeded(node) + extractChildByName(node, node.loadedChildren, psiName)?.let { + return it + } + } + return null + } + + private fun prefixMatch(node: TreeNode, result: MutableList, prefix: String) { + (node as? XValueNodeImpl)?.name?.let { + if (it.startsWith(prefix)) { + result.add(it) + } + } + } + + internal fun getNodesByPrefix(children: List, prefix: String, completionType: CompletionType): List { + val result = mutableListOf() + children.forEach { prefixMatch(it, result, prefix) } + + if (completionType == CompletionType.BASIC) return result + children.forEach { node -> + if (node is XValueGroupNodeImpl && (node.valueContainer as PyXValueGroup).groupType == ProcessDebugger.GROUP_TYPE.SPECIAL) { + computeChildrenIfNeeded(node) + node.children.forEach { prefixMatch(it, result, prefix) } + } } return result } - return null -} -private fun getCallInnerReferenceExpression(parameters: CompletionParameters): PyExpression? { - var callExpression: PyCallExpression? = PsiTreeUtil.getParentOfType(parameters.position, PyCallExpression::class.java) ?: return null - var referenceExpression: PyReferenceExpression? = null + internal 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), + "builtins.dict" to setOf(PyTokenTypes.LBRACKET) + ) - while (true) { - val tmp = PsiTreeUtil.findChildOfType(callExpression, PyReferenceExpression::class.java) - if (tmp == null) break - referenceExpression = tmp - callExpression = PsiTreeUtil.getChildOfType(referenceExpression, PyCallExpression::class.java) - } - if (referenceExpression != null) { - return PyPsiUtils.getFirstQualifier(referenceExpression) - } - return null -} - -private fun getSliceSubscriptionReferenceExpression(parameters: CompletionParameters): PyExpression? { - val expression = PsiTreeUtil.getParentOfType(parameters.position, PySubscriptionExpression::class.java) - val result = PsiTreeUtil.getChildOfType(expression, PyReferenceExpression::class.java) - if (result != null) { - return PyPsiUtils.getFirstQualifier(result) - } - if (expression != null) { - return getSliceSubscriptionReferenceExpressionWithMultiIndex(expression) - } - return null -} - -private fun getSliceSubscriptionReferenceExpressionWithMultiIndex(expression: PyExpression): PyExpression? { - var count = 0 - var child = expression.copy() - - while (PsiTreeUtil.getChildOfType(child, PyReferenceExpression::class.java) == null && child != null) { - count++ - child = PsiTreeUtil.getChildOfType(child, PySubscriptionExpression::class.java) + internal fun checkDelimiterByType(qualifiedType: String?, delimiter: IElementType?): Boolean { + delimiter ?: return false + qualifiedType ?: return false + val delimiters = typeToDelimiter[qualifiedType] + return delimiters != null && delimiter !in delimiters } - val result = PsiTreeUtil.getChildOfType(child, PyReferenceExpression::class.java) - if (result != null) { - return PyPsiUtils.getFirstQualifier(result) - } - return null -} - -private fun getAttributeReferenceExpression(parameters: CompletionParameters): PyExpression? { - val result = PsiTreeUtil.getParentOfType(parameters.position, PyReferenceExpression::class.java) - - if (result != null) { - val child = PsiTreeUtil.getChildrenOfType(result, PyExpression::class.java) ?: emptyArray() - if (!child.isEmpty()) { - return child[0] - } - return PyPsiUtils.getFirstQualifier(result) + internal fun checkRequiredType(qualifiedType: String?, requiredTypes: List?): Boolean { + requiredTypes ?: return true + qualifiedType ?: return false + return requiredTypes.contains(qualifiedType) } - return null -} - -private fun collectParentOfLambdaExpression(element: PyExpression, callInnerReferenceExpression: PyExpression?): PyExpression? { - if (callInnerReferenceExpression != null) { - val lambdaExpression = PsiTreeUtil.getParentOfType(element, PyLambdaExpression::class.java) - - if (lambdaExpression != null && callInnerReferenceExpression is PyQualifiedExpression) { - if (lambdaExpression.parameterList.parameters.any { it.name == element.name }) { - val callInnerReferencePyExpression = callInnerReferenceExpression as? PyQualifiedExpression - return callInnerReferencePyExpression?.let { PyPsiUtils.getFirstQualifier(it) } - } - } - } - return null -} - -internal fun createPrioritizedLookupElement(lookupElement: LookupElement, ignoreML: Boolean): LookupElement { - val prioritizedElement = PrioritizedLookupElement.withPriority(lookupElement, RUNTIME_COMPLETION_PRIORITY) - if (ignoreML) { - return prioritizedElement.asMLIgnorable() - } - return prioritizedElement -} - -private fun LookupElement.asMLIgnorable(): LookupElement { - return object : LookupElementDecorator(this), MLRankingIgnorable {} -} - - -internal fun computeChildrenIfNeeded(valueNode: XValueContainerNode<*>) { - if (!valueNode.isLeaf && valueNode.loadedChildren.isEmpty()) { - val futureChildrenReady = CompletableFuture() - val listener = object : XDebuggerTreeListener { - override fun childrenLoaded(node: XDebuggerTreeNode, children: MutableList>, last: Boolean) { - if (node == valueNode) { - futureChildrenReady.complete(true) + internal fun getSetOfChildrenByListOfCall( + valueNode: XValueNodeImpl?, + candidate: PyObjectCandidate, + completionType: CompletionType, + ): Pair>? { + var currentNode = valueNode ?: return null + val listOfCall = candidate.pyQualifiedExpressionList + listOfCall.forEachIndexed { index, call -> + when (currentNode.valueContainer) { + is DataFrameDebugValue -> { + if (checkRequiredType((currentNode.valueContainer as? DataFrameDebugValue)?.qualifiedType, candidate.requiredTypes)) { + return Pair(currentNode, listOfCall.subList(index, listOfCall.size)) + } + return null + } + else -> { + if (completionType == CompletionType.BASIC) return null + computeChildrenIfNeeded(currentNode) + currentNode = extractChildByName(currentNode, currentNode.children, call.pyQualifiedName) ?: return null } } - } - // Start to listen to tree changes. - valueNode.tree.addTreeListener(listener) - invokeLater { - valueNode.startComputingChildren() - } - // Additional check (children did not empty yet) to prevent race condition. - if (valueNode.loadedChildren.isEmpty()) { - // Wait until children will be added to the tree. - futureChildrenReady.get() - } - // Stop to listen to tree changes. - valueNode.tree.removeTreeListener(listener) - } -} - -private fun extractChildByName(currentNode: XValueContainerNode<*>, childrenNodes: List, name: String): XValueNodeImpl? { - if ((currentNode.valueContainer as? PyDebugValue)?.qualifiedType == "builtins.dict") { - return childrenNodes.firstOrNull { it is XValueNodeImpl && it.name == "'${name}'" } as XValueNodeImpl? - } - return childrenNodes.firstOrNull { it is XValueNodeImpl && it.name == name } as XValueNodeImpl? -} - -internal fun getParentNodeByName(children: List, psiName: String, completionType: CompletionType): XValueNodeImpl? { - /** - * For preventing an extra load of "Special variables". - * Firstly, looking through loaded variables and if not found - load values inside the group (make a request to jupyter server). - */ - val globalVariables = children.filterIsInstance() - globalVariables.firstOrNull { it.name == psiName }?.let { return it } - - if (completionType == CompletionType.BASIC) return null - val specialVariables = children.filterIsInstance().filter { node -> - (node.valueContainer as PyXValueGroup).groupType == ProcessDebugger.GROUP_TYPE.SPECIAL - } - specialVariables.forEach { node -> - computeChildrenIfNeeded(node) - extractChildByName(node, node.loadedChildren, psiName)?.let { - return it - } - } - return null -} - -private fun prefixMatch(node: TreeNode, result: MutableList, prefix: String) { - (node as? XValueNodeImpl)?.name?.let { - if (it.startsWith(prefix)) { - result.add(it) - } - } -} - -internal fun getNodesByPrefix(children: List, prefix: String, completionType: CompletionType): List { - val result = mutableListOf() - children.forEach { prefixMatch(it, result, prefix) } - - if (completionType == CompletionType.BASIC) return result - children.forEach { node -> - if (node is XValueGroupNodeImpl && (node.valueContainer as PyXValueGroup).groupType == ProcessDebugger.GROUP_TYPE.SPECIAL) { - computeChildrenIfNeeded(node) - node.children.forEach { prefixMatch(it, result, prefix) } - } - } - return result -} - -internal 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), - "builtins.dict" to setOf(PyTokenTypes.LBRACKET) -) - -internal fun checkDelimiterByType(qualifiedType: String?, delimiter: IElementType?): Boolean { - delimiter ?: return false - qualifiedType ?: return false - val delimiters = typeToDelimiter[qualifiedType] - return delimiters != null && delimiter !in delimiters -} - -internal fun checkRequiredType(qualifiedType: String?, requiredTypes: List?): Boolean { - requiredTypes ?: return true - qualifiedType ?: return false - return requiredTypes.contains(qualifiedType) -} - -internal fun getSetOfChildrenByListOfCall(valueNode: XValueNodeImpl?, - candidate: PyObjectCandidate, - completionType: CompletionType): Pair>? { - var currentNode = valueNode ?: return null - val listOfCall = candidate.pyQualifiedExpressionList - listOfCall.forEachIndexed { index, call -> - when (currentNode.valueContainer) { - is DataFrameDebugValue -> { - if (checkRequiredType((currentNode.valueContainer as? DataFrameDebugValue)?.qualifiedType, candidate.requiredTypes)) { - return Pair(currentNode, listOfCall.subList(index, listOfCall.size)) - } - return null - } - else -> { - if (completionType == CompletionType.BASIC) return null - computeChildrenIfNeeded(currentNode) - currentNode = extractChildByName(currentNode, currentNode.children, call.pyQualifiedName) ?: return null + val valueContainer = currentNode.valueContainer + if (valueContainer is PyDebugValue) { + if (checkDelimiterByType(valueContainer.qualifiedType, call.delimiter)) return null } } - val valueContainer = currentNode.valueContainer - if (valueContainer is PyDebugValue) { - if (checkDelimiterByType(valueContainer.qualifiedType, call.delimiter)) return null + (currentNode.valueContainer as? PyDebugValue)?.let { + if (checkRequiredType(it.qualifiedType, candidate.requiredTypes)) { + return Pair(currentNode, emptyList()) + } } + return null } - (currentNode.valueContainer as? PyDebugValue)?.let { - if (checkRequiredType(it.qualifiedType, candidate.requiredTypes)) { - return Pair(currentNode, emptyList()) - } - } - return null -} - -internal fun createCustomMatcher(parameters: CompletionParameters, result: CompletionResultSet): PrefixMatcher { - val currentElement = parameters.position - if (currentElement is PyStringElement) { - val newPrefix = TextRange.create(currentElement.contentRange.startOffset, - parameters.offset - currentElement.textRange.startOffset).substring(currentElement.text) - return PlainPrefixMatcher(newPrefix) - } - return PlainPrefixMatcher(result.prefixMatcher.prefix) -} +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/codeInsight/completion/RuntimeCompletionMethods.kt b/python/src/com/jetbrains/python/codeInsight/completion/RuntimeCompletionMethods.kt new file mode 100644 index 000000000000..12594eb82455 --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/completion/RuntimeCompletionMethods.kt @@ -0,0 +1,8 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.codeInsight.completion + +/** + * This data class retains types and methods about particular expressions associated with certain module usages. + * @see moduleToMethods + */ +internal data class RuntimeCompletionMethods(val requiredTypes: List?, val methodNames: List) \ No newline at end of file diff --git a/python/src/com/jetbrains/python/codeInsight/completion/RuntimeLookupElement.kt b/python/src/com/jetbrains/python/codeInsight/completion/RuntimeLookupElement.kt new file mode 100644 index 000000000000..e9e5c8e5deef --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/completion/RuntimeLookupElement.kt @@ -0,0 +1,11 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.codeInsight.completion + +import com.intellij.codeInsight.completion.PrefixMatcher +import com.intellij.codeInsight.lookup.LookupElement + +/** + * @param lookupElement - completion item + * @param prefix - prefix for prefix matcher corresponding to lookupElement + */ +data class RuntimeLookupElement(val lookupElement: LookupElement, val prefix: PrefixMatcher) \ No newline at end of file diff --git a/python/src/com/jetbrains/python/codeInsight/completion/runtime/patProvider/DummyRemoteFilePathRetrievalService.kt b/python/src/com/jetbrains/python/codeInsight/completion/runtime/patProvider/DummyRemoteFilePathRetrievalService.kt new file mode 100644 index 000000000000..e468061a7024 --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/completion/runtime/patProvider/DummyRemoteFilePathRetrievalService.kt @@ -0,0 +1,9 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.codeInsight.completion.runtime.patProvider + +import com.intellij.codeInsight.completion.CompletionParameters +import com.jetbrains.python.codeInsight.completion.RuntimeLookupElement + +class DummyRemoteFilePathRetrievalService : RemoteFilePathRetrievalService { + override fun retrieveRemoteFileLookupElements(parameters: CompletionParameters): Map = emptyMap() +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/codeInsight/completion/runtime/patProvider/RemoteFilePathRetrievalService.kt b/python/src/com/jetbrains/python/codeInsight/completion/runtime/patProvider/RemoteFilePathRetrievalService.kt new file mode 100644 index 000000000000..e9a8afdbc7fc --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/completion/runtime/patProvider/RemoteFilePathRetrievalService.kt @@ -0,0 +1,13 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.codeInsight.completion.runtime.patProvider + +import com.intellij.codeInsight.completion.CompletionParameters +import com.jetbrains.python.codeInsight.completion.RuntimeLookupElement + +interface RemoteFilePathRetrievalService { + /** + * This function returns a map where the key is file name and value - RuntimeLookupElement. + * @see com.jetbrains.python.codeInsight.completion.RuntimeLookupElement + */ + fun retrieveRemoteFileLookupElements(parameters: CompletionParameters): Map +} \ No newline at end of file