PY-19242 Add autocompletion for % format strings

* Extract separate completion provider for formatted string arguments add patterns
* Add tests
This commit is contained in:
Valentina Kiryushkina
2016-04-08 18:52:46 +03:00
parent 3601222476
commit 3a2a8f142c
18 changed files with 336 additions and 93 deletions

View File

@@ -17,38 +17,205 @@ package com.jetbrains.python.codeInsight.completion;
import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.lookup.AutoCompletionPolicy;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.patterns.PatternCondition;
import com.intellij.patterns.PsiElementPattern;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.ProcessingContext;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.inspections.PyStringFormatParser;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static com.intellij.patterns.PlatformPatterns.psiElement;
import static com.intellij.patterns.StandardPatterns.or;
import static java.util.Arrays.asList;
public class PyStringFormatCompletionContributor extends CompletionContributor {
private static final String DICT_NAME = "dict";
private static final PatternCondition<PyReferenceExpression> FORMAT_CALL_PATTERN_CONDITION =
new PatternCondition<PyReferenceExpression>("isFormatFunction") {
@Override
public boolean accepts(@NotNull PyReferenceExpression expression, ProcessingContext context) {
String expressionName = expression.getName();
return expressionName != null && expressionName.equals(PyNames.FORMAT);
}
};
private static final PatternCondition<PyReferenceExpression> DICT_CALL_PATTERN_CONDITION =
new PatternCondition<PyReferenceExpression>("isDictCall") {
@Override
public boolean accepts(@NotNull PyReferenceExpression expression, ProcessingContext context) {
String expressionName = expression.getName();
return expressionName != null && expressionName.equals(DICT_NAME);
}
};
private static final PsiElementPattern.Capture<PyStringLiteralExpression> FORMAT_STRING_CAPTURE =
psiElement(PyStringLiteralExpression.class)
.withParent(psiElement(PyReferenceExpression.class).with(FORMAT_CALL_PATTERN_CONDITION))
.withSuperParent(2, PyCallExpression.class);
private static final PsiElementPattern.Capture<PyStringLiteralExpression> PERCENT_STRING_CAPTURE =
psiElement(PyStringLiteralExpression.class).beforeLeaf(psiElement().withText("%")).withParent(PyBinaryExpression.class);
@Nullable private static final PatternCondition<PyBinaryExpression> PERCENT_BINARY_EXPRESSION_PATTERN =
new PatternCondition<PyBinaryExpression>("isBinaryFormatExpression") {
@Override
public boolean accepts(@NotNull PyBinaryExpression expression, ProcessingContext context) {
return expression.isOperator("%");
}
};
private static final PsiElementPattern.Capture<PyKeywordArgument> DICT_FUNCTION_KEYWORD_ARGUMENT_CAPTURE =
(psiElement(PyKeywordArgument.class))
.withSuperParent(3,
psiElement(PyBinaryExpression.class)
.withChild(psiElement(PyCallExpression.class)
.withChild(psiElement(PyReferenceExpression.class)
.with(DICT_CALL_PATTERN_CONDITION)))
.with(PERCENT_BINARY_EXPRESSION_PATTERN));
private static final PsiElementPattern.Capture<PyReferenceExpression> DICT_FUNCTION_REFERENCE_ARGUMENT_CAPTURE =
(psiElement(PyReferenceExpression.class))
.withSuperParent(3,
psiElement(PyBinaryExpression.class)
.withChild(psiElement(PyCallExpression.class)
.withChild(psiElement(PyReferenceExpression.class)
.with(DICT_CALL_PATTERN_CONDITION)))
.with(PERCENT_BINARY_EXPRESSION_PATTERN));
private static final PsiElementPattern.Capture<PyKeywordArgument> FORMAT_FUNCTION_ARGUMENT_CAPTURE =
psiElement(PyKeywordArgument.class)
.withSuperParent(2, psiElement(PyCallExpression.class)
.withChild(psiElement(PyReferenceExpression.class).with(FORMAT_CALL_PATTERN_CONDITION)));
// to provide completion for: "{foo}".format(fo<caret>)
private static final PsiElementPattern.Capture<PyReferenceExpression> FORMAT_FUNCTION_REFERENCE_ARGUMENT_CAPTURE =
psiElement(PyReferenceExpression.class)
.withSuperParent(2, psiElement(PyCallExpression.class)
.withChild(psiElement(PyReferenceExpression.class).with(FORMAT_CALL_PATTERN_CONDITION)));
private static final PsiElementPattern.Capture<PyStringLiteralExpression> DICT_LITERAL_STRING_KEY_CAPTURE =
psiElement(PyStringLiteralExpression.class)
.withParent(or(psiElement(PyKeyValueExpression.class)
.withParent(psiElement(PyDictLiteralExpression.class)
.withParent(psiElement(PyBinaryExpression.class).with(PERCENT_BINARY_EXPRESSION_PATTERN))),
psiElement(PyDictLiteralExpression.class)
.withParent(psiElement(PyBinaryExpression.class).with(PERCENT_BINARY_EXPRESSION_PATTERN))));
// to provide completion for: "%(foo)s % {"f<caret>"}
private static final PsiElementPattern.Capture<PyStringLiteralExpression> SET_LITERAL_STRING_KEY_CAPTURE =
psiElement(PyStringLiteralExpression.class)
.withParent(psiElement(PySetLiteralExpression.class).withParent(psiElement(PyBinaryExpression.class)
.with(PERCENT_BINARY_EXPRESSION_PATTERN)));
public PyStringFormatCompletionContributor() {
extend(
CompletionType.BASIC,
or(psiElement().inside(PyArgumentList
.class), psiElement().inside(PyStringLiteralExpression.class)),
new FormattedStringCompletionProvider()
or(
psiElement().inside(PERCENT_STRING_CAPTURE),
psiElement().inside(FORMAT_STRING_CAPTURE)),
new StringFormatCompletionProvider()
);
extend(
CompletionType.BASIC,
or(psiElement().inside(DICT_LITERAL_STRING_KEY_CAPTURE),
psiElement().inside(DICT_FUNCTION_KEYWORD_ARGUMENT_CAPTURE),
psiElement().inside(DICT_FUNCTION_REFERENCE_ARGUMENT_CAPTURE),
psiElement().inside(SET_LITERAL_STRING_KEY_CAPTURE),
psiElement().inside(FORMAT_FUNCTION_ARGUMENT_CAPTURE),
psiElement().inside(FORMAT_FUNCTION_REFERENCE_ARGUMENT_CAPTURE)
),
new StringFormatArgumentsCompletionProvider()
);
}
private static class StringFormatArgumentsCompletionProvider extends CompletionProvider<CompletionParameters> {
private static class FormattedStringCompletionProvider extends CompletionProvider<CompletionParameters> {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
ProcessingContext context,
@NotNull CompletionResultSet result) {
final PsiElement original = parameters.getOriginalPosition();
if (original != null) {
result = result.withPrefixMatcher(getPrefix(parameters.getOffset(), parameters.getOriginalFile()));
final PsiElement parent = original.getParent();
if (parent.getParent() instanceof PyKeyValueExpression || parent instanceof PyStringLiteralExpression) {
final PyBinaryExpression binExpr = PsiTreeUtil.getParentOfType(parent, PyBinaryExpression.class);
if (binExpr != null) {
final PyStringLiteralExpression strExpr = PyUtil.as(binExpr.getLeftExpression(), PyStringLiteralExpression.class);
if (strExpr != null) {
result.addAllElements(getPercentLookupBuilders(strExpr));
}
}
}
else if (PyUtil.instanceOf(parent, PyKeywordArgument.class, PyReferenceExpression.class)) {
result.addAllElements(getElementsFromString(PsiTreeUtil.getParentOfType(original, PyArgumentList.class)));
}
}
}
@NotNull
private static List<LookupElement> getElementsFromString(@Nullable final PyArgumentList argumentList) {
if (argumentList != null) {
final PyReferenceExpression refExpr = PsiTreeUtil.getPrevSiblingOfType(argumentList, PyReferenceExpression.class);
final PyStringLiteralExpression strExpr = PsiTreeUtil.getChildOfType(refExpr, PyStringLiteralExpression.class);
if (strExpr != null) {
return getFormatLookupBuilders(strExpr);
}
else {
final PyBinaryExpression binExpr = PsiTreeUtil.getParentOfType(refExpr, PyBinaryExpression.class);
if (binExpr != null) {
final PyStringLiteralExpression stringLiteralExpr = PyUtil.as(binExpr.getLeftExpression(), PyStringLiteralExpression.class);
if (stringLiteralExpr != null) {
return getPercentLookupBuilders(stringLiteralExpr);
}
}
}
}
return Collections.emptyList();
}
@NotNull
private static List<LookupElement> getFormatLookupBuilders(@NotNull final PyStringLiteralExpression expression) {
final Map<String, PyStringFormatParser.SubstitutionChunk> chunks = PyStringFormatParser.getKeywordSubstitutions(
PyStringFormatParser.filterSubstitutions(PyStringFormatParser.parseNewStyleFormat(expression.getStringValue())));
return getLookupBuilders(chunks);
}
private static List<LookupElement> getPercentLookupBuilders(@NotNull final PyStringLiteralExpression expression) {
final Map<String, PyStringFormatParser.SubstitutionChunk> chunks = PyStringFormatParser.getKeywordSubstitutions(
PyStringFormatParser.filterSubstitutions(PyStringFormatParser.parsePercentFormat(expression.getStringValue())));
return getLookupBuilders(chunks);
}
@NotNull
private static List<LookupElement> getLookupBuilders(@NotNull final Map<String, PyStringFormatParser.SubstitutionChunk> chunks) {
return chunks.keySet().stream()
.map(PyStringFormatCompletionContributor::createLookUpElement)
.collect(Collectors.toList());
}
}
private static class StringFormatCompletionProvider extends CompletionProvider<CompletionParameters> {
@Override
protected void addCompletions(@NotNull final CompletionParameters parameters,
final ProcessingContext context,
@@ -56,63 +223,111 @@ public class PyStringFormatCompletionContributor extends CompletionContributor {
final PsiElement original = parameters.getOriginalPosition();
if (original != null) {
final PsiElement parent = original.getParent();
result = result.withPrefixMatcher(getPrefix(parameters.getOffset(), parent.getContainingFile()));
if (parent instanceof PyStringLiteralExpression) {
final int stringOffset = parameters.getOffset() - parameters.getPosition().getTextRange().getStartOffset();
if (isInsideSubstitutionChunk((PyStringLiteralExpression)parent,
stringOffset)) {
final PyExpression[] arguments = getFormatFunctionKeyWordArguments(original);
for (PyExpression argument : arguments) {
result = result.withPrefixMatcher(getPrefix(parameters.getOffset(), argument.getContainingFile()));
addKeysFromStarArgument(result, argument);
addKeyWordArgument(result, argument);
}
}
}
else if (PyUtil.instanceOf(parent, PyKeywordArgument.class, PyReferenceExpression.class)) {
final PyArgumentList argumentList = PsiTreeUtil.getParentOfType(original, PyArgumentList.class);
result = result.withPrefixMatcher(getPrefix(parameters.getOffset(), parent.getContainingFile()));
addElementsFromFormattedString(result, argumentList);
result.addAllElements(addCompletionsForSubstitutions(parameters, original, (PyStringLiteralExpression)parent));
}
}
}
private static boolean isInsideSubstitutionChunk(@NotNull final PyStringLiteralExpression expression, final int offset) {
final List<PyStringFormatParser.SubstitutionChunk> substitutions = PyStringFormatParser.filterSubstitutions(
PyStringFormatParser.parseNewStyleFormat(expression.getStringValue()));
for (PyStringFormatParser.SubstitutionChunk substitution: substitutions) {
if (offset >= substitution.getStartIndex() && offset <= substitution.getEndIndex()) {
return true;
}
}
return false;
}
@NotNull
private static PyExpression[] getFormatFunctionKeyWordArguments(final PsiElement original) {
final PsiElement pyReferenceExpression = PsiTreeUtil.getParentOfType(original, PyReferenceExpression.class);
final PyArgumentList argumentList = PsiTreeUtil.getNextSiblingOfType(pyReferenceExpression, PyArgumentList.class);
if (argumentList != null) {
return argumentList.getArguments();
}
return PyExpression.EMPTY_ARRAY;
}
private static List<LookupElement> addCompletionsForSubstitutions(@NotNull final CompletionParameters parameters,
@NotNull final PsiElement original,
@NotNull final PyStringLiteralExpression stringExpression) {
final int stringOffset = getCaretStartOffsetInsideString(parameters, stringExpression);
private static void addKeysFromStarArgument(@NotNull final CompletionResultSet result, @NotNull final PyExpression arg) {
if (arg instanceof PyStarArgument) {
final PyDictLiteralExpression dict = ObjectUtils.chooseNotNull(PsiTreeUtil.getChildOfType(arg, PyDictLiteralExpression.class),
getDictFromReference(arg));
if (dict != null) {
for (PyKeyValueExpression keyValue: dict.getElements()) {
if (keyValue.getKey() instanceof PyStringLiteralExpression) {
final String key = ((PyStringLiteralExpression) keyValue.getKey()).getStringValue();
result.addElement(createLookUpElement(key));
if (isInsideFormatSubstitutionChunk(stringExpression, stringOffset)) {
final PyExpression[] arguments = getFormatFunctionKeyWordArguments(original);
final ArrayList<LookupElement> elements = new ArrayList<>();
for (PyExpression argument : arguments) {
if (argument instanceof PyKeywordArgument) {
elements.add(getKeywordArgument((PyKeywordArgument)argument));
}
else if (argument instanceof PyStarArgument) {
elements.addAll(getKeysFromStarArgument((PyStarArgument)argument));
}
}
return elements;
}
if (isInsidePercentSubstitutionChunk(stringExpression, stringOffset)) {
final PyBinaryExpression binExpr = PyUtil.as(PsiTreeUtil.getParentOfType(stringExpression, PyBinaryExpression.class),
PyBinaryExpression.class);
if (binExpr != null) {
final PyExpression rightExpr = PyPsiUtils.flattenParens(binExpr.getRightExpression());
final PyDictLiteralExpression dict = PyUtil.as(rightExpr, PyDictLiteralExpression.class);
if (dict != null) {
return getElementsFromDict(dict);
}
final PyCallExpression callExpression = PyUtil.as(rightExpr, PyCallExpression.class);
if (callExpression != null) {
final PyExpression callee = callExpression.getCallee();
if (callee != null && callee.getName() != null && callee.getName().equals(DICT_NAME)) {
final PyExpression[] arguments = callExpression.getArguments();
return asList(arguments).stream()
.filter(a -> a instanceof PyKeywordArgument)
.map(a -> getKeywordArgument((PyKeywordArgument)a))
.filter(e -> e != null)
.collect(Collectors.toList());
}
}
}
}
return Collections.emptyList();
}
private static int getCaretStartOffsetInsideString(@NotNull final CompletionParameters parameters,
@NotNull final PyStringLiteralExpression parent) {
final int caretAbsoluteOffset = parameters.getOffset();
final int stringExprStartOffset = parameters.getPosition().getTextRange().getStartOffset();
final int stringValueStartOffset = parent.getStringValueTextRange().getStartOffset();
return caretAbsoluteOffset - stringExprStartOffset - stringValueStartOffset;
}
private static boolean isInsideFormatSubstitutionChunk(@NotNull final PyStringLiteralExpression expression, final int offset) {
List<PyStringFormatParser.SubstitutionChunk> substitutions = PyStringFormatParser.filterSubstitutions(
PyStringFormatParser.parseNewStyleFormat(expression.getStringValue()));
return isInsideSubstitutionChunk(offset, substitutions);
}
private static boolean isInsidePercentSubstitutionChunk(@NotNull final PyStringLiteralExpression expression, final int offset) {
List<PyStringFormatParser.SubstitutionChunk> substitutions = PyStringFormatParser.filterSubstitutions(
PyStringFormatParser.parsePercentFormat(expression.getStringValue()));
return isInsideSubstitutionChunk(offset, substitutions);
}
private static boolean isInsideSubstitutionChunk(int offset, @NotNull List<PyStringFormatParser.SubstitutionChunk> substitutions) {
return substitutions.stream().anyMatch(s -> offset >= s.getStartIndex() && offset <= s.getEndIndex());
}
@NotNull
private static PyExpression[] getFormatFunctionKeyWordArguments(@NotNull final PsiElement original) {
final PsiElement pyReferenceExpression = PsiTreeUtil.getParentOfType(original, PyReferenceExpression.class);
final PyArgumentList argumentList = PsiTreeUtil.getNextSiblingOfType(pyReferenceExpression, PyArgumentList.class);
return argumentList != null ? argumentList.getArguments() : PyExpression.EMPTY_ARRAY;
}
@NotNull
private static List<LookupElement> getKeysFromStarArgument(@NotNull final PyStarArgument arg) {
final PyDictLiteralExpression dict = ObjectUtils.chooseNotNull(PsiTreeUtil.getChildOfType(arg, PyDictLiteralExpression.class),
getDictFromReference(arg));
return dict != null ? getElementsFromDict(dict) : Collections.emptyList();
}
@NotNull
private static List<LookupElement> getElementsFromDict(@NotNull final PyDictLiteralExpression dict) {
return asList(dict.getElements()).stream()
.map(e -> PyUtil.as(e.getKey(), PyStringLiteralExpression.class))
.filter(k-> k != null)
.map(k -> createLookUpElement(k.getStringValue()))
.collect(Collectors.toList());
}
@Nullable
private static PyDictLiteralExpression getDictFromReference(@NotNull final PyExpression arg) {
final PyReferenceExpression referenceExpression = PsiTreeUtil.getChildOfType(arg, PyReferenceExpression.class);
if (referenceExpression != null) {
@@ -125,58 +340,31 @@ public class PyStringFormatCompletionContributor extends CompletionContributor {
return null;
}
private static void addKeyWordArgument(@NotNull final CompletionResultSet result, @NotNull final PyExpression arg) {
if (arg instanceof PyKeywordArgument) {
final String keyword = ((PyKeywordArgument)arg).getKeyword();
if (keyword!= null) {
result.addElement(createLookUpElement(keyword));
}
}
@Nullable
private static LookupElement getKeywordArgument(@NotNull final PyKeywordArgument arg) {
final String keyword = arg.getKeyword();
return keyword != null ? createLookUpElement(keyword) : null;
}
@NotNull
private static LookupElement createLookUpElement(@NotNull final String element) {
return LookupElementBuilder
.create(element)
.withTypeText("arg")
.withAutoCompletionPolicy(AutoCompletionPolicy.ALWAYS_AUTOCOMPLETE);
}
private static void addElementsFromFormattedString(@NotNull final CompletionResultSet result,
@Nullable final PyArgumentList argumentList) {
if (argumentList != null) {
final PyReferenceExpression pyReferenceExpression = PsiTreeUtil.getPrevSiblingOfType(argumentList, PyReferenceExpression.class);
final PyStringLiteralExpression formattedString = PsiTreeUtil.getChildOfType(pyReferenceExpression, PyStringLiteralExpression.class);
if (formattedString != null) {
result.addAllElements(getLookupBuilders(formattedString));
}
}
}
@NotNull
private static List<LookupElement> getLookupBuilders(@NotNull final PyStringLiteralExpression literalExpression) {
final Map<String, PyStringFormatParser.SubstitutionChunk> chunks = PyStringFormatParser.getKeywordSubstitutions(
PyStringFormatParser.filterSubstitutions(PyStringFormatParser.parseNewStyleFormat(literalExpression.getStringValue())));
final List<LookupElement> keys = new ArrayList<>();
for (String chunk: chunks.keySet()) {
keys.add(createLookUpElement(chunk));
}
return keys;
}
}
@NotNull
private static LookupElement createLookUpElement(@NotNull final String element) {
return LookupElementBuilder
.create(element)
.withTypeText("arg");
}
@NotNull
private static String getPrefix(int offset, @NotNull final PsiFile file) {
if (offset > 0) {
offset--;
}
final String text = file.getText();
final StringBuilder prefixBuilder = new StringBuilder();
while(offset > 0 && Character.isLetterOrDigit(text.charAt(offset))) {
while (offset > 0 && Character.isLetterOrDigit(text.charAt(offset))) {
prefixBuilder.insert(0, text.charAt(offset));
offset--;
}
return prefixBuilder.toString();
}
}

View File

@@ -0,0 +1 @@
r"{completed}".format(completed="hood")

View File

@@ -0,0 +1 @@
r"{compl<caret>}".format(completed="hood")

View File

@@ -0,0 +1 @@
"%(completion)s" % dict(completion)

View File

@@ -0,0 +1 @@
"%(completion)s" % dict(complet<caret>)

View File

@@ -0,0 +1 @@
"format: %(fooo)s" % {"boo":1, "fooo"}

View File

@@ -0,0 +1 @@
"format: %(fooo)s" % {"boo":1, "foo<caret>"}

View File

@@ -0,0 +1 @@
"format: %(fooo)s" % {"fooo"}

View File

@@ -0,0 +1 @@
"format: %(fooo)s" % {"fo<caret>"}

View File

@@ -0,0 +1 @@
"to be %(completed)s" % dict(completed="smth")

View File

@@ -0,0 +1 @@
"to be %(comple<caret>)s" % dict(completed="smth")

View File

@@ -0,0 +1 @@
"format: %(completion)s" % {"completion": "smth"}

View File

@@ -0,0 +1 @@
"format: %(complet<caret>)s" % {"completion": "smth"}

View File

@@ -0,0 +1 @@
r"%(completion)s" % dict(completion="smth")

View File

@@ -0,0 +1 @@
r"%(compl<caret>)s" % dict(completion="smth")

View File

@@ -0,0 +1 @@
"to be %(completion)s" % dict(completion="smth")

View File

@@ -0,0 +1 @@
"to be %(complet<caret>)s" % dict(completion="smth")

View File

@@ -864,31 +864,70 @@ public class PythonCompletionTest extends PyTestCase {
doTest();
}
//PY-3077
// PY-3077
public void testFormatString() {
doTest();
}
//PY-3077
// PY-3077
public void testFormatFuncArgument() {
doTest();
}
//PY-3077
// PY-3077
public void testFormatStringFromStarArg() {
doTest();
}
//PY-3077
// PY-3077
public void testFormatStringOutsideBraces() {
doTest();
}
//PY-3077
// PY-3077
public void testFormatStringFromRef() {
doTest();
}
// PY-3077
public void testFormatStringWithFormatModifier() {
doTest();
}
// PY-3077
public void testPercentStringWithDictLiteralArg() {
doTest();
}
// PY-3077
public void testPercentStringWithDictCallArg() {
doTest();
}
// PY-3077
public void testPercentStringWithParenDictCallArg() {
doTest();
}
// PY-3077
public void testPercentStringWithModifiers() {
doTest();
}
// PY-3077
public void testPercentStringDictLiteralStringKey() {
doTest();
}
// PY-3077
public void testPercentStringDictCallStringKey() {
doTest();
}
public void testPercentStringDictLiteralArgument() {
doTest();
}
// PY-17437
public void testStrFormat() {
doTest();