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()) + } +}