From a04d5b85bc64786f7b806a428f609b5d862961b4 Mon Sep 17 00:00:00 2001 From: Dmitry Jemerov Date: Wed, 20 Apr 2011 16:48:23 +0200 Subject: [PATCH] references to parameters in docstrings, with rename and completion support --- python/src/META-INF/python-plugin-common.xml | 1 + .../DocStringParameterReference.java | 44 ++++++++++++ .../DocStringReferenceContributor.java | 18 +++++ .../DocStringReferenceProvider.java | 71 +++++++++++++++++++ .../completion/epydocParamTag.after.py | 2 + python/testData/completion/epydocParamTag.py | 2 + .../rename/epydocRenameParameter.py | 3 + .../rename/epydocRenameParameter_after.py | 3 + .../python/PythonCompletionTest.java | 4 ++ .../python/refactoring/PyRenameTest.java | 4 ++ 10 files changed, 152 insertions(+) create mode 100644 python/src/com/jetbrains/python/documentation/DocStringParameterReference.java create mode 100644 python/src/com/jetbrains/python/documentation/DocStringReferenceContributor.java create mode 100644 python/src/com/jetbrains/python/documentation/DocStringReferenceProvider.java create mode 100644 python/testData/completion/epydocParamTag.after.py create mode 100644 python/testData/completion/epydocParamTag.py create mode 100644 python/testData/refactoring/rename/epydocRenameParameter.py create mode 100644 python/testData/refactoring/rename/epydocRenameParameter_after.py diff --git a/python/src/META-INF/python-plugin-common.xml b/python/src/META-INF/python-plugin-common.xml index 80632ecc2b1b..b579d19fe6ce 100644 --- a/python/src/META-INF/python-plugin-common.xml +++ b/python/src/META-INF/python-plugin-common.xml @@ -287,6 +287,7 @@ + diff --git a/python/src/com/jetbrains/python/documentation/DocStringParameterReference.java b/python/src/com/jetbrains/python/documentation/DocStringParameterReference.java new file mode 100644 index 000000000000..43d118936e3a --- /dev/null +++ b/python/src/com/jetbrains/python/documentation/DocStringParameterReference.java @@ -0,0 +1,44 @@ +package com.jetbrains.python.documentation; + +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReferenceBase; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.ArrayUtil; +import com.jetbrains.python.psi.PyDocStringOwner; +import com.jetbrains.python.psi.PyFunction; +import com.jetbrains.python.psi.PyNamedParameter; +import com.jetbrains.python.psi.impl.ParamHelper; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * @author yole + */ +public class DocStringParameterReference extends PsiReferenceBase { + public DocStringParameterReference(PsiElement element, TextRange range) { + super(element, range); + } + + @Override + public PsiElement resolve() { + PyDocStringOwner owner = PsiTreeUtil.getParentOfType(getElement(), PyDocStringOwner.class); + if (owner instanceof PyFunction) { + final String paramName = getCanonicalText(); + return ((PyFunction) owner).getParameterList().getElementNamed(paramName); + } + return null; + } + + @NotNull + @Override + public Object[] getVariants() { + PyDocStringOwner owner = PsiTreeUtil.getParentOfType(getElement(), PyDocStringOwner.class); + if (owner instanceof PyFunction) { + final List namedParameters = ParamHelper.collectNamedParameters(((PyFunction)owner).getParameterList()); + return ArrayUtil.toObjectArray(namedParameters); + } + return ArrayUtil.EMPTY_OBJECT_ARRAY; + } +} diff --git a/python/src/com/jetbrains/python/documentation/DocStringReferenceContributor.java b/python/src/com/jetbrains/python/documentation/DocStringReferenceContributor.java new file mode 100644 index 000000000000..ba7ddcf73431 --- /dev/null +++ b/python/src/com/jetbrains/python/documentation/DocStringReferenceContributor.java @@ -0,0 +1,18 @@ +package com.jetbrains.python.documentation; + +import com.intellij.psi.PsiReferenceContributor; +import com.intellij.psi.PsiReferenceRegistrar; +import com.jetbrains.python.psi.PyStringLiteralExpression; + +import static com.intellij.patterns.PlatformPatterns.psiElement; + +/** + * @author yole + */ +public class DocStringReferenceContributor extends PsiReferenceContributor { + @Override + public void registerReferenceProviders(PsiReferenceRegistrar registrar) { + registrar.registerReferenceProvider(psiElement(PyStringLiteralExpression.class), + new DocStringReferenceProvider()); + } +} diff --git a/python/src/com/jetbrains/python/documentation/DocStringReferenceProvider.java b/python/src/com/jetbrains/python/documentation/DocStringReferenceProvider.java new file mode 100644 index 000000000000..a03f1f03534a --- /dev/null +++ b/python/src/com/jetbrains/python/documentation/DocStringReferenceProvider.java @@ -0,0 +1,71 @@ +package com.jetbrains.python.documentation; + +import com.google.common.base.CharMatcher; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import com.intellij.psi.PsiReferenceProvider; +import com.intellij.psi.util.PsiTreeUtil; +import com.intellij.util.ArrayUtil; +import com.intellij.util.ProcessingContext; +import com.jetbrains.python.psi.PyDocStringOwner; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author yole + */ +public class DocStringReferenceProvider extends PsiReferenceProvider { + private final String[] ALL_PARAM_TAGS; + + public DocStringReferenceProvider() { + List allParamTags = new ArrayList(); + for (String tag : EpydocString.PARAM_TAGS) { + allParamTags.add("@" + tag); + allParamTags.add(":" + tag); + } + ALL_PARAM_TAGS = ArrayUtil.toStringArray(allParamTags); + } + + @NotNull + @Override + public PsiReference[] getReferencesByElement(@NotNull PsiElement element, @NotNull ProcessingContext context) { + final PyDocStringOwner docStringOwner = PsiTreeUtil.getParentOfType(element, PyDocStringOwner.class); + if (docStringOwner != null && element == docStringOwner.getDocStringExpression()) { + List result = new ArrayList(); + String docString = element.getText(); + int pos = 0; + while (pos < docString.length()) { + pos = findNextTag(docString, pos, ALL_PARAM_TAGS); + if (pos < 0) { + break; + } + pos = CharMatcher.anyOf(" \t*").negate().indexIn(docString, pos); + int endPos = CharMatcher.JAVA_LETTER_OR_DIGIT.negate().indexIn(docString, pos); + if (endPos < 0) { + endPos = docString.length(); + } + result.add(new DocStringParameterReference(element, new TextRange(pos, endPos))); + pos = endPos; + } + + return result.toArray(new PsiReference[result.size()]); + } + return PsiReference.EMPTY_ARRAY; + } + + private static int findNextTag(String docString, int pos, String[] paramTags) { + int result = Integer.MAX_VALUE; + String foundTag = null; + for (String paramTag : paramTags) { + int tagPos = docString.indexOf(paramTag + " ", pos); + if (tagPos >= 0 && tagPos < result) { + foundTag = paramTag; + result = tagPos; + } + } + return foundTag == null ? -1 : result + foundTag.length(); + } +} diff --git a/python/testData/completion/epydocParamTag.after.py b/python/testData/completion/epydocParamTag.after.py new file mode 100644 index 000000000000..7a9bf5aa1dfa --- /dev/null +++ b/python/testData/completion/epydocParamTag.after.py @@ -0,0 +1,2 @@ +def foo(bar): + """ @param bar """ \ No newline at end of file diff --git a/python/testData/completion/epydocParamTag.py b/python/testData/completion/epydocParamTag.py new file mode 100644 index 000000000000..6323111c8540 --- /dev/null +++ b/python/testData/completion/epydocParamTag.py @@ -0,0 +1,2 @@ +def foo(bar): + """ @param """ \ No newline at end of file diff --git a/python/testData/refactoring/rename/epydocRenameParameter.py b/python/testData/refactoring/rename/epydocRenameParameter.py new file mode 100644 index 000000000000..1c62996851ab --- /dev/null +++ b/python/testData/refactoring/rename/epydocRenameParameter.py @@ -0,0 +1,3 @@ +def func(foo): + """ @param foo: The parameter value.""" + pass diff --git a/python/testData/refactoring/rename/epydocRenameParameter_after.py b/python/testData/refactoring/rename/epydocRenameParameter_after.py new file mode 100644 index 000000000000..8872fb68c263 --- /dev/null +++ b/python/testData/refactoring/rename/epydocRenameParameter_after.py @@ -0,0 +1,3 @@ +def func(bar): + """ @param bar: The parameter value.""" + pass diff --git a/python/testSrc/com/jetbrains/python/PythonCompletionTest.java b/python/testSrc/com/jetbrains/python/PythonCompletionTest.java index 2b26c0f8f833..7e371c6d0a1e 100644 --- a/python/testSrc/com/jetbrains/python/PythonCompletionTest.java +++ b/python/testSrc/com/jetbrains/python/PythonCompletionTest.java @@ -320,4 +320,8 @@ public class PythonCompletionTest extends PyLightFixtureTestCase { public void testReexportModules() { // PY-2385 doMultiFileTest(); } + + public void testEpydocParamTag() { + doTest(); + } } diff --git a/python/testSrc/com/jetbrains/python/refactoring/PyRenameTest.java b/python/testSrc/com/jetbrains/python/refactoring/PyRenameTest.java index 1e13a18edf70..5bc613b1b812 100644 --- a/python/testSrc/com/jetbrains/python/refactoring/PyRenameTest.java +++ b/python/testSrc/com/jetbrains/python/refactoring/PyRenameTest.java @@ -62,6 +62,10 @@ public class PyRenameTest extends PyLightFixtureTestCase { doTest("bar"); } + public void testEpydocRenameParameter() { + doTest("bar"); + } + public void testClassNameConflict() { // PY-2390 doRenameConflictTest("Foo", "A class named 'Foo' is already defined in classNameConflict.py"); }