Use smart PSI pointers in Python LookupElements to avoid invalid elements in completion (PY-17801)

During completion while the user is typing the PSI structure of the
current file may change significantly. The main reason is that Python
doesn't have any braces around syntactic blocks, only indents, and some
syntactically incorrect lines of partially typed code may break the
block structure thus invalidating the completion results.
This commit is contained in:
Andrey Vlasovskikh
2015-12-15 20:07:13 +03:00
parent 5c4dd80d99
commit 89d0ce71a3
12 changed files with 72 additions and 58 deletions
@@ -1,12 +1,9 @@
package org.jetbrains.plugins.ipnb.psi;
import com.google.common.collect.Lists;
import com.intellij.codeInsight.completion.CompletionUtil;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.editor.Editor;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.ResolveResult;
import com.jetbrains.python.psi.PyQualifiedExpression;
@@ -48,18 +45,7 @@ public class IpnbPyReference extends PyReferenceImpl {
if (psiFile == null) continue;
final CompletionVariantsProcessor processor = new CompletionVariantsProcessor(myElement);
PyResolveUtil.scopeCrawlUp(processor, psiFile, null, null);
for (LookupElement e : processor.getResultList()) {
final Object o = e.getObject();
if (o instanceof PsiElement) {
final PsiElement original = CompletionUtil.getOriginalElement((PsiElement)o);
if (original == null) {
continue;
}
}
variants.add(e);
}
variants.addAll(getOriginalElements(processor));
}
}
return variants.toArray();
@@ -75,14 +75,14 @@ public class PyDunderAllReference extends PsiReferenceBase<PyStringLiteralExpres
final String name = ((PsiNamedElement)element).getName();
if (name != null && PyUtil.getInitialUnderscores(name) == 0 && !seenNames.contains(name)) {
seenNames.add(name);
result.add(LookupElementBuilder.create((PsiNamedElement) element).withIcon(element.getIcon(0)));
result.add(LookupElementBuilder.createWithSmartPointer(name, element).withIcon(element.getIcon(0)));
}
}
else if (element instanceof PyImportElement) {
final String visibleName = ((PyImportElement)element).getVisibleName();
if (visibleName != null && !seenNames.contains(visibleName)) {
seenNames.add(visibleName);
result.add(LookupElementBuilder.create(element, visibleName));
result.add(LookupElementBuilder.createWithSmartPointer(visibleName, element));
}
}
return true;
@@ -23,6 +23,7 @@ import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.jetbrains.python.psi.PyClass;
import com.jetbrains.python.psi.PyFunction;
import com.jetbrains.python.psi.PyUtil;
/**
* @author yole
@@ -41,8 +42,8 @@ public class PyClassInsertHandler implements InsertHandler<LookupElement> {
final int offset = context.getTailOffset();
document.insertString(offset, "()");
PyClass pyClass = (PyClass) item.getObject();
PyFunction init = pyClass.findInitOrNew(true, null);
PyClass pyClass = PyUtil.as(item.getPsiElement(), PyClass.class);
PyFunction init = pyClass != null ? pyClass.findInitOrNew(true, null) : null;
if (init != null && PyFunctionInsertHandler.hasParams(context, init)) {
editor.getCaretModel().moveToOffset(offset+1);
AutoPopupController.getInstance(context.getProject()).autoPopupParameterInfo(context.getEditor(), init);
@@ -126,9 +126,12 @@ public class PyClassNameCompletionContributor extends CompletionContributor {
for (final String elementName : CompletionUtil.sortMatching(resultSet.getPrefixMatcher(), keys)) {
for (T element : StubIndex.getElements(key, elementName, project, scope, elementClass)) {
if (condition.value(element)) {
resultSet.addElement(LookupElementBuilder.createWithIcon(element)
.withTailText(" " + ((NavigationItem)element).getPresentation().getLocationString(), true)
.withInsertHandler(insertHandler));
final String name = element.getName();
if (name != null) {
resultSet.addElement(LookupElementBuilder.createWithSmartPointer(name, element).withIcon(element.getIcon(0))
.withTailText(" " + ((NavigationItem)element).getPresentation().getLocationString(), true)
.withInsertHandler(insertHandler));
}
}
}
}
@@ -142,7 +145,7 @@ public class PyClassNameCompletionContributor extends CompletionContributor {
private static final InsertHandler<LookupElement> FUNCTION_INSERT_HANDLER = new PyFunctionInsertHandler() {
public void handleInsert(final InsertionContext context, final LookupElement item) {
public void handleInsert(@NotNull final InsertionContext context, @NotNull final LookupElement item) {
int tailOffset = context.getTailOffset()-1;
super.handleInsert(context, item); // adds parentheses, modifies tail offset
context.commitDocument();
@@ -172,14 +175,14 @@ public class PyClassNameCompletionContributor extends CompletionContributor {
manager.commitDocument(document);
}
final PsiReference ref = context.getFile().findReferenceAt(tailOffset);
if (ref == null || ref.resolve() == item.getObject()) {
if (ref == null || ref.resolve() == item.getPsiElement()) {
// no import statement needed
return;
}
new WriteCommandAction(context.getProject(), context.getFile()) {
@Override
protected void run(@NotNull Result result) throws Throwable {
AddImportHelper.addImport((PsiNamedElement)item.getObject(), context.getFile(), (PyElement)ref.getElement());
AddImportHelper.addImport(PyUtil.as(item.getPsiElement(), PsiNamedElement.class), context.getFile(), (PyElement)ref.getElement());
}
}.execute();
}
@@ -23,8 +23,11 @@ import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.python.psi.PyFunction;
import com.jetbrains.python.psi.PyReferenceExpression;
import com.jetbrains.python.psi.PyUtil;
import com.jetbrains.python.psi.impl.PyCallExpressionHelper;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* @author yole
@@ -33,23 +36,24 @@ public class PyFunctionInsertHandler extends ParenthesesInsertHandler<LookupElem
public static PyFunctionInsertHandler INSTANCE = new PyFunctionInsertHandler();
@Override
public void handleInsert(InsertionContext context, LookupElement item) {
public void handleInsert(@NotNull InsertionContext context, @NotNull LookupElement item) {
super.handleInsert(context, item);
if (hasParams(context, item)) {
AutoPopupController.getInstance(context.getProject()).autoPopupParameterInfo(context.getEditor(), (PyFunction) item.getObject());
AutoPopupController.getInstance(context.getProject()).autoPopupParameterInfo(context.getEditor(), getFunction(item));
}
}
@Override
protected boolean placeCaretInsideParentheses(InsertionContext context, LookupElement item) {
protected boolean placeCaretInsideParentheses(@NotNull InsertionContext context, @NotNull LookupElement item) {
return hasParams(context, item);
}
private static boolean hasParams(InsertionContext context, LookupElement item) {
return hasParams(context, (PyFunction) item.getObject());
private static boolean hasParams(@NotNull InsertionContext context, @NotNull LookupElement item) {
final PyFunction function = getFunction(item);
return function != null && hasParams(context, function);
}
public static boolean hasParams(InsertionContext context, PyFunction function) {
public static boolean hasParams(@NotNull InsertionContext context, @NotNull PyFunction function) {
final PsiElement element = context.getFile().findElementAt(context.getStartOffset());
PyReferenceExpression refExpr = PsiTreeUtil.getParentOfType(element, PyReferenceExpression.class);
int implicitArgsCount = refExpr != null
@@ -57,4 +61,9 @@ public class PyFunctionInsertHandler extends ParenthesesInsertHandler<LookupElem
: 0;
return function.getParameterList().getParameters().length > implicitArgsCount;
}
@Nullable
private static PyFunction getFunction(@NotNull LookupElement item) {
return PyUtil.as(item.getPsiElement(), PyFunction.class);
}
}
@@ -47,7 +47,7 @@ public class PySuperClassAttributesCompletionContributor extends CompletionContr
return;
}
for (PyTargetExpression expr : getSuperClassAttributes(containingClass)) {
result.addElement(LookupElementBuilder.create(expr, expr.getName() + " = "));
result.addElement(LookupElementBuilder.createWithSmartPointer(expr.getName() + " = ", expr));
}
}
}
@@ -71,7 +71,10 @@ public class PyIterableVariableMacro extends Macro {
final PsiElement element = context.getPsiElementAtStartOffset();
if (element != null) {
for (PsiNamedElement iterableElement : getIterableElements(element)) {
results.add(LookupElementBuilder.create(iterableElement));
final String name = iterableElement.getName();
if (name != null) {
results.add(LookupElementBuilder.createWithSmartPointer(name, iterableElement));
}
}
}
return results.toArray(new LookupElement[results.size()]);
@@ -24,4 +24,9 @@ public class PyBoundFunction extends PyFunctionImpl {
public PyBoundFunction(PyFunction function) {
super(function.getNode());
}
@Override
public boolean isPhysical() {
return false;
}
}
@@ -146,8 +146,9 @@ public class PyImportReference extends PyReferenceImpl {
}
else if (item instanceof LookupElement) {
LookupElement lookupElement = (LookupElement) item;
if (lookupElement.getObject() instanceof PsiElement) {
itemElement = (PsiElement) lookupElement.getObject();
final PsiElement element = lookupElement.getPsiElement();
if (element != null) {
itemElement = element;
}
}
return !(itemElement instanceof PsiFile); // TODO deeper check?
@@ -282,8 +282,8 @@ public class PyQualifiedReference extends PyReferenceImpl {
if (name != null && name.endsWith(CompletionUtil.DUMMY_IDENTIFIER_TRIMMED)) {
continue;
}
if (ex instanceof PsiNamedElement && qualifierType instanceof PyClassType) {
variants.add(LookupElementBuilder.create((PsiNamedElement)ex)
if (ex instanceof PsiNamedElement && qualifierType instanceof PyClassType && name != null) {
variants.add(LookupElementBuilder.createWithSmartPointer(name, ex)
.withTypeText(qualifierType.getName())
.withIcon(PlatformIcons.FIELD_ICON));
}
@@ -597,19 +597,26 @@ public class PyReferenceImpl implements PsiReferenceEx, PsiPolyVariantReference
}
}
// Throw away fake elements used for completion internally
for (LookupElement e : processor.getResultList()) {
final Object o = e.getObject();
if (o instanceof PsiElement) {
final PsiElement original = CompletionUtil.getOriginalElement((PsiElement)o);
ret.addAll(getOriginalElements(processor));
return ret.toArray();
}
/**
* Throws away fake elements used for completion internally.
*/
protected List<LookupElement> getOriginalElements(@NotNull CompletionVariantsProcessor processor) {
final List<LookupElement> ret = Lists.newArrayList();
for (LookupElement item : processor.getResultList()) {
final PsiElement e = item.getPsiElement();
if (e != null) {
final PsiElement original = CompletionUtil.getOriginalElement(e);
if (original == null) {
continue;
}
}
ret.add(e);
ret.add(item);
}
return ret.toArray();
return ret;
}
@Override
@@ -22,6 +22,7 @@ import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.QualifiedName;
import com.intellij.util.Function;
import com.intellij.util.PlatformIcons;
import com.jetbrains.python.PyNames;
@@ -31,7 +32,6 @@ import com.jetbrains.python.codeInsight.controlflow.ScopeOwner;
import com.jetbrains.python.codeInsight.dataflow.scope.ScopeUtil;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyBuiltinCache;
import com.intellij.psi.util.QualifiedName;
import com.jetbrains.python.psi.types.TypeEvalContext;
import org.jetbrains.annotations.Nullable;
@@ -59,17 +59,17 @@ public class CompletionVariantsProcessor extends VariantsProcessor {
mySuppressParentheses = true;
}
protected LookupElementBuilder setupItem(LookupElementBuilder item) {
final Object object = item.getObject();
private LookupElementBuilder setupItem(LookupElementBuilder item) {
final PsiElement element = item.getPsiElement();
if (!myPlainNamesOnly) {
if (!mySuppressParentheses &&
object instanceof PyFunction && ((PyFunction)object).getProperty() == null &&
!PyUtil.hasCustomDecorators((PyFunction)object) &&
!isSingleArgDecoratorCall(myContext, (PyFunction)object)) {
final Project project = ((PyFunction)object).getProject();
element instanceof PyFunction && ((PyFunction)element).getProperty() == null &&
!PyUtil.hasCustomDecorators((PyFunction)element) &&
!isSingleArgDecoratorCall(myContext, (PyFunction)element)) {
final Project project = element.getProject();
item = item.withInsertHandler(PyFunctionInsertHandler.INSTANCE);
final TypeEvalContext context = TypeEvalContext.codeCompletion(project, myContext != null ? myContext.getContainingFile() : null);
final List<PyParameter> parameters = PyUtil.getParameters((PyFunction)object, context);
final List<PyParameter> parameters = PyUtil.getParameters((PyFunction)element, context);
final String params = StringUtil.join(parameters, new Function<PyParameter, String>() {
@Override
public String fun(PyParameter pyParameter) {
@@ -78,13 +78,12 @@ public class CompletionVariantsProcessor extends VariantsProcessor {
}, ", ");
item = item.withTailText("(" + params + ")");
}
else if (object instanceof PyClass) {
else if (element instanceof PyClass) {
item = item.withInsertHandler(PyClassInsertHandler.INSTANCE);
}
}
String source = null;
if (object instanceof PsiElement) {
final PsiElement element = (PsiElement)object;
if (element != null) {
PyClass cls = null;
if (element instanceof PyFunction) {
@@ -158,14 +157,14 @@ public class CompletionVariantsProcessor extends VariantsProcessor {
if (PyUtil.isClassPrivateName(name) && !PyUtil.inSameFile(element, myContext)) {
return;
}
myVariants.put(name, setupItem(LookupElementBuilder.create(element, name).withIcon(element.getIcon(0))));
myVariants.put(name, setupItem(LookupElementBuilder.createWithSmartPointer(name, element).withIcon(element.getIcon(0))));
}
protected void addImportedElement(String referencedName, NameDefiner definer, PyElement expr) {
Icon icon = expr.getIcon(0);
// things like PyTargetExpression cannot have a general icon, but here we only have variables
if (icon == null) icon = PlatformIcons.VARIABLE_ICON;
LookupElementBuilder lookupItem = setupItem(LookupElementBuilder.create(expr, referencedName).withIcon(icon));
LookupElementBuilder lookupItem = setupItem(LookupElementBuilder.createWithSmartPointer(referencedName, expr).withIcon(icon));
myVariants.put(referencedName, lookupItem);
}
}