diff --git a/python/pluginResources/intellij.python.community.impl.xml b/python/pluginResources/intellij.python.community.impl.xml
index dda298f9c4d9..cb5323e449dd 100644
--- a/python/pluginResources/intellij.python.community.impl.xml
+++ b/python/pluginResources/intellij.python.community.impl.xml
@@ -70,6 +70,14 @@
optionId="python.type.inlays.function.return"/>
+
+
diff --git a/python/resources/inlayProviders/python.pseudo.keyword.pattern.inlays/preview.py b/python/resources/inlayProviders/python.pseudo.keyword.pattern.inlays/preview.py
new file mode 100644
index 000000000000..d11dfd12ae64
--- /dev/null
+++ b/python/resources/inlayProviders/python.pseudo.keyword.pattern.inlays/preview.py
@@ -0,0 +1,7 @@
+class Point:
+ __match_args__ = ("x", "y")
+
+def f(p: Point):
+ match p:
+ case Point(/*<# x= #>*/1, /*<# y= #>*/2):
+ pass
diff --git a/python/src/com/jetbrains/python/inlayHints/PyPseudoKeywordPatternInlayHintsProvider.kt b/python/src/com/jetbrains/python/inlayHints/PyPseudoKeywordPatternInlayHintsProvider.kt
new file mode 100644
index 000000000000..5a03ff7d138e
--- /dev/null
+++ b/python/src/com/jetbrains/python/inlayHints/PyPseudoKeywordPatternInlayHintsProvider.kt
@@ -0,0 +1,93 @@
+// 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.openapi.application.EDT
+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.openapi.application.readAction
+import com.intellij.util.concurrency.annotations.RequiresEdt
+import com.intellij.util.text.nullize
+import com.jetbrains.python.psi.PyClassPattern
+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.withContext
+import kotlinx.coroutines.launch
+
+/**
+ * Provides inlays for positional patterns in class patterns, showing pseudo keyword argument names.
+ */
+class PyPseudoKeywordPatternInlayHintsProvider : InlayHintsProvider {
+
+ override fun createCollector(file: PsiFile, editor: Editor): InlayHintsCollector = Collector()
+
+ private class Collector : SharedBypassCollector {
+ private val hintFormat = HintFormat.default
+ .withFontSize(HintFontSize.ABitSmallerThanInEditor)
+ .withHorizontalMargin(HintMarginPadding.MarginAndSmallerPadding)
+
+ override fun collectFromElement(element: PsiElement, sink: InlayTreeSink) {
+ val typeEvalContext = TypeEvalContext.codeAnalysis(element.project, element.containingFile)
+ val classPattern = element as? PyClassPattern ?: return
+ 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 // positional patterns can only be before keyword patterns
+ val name = matchArgs.getOrNull(idx)?.nullize() ?: return@mapIndexed
+ sink.addPresentation(position = InlineInlayPosition(pattern.textRange.startOffset, false), hintFormat = hintFormat) {
+ 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, 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)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/python/src/com/jetbrains/python/inlayHints/PyTypeInlayHintsProvider.kt b/python/src/com/jetbrains/python/inlayHints/PyTypeInlayHintsProvider.kt
index 5ef9cc7a9a28..440101d517f6 100644
--- a/python/src/com/jetbrains/python/inlayHints/PyTypeInlayHintsProvider.kt
+++ b/python/src/com/jetbrains/python/inlayHints/PyTypeInlayHintsProvider.kt
@@ -1,42 +1,23 @@
// 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
import com.jetbrains.python.psi.PyCallExpression
-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 {
const val REVEAL_TYPE_OPTION_ID: String = "python.type.inlays.reveal_type"
const val FUNCTION_RETURN_TYPE_OPTION_ID: String = "python.type.inlays.function.return"
- const val PSEUDO_KEYWORD_PATTERN_OPTION_ID: String = "python.type.inlays.pseudo.keyword.pattern"
}
override fun createCollector(file: PsiFile, editor: Editor): InlayHintsCollector? = Collector()
@@ -57,10 +38,6 @@ class PyTypeInlayHintsProvider : InlayHintsProvider {
sink.whenOptionEnabled(FUNCTION_RETURN_TYPE_OPTION_ID) {
getInlaysForReturnType(element, sink, resolveContext)
}
-
- sink.whenOptionEnabled(PSEUDO_KEYWORD_PATTERN_OPTION_ID) {
- getPositionalPatternNames(element, sink, resolveContext)
- }
}
private fun getInlaysForRevealType(element: PsiElement, sink: InlayTreeSink, resolveContext: PyResolveContext) {
@@ -103,58 +80,5 @@ class PyTypeInlayHintsProvider : InlayHintsProvider {
}
}
}
-
- private fun getPositionalPatternNames(element: PsiElement, sink: InlayTreeSink, resolveContext: PyResolveContext) {
- val typeEvalContext = resolveContext.typeEvalContext
- val classPattern = element as? PyClassPattern ?: return
- 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 // 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) {
- 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, 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)
- }
- }
}
}
\ No newline at end of file
diff --git a/python/testSrc/com/jetbrains/python/inlayHints/PyPseudoKeywordPatternInlayHintsProviderTest.kt b/python/testSrc/com/jetbrains/python/inlayHints/PyPseudoKeywordPatternInlayHintsProviderTest.kt
new file mode 100644
index 000000000000..e29e97bd9985
--- /dev/null
+++ b/python/testSrc/com/jetbrains/python/inlayHints/PyPseudoKeywordPatternInlayHintsProviderTest.kt
@@ -0,0 +1,55 @@
+// 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.testFramework.LightProjectDescriptor
+import com.intellij.testFramework.utils.inlays.declarative.DeclarativeInlayHintsProviderTestCase
+import com.jetbrains.python.fixtures.PyLightProjectDescriptor
+import com.jetbrains.python.psi.LanguageLevel
+
+class PyPseudoKeywordPatternInlayHintsProviderTest : DeclarativeInlayHintsProviderTestCase() {
+ fun testSimplePositional() {
+ doTest(
+ """
+class Point:
+ __match_args__ = ("x", "y")
+ def __init__(self, x, y):
+ self.x = x
+ self.y = y
+
+def f(p: Point):
+ match p:
+ case Point(/*<# x= #>*/1, /*<# y= #>*/b):
+ pass
+ """.trimIndent()
+ )
+ }
+
+ fun testStopsBeforeKeyword() {
+ doTest(
+ """
+class Point:
+ __match_args__ = ("x", "y")
+
+def f(p: Point):
+ match p:
+ case Point(/*<# x= #>*/1, y=2): # second is keyword, no inlay expected there
+ pass
+ """.trimIndent()
+ )
+ }
+
+ private fun doTest(text: String) {
+ doTestProvider(
+ "A.py",
+ text,
+ PyPseudoKeywordPatternInlayHintsProvider(),
+ emptyMap(),
+ verifyHintsPresence = true,
+ testMode = ProviderTestMode.SIMPLE
+ )
+ }
+
+ override fun getProjectDescriptor(): LightProjectDescriptor? {
+ return PyLightProjectDescriptor(LanguageLevel.getLatest())
+ }
+}