PY-45729 Intention to convert between single-line comma-separated things and multi-line representation

Merge-request: IJ-MR-155221
Merged-by: Aleksandr Govenko <aleksandr.govenko@jetbrains.com>

GitOrigin-RevId: 1da524fe3f995e28bf23843f02761065ae45f965
This commit is contained in:
Aleksandr.Govenko
2025-03-25 13:43:44 +00:00
committed by intellij-monorepo-bot
parent a04f2e9512
commit ab13d9dfd2
7 changed files with 91 additions and 0 deletions
@@ -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"/>
<listSplitJoinContext language="Python" implementationClass="com.jetbrains.python.codeInsight.editorActions.PyListSplitJoinContext"/>
<defaultLiveTemplates file="liveTemplates/Python.xml"/>
<liveTemplateContext contextId="Python"
implementation="com.jetbrains.python.codeInsight.liveTemplates.PythonTemplateContextType$General"/>
@@ -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)
}
}
@@ -0,0 +1,5 @@
a = (
1,
2,
3,<caret>
)
@@ -0,0 +1 @@
a = (1, 2, 3,)
@@ -0,0 +1 @@
a = (1, 2, 3, <caret>)
@@ -0,0 +1,5 @@
a = (
1,
2,
3,
)
@@ -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"));