diff --git a/python/pluginCore/resources/META-INF/plugin.xml b/python/pluginCore/resources/META-INF/plugin.xml index afd89a6cc3a0..ecd8090be627 100644 --- a/python/pluginCore/resources/META-INF/plugin.xml +++ b/python/pluginCore/resources/META-INF/plugin.xml @@ -269,6 +269,8 @@ The Python plug-in provides smart editing for Python scripts. The feature set of key="INSP.pandas.series.values.replace.with.tolist" enabledByDefault="true" level="WARNING" implementationClass="com.jetbrains.python.inspections.PyPandasSeriesToListInspection"/> + + diff --git a/python/src/com/jetbrains/python/codeInsight/editorActions/PyListSplitJoinContext.kt b/python/src/com/jetbrains/python/codeInsight/editorActions/PyListSplitJoinContext.kt new file mode 100644 index 000000000000..78f357f44a9a --- /dev/null +++ b/python/src/com/jetbrains/python/codeInsight/editorActions/PyListSplitJoinContext.kt @@ -0,0 +1,67 @@ +// 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.editorActions + +import com.intellij.openapi.editor.actions.lists.CommaListSplitJoinContext +import com.intellij.openapi.editor.actions.lists.JoinOrSplit +import com.intellij.openapi.editor.actions.lists.ListWithElements +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.psi.util.PsiTreeUtil +import com.jetbrains.python.psi.* + +class PyListSplitJoinContext : CommaListSplitJoinContext() { + override fun extractData(context: PsiElement): ListWithElements? { + val parent = PsiTreeUtil.findFirstParent(context) { + // This is needed only for intention to work before the first and after the last element of a tuple + (it is PyParenthesizedExpression && it.containedExpression is PyTupleExpression) + || it is PySequenceExpression || it is PyParameterList || it is PyArgumentList + }.let { if (it is PyParenthesizedExpression) it.containedExpression else it } + + return when (parent) { + is PySequenceExpression -> ListWithElements(parent, parent.elements.toList()) + is PyParameterList -> ListWithElements(parent, parent.parameters.toList()) + is PyArgumentList -> ListWithElements(parent, parent.arguments.toList()) + else -> null + } + } + + override fun needTailBreak(data: ListWithElements, lastElement: PsiElement, mode: JoinOrSplit): Boolean { + // if there is a trailing comma, add tail break + return if (mode == JoinOrSplit.SPLIT) hasSeparatorAfter(data, lastElement) else super.needTailBreak(data, lastElement, mode) + } + + override fun needHeadBreak(data: ListWithElements, firstElement: PsiElement, mode: JoinOrSplit): Boolean { + // if there is a trailing comma, also add head break + return needTailBreak(data, data.elements.last(), mode) + } + + override fun nextBreak(data: ListWithElements, element: PsiElement): PsiElement? { + // in PyTupleExpression spaces for first/last elements are stored in parent psi + val nextBreak = super.nextBreak(data, element) + if (nextBreak != null || data.list !is PyTupleExpression || data.elements.last() != element) { + return nextBreak + } + return super.nextBreak(data, data.list) + } + + override fun prevBreak(data: ListWithElements, element: PsiElement): PsiElement? { + // in PyTupleExpression spaces for first/last elements are stored in parent psi + val prevBreak = super.prevBreak(data, element) + if (prevBreak != null || data.list !is PyTupleExpression || data.elements.first() != element) { + return prevBreak + } + return super.prevBreak(data, data.list) + } + + override fun reformatRange(file: PsiFile, rangeToAdjust: TextRange, split: JoinOrSplit) { + val tupleExpression = PsiTreeUtil.findElementOfClassAtRange(file, rangeToAdjust.startOffset, rangeToAdjust.endOffset, PyTupleExpression::class.java) + if (tupleExpression != null && tupleExpression.parent is PyParenthesizedExpression) { + CodeStyleManager.getInstance(file.project).reformat(tupleExpression.parent) + return + } + + CodeStyleManager.getInstance(file.project).reformatText(file, rangeToAdjust.startOffset, rangeToAdjust.endOffset) + } +} \ No newline at end of file diff --git a/python/testData/intentions/joinTupleToSingleLine.py b/python/testData/intentions/joinTupleToSingleLine.py new file mode 100644 index 000000000000..72e596319bcb --- /dev/null +++ b/python/testData/intentions/joinTupleToSingleLine.py @@ -0,0 +1,5 @@ +a = ( + 1, + 2, + 3, +) \ No newline at end of file diff --git a/python/testData/intentions/joinTupleToSingleLine_after.py b/python/testData/intentions/joinTupleToSingleLine_after.py new file mode 100644 index 000000000000..8459a66d24c8 --- /dev/null +++ b/python/testData/intentions/joinTupleToSingleLine_after.py @@ -0,0 +1 @@ +a = (1, 2, 3,) diff --git a/python/testData/intentions/splitTupleToMultiLine.py b/python/testData/intentions/splitTupleToMultiLine.py new file mode 100644 index 000000000000..de6c480082dd --- /dev/null +++ b/python/testData/intentions/splitTupleToMultiLine.py @@ -0,0 +1 @@ +a = (1, 2, 3, ) \ No newline at end of file diff --git a/python/testData/intentions/splitTupleToMultiLine_after.py b/python/testData/intentions/splitTupleToMultiLine_after.py new file mode 100644 index 000000000000..649f1d2fa15f --- /dev/null +++ b/python/testData/intentions/splitTupleToMultiLine_after.py @@ -0,0 +1,5 @@ +a = ( + 1, + 2, + 3, +) diff --git a/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java b/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java index e85a15399ae3..db30a3a644df 100644 --- a/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java +++ b/python/testSrc/com/jetbrains/python/intentions/PyIntentionTest.java @@ -1,6 +1,7 @@ // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.python.intentions; +import com.intellij.codeInsight.CodeInsightBundle; import com.intellij.codeInsight.CodeInsightSettings; import com.intellij.codeInsight.intention.IntentionAction; import com.intellij.openapi.application.impl.NonBlockingReadActionImpl; @@ -94,6 +95,15 @@ public class PyIntentionTest extends PyTestCase { runWithLanguageLevel(LanguageLevel.PYTHON35, () -> doTest(PyPsiBundle.message("QFIX.remove.string.prefix", "F"))); } + // PY-45729 + public void testJoinTupleToSingleLine() { + doTest(CodeInsightBundle.message("intention.name.join.comma.values")); + } + + public void testSplitTupleToMultiLine() { + doTest(CodeInsightBundle.message("intention.name.split.comma.values")); + } + // PY-18972 public void testRemoveTrailingL() { doTest(PyPsiBundle.message("QFIX.remove.trailing.suffix"));