mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-06 03:21:12 +07:00
Split PyConvertToFStringIntention into several classes
This commit is contained in:
@@ -312,7 +312,7 @@
|
||||
</intentionAction>
|
||||
|
||||
<intentionAction>
|
||||
<className>com.jetbrains.python.codeInsight.intentions.PyConvertToFStringIntention</className>
|
||||
<className>com.jetbrains.python.codeInsight.intentions.convertToFString.PyConvertToFStringIntention</className>
|
||||
<category>Python</category>
|
||||
</intentionAction>
|
||||
|
||||
|
||||
@@ -1,456 +0,0 @@
|
||||
/*
|
||||
* Copyright 2000-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.jetbrains.python.codeInsight.intentions;
|
||||
|
||||
import com.intellij.lang.ASTNode;
|
||||
import com.intellij.openapi.editor.Editor;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.Pair;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.intellij.util.IncorrectOperationException;
|
||||
import com.intellij.util.ObjectUtils;
|
||||
import com.jetbrains.python.PyBundle;
|
||||
import com.jetbrains.python.PyNames;
|
||||
import com.jetbrains.python.PyTokenTypes;
|
||||
import com.jetbrains.python.codeInsight.PySubstitutionChunkReference;
|
||||
import com.jetbrains.python.codeInsight.PythonFormattedStringReferenceProvider;
|
||||
import com.jetbrains.python.inspections.PyNewStyleStringFormatParser;
|
||||
import com.jetbrains.python.inspections.PyStringFormatParser;
|
||||
import com.jetbrains.python.inspections.PyStringFormatParser.ConstantChunk;
|
||||
import com.jetbrains.python.inspections.PyStringFormatParser.FormatStringChunk;
|
||||
import com.jetbrains.python.inspections.PyStringFormatParser.SubstitutionChunk;
|
||||
import com.jetbrains.python.psi.*;
|
||||
import com.jetbrains.python.psi.PyUtil.StringNodeInfo;
|
||||
import com.jetbrains.python.psi.impl.PyStringLiteralExpressionImpl;
|
||||
import org.jetbrains.annotations.Nls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static com.jetbrains.python.codeInsight.intentions.ConvertFormatOperatorToMethodIntention.convertFormatSpec;
|
||||
import static com.jetbrains.python.psi.PyUtil.as;
|
||||
|
||||
/**
|
||||
* @author Mikhail Golubev
|
||||
*/
|
||||
public class PyConvertToFStringIntention extends PyBaseIntentionAction {
|
||||
@Nls
|
||||
@NotNull
|
||||
@Override
|
||||
public String getFamilyName() {
|
||||
return PyBundle.message("INTN.convert.to.fstring.literal");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getText() {
|
||||
return getFamilyName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
|
||||
if (!(file instanceof PyFile) || !LanguageLevel.forElement(file).isAtLeast(LanguageLevel.PYTHON36)) return false;
|
||||
|
||||
final Pair<PyStringLiteralExpression, Boolean> pair = findTargetStringUnderCaret(editor, file);
|
||||
if (pair == null) return false;
|
||||
|
||||
final PyStringLiteralExpression pyString = pair.getFirst();
|
||||
final boolean percentOperator = pair.getSecond();
|
||||
|
||||
// TODO handle "glued" literals
|
||||
if (pyString != null && pyString.getStringNodes().size() == 1) {
|
||||
final String stringText = pyString.getText();
|
||||
final String prefix = PyStringLiteralUtil.getPrefix(stringText);
|
||||
if (PyStringLiteralUtil.isBytesPrefix(prefix) || PyStringLiteralUtil.isFormattedPrefix(prefix)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final List<SubstitutionChunk> substitutions;
|
||||
if (percentOperator) {
|
||||
final List<FormatStringChunk> chunks = PyStringFormatParser.parsePercentFormat(stringText);
|
||||
substitutions = PyStringFormatParser.filterSubstitutions(chunks);
|
||||
}
|
||||
else {
|
||||
substitutions = new ArrayList<>(PyNewStyleStringFormatParser.parse(stringText).getFields());
|
||||
}
|
||||
|
||||
// TODO handle dynamic width and precision in old-style/"percent" formatting
|
||||
if (percentOperator && substitutions.stream().anyMatch(s -> "*".equals(s.getWidth()) || "*".equals(s.getPrecision()))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final PySubstitutionChunkReference[] references = createChunkReferences(substitutions, pyString, percentOperator);
|
||||
|
||||
final PsiElement valuesSource;
|
||||
if (percentOperator) {
|
||||
final PyBinaryExpression binaryExpression = as(pyString.getParent(), PyBinaryExpression.class);
|
||||
assert binaryExpression != null;
|
||||
valuesSource = binaryExpression.getRightExpression();
|
||||
}
|
||||
else {
|
||||
final PyCallExpression callExpression = PsiTreeUtil.getParentOfType(pyString, PyCallExpression.class);
|
||||
assert callExpression != null;
|
||||
valuesSource = callExpression.getArgumentList();
|
||||
}
|
||||
|
||||
if (percentOperator) {
|
||||
for (int i = 0; i < substitutions.size(); i++) {
|
||||
final SubstitutionChunk chunk = substitutions.get(i);
|
||||
if ((chunk.getMappingKey() != null || substitutions.size() > 1) && references[i].resolve() == valuesSource) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Arrays.stream(references)
|
||||
.map(PyConvertToFStringIntention::getActualReplacementExpression)
|
||||
.allMatch(element -> element != null &&
|
||||
!(element instanceof PyStarExpression) &&
|
||||
!(element instanceof PyStarArgument) &&
|
||||
PsiTreeUtil.isAncestor(valuesSource, element, false) &&
|
||||
expressionCanBeInlined(pyString, element));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static PySubstitutionChunkReference[] createChunkReferences(@NotNull List<SubstitutionChunk> substitutions,
|
||||
@NotNull PyStringLiteralExpression pyString,
|
||||
boolean percentOperator) {
|
||||
if (percentOperator) {
|
||||
return PythonFormattedStringReferenceProvider.getReferencesFromChunks(pyString, substitutions, true);
|
||||
}
|
||||
return substitutions.stream()
|
||||
.filter(PyNewStyleStringFormatParser.Field.class::isInstance)
|
||||
.map(PyNewStyleStringFormatParser.Field.class::cast)
|
||||
.map(field -> new PySubstitutionChunkReference(pyString, field, ObjectUtils.chooseNotNull(field.getAutoPosition(), 0), false))
|
||||
.toArray(PySubstitutionChunkReference[]::new);
|
||||
}
|
||||
|
||||
private static boolean expressionCanBeInlined(@NotNull PyStringLiteralExpression host, @NotNull PyExpression target) {
|
||||
// Cannot inline multi-line expressions or expressions that contains backslashes (yet)
|
||||
if (target.textContains('\\') || target.textContains('\n')) return false;
|
||||
return adjustQuotesInside((PyExpression)target.copy(), host) != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static PsiElement adjustQuotesInside(@NotNull PyExpression element, @NotNull PyStringLiteralExpression host) {
|
||||
final StringNodeInfo hostInfo = new StringNodeInfo(host.getStringNodes().get(0));
|
||||
final char hostQuote = hostInfo.getSingleQuote();
|
||||
final PyElementGenerator generator = PyElementGenerator.getInstance(host.getProject());
|
||||
|
||||
final Collection<PyStringLiteralExpression> innerStrings = PsiTreeUtil.collectElementsOfType(element, PyStringLiteralExpression.class);
|
||||
for (PyStringLiteralExpression literal : innerStrings) {
|
||||
final List<ASTNode> nodes = literal.getStringNodes();
|
||||
// TODO figure out what to do with those
|
||||
if (nodes.size() > 1) {
|
||||
return null;
|
||||
}
|
||||
final StringNodeInfo info = new StringNodeInfo(nodes.get(0));
|
||||
// Nest string contain the same type of quote as host string inside, and we cannot escape inside f-string -- retreat
|
||||
final String content = info.getContent();
|
||||
final char targetSingleQuote = flipQuote(hostQuote);
|
||||
if (content.indexOf(hostQuote) >= 0) {
|
||||
return null;
|
||||
}
|
||||
if (!info.isTerminated()) {
|
||||
return null;
|
||||
}
|
||||
if (info.getQuote().startsWith(hostInfo.getQuote())) {
|
||||
if (content.indexOf(targetSingleQuote) >= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String targetQuote = info.getQuote().replace(hostQuote, targetSingleQuote);
|
||||
final String stringWithSwappedQuotes = info.getPrefix() + targetQuote + content + targetQuote;
|
||||
final PsiElement replaced = literal.replace(generator.createStringLiteralAlreadyEscaped(stringWithSwappedQuotes));
|
||||
if (literal == element) {
|
||||
return replaced;
|
||||
}
|
||||
}
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Pair<PyStringLiteralExpression, Boolean> findTargetStringUnderCaret(@NotNull Editor editor, @NotNull PsiFile file) {
|
||||
final PsiElement anchor = file.findElementAt(editor.getCaretModel().getOffset());
|
||||
if (anchor == null) return null;
|
||||
|
||||
final PyBinaryExpression binaryExpr = PsiTreeUtil.getParentOfType(anchor, PyBinaryExpression.class);
|
||||
if (binaryExpr != null && binaryExpr.getOperator() == PyTokenTypes.PERC) {
|
||||
final PyStringLiteralExpression pyString = as(binaryExpr.getLeftExpression(), PyStringLiteralExpression.class);
|
||||
if (pyString != null) {
|
||||
return Pair.create(pyString, true);
|
||||
}
|
||||
}
|
||||
final PyCallExpression callExpr = PsiTreeUtil.getParentOfType(anchor, PyCallExpression.class);
|
||||
if (callExpr != null) {
|
||||
final PyReferenceExpression callee = as(callExpr.getCallee(), PyReferenceExpression.class);
|
||||
if (callee != null && PyNames.FORMAT.equals(callee.getName())) {
|
||||
final PyStringLiteralExpression pyString = as(callee.getQualifier(), PyStringLiteralExpression.class);
|
||||
if (pyString != null) {
|
||||
return Pair.create(pyString, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doInvoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
|
||||
final Pair<PyStringLiteralExpression, Boolean> pair = findTargetStringUnderCaret(editor, file);
|
||||
assert pair != null;
|
||||
final Boolean percentOperator = pair.getSecond();
|
||||
if (percentOperator) {
|
||||
convertPercentOperatorFormatting(pair.getFirst());
|
||||
}
|
||||
else {
|
||||
convertFormatMethodFormatting(pair.getFirst());
|
||||
}
|
||||
}
|
||||
|
||||
private static void convertPercentOperatorFormatting(@NotNull PyStringLiteralExpression pyString) {
|
||||
final String stringText = pyString.getText();
|
||||
final Pair<String, String> quotes = PyStringLiteralUtil.getQuotes(stringText);
|
||||
assert quotes != null;
|
||||
final StringBuilder result = new StringBuilder();
|
||||
result.append("f");
|
||||
result.append(quotes.getFirst().replaceAll("[uU]", ""));
|
||||
final List<FormatStringChunk> chunks = PyStringFormatParser.parsePercentFormat(stringText);
|
||||
final TextRange contentRange = PyStringLiteralExpressionImpl.getNodeTextRange(stringText);
|
||||
int subsChunkPosition = 0;
|
||||
for (FormatStringChunk chunk : chunks) {
|
||||
if (chunk instanceof ConstantChunk) {
|
||||
final TextRange rangeWithoutQuotes = chunk.getTextRange().intersection(contentRange);
|
||||
assert rangeWithoutQuotes != null;
|
||||
result.append(rangeWithoutQuotes.substring(stringText));
|
||||
}
|
||||
else {
|
||||
final SubstitutionChunk subsChunk = (SubstitutionChunk)chunk;
|
||||
final char conversionChar = subsChunk.getConversionType();
|
||||
|
||||
String widthAndPrecision = StringUtil.notNullize(subsChunk.getWidth());
|
||||
if (StringUtil.isNotEmpty(subsChunk.getPrecision())) {
|
||||
widthAndPrecision += "." + subsChunk.getPrecision();
|
||||
}
|
||||
|
||||
final String conversionFlags = subsChunk.getConversionFlags();
|
||||
|
||||
result.append("{");
|
||||
final PySubstitutionChunkReference reference = new PySubstitutionChunkReference(pyString, subsChunk, subsChunkPosition, true);
|
||||
final PyExpression resolveResult = getActualReplacementExpression(reference);
|
||||
assert resolveResult != null;
|
||||
|
||||
final PsiElement adjusted = adjustQuotesInside(resolveResult, pyString);
|
||||
if (adjusted == null) return;
|
||||
|
||||
result.append(adjusted.getText());
|
||||
|
||||
// TODO mostly duplicates the logic of ConvertFormatOperatorToMethodIntention
|
||||
if (conversionChar == 'r') {
|
||||
result.append("!r");
|
||||
}
|
||||
|
||||
if ((conversionChar != 'r' && conversionChar != 's')
|
||||
|| StringUtil.isNotEmpty(conversionFlags)
|
||||
|| StringUtil.isNotEmpty(widthAndPrecision)) {
|
||||
result.append(":");
|
||||
}
|
||||
|
||||
result.append(convertFormatSpec(StringUtil.notNullize(conversionFlags), widthAndPrecision, String.valueOf(conversionChar)));
|
||||
|
||||
if (StringUtil.isNotEmpty(widthAndPrecision)) {
|
||||
result.append(widthAndPrecision);
|
||||
}
|
||||
|
||||
if ('i' == conversionChar || 'u' == conversionChar) {
|
||||
result.append("d");
|
||||
}
|
||||
else if ('s' != conversionChar && 'r' != conversionChar) {
|
||||
result.append(conversionChar);
|
||||
}
|
||||
result.append("}");
|
||||
subsChunkPosition++;
|
||||
}
|
||||
}
|
||||
result.append(quotes.getSecond());
|
||||
|
||||
final PyBinaryExpression expressionToReplace = PsiTreeUtil.getParentOfType(pyString, PyBinaryExpression.class);
|
||||
assert expressionToReplace != null;
|
||||
|
||||
final PyElementGenerator generator = PyElementGenerator.getInstance(pyString.getProject());
|
||||
final PyExpression fString = generator.createExpressionFromText(LanguageLevel.PYTHON36, result.toString());
|
||||
expressionToReplace.replace(fString);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static PyExpression getActualReplacementExpression(@NotNull PySubstitutionChunkReference reference) {
|
||||
final PsiElement resolveResult = reference.resolve();
|
||||
if (resolveResult == null) {
|
||||
return null;
|
||||
}
|
||||
final PyKeywordArgument argument = as(resolveResult, PyKeywordArgument.class);
|
||||
if (argument != null) {
|
||||
return argument.getValueExpression();
|
||||
}
|
||||
final PyKeyValueExpression parent = as(resolveResult.getParent(), PyKeyValueExpression.class);
|
||||
if (parent != null && parent.getKey() == resolveResult) {
|
||||
return parent.getValue();
|
||||
}
|
||||
return as(resolveResult, PyExpression.class);
|
||||
}
|
||||
|
||||
private static void convertFormatMethodFormatting(@NotNull PyStringLiteralExpression pyString) {
|
||||
// TODO get rid of duplication with #convertPercentOperatorFormatting
|
||||
final String stringText = pyString.getText();
|
||||
final StringNodeInfo stringInfo = new StringNodeInfo(pyString.getStringNodes().get(0));
|
||||
final StringBuilder newStringText = new StringBuilder();
|
||||
newStringText.append("f");
|
||||
newStringText.append(stringInfo.getPrefix().replaceAll("[uU]", ""));
|
||||
newStringText.append(stringInfo.getQuote());
|
||||
|
||||
final PyNewStyleStringFormatParser.ParseResult parseResult = PyNewStyleStringFormatParser.parse(stringText);
|
||||
|
||||
final TextRange contentRange = stringInfo.getContentRange();
|
||||
int offset = contentRange.getStartOffset();
|
||||
for (PyNewStyleStringFormatParser.Field field : parseResult.getFields()) {
|
||||
// Preceding literal text
|
||||
newStringText.append(stringText.substring(offset, field.getLeftBraceOffset()));
|
||||
offset = field.getFieldEnd();
|
||||
|
||||
if (!processField(field, pyString, newStringText, true)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (offset < contentRange.getEndOffset()) {
|
||||
newStringText.append(stringText.substring(offset, contentRange.getEndOffset()));
|
||||
}
|
||||
|
||||
newStringText.append(stringInfo.getQuote());
|
||||
|
||||
final PyCallExpression expressionToReplace = PsiTreeUtil.getParentOfType(pyString, PyCallExpression.class);
|
||||
assert expressionToReplace != null;
|
||||
|
||||
final PyElementGenerator generator = PyElementGenerator.getInstance(pyString.getProject());
|
||||
final PyExpression fString = generator.createExpressionFromText(LanguageLevel.PYTHON36, newStringText.toString());
|
||||
expressionToReplace.replace(fString);
|
||||
}
|
||||
|
||||
private static boolean processField(@NotNull PyNewStyleStringFormatParser.Field field,
|
||||
@NotNull PyStringLiteralExpression pyString,
|
||||
@NotNull StringBuilder newStringText,
|
||||
boolean withNestedFields) {
|
||||
|
||||
String stringText = pyString.getText();
|
||||
StringNodeInfo stringInfo = new StringNodeInfo(pyString.getStringNodes().get(0));
|
||||
|
||||
// Actual format field
|
||||
newStringText.append("{");
|
||||
// Isn't supposed to be used by PySubstitutionChunkReference if explicit name or index is given
|
||||
final int autoNumber = field.getAutoPosition() == null ? 0 : field.getAutoPosition();
|
||||
final PySubstitutionChunkReference reference = new PySubstitutionChunkReference(pyString, field, autoNumber, false);
|
||||
final PyExpression resolveResult = getActualReplacementExpression(reference);
|
||||
if (resolveResult == null) return false;
|
||||
|
||||
final PsiElement adjusted = adjustQuotesInside(resolveResult, pyString);
|
||||
if (adjusted == null) return false;
|
||||
|
||||
newStringText.append(adjusted.getText());
|
||||
final String quotedAttrsAndItems = quoteItemsInFragments(field, stringInfo);
|
||||
if (quotedAttrsAndItems == null) return false;
|
||||
|
||||
newStringText.append(quotedAttrsAndItems);
|
||||
|
||||
// Conversion is copied as is if it's present
|
||||
final String conversion = field.getConversion();
|
||||
if (conversion != null) {
|
||||
newStringText.append(conversion);
|
||||
}
|
||||
|
||||
// Format spec is copied if present handling nested fields
|
||||
final TextRange specRange = field.getFormatSpecRange();
|
||||
if (specRange != null) {
|
||||
if (withNestedFields) {
|
||||
int specOffset = specRange.getStartOffset();
|
||||
for (PyNewStyleStringFormatParser.Field nestedField : field.getNestedFields()) {
|
||||
// Copy text of the format spec between nested fragments
|
||||
newStringText.append(stringText.substring(specOffset, nestedField.getLeftBraceOffset()));
|
||||
specOffset = nestedField.getFieldEnd();
|
||||
|
||||
// recursively format nested field
|
||||
if (!processField(nestedField, pyString, newStringText, false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (specOffset < specRange.getEndOffset()) {
|
||||
newStringText.append(stringText.substring(specOffset, specRange.getEndOffset()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Fields nested deeper that twice append as is
|
||||
newStringText.append(field.getFormatSpec());
|
||||
}
|
||||
}
|
||||
|
||||
newStringText.append("}");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String quoteItemsInFragments(@NotNull PyNewStyleStringFormatParser.Field field, @NotNull StringNodeInfo hostStringInfo) {
|
||||
List<String> escaped = new ArrayList<>();
|
||||
for (String part : field.getAttributesAndLookups()) {
|
||||
if (part.startsWith(".")) {
|
||||
escaped.add(part);
|
||||
}
|
||||
else if (part.startsWith("[")) {
|
||||
if (part.contains("\\")) {
|
||||
return null;
|
||||
}
|
||||
final String indexText = part.substring(1, part.length() - 1);
|
||||
if (indexText.matches("\\d+")) {
|
||||
escaped.add(part);
|
||||
continue;
|
||||
}
|
||||
final char originalQuote = hostStringInfo.getSingleQuote();
|
||||
char targetQuote = flipQuote(originalQuote);
|
||||
// there are no escapes inside the fragment, so the lookup key cannot contain
|
||||
// the host string quote unless it's a multiline string literal
|
||||
if (indexText.indexOf(targetQuote) >= 0) {
|
||||
if (!hostStringInfo.isTripleQuoted() || indexText.indexOf(originalQuote) >= 0) {
|
||||
return null;
|
||||
}
|
||||
targetQuote = originalQuote;
|
||||
}
|
||||
escaped.add("[" + targetQuote + indexText + targetQuote + "]");
|
||||
}
|
||||
}
|
||||
return StringUtil.join(escaped, "");
|
||||
}
|
||||
|
||||
private static char flipQuote(char quote) {
|
||||
return quote == '"' ? '\'' : '"';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright 2000-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.jetbrains.python.codeInsight.intentions.convertToFString;
|
||||
|
||||
import com.intellij.lang.ASTNode;
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.jetbrains.python.codeInsight.PySubstitutionChunkReference;
|
||||
import com.jetbrains.python.inspections.PyStringFormatParser;
|
||||
import com.jetbrains.python.psi.*;
|
||||
import com.jetbrains.python.psi.PyUtil.StringNodeInfo;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import static com.jetbrains.python.psi.PyUtil.as;
|
||||
|
||||
/**
|
||||
* @author Mikhail Golubev
|
||||
*/
|
||||
public abstract class BaseConvertToFStringProcessor<T extends PyStringFormatParser.SubstitutionChunk> {
|
||||
protected final PyStringLiteralExpression myPyString;
|
||||
protected final StringNodeInfo myNodeInfo;
|
||||
|
||||
protected BaseConvertToFStringProcessor(@NotNull PyStringLiteralExpression pyString) {
|
||||
myPyString = pyString;
|
||||
myNodeInfo = new StringNodeInfo(pyString.getStringNodes().get(0));
|
||||
}
|
||||
|
||||
public final boolean isRefactoringAvailable() {
|
||||
// TODO support glued/concatenated string literal with multiple nodes
|
||||
if (myPyString.getStringNodes().size() > 1 || myNodeInfo.isBytes() || myNodeInfo.isFormatted()) return false;
|
||||
|
||||
final PsiElement valuesSource = getValuesSource();
|
||||
if (valuesSource == null) return false;
|
||||
final List<T> chunks = extractAllSubstitutionChunks();
|
||||
for (int i = 0; i < chunks.size(); i++) {
|
||||
final T chunk = chunks.get(i);
|
||||
if (!checkChunk(chunk)) return false;
|
||||
final PySubstitutionChunkReference reference = createReference(chunk, i);
|
||||
final PsiElement referencedExpr = adjustResolveResult(reference.resolve());
|
||||
if (referencedExpr == null) return false;
|
||||
if (!PsiTreeUtil.isAncestor(valuesSource, referencedExpr, false)) return false;
|
||||
if (referencedExpr instanceof PyStarExpression || referencedExpr instanceof PyStarArgument) return false;
|
||||
if (!checkReferencedExpression(chunks, i, valuesSource, referencedExpr)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean checkReferencedExpression(@NotNull List<T> chunks,
|
||||
int position,
|
||||
@NotNull PsiElement valueSource,
|
||||
@NotNull PsiElement expression) {
|
||||
if (expression.textContains('\\') || expression.textContains('\n')) return false;
|
||||
return adjustQuotesInsideInjectedExpression(expression) != null;
|
||||
}
|
||||
|
||||
public final void doRefactoring() {
|
||||
final String stringText = myPyString.getText();
|
||||
final StringBuilder fStringText = new StringBuilder();
|
||||
fStringText.append("f");
|
||||
fStringText.append(StringUtil.replaceIgnoreCase(myNodeInfo.getPrefix(), "u", ""));
|
||||
fStringText.append(myNodeInfo.getQuote());
|
||||
|
||||
final TextRange contentRange = myNodeInfo.getContentRange();
|
||||
int offset = contentRange.getStartOffset();
|
||||
|
||||
final List<T> chunks = extractTopLevelSubstitutionChunks();
|
||||
for (int i = 0; i < chunks.size(); i++) {
|
||||
final T chunk = chunks.get(i);
|
||||
|
||||
// Preceding literal text
|
||||
fStringText.append(stringText, offset, chunk.getStartIndex());
|
||||
offset = chunk.getEndIndex();
|
||||
|
||||
if (!convertSubstitutionChunk(chunk, i, fStringText)) return;
|
||||
}
|
||||
|
||||
if (offset < contentRange.getEndOffset()) {
|
||||
fStringText.append(stringText, offset, contentRange.getEndOffset());
|
||||
}
|
||||
|
||||
fStringText.append(myNodeInfo.getQuote());
|
||||
|
||||
final PyExpression expressionToReplace = getWholeExpressionToReplace();
|
||||
|
||||
final PyElementGenerator generator = PyElementGenerator.getInstance(myPyString.getProject());
|
||||
final PyExpression fString = generator.createExpressionFromText(LanguageLevel.forElement(myPyString), fStringText.toString());
|
||||
expressionToReplace.replace(fString);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected abstract List<T> extractAllSubstitutionChunks();
|
||||
|
||||
@NotNull
|
||||
protected List<T> extractTopLevelSubstitutionChunks() {
|
||||
return extractAllSubstitutionChunks();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected abstract PySubstitutionChunkReference createReference(@NotNull T chunk, int position);
|
||||
|
||||
protected abstract boolean checkChunk(@NotNull T chunk);
|
||||
|
||||
protected abstract boolean convertSubstitutionChunk(@NotNull T chunk, int position, @NotNull StringBuilder fStringText);
|
||||
|
||||
@Nullable
|
||||
protected PsiElement adjustQuotesInsideInjectedExpression(@NotNull PsiElement expression) {
|
||||
final PsiElement copied = expression.copy();
|
||||
|
||||
final char hostQuote = myNodeInfo.getSingleQuote();
|
||||
final PyElementGenerator generator = PyElementGenerator.getInstance(myPyString.getProject());
|
||||
|
||||
final Collection<PyStringLiteralExpression> innerStrings = PsiTreeUtil.collectElementsOfType(copied, PyStringLiteralExpression.class);
|
||||
for (PyStringLiteralExpression literal : innerStrings) {
|
||||
final List<ASTNode> nodes = literal.getStringNodes();
|
||||
// TODO figure out what to do with those
|
||||
if (nodes.size() > 1) {
|
||||
return copied;
|
||||
}
|
||||
final StringNodeInfo info = new StringNodeInfo(nodes.get(0));
|
||||
// Nest string contain the same type of quote as host string inside, and we cannot escape inside f-string -- retreat
|
||||
final String content = info.getContent();
|
||||
if (content.indexOf(hostQuote) >= 0) {
|
||||
return null;
|
||||
}
|
||||
if (!info.isTerminated()) {
|
||||
return null;
|
||||
}
|
||||
if (info.getQuote().startsWith(myNodeInfo.getQuote())) {
|
||||
final char targetSingleQuote = PyStringLiteralUtil.flipQuote(hostQuote);
|
||||
if (content.indexOf(targetSingleQuote) >= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String targetQuote = info.getQuote().replace(hostQuote, targetSingleQuote);
|
||||
final String stringWithSwappedQuotes = info.getPrefix() + targetQuote + content + targetQuote;
|
||||
final PsiElement replaced = literal.replace(generator.createStringLiteralAlreadyEscaped(stringWithSwappedQuotes));
|
||||
if (literal == copied) {
|
||||
return replaced;
|
||||
}
|
||||
}
|
||||
}
|
||||
return copied;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected abstract PyExpression getWholeExpressionToReplace();
|
||||
|
||||
@Nullable
|
||||
protected abstract PsiElement getValuesSource();
|
||||
|
||||
@Nullable
|
||||
protected static PyExpression adjustResolveResult(@Nullable PsiElement resolveResult) {
|
||||
if (resolveResult == null) return null;
|
||||
final PyKeywordArgument argument = as(resolveResult, PyKeywordArgument.class);
|
||||
if (argument != null) {
|
||||
return argument.getValueExpression();
|
||||
}
|
||||
final PyKeyValueExpression parent = as(resolveResult.getParent(), PyKeyValueExpression.class);
|
||||
if (parent != null && parent.getKey() == resolveResult) {
|
||||
return parent.getValue();
|
||||
}
|
||||
return as(resolveResult, PyExpression.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2000-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.jetbrains.python.codeInsight.intentions.convertToFString;
|
||||
|
||||
import com.intellij.openapi.util.TextRange;
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.intellij.util.ObjectUtils;
|
||||
import com.jetbrains.python.codeInsight.PySubstitutionChunkReference;
|
||||
import com.jetbrains.python.inspections.PyNewStyleStringFormatParser;
|
||||
import com.jetbrains.python.inspections.PyNewStyleStringFormatParser.Field;
|
||||
import com.jetbrains.python.psi.PyCallExpression;
|
||||
import com.jetbrains.python.psi.PyExpression;
|
||||
import com.jetbrains.python.psi.PyStringLiteralExpression;
|
||||
import com.jetbrains.python.psi.PyStringLiteralUtil;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Mikhail Golubev
|
||||
*/
|
||||
public class NewStyleConvertToFStringProcessor extends BaseConvertToFStringProcessor<Field> {
|
||||
public NewStyleConvertToFStringProcessor(@NotNull PyStringLiteralExpression pyString) {
|
||||
super(pyString);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected List<Field> extractAllSubstitutionChunks() {
|
||||
return PyNewStyleStringFormatParser.parse(myPyString.getText()).getAllFields();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected List<Field> extractTopLevelSubstitutionChunks() {
|
||||
return PyNewStyleStringFormatParser.parse(myPyString.getText()).getFields();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected PySubstitutionChunkReference createReference(@NotNull Field field, int position /* unused */) {
|
||||
return new PySubstitutionChunkReference(myPyString, field, ObjectUtils.chooseNotNull(field.getAutoPosition(), 0), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkChunk(@NotNull Field chunk) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public PyExpression getWholeExpressionToReplace() {
|
||||
//noinspection ConstantConditions
|
||||
return PsiTreeUtil.getParentOfType(myPyString, PyCallExpression.class);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected PsiElement getValuesSource() {
|
||||
final PyCallExpression callExpression = PsiTreeUtil.getParentOfType(myPyString, PyCallExpression.class);
|
||||
assert callExpression != null;
|
||||
return callExpression.getArgumentList();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean convertSubstitutionChunk(@NotNull Field field, int position, @NotNull StringBuilder fStringText) {
|
||||
|
||||
final String stringText = myPyString.getText();
|
||||
|
||||
// Actual format field
|
||||
fStringText.append("{");
|
||||
// Isn't supposed to be used by PySubstitutionChunkReference if explicit name or index is given
|
||||
final PySubstitutionChunkReference reference = createReference(field, 0);
|
||||
final PyExpression resolveResult = adjustResolveResult(reference.resolve());
|
||||
if (resolveResult == null) return false;
|
||||
|
||||
final PsiElement adjusted = adjustQuotesInsideInjectedExpression(resolveResult);
|
||||
if (adjusted == null) return false;
|
||||
|
||||
fStringText.append(adjusted.getText());
|
||||
final String quotedAttrsAndItems = quoteItemsInFragments(field);
|
||||
if (quotedAttrsAndItems == null) return false;
|
||||
|
||||
fStringText.append(quotedAttrsAndItems);
|
||||
|
||||
// Conversion is copied as is if it's present
|
||||
final String conversion = field.getConversion();
|
||||
if (conversion != null) {
|
||||
fStringText.append(conversion);
|
||||
}
|
||||
|
||||
// Format spec is copied if present handling nested fields
|
||||
final TextRange specRange = field.getFormatSpecRange();
|
||||
if (specRange != null) {
|
||||
int specOffset = specRange.getStartOffset();
|
||||
// Do not proceed too nested fields
|
||||
if (field.getDepth() == 1) {
|
||||
for (Field nestedField : field.getNestedFields()) {
|
||||
// Copy text of the format spec between nested fragments
|
||||
fStringText.append(stringText, specOffset, nestedField.getLeftBraceOffset());
|
||||
specOffset = nestedField.getFieldEnd();
|
||||
|
||||
// recursively format nested field
|
||||
if (!convertSubstitutionChunk(nestedField, 0, fStringText)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (specOffset < specRange.getEndOffset()) {
|
||||
fStringText.append(stringText, specOffset, specRange.getEndOffset());
|
||||
}
|
||||
}
|
||||
|
||||
fStringText.append("}");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String quoteItemsInFragments(@NotNull Field field) {
|
||||
final List<String> escaped = new ArrayList<>();
|
||||
for (String part : field.getAttributesAndLookups()) {
|
||||
if (part.startsWith(".")) {
|
||||
escaped.add(part);
|
||||
}
|
||||
else if (part.startsWith("[")) {
|
||||
if (part.contains("\\")) {
|
||||
return null;
|
||||
}
|
||||
final String indexText = part.substring(1, part.length() - 1);
|
||||
if (indexText.matches("\\d+")) {
|
||||
escaped.add(part);
|
||||
continue;
|
||||
}
|
||||
final char originalQuote = myNodeInfo.getSingleQuote();
|
||||
char targetQuote = PyStringLiteralUtil.flipQuote(originalQuote);
|
||||
// there are no escapes inside the fragment, so the lookup key cannot contain
|
||||
// the host string quote unless it's a multiline string literal
|
||||
if (indexText.indexOf(targetQuote) >= 0) {
|
||||
if (!myNodeInfo.isTripleQuoted() || indexText.indexOf(originalQuote) >= 0) {
|
||||
return null;
|
||||
}
|
||||
targetQuote = originalQuote;
|
||||
}
|
||||
escaped.add("[" + targetQuote + indexText + targetQuote + "]");
|
||||
}
|
||||
}
|
||||
return StringUtil.join(escaped, "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2000-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.jetbrains.python.codeInsight.intentions.convertToFString;
|
||||
|
||||
import com.intellij.openapi.util.text.StringUtil;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.jetbrains.python.codeInsight.PySubstitutionChunkReference;
|
||||
import com.jetbrains.python.inspections.PyStringFormatParser;
|
||||
import com.jetbrains.python.inspections.PyStringFormatParser.SubstitutionChunk;
|
||||
import com.jetbrains.python.psi.PyBinaryExpression;
|
||||
import com.jetbrains.python.psi.PyExpression;
|
||||
import com.jetbrains.python.psi.PyStringLiteralExpression;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.jetbrains.python.codeInsight.intentions.ConvertFormatOperatorToMethodIntention.convertFormatSpec;
|
||||
import static com.jetbrains.python.psi.PyUtil.as;
|
||||
|
||||
/**
|
||||
* @author Mikhail Golubev
|
||||
*/
|
||||
public class OldStyleConvertToFStringProcessor extends BaseConvertToFStringProcessor<SubstitutionChunk> {
|
||||
public OldStyleConvertToFStringProcessor(@NotNull PyStringLiteralExpression pyString) {
|
||||
super(pyString);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected List<SubstitutionChunk> extractAllSubstitutionChunks() {
|
||||
return PyStringFormatParser.filterSubstitutions(PyStringFormatParser.parsePercentFormat(myPyString.getText()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
protected PySubstitutionChunkReference createReference(@NotNull SubstitutionChunk chunk, int position) {
|
||||
return new PySubstitutionChunkReference(myPyString, chunk, position, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkChunk(@NotNull SubstitutionChunk chunk) {
|
||||
// TODO handle dynamic width and precision in old-style/"percent" formatting
|
||||
return !("*".equals(chunk.getPrecision()) || "*".equals(chunk.getWidth()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkReferencedExpression(@NotNull List<SubstitutionChunk> chunks,
|
||||
int position,
|
||||
@NotNull PsiElement valueSource,
|
||||
@NotNull PsiElement expression) {
|
||||
final SubstitutionChunk chunk = chunks.get(position);
|
||||
if ((chunk.getMappingKey() != null || chunks.size() > 1) && expression == valueSource) {
|
||||
return false;
|
||||
}
|
||||
return super.checkReferencedExpression(chunks, position, valueSource, expression);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public PyExpression getWholeExpressionToReplace() {
|
||||
//noinspection ConstantConditions
|
||||
return PsiTreeUtil.getParentOfType(myPyString, PyBinaryExpression.class);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected PsiElement getValuesSource() {
|
||||
final PyBinaryExpression binaryExpression = as(myPyString.getParent(), PyBinaryExpression.class);
|
||||
assert binaryExpression != null;
|
||||
return binaryExpression.getRightExpression();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean convertSubstitutionChunk(@NotNull SubstitutionChunk subsChunk, int position, @NotNull StringBuilder fStringText) {
|
||||
final char conversionChar = subsChunk.getConversionType();
|
||||
|
||||
String widthAndPrecision = StringUtil.notNullize(subsChunk.getWidth());
|
||||
if (StringUtil.isNotEmpty(subsChunk.getPrecision())) {
|
||||
widthAndPrecision += "." + subsChunk.getPrecision();
|
||||
}
|
||||
|
||||
final String conversionFlags = subsChunk.getConversionFlags();
|
||||
|
||||
fStringText.append("{");
|
||||
final PySubstitutionChunkReference reference = createReference(subsChunk, position);
|
||||
final PyExpression resolveResult = adjustResolveResult(reference.resolve());
|
||||
assert resolveResult != null;
|
||||
|
||||
final PsiElement adjusted = adjustQuotesInsideInjectedExpression(resolveResult);
|
||||
if (adjusted == null) return false;
|
||||
|
||||
fStringText.append(adjusted.getText());
|
||||
|
||||
// TODO mostly duplicates the logic of ConvertFormatOperatorToMethodIntention
|
||||
if (conversionChar == 'r') {
|
||||
fStringText.append("!r");
|
||||
}
|
||||
|
||||
if ((conversionChar != 'r' && conversionChar != 's')
|
||||
|| StringUtil.isNotEmpty(conversionFlags)
|
||||
|| StringUtil.isNotEmpty(widthAndPrecision)) {
|
||||
fStringText.append(":");
|
||||
}
|
||||
|
||||
fStringText.append(convertFormatSpec(StringUtil.notNullize(conversionFlags), widthAndPrecision, String.valueOf(conversionChar)));
|
||||
|
||||
if (StringUtil.isNotEmpty(widthAndPrecision)) {
|
||||
fStringText.append(widthAndPrecision);
|
||||
}
|
||||
|
||||
if ('i' == conversionChar || 'u' == conversionChar) {
|
||||
fStringText.append("d");
|
||||
}
|
||||
else if ('s' != conversionChar && 'r' != conversionChar) {
|
||||
fStringText.append(conversionChar);
|
||||
}
|
||||
fStringText.append("}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2000-2016 JetBrains s.r.o.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.jetbrains.python.codeInsight.intentions.convertToFString;
|
||||
|
||||
import com.intellij.openapi.editor.Editor;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiFile;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.intellij.util.IncorrectOperationException;
|
||||
import com.jetbrains.python.PyBundle;
|
||||
import com.jetbrains.python.PyNames;
|
||||
import com.jetbrains.python.PyTokenTypes;
|
||||
import com.jetbrains.python.codeInsight.intentions.PyBaseIntentionAction;
|
||||
import com.jetbrains.python.psi.*;
|
||||
import org.jetbrains.annotations.Nls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import static com.jetbrains.python.psi.PyUtil.as;
|
||||
|
||||
/**
|
||||
* @author Mikhail Golubev
|
||||
*/
|
||||
public class PyConvertToFStringIntention extends PyBaseIntentionAction {
|
||||
@Nls
|
||||
@NotNull
|
||||
@Override
|
||||
public String getFamilyName() {
|
||||
return PyBundle.message("INTN.convert.to.fstring.literal");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getText() {
|
||||
return getFamilyName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
|
||||
if (!(file instanceof PyFile) || LanguageLevel.forElement(file).isOlderThan(LanguageLevel.PYTHON36)) return false;
|
||||
|
||||
final BaseConvertToFStringProcessor processor = findSuitableProcessor(editor, file);
|
||||
return processor != null && processor.isRefactoringAvailable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doInvoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
|
||||
final BaseConvertToFStringProcessor processor = findSuitableProcessor(editor, file);
|
||||
assert processor != null;
|
||||
processor.doRefactoring();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static BaseConvertToFStringProcessor findSuitableProcessor(@NotNull Editor editor, @NotNull PsiFile file) {
|
||||
final PsiElement anchor = file.findElementAt(editor.getCaretModel().getOffset());
|
||||
if (anchor == null) return null;
|
||||
|
||||
final PyBinaryExpression binaryExpr = PsiTreeUtil.getParentOfType(anchor, PyBinaryExpression.class);
|
||||
if (binaryExpr != null && binaryExpr.getOperator() == PyTokenTypes.PERC) {
|
||||
final PyStringLiteralExpression pyString = as(binaryExpr.getLeftExpression(), PyStringLiteralExpression.class);
|
||||
if (pyString != null) {
|
||||
return new OldStyleConvertToFStringProcessor(pyString);
|
||||
}
|
||||
}
|
||||
|
||||
final PyCallExpression callExpr = PsiTreeUtil.getParentOfType(anchor, PyCallExpression.class);
|
||||
if (callExpr != null) {
|
||||
final PyReferenceExpression callee = as(callExpr.getCallee(), PyReferenceExpression.class);
|
||||
if (callee != null && PyNames.FORMAT.equals(callee.getName())) {
|
||||
final PyStringLiteralExpression pyString = as(callee.getQualifier(), PyStringLiteralExpression.class);
|
||||
if (pyString != null) {
|
||||
return new NewStyleConvertToFStringProcessor(pyString);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -159,6 +159,13 @@ public class PyStringLiteralUtil {
|
||||
return StringUtil.indexOfIgnoreCase(prefix, 'f', 0) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return alternative quote character, i.e. " for ' and ' for "
|
||||
*/
|
||||
public static char flipQuote(char quote) {
|
||||
return quote == '"' ? '\'' : '"';
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Pair<String, String> getQuotes(@NotNull String text, @NotNull String prefix, @NotNull String quote) {
|
||||
final int length = text.length();
|
||||
|
||||
@@ -1 +1 @@
|
||||
'{0.attr[item]:{foo[item]:5} {bar.attr:{baz}}}'.format(42, foo=func(), bar=MyClass(1, 2), baz=unused)
|
||||
'{0.attr[item]:{foo[item]:5} {bar.attr:{baz} {quux}}}'.format(42, foo=func(), bar=MyClass(1, 2), baz=unused, quux=unused)
|
||||
@@ -1 +1 @@
|
||||
f'{42.attr["item"]:{func()["item"]:5} {MyClass(1, 2).attr:{baz}}}'
|
||||
f'{42.attr["item"]:{func()["item"]:5} {MyClass(1, 2).attr:{baz} {quux}}}'
|
||||
Reference in New Issue
Block a user