mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
PY-90293 inlay-hints: make types clickable
Conflicts: community/python/src/com/jetbrains/python/inlayHints/PyTypeInlayHintsProvider.kt community/python/testSrc/com/jetbrains/python/inlayHints/PyTypeInlayHintsProviderTest.kt (cherry picked from commit 066a0701502abe27e6fa4ef32c6c5296aeb82f7b) GitOrigin-RevId: 1a4e9f40de1f2e25e09f1b4940f3c331f38d85a1
This commit is contained in:
committed by
intellij-monorepo-bot
parent
637f091859
commit
dfa8ca325e
@@ -0,0 +1,121 @@
|
||||
// Copyright 2000-2026 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.hints.declarative.InlayActionData
|
||||
import com.intellij.codeInsight.hints.declarative.PresentationTreeBuilder
|
||||
import com.intellij.codeInsight.hints.declarative.PsiPointerInlayActionNavigationHandler
|
||||
import com.intellij.codeInsight.hints.declarative.PsiPointerInlayActionPayload
|
||||
import com.intellij.codeInsight.hints.declarative.impl.PresentationTreeBuilderImpl.Companion.MAX_SEGMENT_TEXT_LENGTH
|
||||
import com.intellij.psi.createSmartPointer
|
||||
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider
|
||||
import com.jetbrains.python.documentation.PythonDocumentationProvider
|
||||
import com.jetbrains.python.getEffectiveLanguageLevel
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.psi.PyClass
|
||||
import com.jetbrains.python.psi.types.PyClassType
|
||||
import com.jetbrains.python.psi.types.PyCollectionType
|
||||
import com.jetbrains.python.psi.types.PyLiteralStringType
|
||||
import com.jetbrains.python.psi.types.PyLiteralType
|
||||
import com.jetbrains.python.psi.types.PyNamedTupleType
|
||||
import com.jetbrains.python.psi.types.PyTupleType
|
||||
import com.jetbrains.python.psi.types.PyType
|
||||
import com.jetbrains.python.psi.types.PyUnionType
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext
|
||||
import com.jetbrains.python.psi.types.isNoneType
|
||||
|
||||
/**
|
||||
* Renders [type] as a PEP 484-compliant type hint into a declarative inlay, making class names clickable so that
|
||||
* Ctrl/Cmd-clicking a name navigates to the corresponding class definition.
|
||||
*
|
||||
* For the type shapes handled explicitly, the produced text matches [PythonDocumentationProvider.getTypeHint]; for
|
||||
* everything else the rendering falls back to that method's plain (non-navigable) text, so the inlay text always stays
|
||||
* in sync with the canonical renderer.
|
||||
*/
|
||||
internal fun PresentationTreeBuilder.printPyTypeHint(type: PyType?, context: TypeEvalContext) {
|
||||
when {
|
||||
type == null || type.isNoneType -> plainText(PythonDocumentationProvider.getTypeHint(type, context))
|
||||
// Literal[...] renders its expression text rather than a class name.
|
||||
type is PyLiteralType || type is PyLiteralStringType -> fallbackText(type, context)
|
||||
// tuple[...] (homogeneous/empty forms) and NamedTuple have dedicated formatting.
|
||||
type is PyTupleType || type is PyNamedTupleType -> fallbackText(type, context)
|
||||
type is PyUnionType -> printUnion(type, context)
|
||||
type is PyCollectionType && !type.isDefinition -> printGenericType(type, context)
|
||||
type is PyClassType && !type.isDefinition -> printClassType(type, context)
|
||||
else -> fallbackText(type, context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun PresentationTreeBuilder.printUnion(union: PyUnionType, context: TypeEvalContext) {
|
||||
// Without PEP 604 `X | Y` syntax, unions are rendered as Union[...]/Optional[...]; defer those to the canonical renderer.
|
||||
if (!PyTypingTypeProvider.isBitwiseOrUnionAvailable(context)) {
|
||||
fallbackText(union, context)
|
||||
return
|
||||
}
|
||||
val members = union.members
|
||||
// Optional: `X | None`, with the non-None member always rendered first (as PythonDocumentationProvider does).
|
||||
if (members.size == 2 && members.any { it.isNoneType }) {
|
||||
val nonNone = members.firstOrNull { !it.isNoneType }
|
||||
if (nonNone != null) {
|
||||
printPyTypeHint(nonNone, context)
|
||||
plainText(" | ")
|
||||
printPyTypeHint(members.first { it.isNoneType }, context)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Plain `A | B | C`. Unions with the unknown type (null) or 2+ literals are formatted specially downstream.
|
||||
if (members.none { it == null || it.isNoneType } && members.count { it is PyLiteralType } < 2) {
|
||||
members.forEachIndexed { index, member ->
|
||||
if (index > 0) plainText(" | ")
|
||||
printPyTypeHint(member, context)
|
||||
}
|
||||
return
|
||||
}
|
||||
fallbackText(union, context)
|
||||
}
|
||||
|
||||
private fun PresentationTreeBuilder.printGenericType(type: PyCollectionType, context: TypeEvalContext) {
|
||||
val name = type.name
|
||||
val origin = context.origin
|
||||
val builtinGenericsAvailable = origin == null || getEffectiveLanguageLevel(origin).isAtLeast(LanguageLevel.PYTHON39)
|
||||
// On older language levels typing.List/Dict/... are substituted for list[]/dict[]/...; defer that to the canonical renderer.
|
||||
if (name == null || (!builtinGenericsAvailable && PyTypingTypeProvider.TYPING_COLLECTION_CLASSES.containsKey(name))) {
|
||||
fallbackText(type, context)
|
||||
return
|
||||
}
|
||||
clickableClassName(name, type.pyClass)
|
||||
plainText("[")
|
||||
type.elementTypes.forEachIndexed { index, element ->
|
||||
if (index > 0) plainText(", ")
|
||||
printPyTypeHint(element, context)
|
||||
}
|
||||
plainText("]")
|
||||
}
|
||||
|
||||
private fun PresentationTreeBuilder.printClassType(type: PyClassType, context: TypeEvalContext) {
|
||||
val name = type.name
|
||||
if (name == null) {
|
||||
fallbackText(type, context)
|
||||
return
|
||||
}
|
||||
clickableClassName(name, type.pyClass)
|
||||
}
|
||||
|
||||
private fun PresentationTreeBuilder.clickableClassName(name: String, pyClass: PyClass) {
|
||||
val actionData = InlayActionData(PsiPointerInlayActionPayload(pyClass.createSmartPointer()),
|
||||
PsiPointerInlayActionNavigationHandler.HANDLER_ID)
|
||||
// The platform forbids a single text segment longer than MAX_SEGMENT_TEXT_LENGTH characters.
|
||||
for (segment in name.chunked(MAX_SEGMENT_TEXT_LENGTH)) {
|
||||
text(segment, actionData)
|
||||
}
|
||||
}
|
||||
|
||||
private fun PresentationTreeBuilder.fallbackText(type: PyType?, context: TypeEvalContext) {
|
||||
plainText(PythonDocumentationProvider.getTypeHint(type, context))
|
||||
}
|
||||
|
||||
private fun PresentationTreeBuilder.plainText(text: String) {
|
||||
// The platform forbids a single text segment longer than MAX_SEGMENT_TEXT_LENGTH characters.
|
||||
for (segment in text.chunked(MAX_SEGMENT_TEXT_LENGTH)) {
|
||||
text(segment)
|
||||
}
|
||||
}
|
||||
@@ -116,11 +116,10 @@ class PyTypeInlayHintsProvider : InlayHintsProvider {
|
||||
else -> type
|
||||
}
|
||||
|
||||
val typeHint = PythonDocumentationProvider.getTypeHint(type, typeEvalContext)
|
||||
sink.addPresentation(position = InlineInlayPosition(function.parameterList.textRange.endOffset, true),
|
||||
hintFormat = returnTypeHintFormat) {
|
||||
text("-> ")
|
||||
appendTypeHint(typeHint)
|
||||
printPyTypeHint(type, typeEvalContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,16 +174,14 @@ class PyTypeInlayHintsProvider : InlayHintsProvider {
|
||||
else -> rawParameterType
|
||||
} ?: return
|
||||
|
||||
val typeHint = PythonDocumentationProvider.getTypeHint(parameterType, typeEvalContext)
|
||||
|
||||
val offset = parameter.nameIdentifier?.textRange?.endOffset ?: parameter.textRange.endOffset
|
||||
|
||||
|
||||
sink.addPresentation(
|
||||
position = InlineInlayPosition(offset, true),
|
||||
hintFormat = HintFormat.default
|
||||
) {
|
||||
text(": $typeHint")
|
||||
text(": ")
|
||||
printPyTypeHint(parameterType, typeEvalContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.jetbrains.python.inlayHints.PyTypeInlayHintsProvider.Companion.SOLVED
|
||||
import com.jetbrains.python.inlayHints.PyTypeInlayHintsProvider.Companion.SOLVED_FUNCTION_TYPE_PARAMETERS_OPTION_ID
|
||||
import com.jetbrains.python.inlayHints.PyTypeInlayHintsProvider.Companion.VARIANCE_OPTION_ID
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.psi.PyClass
|
||||
|
||||
class PyTypeInlayHintsProviderTest : DeclarativeInlayHintsProviderTestCase() {
|
||||
|
||||
@@ -35,7 +36,7 @@ class PyTypeInlayHintsProviderTest : DeclarativeInlayHintsProviderTestCase() {
|
||||
def bar(a: int)/*<# -> Literal["Hi!"] #>*/:
|
||||
return "Hi!"
|
||||
|
||||
def gen(a: int)/*<# -> Generator[Literal[42, "str"] | float, Any, Literal["Hi!", 42… #>*/:
|
||||
def gen(a: int)/*<# -> Generator[Literal[42, "str"] | float, Any, Literal["Hi!", 42]] #>*/:
|
||||
yield 42
|
||||
yield "str"
|
||||
yield 42.5
|
||||
@@ -322,6 +323,38 @@ class PyTypeInlayHintsProviderTest : DeclarativeInlayHintsProviderTestCase() {
|
||||
""", false, SOLVED_CLASS_TYPE_PARAMETERS_OPTION_ID)
|
||||
}
|
||||
|
||||
@TestFor(issues = ["PY-90293"])
|
||||
fun `test return type name is navigable`() {
|
||||
doTestNavigable("""
|
||||
def f(x: int)/*<# -> |<int>int #>*/:
|
||||
return x
|
||||
""", FUNCTION_RETURN_TYPE_OPTION_ID)
|
||||
}
|
||||
|
||||
@TestFor(issues = ["PY-90293"])
|
||||
fun `test return type names in union are navigable`() {
|
||||
doTestNavigable("""
|
||||
def f(x: int, y: float)/*<# -> |<float>float| | |<int>int #>*/:
|
||||
return x + y
|
||||
""", FUNCTION_RETURN_TYPE_OPTION_ID)
|
||||
}
|
||||
|
||||
@TestFor(issues = ["PY-90293"])
|
||||
fun `test return type names in generic are navigable`() {
|
||||
doTestNavigable("""
|
||||
def f()/*<# -> |<list>list|[|<int>int|] #>*/:
|
||||
return [1]
|
||||
""", FUNCTION_RETURN_TYPE_OPTION_ID)
|
||||
}
|
||||
|
||||
@TestFor(issues = ["PY-90293"])
|
||||
fun `test parameter type name is navigable`() {
|
||||
doTestNavigable("""
|
||||
def f(a/*<# : |<int>int #>*/=1):
|
||||
pass
|
||||
""", PARAMETER_TYPE_ANNOTATION)
|
||||
}
|
||||
|
||||
private val allOptions = mapOf(
|
||||
REVEAL_TYPE_OPTION_ID to true,
|
||||
FUNCTION_RETURN_TYPE_OPTION_ID to true,
|
||||
@@ -345,6 +378,26 @@ class PyTypeInlayHintsProviderTest : DeclarativeInlayHintsProviderTestCase() {
|
||||
testMode = ProviderTestMode.SIMPLE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders hints in [ProviderTestMode.DETAILED] mode, where each navigable name carries the resolved class wrapped in
|
||||
* `<>` so the expected text can assert both the rendered text and the navigation target of each clickable name.
|
||||
*/
|
||||
private fun doTestNavigable(text: String, vararg enabledOptions: String) {
|
||||
customToStringProvider = { element -> "<${(element as PyClass).name}>" }
|
||||
try {
|
||||
val testOptions = allOptions.mapValues { (key, _) -> key in enabledOptions }
|
||||
doTestProvider("A.py",
|
||||
text.trimIndent(),
|
||||
PyTypeInlayHintsProvider(),
|
||||
testOptions,
|
||||
verifyHintsPresence = true,
|
||||
testMode = ProviderTestMode.DETAILED)
|
||||
}
|
||||
finally {
|
||||
customToStringProvider = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getProjectDescriptor(): LightProjectDescriptor {
|
||||
return PyLightProjectDescriptor(LanguageLevel.getLatest())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user