PY-86123 Add navigation to attribute for keyword pattern inlay hints

GitOrigin-RevId: aeabf8a75d4725be7a17a20be34a22ad45a31c15
This commit is contained in:
Aleksandr.Govenko
2025-12-22 22:36:14 +00:00
committed by intellij-monorepo-bot
parent 1c34e08d66
commit af0f19abb9
3 changed files with 63 additions and 5 deletions
@@ -70,6 +70,9 @@
optionId="python.type.inlays.function.return"/>
</codeInsight.declarativeInlayProvider>
<codeInsight.inlayActionHandler implementationClass="com.jetbrains.python.inlayHints.PyPseudoKeywordPatternInlayActionHandler"
handlerId="py.pseudo.keyword.pattern"/>
<!-- Requirements.txt support. Can't extract module due to circular dependency between it and community.impl (RequirementsFile inspection)-->
<completion.contributor language="Requirements"
@@ -41,9 +41,12 @@ public final class PyKeywordPatternReference extends PsiReferenceBase.Poly<PyKey
PyKeywordPattern keywordPattern = getElement();
TypeEvalContext typeContext = TypeEvalContext.codeInsightFallback(keywordPattern.getProject());
PyResolveContext resolveContext = PyResolveContext.defaultContext(typeContext);
return resolveKeyword(classPattern, keywordPattern.getKeyword(), resolveContext);
}
public static ResolveResult[] resolveKeyword(PyClassPattern classPattern, String keyword, PyResolveContext resolveContext) {
return StreamEx.of(resolveToClassTypes(classPattern, resolveContext))
.flatMap(t -> StreamEx.of(ContainerUtil.notNullize(t.resolveMember(keywordPattern.getKeyword(),
null, AccessDirection.READ, resolveContext))))
.flatMap(t -> StreamEx.of(ContainerUtil.notNullize(t.resolveMember(keyword, null, AccessDirection.READ, resolveContext))))
.toArray(ResolveResult.EMPTY_ARRAY);
}
@@ -1,11 +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.inlayHints
import com.intellij.codeInsight.CodeInsightBundle
import com.intellij.codeInsight.hint.HintManager
import com.intellij.codeInsight.hints.declarative.*
import com.intellij.codeInsight.hints.declarative.impl.PresentationTreeBuilderImpl.Companion.MAX_SEGMENT_TEXT_LENGTH
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.readAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.event.EditorMouseEvent
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.SmartPointerManager
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.util.PsiNavigateUtil
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider.REVEAL_TYPE
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider.REVEAL_TYPE_EXT
import com.jetbrains.python.documentation.PythonDocumentationProvider
@@ -14,9 +23,14 @@ import com.jetbrains.python.psi.PyClassPattern
import com.jetbrains.python.psi.PyFunction
import com.jetbrains.python.psi.PyKeywordPattern
import com.jetbrains.python.psi.impl.PyClassPatternImpl
import com.jetbrains.python.psi.impl.references.PyKeywordPatternReference
import com.jetbrains.python.psi.resolve.PyResolveContext
import com.jetbrains.python.psi.types.PyClassType
import com.jetbrains.python.psi.types.TypeEvalContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class PyTypeInlayHintsProvider : InlayHintsProvider {
companion object {
@@ -28,7 +42,7 @@ class PyTypeInlayHintsProvider : InlayHintsProvider {
override fun createCollector(file: PsiFile, editor: Editor): InlayHintsCollector? = Collector()
private class Collector : SharedBypassCollector {
val hintFormat = HintFormat.Companion.default
val hintFormat = HintFormat.default
.withFontSize(HintFontSize.ABitSmallerThanInEditor)
.withHorizontalMargin(HintMarginPadding.MarginAndSmallerPadding)
@@ -96,13 +110,51 @@ class PyTypeInlayHintsProvider : InlayHintsProvider {
val classType = typeEvalContext.getType(classPattern) as? PyClassType ?: return
val matchArgs = PyClassPatternImpl.getMatchArgs(classType, typeEvalContext) ?: return
classPattern.argumentList.patterns.mapIndexed { idx, pattern ->
if (pattern is PyKeywordPattern) return
if (pattern is PyKeywordPattern) return // positional patterns can only be before keyword patterns
val name = matchArgs.getOrNull(idx) ?: return@mapIndexed
sink.addPresentation(position = InlineInlayPosition(pattern.textRange.startOffset, false), hintFormat = hintFormat) {
text(name)
val navigationData = InlayActionData(
PyPseudoKeywordPatternInlayActionHandler.Payload(SmartPointerManager.createPointer(classPattern), name),
PyPseudoKeywordPatternInlayActionHandler.HANDLER_NAME
)
text(name, actionData = navigationData)
text("=")
}
}
}
}
}
class PyPseudoKeywordPatternInlayActionHandler(private val cs: CoroutineScope) : InlayActionHandler {
companion object {
const val HANDLER_NAME: String = "py.pseudo.keyword.pattern"
}
data class Payload(val classPattern: SmartPsiElementPointer<PyClassPattern>, val keyword: String) : InlayActionPayload
@RequiresEdt
override fun handleClick(e: EditorMouseEvent, payload: InlayActionPayload) {
payload as Payload
cs.launch {
val resolveResults = readAction {
val classPattern = payload.classPattern.element ?: return@readAction null
val keyword = payload.keyword
val typeEvalContext = TypeEvalContext.userInitiated(classPattern.project, classPattern.containingFile)
PyKeywordPatternReference.resolveKeyword(classPattern, keyword, PyResolveContext.defaultContext(typeEvalContext))
}
val element = resolveResults?.firstOrNull()?.element ?: run {
withContext(Dispatchers.EDT) {
HintManager.getInstance().showInformationHint(e.editor, CodeInsightBundle.message("declaration.navigation.nowhere.to.go"))
}
return@launch
}
withContext(Dispatchers.EDT) {
PsiNavigateUtil.navigate(element)
}
}
}
}