PY-20003 Fixed: Breadcrumbs for Python files

Introduce PyBreadcrumbsInfoProvider which supports:
classes and functions (incl. async) definitions,
lambdas, try-except-else-finally, if-elif-else,
for-else (incl. async), while-else, with and
key-value statements
This commit is contained in:
Semyon Proshev
2016-12-20 18:21:50 +03:00
committed by Semyon Proshev
parent f54070028a
commit cde66f2f64
61 changed files with 557 additions and 2 deletions
+2 -2
View File
@@ -24,6 +24,6 @@
<orderEntry type="module" module-name="xdebugger-impl" />
<orderEntry type="module" module-name="xml-psi-impl" />
<orderEntry type="library" name="gson" level="project" />
<orderEntry type="module" module-name="xml-openapi" />
</component>
</module>
</module>
@@ -602,6 +602,8 @@
<!-- postfix templates!-->
<codeInsight.template.postfixTemplateProvider language="Python"
implementationClass="com.jetbrains.python.codeInsight.postfix.PyPostfixTemplateProvider"/>
<breadcrumbsInfoProvider implementation="com.jetbrains.python.breadcrumbs.PyBreadcrumbsInfoProvider"/>
</extensions>
<extensionPoints>
@@ -0,0 +1,154 @@
/*
* 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.breadcrumbs
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiElement
import com.intellij.xml.breadcrumbs.BreadcrumbsInfoProvider
import com.jetbrains.python.PythonLanguage
import com.jetbrains.python.psi.*
class PyBreadcrumbsInfoProvider : BreadcrumbsInfoProvider() {
companion object {
private val LANGUAGES = arrayOf(PythonLanguage.getInstance())
private val HELPERS = listOf<Helper<*>>(
LambdaHelper,
SimpleHelper<PyTryPart>(PyTryPart::class.java, "try"),
ExceptHelper,
SimpleHelper<PyFinallyPart>(PyFinallyPart::class.java, "finally"),
SimpleHelper<PyElsePart>(PyElsePart::class.java, "else"),
IfHelper,
ForHelper,
WhileHelper,
WithHelper,
ClassHelper,
FunctionHelper,
KeyValueHelper
)
}
override fun getLanguages() = LANGUAGES
override fun acceptElement(e: PsiElement) = getHelper(e) != null
override fun getElementInfo(e: PsiElement) = getHelper(e)!!.elementInfo(e as PyElement)
override fun getElementTooltip(e: PsiElement) = getHelper(e)!!.elementTooltip(e as PyElement)
private fun getHelper(e: PsiElement): Helper<in PyElement>? {
if (e !is PyElement) return null
@Suppress("UNCHECKED_CAST")
return HELPERS.firstOrNull { it.type.isInstance(e) && (it as Helper<in PyElement>).accepts(e) } as Helper<in PyElement>?
}
private abstract class Helper<T: PyElement>(val type: Class<T>) {
abstract fun accepts(e: T): Boolean
abstract fun elementInfo(e: T): String
abstract fun elementTooltip(e: T): String
}
private abstract class AbstractHelper<T : PyElement>(type: Class<T>) : Helper<T>(type) {
override fun accepts(e: T): Boolean = true
override fun elementInfo(e: T): String = getTruncatedPresentation(e, 16)
override fun elementTooltip(e: T): String = getTruncatedPresentation(e, 96)
abstract fun getPresentation(e: T): String
private fun getTruncatedPresentation(e: T, maxLength: Int) = StringUtil.shortenTextWithEllipsis(getPresentation(e), maxLength, 0, true)
}
private class SimpleHelper<T : PyElement>(type: Class<T>, val representation: String) : AbstractHelper<T>(type) {
override fun getPresentation(e: T) = representation
}
private object LambdaHelper : AbstractHelper<PyLambdaExpression>(PyLambdaExpression::class.java) {
override fun getPresentation(e: PyLambdaExpression) = "lambda ${e.parameterList.getPresentableText(false)}"
}
private object ExceptHelper : AbstractHelper<PyExceptPart>(PyExceptPart::class.java) {
override fun getPresentation(e: PyExceptPart): String {
val exceptClass = e.exceptClass ?: return "except"
val target = e.target ?: return "except ${exceptClass.text}"
return "except ${exceptClass.text} as ${target.text}"
}
}
private object IfHelper : AbstractHelper<PyIfPart>(PyIfPart::class.java) {
override fun getPresentation(e: PyIfPart): String {
val prefix = if (e.isElif) "elif" else "if"
val condition = e.condition ?: return prefix
return "$prefix ${condition.text}"
}
}
private object ForHelper : AbstractHelper<PyForPart>(PyForPart::class.java) {
override fun getPresentation(e: PyForPart): String {
val parent = e.parent
val prefix = if (parent is PyForStatement && parent.isAsync) "async for" else "for"
val target = e.target ?: return prefix
val source = e.source ?: return prefix
return "$prefix ${target.text} in ${source.text}"
}
}
private object WhileHelper : AbstractHelper<PyWhilePart>(PyWhilePart::class.java) {
override fun getPresentation(e: PyWhilePart): String {
val condition = e.condition ?: return "while"
return "while ${condition.text}"
}
}
private object WithHelper : AbstractHelper<PyWithStatement>(PyWithStatement::class.java) {
override fun getPresentation(e: PyWithStatement): String {
val getItemPresentation = fun(item: PyWithItem): String? {
val expression = item.expression ?: return null
val target = item.target ?: return expression.text
return "${expression.text} as ${target.text}"
}
val prefix = if (e.isAsync) "async with " else "with "
return e.withItems
.orEmpty()
.asSequence()
.map(getItemPresentation)
.filterNotNull()
.joinToString(prefix = prefix)
}
}
private object ClassHelper : AbstractHelper<PyClass>(PyClass::class.java) {
override fun getPresentation(e: PyClass) = e.name ?: "class"
}
private object FunctionHelper : AbstractHelper<PyFunction>(PyFunction::class.java) {
override fun getPresentation(e: PyFunction): String {
val prefix = if (e.isAsync) "async " else ""
val name = e.name ?: return "function"
return "$prefix$name()"
}
}
private object KeyValueHelper : AbstractHelper<PyKeyValueExpression>(PyKeyValueExpression::class.java) {
override fun getPresentation(e: PyKeyValueExpression): String = e.key.text ?: "key"
}
}
+3
View File
@@ -0,0 +1,3 @@
async def foo():
async for i in range(10):
print(<caret>i)
@@ -0,0 +1,8 @@
Crumb:
async foo()
Tooltip:
async foo()
Crumb:
async for i in …
Tooltip:
async for i in range(10)
@@ -0,0 +1,3 @@
async def foo():
async with open("file.txt"), open("file2.txt"):
print("a<caret>bc")
@@ -0,0 +1,3 @@
async def foo():
async with open("file.txt") as f, open("file2.txt") as f2:
print("a<caret>bc")
@@ -0,0 +1,8 @@
Crumb:
async foo()
Tooltip:
async foo()
Crumb:
async with open…
Tooltip:
async with open("file.txt") as f, open("file2.txt") as f2
@@ -0,0 +1,8 @@
Crumb:
async foo()
Tooltip:
async foo()
Crumb:
async with open…
Tooltip:
async with open("file.txt"), open("file2.txt")
+3
View File
@@ -0,0 +1,3 @@
async def foo():
async with open("file.txt"):
print("a<caret>bc")
@@ -0,0 +1,3 @@
async def foo():
async with open("file.txt") as f:
print("a<caret>bc")
@@ -0,0 +1,8 @@
Crumb:
async foo()
Tooltip:
async foo()
Crumb:
async with open…
Tooltip:
async with open("file.txt") as f
@@ -0,0 +1,8 @@
Crumb:
async foo()
Tooltip:
async foo()
Crumb:
async with open…
Tooltip:
async with open("file.txt")
+2
View File
@@ -0,0 +1,2 @@
class A:
pa<caret>ss
@@ -0,0 +1,4 @@
Crumb:
A
Tooltip:
A
+1
View File
@@ -0,0 +1 @@
a = {"abc": "d<caret>ef"}
@@ -0,0 +1,4 @@
Crumb:
"abc"
Tooltip:
"abc"
+4
View File
@@ -0,0 +1,4 @@
if True:
print "abc"
elif False:
print "d<caret>ef"
@@ -0,0 +1,4 @@
Crumb:
elif False
Tooltip:
elif False
+4
View File
@@ -0,0 +1,4 @@
try:
print "abc"
except KeyError:
print "d<caret>ef"
+4
View File
@@ -0,0 +1,4 @@
try:
print "abc"
except:
print "d<caret>ef"
@@ -0,0 +1,4 @@
Crumb:
except
Tooltip:
except
+4
View File
@@ -0,0 +1,4 @@
try:
print "abc"
except KeyError as e:
print "d<caret>ef"
@@ -0,0 +1,4 @@
Crumb:
except KeyError…
Tooltip:
except KeyError as e
@@ -0,0 +1,4 @@
Crumb:
except KeyError
Tooltip:
except KeyError
+4
View File
@@ -0,0 +1,4 @@
try:
print "abc"
finally:
print "d<caret>ef"
@@ -0,0 +1,4 @@
Crumb:
finally
Tooltip:
finally
+2
View File
@@ -0,0 +1,2 @@
for i in range(10):
print <caret>i
+4
View File
@@ -0,0 +1,4 @@
for i in range(10):
print i
else:
print "a<caret>bc"
@@ -0,0 +1,4 @@
Crumb:
else
Tooltip:
else
@@ -0,0 +1,4 @@
Crumb:
for i in range(…
Tooltip:
for i in range(10)
+2
View File
@@ -0,0 +1,2 @@
def foo(self):
pa<caret>ss
@@ -0,0 +1,4 @@
Crumb:
foo()
Tooltip:
foo()
+2
View File
@@ -0,0 +1,2 @@
if True:
print "a<caret>bc"
+4
View File
@@ -0,0 +1,4 @@
if True:
print "abc"
else:
print "d<caret>ef"
@@ -0,0 +1,4 @@
Crumb:
else
Tooltip:
else
@@ -0,0 +1,4 @@
Crumb:
if True
Tooltip:
if True
+1
View File
@@ -0,0 +1 @@
map(lambda x, y: <caret>x * y, [])
@@ -0,0 +1,4 @@
Crumb:
lambda (x, y)
Tooltip:
lambda (x, y)
@@ -0,0 +1 @@
map(lambda a, aa, aaa, aaaa, aaaaa, aaaaaa, aaaaaaa, aaaaaaaa, aaaaaaaaa, aaaaaaaaaa, aaaaaaaaaaa, aaaaaaaaaaaa: <caret>a * a, [])
@@ -0,0 +1,4 @@
Crumb:
lambda (a, aa, …
Tooltip:
lambda (a, aa, aaa, aaaa, aaaaa, aaaaaa, aaaaaaa, aaaaaaaa, aaaaaaaaa, aaaaaaaaaa, aaaaaaaaaaa,…
+3
View File
@@ -0,0 +1,3 @@
class A:
def foo(self):
pa<caret>ss
@@ -0,0 +1,8 @@
Crumb:
A
Tooltip:
A
Crumb:
foo()
Tooltip:
foo()
@@ -0,0 +1,2 @@
with open("file.txt"), open("file2.txt"):
print("a<caret>bc")
@@ -0,0 +1,2 @@
with open("file.txt") as f, open("file2.txt") as f2:
print("a<caret>bc")
@@ -0,0 +1,4 @@
Crumb:
with open("file…
Tooltip:
with open("file.txt") as f, open("file2.txt") as f2
@@ -0,0 +1,4 @@
Crumb:
with open("file…
Tooltip:
with open("file.txt"), open("file2.txt")
+4
View File
@@ -0,0 +1,4 @@
try:
print "a<caret>bc"
finally:
print "def"
+6
View File
@@ -0,0 +1,6 @@
try:
print "abc"
except:
print "def"
else:
print "g<caret>hi"
@@ -0,0 +1,4 @@
Crumb:
else
Tooltip:
else
@@ -0,0 +1,4 @@
Crumb:
try
Tooltip:
try
+3
View File
@@ -0,0 +1,3 @@
i = 10
while i < 30:
print <caret>i
+5
View File
@@ -0,0 +1,5 @@
i = 10
while i < 30:
print i
else:
print "a<caret>bc"
@@ -0,0 +1,4 @@
Crumb:
else
Tooltip:
else
@@ -0,0 +1,4 @@
Crumb:
while i < 30
Tooltip:
while i < 30
+2
View File
@@ -0,0 +1,2 @@
with open("file.txt"):
print("a<caret>bc")
+2
View File
@@ -0,0 +1,2 @@
with open("file.txt") as f:
print("a<caret>bc")
@@ -0,0 +1,4 @@
Crumb:
with open("file…
Tooltip:
with open("file.txt") as f
@@ -0,0 +1,4 @@
Crumb:
with open("file…
Tooltip:
with open("file.txt")
@@ -0,0 +1,179 @@
/*
* 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.breadcrumbs;
import com.google.common.collect.Lists;
import com.intellij.psi.PsiElement;
import com.intellij.xml.breadcrumbs.BreadcrumbsInfoProvider;
import com.jetbrains.python.fixtures.PyTestCase;
import com.jetbrains.python.psi.LanguageLevel;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class PyBreadcrumbsTest extends PyTestCase {
@Override
protected String getTestDataPath() {
return super.getTestDataPath() + "/breadcrumbs";
}
public void testDictKey() {
doTest();
}
public void testLambda() {
doTest();
}
public void testLargeLambda() {
doTest();
}
public void testTry() {
doTest();
}
public void testExcept() {
doTest();
}
public void testExceptAll() {
doTest();
}
public void testExceptAs() {
doTest();
}
public void testFinally() {
doTest();
}
public void testTryElse() {
doTest();
}
public void testIf() {
doTest();
}
public void testElif() {
doTest();
}
public void testIfElse() {
doTest();
}
public void testFor() {
doTest();
}
public void testAsyncFor() {
runWithLanguageLevel(LanguageLevel.PYTHON35, this::doTest);
}
public void testForElse() {
doTest();
}
public void testWhile() {
doTest();
}
public void testWhileElse() {
doTest();
}
public void testWith() {
doTest();
}
public void testWithAs() {
doTest();
}
public void testSeveralWith() {
doTest();
}
public void testSeveralWithAs() {
doTest();
}
public void testAsyncWith() {
runWithLanguageLevel(LanguageLevel.PYTHON35, this::doTest);
}
public void testAsyncWithAs() {
runWithLanguageLevel(LanguageLevel.PYTHON35, this::doTest);
}
public void testAsyncSeveralWith() {
runWithLanguageLevel(LanguageLevel.PYTHON35, this::doTest);
}
public void testAsyncSeveralWithAs() {
runWithLanguageLevel(LanguageLevel.PYTHON35, this::doTest);
}
public void testClass() {
doTest();
}
public void testMethod() {
doTest();
}
public void testFunction() {
doTest();
}
private void doTest() {
final String testName = getTestName(true);
myFixture.configureByFile(testName + ".py");
final PyBreadcrumbsInfoProvider provider = new PyBreadcrumbsInfoProvider();
final String breadcrumbsAndTooltips = getBreadcrumbsAndTooltips(provider, elementsFromTopToCaret(provider));
assertSameLinesWithFile(getTestDataPath() + "/" + testName + "_crumbs.txt", breadcrumbsAndTooltips);
}
@NotNull
private List<PsiElement> elementsFromTopToCaret(@NotNull BreadcrumbsInfoProvider provider) {
final List<PsiElement> fromElementToNullExcluded = StreamEx
.iterate(myFixture.getFile().findElementAt(myFixture.getCaretOffset()), provider::getParent)
.takeWhile(Objects::nonNull)
.filter(provider::acceptElement)
.toList();
return Lists.reverse(fromElementToNullExcluded);
}
@NotNull
private static String getBreadcrumbsAndTooltips(@NotNull BreadcrumbsInfoProvider provider, @NotNull List<PsiElement> elements) {
return elements
.stream()
.flatMap(element -> Stream.of("Crumb:", provider.getElementInfo(element), "Tooltip:", provider.getElementTooltip(element)))
.collect(Collectors.joining("\n"));
}
}
@@ -22,5 +22,6 @@
<orderEntry type="library" scope="TEST" name="mockito" level="project" />
<orderEntry type="library" scope="TEST" name="Mocks" level="project" />
<orderEntry type="library" exported="" scope="TEST" name="hamcrest" level="project" />
<orderEntry type="module" module-name="xml-openapi" />
</component>
</module>