PY-80844 Add a new intention for converting between f-strings and t-strings

GitOrigin-RevId: be3ab4d72de05a92c986af31ac8e40309d9754b1
This commit is contained in:
Mikhail Golubev
2025-05-01 12:20:46 +03:00
committed by intellij-monorepo-bot
parent b8c67616f1
commit f41aae4760
21 changed files with 147 additions and 2 deletions

View File

@@ -1,4 +1,4 @@
<idea-plugin xmlns:xi="http://www.w3.org/2001/XInclude">
<idea-plugin>
<dependencies>
<module name="intellij.python.psi"/>
</dependencies>
@@ -387,6 +387,12 @@
<categoryKey>INTN.category.python</categoryKey>
</intentionAction>
<intentionAction>
<className>com.jetbrains.python.codeInsight.intentions.PyFStringToTStringIntention</className>
<bundleName>messages.PyPsiBundle</bundleName>
<categoryKey>INTN.category.python</categoryKey>
</intentionAction>
<intentionAction>
<className>com.jetbrains.python.codeInsight.intentions.PyConvertLambdaToFunctionIntention</className>
<bundleName>messages.PyPsiBundle</bundleName>
@@ -603,4 +609,4 @@
<pythonHelpersLocator implementation="com.jetbrains.python.PythonHelpersLocatorDefault" />
</extensions>
</idea-plugin>
</idea-plugin>

View File

@@ -0,0 +1 @@
t"""<span">{content}</span>"""

View File

@@ -0,0 +1 @@
f"""<span">{content}</span>"""

View File

@@ -0,0 +1,7 @@
<html>
<body>
<span>
Converts between f-strings and t-strings (<a href="https://peps.python.org/pep-0750/">PEP-750</a>).
</span>
</body>
</html>

View File

@@ -342,6 +342,11 @@ INTN.convert.absolute.to.relative=Convert absolute import to relative
#PyInvertIfConditionIntention
INTN.invert.if.condition=Invert 'if' condition
# PyFStringToTStringIntention
INTN.NAME.convert.between.f.string.t.string=Convert between f-string and t-string
INTN.convert.f.string.to.t.string=Convert f-string to t-string
INTN.convert.t.string.to.f.string=Convert t-string to f-string
### Quick-fixes ###
QFIX.add.qualifier=Add qualifier

View File

@@ -0,0 +1,48 @@
// 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.codeInsight.intentions
import com.intellij.modcommand.ActionContext
import com.intellij.modcommand.ModPsiUpdater
import com.intellij.modcommand.Presentation
import com.intellij.modcommand.PsiUpdateModCommandAction
import com.intellij.openapi.editor.Document
import com.intellij.psi.util.parentOfType
import com.jetbrains.python.PyPsiBundle
import com.jetbrains.python.psi.PyDocStringOwner
import com.jetbrains.python.psi.PyFormattedStringElement
import com.jetbrains.python.psi.PyStringLiteralExpression
import com.jetbrains.python.psi.PyUtil
/**
* Intention to convert between f-strings and t-strings (PEP-750)
*/
class PyFStringToTStringIntention : PsiUpdateModCommandAction<PyFormattedStringElement>(PyFormattedStringElement::class.java) {
override fun getPresentation(context: ActionContext, stringElement: PyFormattedStringElement): Presentation? {
val stringLiteral = stringElement.getParent() as? PyStringLiteralExpression ?: return null
val docStringOwner = stringLiteral.parentOfType<PyDocStringOwner>()
if (docStringOwner != null && docStringOwner.getDocStringExpression() === stringLiteral) return null
if (stringElement.isFormatted) {
return Presentation.of(PyPsiBundle.message("INTN.convert.f.string.to.t.string"))
}
else if (stringElement.isTemplate) {
return Presentation.of(PyPsiBundle.message("INTN.convert.t.string.to.f.string"))
}
return null
}
override fun getFamilyName(): String = PyPsiBundle.message("INTN.NAME.convert.between.f.string.t.string")
override fun invoke(context: ActionContext, stringElement: PyFormattedStringElement, updater: ModPsiUpdater) {
val prefixOffset = stringElement.textOffset
val oldPrefix = stringElement.prefix
val newPrefix = when {
oldPrefix.contains("f") || oldPrefix.contains("F") -> oldPrefix.replace("f", "t").replace("F", "T")
oldPrefix.contains("t") || oldPrefix.contains("T") -> oldPrefix.replace("t", "f").replace("T", "F")
else -> return
}
PyUtil.updateDocumentUnblockedAndCommitted(stringElement) { document: Document ->
document.replaceString(prefixOffset, prefixOffset + oldPrefix.length, newPrefix)
}
}
}

View File

@@ -0,0 +1,4 @@
class MyClass:
f"""This <caret>is a docstring with an f-prefix.
It should not be convertible to a t-string because it's a docstring.
"""

View File

@@ -0,0 +1 @@
s = t"This is a f-string with an {f"emb<caret>edded {value} f-string"}"

View File

@@ -0,0 +1 @@
s = t"This is a f-string with an {t"emb<caret>edded {value} f-string"}"

View File

@@ -0,0 +1,4 @@
s = f"""<caret>
This is a multiline string with {variable1}
and another {variable2} on a different line.
"""

View File

@@ -0,0 +1,4 @@
s = t"""<caret>
This is a multiline string with {variable1}
and another {variable2} on a different line.
"""

View File

@@ -0,0 +1 @@
s = rf"Hel<caret>lo, {name}!"

View File

@@ -0,0 +1 @@
s = rt"Hel<caret>lo, {name}!"

View File

@@ -0,0 +1 @@
s = "<caret>This is a regular string without any prefix"

View File

@@ -0,0 +1 @@
s = f"Hel<caret>lo, {name}!"

View File

@@ -0,0 +1 @@
s = t"Hel<caret>lo, {name}!"

View File

@@ -0,0 +1 @@
s = t"Hel<caret>lo, {name}!"

View File

@@ -0,0 +1 @@
s = f"Hel<caret>lo, {name}!"

View File

@@ -0,0 +1 @@
s = F"Hel<caret>lo, {name}!"

View File

@@ -0,0 +1 @@
s = T"Hel<caret>lo, {name}!"

View File

@@ -0,0 +1,54 @@
// 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.intentions;
import com.jetbrains.python.PyPsiBundle;
import com.jetbrains.python.psi.LanguageLevel;
/**
* Tests for PyFStringToTStringIntention
*/
public class PyFStringToTStringIntentionTest extends PyIntentionTestCase {
public void testSimpleFString() {
doFStringToTStringTest();
}
public void testMultilineFString() {
doFStringToTStringTest();
}
public void testFStringInsideTString() {
doFStringToTStringTest();
}
public void testSimpleTString() {
doTStringToFStringTest();
}
public void testRegularString() {
doNegativeTest();
}
public void testDocString() {
doNegativeTest();
}
public void testRawFString() {
doFStringToTStringTest();
}
public void testUppercaseFStringPrefix() {
doFStringToTStringTest();
}
private void doFStringToTStringTest() {
doTest(PyPsiBundle.message("INTN.convert.f.string.to.t.string"), LanguageLevel.getLatest());
}
private void doTStringToFStringTest() {
doTest(PyPsiBundle.message("INTN.convert.t.string.to.f.string"), LanguageLevel.getLatest());
}
private void doNegativeTest() {
runWithLanguageLevel(LanguageLevel.getLatest(), () -> doNegativeTest(PyPsiBundle.message("INTN.convert.f.string.to.t.string")));
}
}