[python] postfix templates: support editable templates and conditions PY-30217

GitOrigin-RevId: 03b1a49f8ecfd7c765ed2900b2adf14b07709a5f
This commit is contained in:
Vladislav Rassokhin
2023-03-07 18:56:25 +00:00
committed by intellij-monorepo-bot
parent 5199718c84
commit a396b4cdd2
21 changed files with 712 additions and 29 deletions
@@ -118,5 +118,6 @@
<orderEntry type="library" name="jackson-databind" level="project" />
<orderEntry type="module" module-name="intellij.python.community.core.impl" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.python.psi" />
</component>
</module>
@@ -1207,6 +1207,25 @@ dialog.message.error.on.python.side.exit.code.stderr.stdout=Error on Python side
live.template.context.class=Class
live.template.context.top.level=Top-level
postfix.template.provider.name=Python
postfix.template.condition.iterable.name=iterable
postfix.template.condition.list.name=list
postfix.template.condition.set.name=set
postfix.template.condition.tuple.name=tuple
postfix.template.condition.dict.name=dict
postfix.template.condition.builtin.len.applicable.name=builtin len applicable
postfix.template.condition.non.none.name=non None
postfix.template.condition.exception.name=exception
postfix.template.condition.number.name=number
postfix.template.condition.boolean.name=boolean
postfix.template.condition.string.name=string
settings.postfix.choose.class.action.name=Choose Class\u2026
settings.postfix.choose.class.prompt.text=Choose class
settings.postfix.enter.class.action.name=Enter Class Name\u2026
settings.postfix.enter.class.dialog.name=Choose Class
settings.postfix.enter.fully.qualified.class.name=Enter fully qualified class name
line.markers.popup.title.choose.super.method=Choose Super Method of {0}
line.markers.popup.title.choose.super.attribute=Choose Super Attribute of {0}
line.markers.popup.title.choose.subclass=Choose Subclass of {0}
@@ -0,0 +1,43 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.codeInsight.postfix
import com.intellij.codeInsight.template.impl.TemplateImpl
import com.intellij.codeInsight.template.postfix.templates.PostfixTemplateProvider
import com.intellij.codeInsight.template.postfix.templates.editable.EditablePostfixTemplateWithMultipleExpressions
import com.intellij.openapi.editor.Document
import com.intellij.openapi.util.Conditions
import com.intellij.psi.PsiElement
class PyEditablePostfixTemplate(templateId: String, templateName: String, liveTemplate: TemplateImpl, example: String,
conditions: Set<PyPostfixTemplateExpressionCondition?>, topmost: Boolean, provider: PostfixTemplateProvider,
private val myBuiltin: Boolean) : EditablePostfixTemplateWithMultipleExpressions<PyPostfixTemplateExpressionCondition?>(
templateId, templateName, liveTemplate, example, conditions, topmost, provider) {
constructor(templateId: String, templateName: String, templateText: String, example: String,
conditions: Set<PyPostfixTemplateExpressionCondition?>, topmost: Boolean, provider: PostfixTemplateProvider,
builtin: Boolean) : this(templateId, templateName, createTemplate(templateText),
example, conditions, topmost, provider, builtin)
override fun getExpressions(context: PsiElement, document: Document, offset: Int): List<PsiElement> {
val selector = if (myUseTopmostExpression) PyPostfixUtils.selectorTopmost() else PyPostfixUtils.selectorAllExpressionsWithCurrentOffset()
val expressions = selector.getExpressions(context, document, offset)
val condition = Conditions.and({ e: PsiElement -> e.textRange.endOffset == offset }, expressionCompositeCondition)
return expressions.filter { condition.value(it) }
}
override fun isBuiltin(): Boolean {
return myBuiltin
}
override fun isEditable(): Boolean {
return expressionConditions.all {
it != null && (PyPostfixTemplateExpressionCondition.PUBLIC_CONDITIONS.containsKey(it.id) || PyPostfixTemplateExpressionCondition.PyClassCondition.ID == it.id)
}
}
override fun getTopmostExpression(element: PsiElement): PsiElement {
val expressionsInRange = PyPostfixUtils.getAllExpressionsAtOffset(element.containingFile, element.textOffset)
val lastItem = expressionsInRange.lastOrNull()
return lastItem ?: element
}
}
@@ -0,0 +1,144 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.codeInsight.postfix
import com.intellij.codeInsight.template.postfix.settings.PostfixTemplateEditorBase
import com.intellij.ide.util.gotoByName.*
import com.intellij.navigation.ChooseByNameContributor
import com.intellij.navigation.NavigationItem
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.ui.Messages
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.Processor
import com.intellij.util.indexing.FindSymbolParameters
import com.intellij.util.indexing.IdFilter
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PyGotoClassContributor
import com.jetbrains.python.codeInsight.postfix.PyPostfixTemplateExpressionCondition.PyClassCondition.Companion.create
import com.jetbrains.python.psi.PyClass
import javax.swing.JComponent
class PyPostfixTemplateEditor(provider: PyPostfixTemplateProvider) : PostfixTemplateEditorBase<PyPostfixTemplateExpressionCondition?>(
provider, true) {
override fun fillConditions(group: DefaultActionGroup) {
for (condition in PyPostfixTemplateExpressionCondition.PUBLIC_CONDITIONS.values) {
group.add(AddConditionAction(condition))
}
val projects = ProjectManager.getInstance().openProjects
if (projects.isNotEmpty()) {
group.add(ChooseClassAction(projects))
}
group.add(EnterClassAction())
}
override fun createTemplate(templateId: String, templateName: String): PyEditablePostfixTemplate {
val templateText = myTemplateEditor.document.text
val conditions = LinkedHashSet(myExpressionTypesListModel.elements().toList())
val useTopmostExpression = myApplyToTheTopmostJBCheckBox.isSelected
return PyEditablePostfixTemplate(templateId, templateName, templateText, "", conditions, useTopmostExpression, myProvider, false)
}
override fun getComponent(): JComponent {
return myEditTemplateAndConditionsPanel
}
private inner class ChooseClassAction(private val projects: Array<Project>) : DumbAwareAction(
PyBundle.messagePointer("settings.postfix.choose.class.action.name")) {
override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val contributor = MultiProjectPyClassesContributor(projects)
val model: GotoClassModel2 = object : GotoClassModel2(project) {
override fun getPromptText(): String {
return PyBundle.message("settings.postfix.choose.class.prompt.text")
}
override fun getContributorList(): List<ChooseByNameContributor> {
return listOf<ChooseByNameContributor>(contributor)
}
override fun getCheckBoxName(): String? {
return null // don't show checkbox, always search in libraries
}
}
val popup = Companion.createPopup(project, model)
popup.invoke(object : ChooseByNamePopupComponent.Callback() {
override fun elementChosen(element: Any) {}
override fun onClose() {
if (!popup.closedCorrectly) {
return
}
val chosenElement = popup.chosenElement!!
if (chosenElement is PyClass) {
val condition = create(chosenElement)
if (condition != null) {
myExpressionTypesListModel.addElement(condition)
}
}
}
}, ModalityState.current(), false)
}
}
companion object {
private fun createPopup(project: Project?, model: GotoClassModel2): ChooseClassByNamePopup {
val provider: ChooseByNameItemProvider = DefaultChooseByNameItemProvider(null)
val oldPopup = project?.getUserData(ChooseByNamePopup.CHOOSE_BY_NAME_POPUP_IN_PROJECT_KEY)
oldPopup?.close(false)
val popup = ChooseClassByNamePopup(project, model, provider, oldPopup)
project?.putUserData(ChooseByNamePopup.CHOOSE_BY_NAME_POPUP_IN_PROJECT_KEY, popup)
popup.isSearchInAnyPlace = true
return popup
}
}
private class MultiProjectPyClassesContributor(private val projects: Array<Project>) : PyGotoClassContributor() {
override fun processNames(processor: Processor<in String>, scope: GlobalSearchScope, filter: IdFilter?) {
for (project in projects) {
super.processNames(processor, FindSymbolParameters.searchScopeFor(project, true), null)
}
}
override fun processElementsWithName(name: String,
processor: Processor<in NavigationItem?>,
parameters: FindSymbolParameters) {
for (project in projects) {
val params = FindSymbolParameters(
parameters.completePattern, parameters.localPatternName, FindSymbolParameters.searchScopeFor(project, true))
super.processElementsWithName(name, processor, params)
}
}
}
private class ChooseClassByNamePopup(project: Project?,
model: ChooseByNameModel,
provider: ChooseByNameItemProvider,
oldPopup: ChooseByNamePopup?) : ChooseByNamePopup(project, model, provider, oldPopup, null, false,
0) {
var closedCorrectly = false
override fun close(isOk: Boolean) {
if (!checkDisposed()) {
closedCorrectly = isOk
}
super.close(isOk)
}
}
private inner class EnterClassAction : DumbAwareAction(
PyBundle.messagePointer("settings.postfix.enter.class.action.name")) {
override fun actionPerformed(e: AnActionEvent) {
val name = Messages.showInputDialog(myEditTemplateAndConditionsPanel,
PyBundle.message("settings.postfix.enter.fully.qualified.class.name"),
PyBundle.message("settings.postfix.enter.class.dialog.name"), null)
if (name != null) {
val condition = create(name)
if (condition != null) {
myExpressionTypesListModel.addElement(condition)
}
}
}
}
}
@@ -0,0 +1,246 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.codeInsight.postfix
import com.intellij.codeInsight.completion.CompletionUtil
import com.intellij.codeInsight.template.postfix.templates.editable.PostfixTemplateExpressionCondition
import com.intellij.openapi.util.NlsSafe
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.QualifiedName
import com.jetbrains.extensions.python.inherits
import com.jetbrains.python.PyBundle
import com.jetbrains.python.PyNames
import com.jetbrains.python.psi.*
import com.jetbrains.python.psi.impl.PyBuiltinCache
import com.jetbrains.python.psi.types.*
import org.jdom.Element
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
interface PyPostfixTemplateExpressionCondition : PostfixTemplateExpressionCondition<PyExpression?> {
override fun equals(other: Any?): Boolean
override fun hashCode(): Int
abstract class PySimpleConditionBase(@JvmField val id:String) : PyPostfixTemplateExpressionCondition {
override fun getId(): String = id
override fun equals(other: Any?): Boolean {
return if (this === other) true else other != null && javaClass == other.javaClass
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
}
class PyBooleanExpression : PySimpleConditionBase(ID) {
override fun getPresentableName(): @Nls String = PyBundle.message("postfix.template.condition.boolean.name")
override fun value(element: PyExpression): Boolean {
val context = TypeEvalContext.codeCompletion(element.project, element.containingFile)
val type = context.getType(element) ?: return false
return PyBuiltinCache.getInstance(element).boolType == type
}
companion object {
private const val ID: @NonNls String = "boolean"
}
}
class PyNumberExpression : PySimpleConditionBase(ID) {
override fun getPresentableName(): @Nls String = PyBundle.message("postfix.template.condition.number.name")
override fun value(element: PyExpression): Boolean {
val context = TypeEvalContext.codeCompletion(element.project, element.containingFile)
val type = context.getType(element) ?: return false
val builtinCache = PyBuiltinCache.getInstance(element)
val intType = builtinCache.intType
val floatType = builtinCache.floatType
val complexType = builtinCache.complexType
return type == intType || type == floatType || type == complexType
}
companion object {
private const val ID: @NonNls String = "number"
}
}
class PyStringExpression : PySimpleConditionBase(ID) {
override fun getPresentableName(): @Nls String = PyBundle.message("postfix.template.condition.string.name")
override fun value(element: PyExpression): Boolean {
val context = TypeEvalContext.codeCompletion(element.project, element.containingFile)
val type = context.getType(element) ?: return false
val builtinCache = PyBuiltinCache.getInstance(element)
val languageLevel = LanguageLevel.forElement(element)
val types = setOfNotNull(builtinCache.getStringType(languageLevel), builtinCache.getByteStringType(languageLevel),
builtinCache.getUnicodeType(languageLevel))
if (types.contains(type)) return true
return types.filterIsInstance<PyUnionType>().any { it.members.contains(type) }
}
companion object {
private const val ID: @NonNls String = "string"
}
}
class PyIterable : PySimpleConditionBase(ID) {
override fun getPresentableName(): @Nls String = PyBundle.message("postfix.template.condition.iterable.name")
override fun value(element: PyExpression): Boolean {
val context = TypeEvalContext.codeCompletion(element.project, element.containingFile)
val type = context.getType(element) ?: return false
return PyABCUtil.isSubtype(type, PyNames.ITERABLE, context)
}
companion object {
private const val ID: @NonNls String = "iterable"
}
}
abstract class PyCollectionTypeConditionBase(private val type: String, private val presentableNameKey: String) : PySimpleConditionBase(type) {
override fun getPresentableName(): @Nls String = PyBundle.message(presentableNameKey)
override fun value(element: PyExpression): Boolean {
val context = TypeEvalContext.codeCompletion(element.project, element.containingFile)
val elementType = context.getType(element) ?: return false
if (elementType is PyCollectionType) {
return elementType.classQName == type || elementType.pyClass.inherits(context, type)
}
if (elementType is PyClassLikeType) {
return elementType.classQName == type || elementType.inherits(context, type)
}
return false
}
}
class PyDict : PyCollectionTypeConditionBase("dict", "postfix.template.condition.dict.name")
class PyList : PyCollectionTypeConditionBase("list", "postfix.template.condition.list.name")
class PySet : PyCollectionTypeConditionBase("set", "postfix.template.condition.set.name")
class PyTuple : PyCollectionTypeConditionBase("tuple", "postfix.template.condition.tuple.name")
class PyNonNoneExpression : PySimpleConditionBase(ID) {
override fun getPresentableName(): @Nls String = PyBundle.message("postfix.template.condition.non.none.name")
override fun value(element: PyExpression): Boolean {
val context = TypeEvalContext.codeCompletion(element.project, element.containingFile)
val type = context.getType(element) ?: return false
return type != PyNoneType.INSTANCE
}
companion object {
private const val ID: @NonNls String = "non none"
}
}
class PyExceptionExpression : PySimpleConditionBase(ID) {
override fun getPresentableName(): @Nls String = PyBundle.message("postfix.template.condition.exception.name")
override fun value(element: PyExpression): Boolean {
val context = TypeEvalContext.codeCompletion(element.project, element.containingFile)
val type = context.getType(element) ?: return false
if (type !is PyClassType) return false
return PyUtil.isExceptionClass(type.pyClass)
}
companion object {
private const val ID: @NonNls String = "exception"
}
}
class PyBuiltinLenApplicable : PySimpleConditionBase(ID) {
override fun getPresentableName(): @Nls String = PyBundle.message("postfix.template.condition.builtin.len.applicable.name")
override fun value(element: PyExpression): Boolean {
val context = TypeEvalContext.codeCompletion(element.project, element.containingFile)
val type = context.getType(element) ?: return false
return PyABCUtil.isSubtype(type, PyNames.SIZED, context)
}
companion object {
private const val ID: @NonNls String = "builtin len applicable"
}
}
data class PyClassCondition(private val name: QualifiedName) : PySimpleConditionBase(ID) {
override fun getPresentableName(): @NlsSafe String = name.toString()
override fun value(element: PyExpression): Boolean {
var expression: PyExpression? = element
if (!element.containingFile.isPhysical) { // Template engine creates a copy of the psi file and uses an element from the copy.
// We need to use original expression because type compatibility check relies
// on `PsiElement.isEquivalentTo` which fails if a type of the expression is defined
// in the same file as an expression (we get unequal (!=) objects after resolve: desired
// type is in the real file and the expressionType is not).
val at = CompletionUtil.getOriginalElement(element)
expression = PsiTreeUtil.getParentOfType(at, PyExpression::class.java, false)
}
if (expression == null) return false
val context = TypeEvalContext.codeCompletion(expression.project, expression.containingFile)
val expressionType = context.getType(expression) ?: return false
if (expressionType is PyClassLikeType) {
val expected = name.toString()
return expressionType.classQName == expected || expressionType.inherits(context, expected)
}
return false
}
override fun serializeTo(element: Element) {
super.serializeTo(element)
element.setAttribute(TYPE_ATTR, name.toString())
}
companion object {
const val ID: @NonNls String = "type"
const val TYPE_ATTR: @NonNls String = "type"
fun readFrom(element: Element): PyClassCondition? {
val value = element.getAttributeValue(TYPE_ATTR)
return if (value != null) PyClassCondition(QualifiedName.fromDottedString(value)) else null
}
fun create(typeSpec: PyClass): PyClassCondition? {
val name: String? = typeSpec.qualifiedName
return if (name.isNullOrEmpty()) {
null
}
else PyClassCondition(QualifiedName.fromDottedString(name))
}
fun create(typeName: String?): PyClassCondition? {
return if (typeName.isNullOrEmpty()) {
null
}
else PyClassCondition(QualifiedName.fromDottedString(typeName))
}
}
}
companion object {
private fun getConditionsMap(vararg conditions: PyPostfixTemplateExpressionCondition): Map<String, PyPostfixTemplateExpressionCondition> {
val result: MutableMap<String, PyPostfixTemplateExpressionCondition> = LinkedHashMap()
for (condition in conditions) {
result[condition.id] = condition
}
return result
}
// conditions we allow to select in postfix template editor UI
@JvmField
val PUBLIC_CONDITIONS = getConditionsMap(
PyBooleanExpression(),
PyNumberExpression(),
PyStringExpression(),
PyIterable(),
PyDict(),
PyList(),
PySet(),
PyTuple(),
PyNonNoneExpression(),
PyExceptionExpression(),
)
}
}
private fun PyWithAncestors.inherits(evalContext: TypeEvalContext, vararg parentNames: String): Boolean {
val names = parentNames.toHashSet()
return this.getAncestorTypes(evalContext).filterNotNull().mapNotNull(PyClassLikeType::getClassQName).any(names::contains)
}
@@ -1,15 +1,24 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.codeInsight.postfix;
import com.intellij.codeInsight.template.impl.TemplateImpl;
import com.intellij.codeInsight.template.postfix.templates.PostfixTemplate;
import com.intellij.codeInsight.template.postfix.templates.PostfixTemplateProvider;
import com.intellij.codeInsight.template.postfix.templates.PostfixTemplatesUtils;
import com.intellij.codeInsight.template.postfix.templates.editable.PostfixTemplateEditor;
import com.intellij.codeInsight.template.postfix.templates.editable.PostfixTemplateExpressionCondition;
import com.intellij.openapi.editor.Editor;
import com.intellij.psi.PsiFile;
import com.intellij.util.containers.ContainerUtil;
import com.jetbrains.python.PyBundle;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Set;
import static com.intellij.codeInsight.template.postfix.templates.PostfixTemplatesUtils.*;
public class PyPostfixTemplateProvider implements PostfixTemplateProvider {
private final @NotNull Set<PostfixTemplate> myTemplates = ContainerUtil.newHashSet(
@@ -22,6 +31,7 @@ public class PyPostfixTemplateProvider implements PostfixTemplateProvider {
new PyForPostfixTemplate("iter", this),
new PyIsNonePostfixTemplate(this),
new PyIsNotNonePostfixTemplate(this),
new PyIsNotNonePostfixTemplate(this),
new PyPrintPostfixTemplate(this),
new PyMainPostfixTemplate(this),
new PyLenPostfixTemplate(this)
@@ -33,12 +43,54 @@ public class PyPostfixTemplateProvider implements PostfixTemplateProvider {
return "builtin.python";
}
@Override
public @Nullable String getPresentableName() {
return PyBundle.message("postfix.template.provider.name");
}
@NotNull
@Override
public Set<PostfixTemplate> getTemplates() {
return myTemplates;
}
@Override
public @Nullable PostfixTemplateEditor createEditor(@Nullable PostfixTemplate templateToEdit) {
if (templateToEdit == null || templateToEdit instanceof PyEditablePostfixTemplate) {
PyPostfixTemplateEditor result = new PyPostfixTemplateEditor(this);
result.setTemplate(templateToEdit);
return result;
}
return null;
}
@Nullable
@Override
public PostfixTemplate readExternalTemplate(@NotNull String id, @NotNull String name, @NotNull Element templateElement) {
TemplateImpl template = readExternalLiveTemplate(templateElement, this);
if (template == null) return null;
Set<PyPostfixTemplateExpressionCondition> conditions =
readExternalConditions(templateElement, PyPostfixTemplateProvider::readCondition);
boolean useTopmostExpression = readExternalTopmostAttribute(templateElement);
return new PyEditablePostfixTemplate(id, name, template, "", conditions, useTopmostExpression, this, false /*?*/);
}
@Override
public void writeExternalTemplate(@NotNull PostfixTemplate template, @NotNull Element parentElement) {
if (template instanceof PyEditablePostfixTemplate) {
PostfixTemplatesUtils.writeExternalTemplate(template, parentElement);
}
}
@Nullable
private static PyPostfixTemplateExpressionCondition readCondition(@NotNull Element conditionElement) {
String id = conditionElement.getAttributeValue(PostfixTemplateExpressionCondition.ID_ATTR);
return PyPostfixTemplateExpressionCondition.PyClassCondition.ID.equals(id) ?
PyPostfixTemplateExpressionCondition.PyClassCondition.Companion.readFrom(conditionElement) :
PyPostfixTemplateExpressionCondition.PUBLIC_CONDITIONS.get(id);
}
@Override
public boolean isTerminalSymbol(char currentChar) {
return currentChar == '.'|| currentChar == '!';
@@ -1,4 +1,4 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.codeInsight.postfix;
import com.intellij.codeInsight.template.postfix.templates.PostfixTemplateExpressionSelector;
@@ -9,6 +9,7 @@ import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Conditions;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.util.containers.ContainerUtil;
@@ -42,26 +43,30 @@ public final class PyPostfixUtils {
};
public static PostfixTemplateExpressionSelector selectorAllExpressionsWithCurrentOffset(final Condition<PsiElement> additionalFilter) {
public static PostfixTemplateExpressionSelector selectorAllExpressionsWithCurrentOffset(final Condition<? super PsiElement> additionalFilter) {
return new PostfixTemplateExpressionSelectorBase(additionalFilter) {
@Override
protected List<PsiElement> getNonFilteredExpressions(@NotNull PsiElement context, @NotNull Document document, int newOffset) {
PsiElement elementAtCaret = PsiUtilCore.getElementAtOffset(context.getContainingFile(), newOffset - 1);
final List<PsiElement> expressions = new ArrayList<>();
while (elementAtCaret != null) {
if (elementAtCaret instanceof PyStatement || elementAtCaret instanceof PyFile) {
break;
}
if (elementAtCaret instanceof PyExpression) {
expressions.add(elementAtCaret);
}
elementAtCaret = elementAtCaret.getParent();
}
return expressions;
return getAllExpressionsAtOffset(context.getContainingFile(), newOffset - 1);
}
};
}
public static @NotNull List<PsiElement> getAllExpressionsAtOffset(PsiFile file, int offset) {
PsiElement elementAtCaret = PsiUtilCore.getElementAtOffset(file, offset);
final List<PsiElement> expressions = new ArrayList<>();
while (elementAtCaret != null) {
if (elementAtCaret instanceof PyStatement || elementAtCaret instanceof PyFile) {
break;
}
if (elementAtCaret instanceof PyExpression) {
expressions.add(elementAtCaret);
}
elementAtCaret = elementAtCaret.getParent();
}
return expressions;
}
public static PostfixTemplateExpressionSelector selectorAllExpressionsWithCurrentOffset() {
return selectorAllExpressionsWithCurrentOffset(Conditions.alwaysTrue());
}
@@ -0,0 +1,7 @@
class CT:
def __init__(self):
pass
def _():
x = CT()
x.foo<caret>
@@ -0,0 +1,3 @@
def _():
x = set()
x.foo<caret>
@@ -0,0 +1,3 @@
def _():
x = set()
x.foo
@@ -0,0 +1,7 @@
class CT:
def __init__(self):
pass
def _():
x = CT()
foo(x)
@@ -0,0 +1,5 @@
def e() -> BaseException:
return BaseException()
def _():
e().foo<caret>
@@ -0,0 +1,5 @@
def e() -> BaseException:
return BaseException()
def _():
foo(e())
@@ -0,0 +1,5 @@
def _():
try:
raise Exception()
except Exception as e:
e.foo<caret>
@@ -0,0 +1,5 @@
def _():
try:
raise Exception()
except Exception as e:
foo(e)
@@ -0,0 +1,2 @@
def _(e: Exception):
e.foo<caret>
@@ -0,0 +1,2 @@
def _(e: Exception):
foo(e)<caret>
@@ -0,0 +1,7 @@
class CIT:
def __iter__(self):
pass
def _():
x = CIT()
x.foo<caret>
@@ -0,0 +1,7 @@
class CIT:
def __iter__(self):
pass
def _():
x = CIT()
foo(x)
@@ -0,0 +1,119 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.postfix;
import com.intellij.codeInsight.template.postfix.settings.PostfixTemplateStorage;
import com.intellij.codeInsight.template.postfix.templates.LanguagePostfixTemplate;
import com.intellij.codeInsight.template.postfix.templates.PostfixTemplate;
import com.intellij.lang.LanguageExtensionPoint;
import com.intellij.util.ObjectUtils;
import com.jetbrains.python.codeInsight.postfix.PyEditablePostfixTemplate;
import com.jetbrains.python.codeInsight.postfix.PyPostfixTemplateExpressionCondition;
import com.jetbrains.python.codeInsight.postfix.PyPostfixTemplateProvider;
import com.jetbrains.python.psi.LanguageLevel;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Collections;
public class PyEditablePostfixTemplatesTest extends PyPostfixTemplateTestCase {
public void testEditableTemplateBoolean() {
registerTemplate(createTemplate(new PyPostfixTemplateExpressionCondition.PyBooleanExpression()));
Arrays.asList("True", "False", "True or False", "1 == 0").forEach(this::doSimpleTest);
}
public void testEditableTemplateNumeric() {
registerTemplate(createTemplate(new PyPostfixTemplateExpressionCondition.PyNumberExpression()));
Arrays.asList("1", "int(1)", "1.0", "float(1.0)", "1j", "complex(1,-1)", "1-1j").forEach(this::doSimpleTest);
}
public void testEditableTemplateString() {
registerTemplate(createTemplate(new PyPostfixTemplateExpressionCondition.PyStringExpression()));
Arrays.asList(LanguageLevel.PYTHON27, LanguageLevel.PYTHON312).forEach(level -> runWithLanguageLevel(level, () -> {
Arrays.asList("'text'", "\"text\"", "b't'", "\"\\u0394\"").forEach(this::doSimpleTest);
}));
}
public void testEditableTemplateIterable() {
registerTemplate(createTemplate(new PyPostfixTemplateExpressionCondition.PyIterable()));
doTest();
}
public void testEditableTemplateDict() {
registerTemplate(createTemplate(new PyPostfixTemplateExpressionCondition.PyDict()));
Arrays.asList("{}", "{'a':1}", "dict()").forEach(this::doSimpleTest);
}
public void testEditableTemplateList() {
registerTemplate(createTemplate(new PyPostfixTemplateExpressionCondition.PyList()));
Arrays.asList("[]", "[1,2,3]", "list()").forEach(this::doSimpleTest);
}
public void testEditableTemplateSet() {
registerTemplate(createTemplate(new PyPostfixTemplateExpressionCondition.PySet()));
Arrays.asList("{1,2,3}", "set()").forEach(this::doSimpleTest);
}
public void testEditableTemplateTuple() {
registerTemplate(createTemplate(new PyPostfixTemplateExpressionCondition.PyTuple()));
Arrays.asList("(1,2,3)", "tuple()").forEach(this::doSimpleTest);
}
public void testEditableTemplateLenCapable() {
registerTemplate(createTemplate(new PyPostfixTemplateExpressionCondition.PyBuiltinLenApplicable()));
Arrays.asList("{}", "[]", "set()", "tuple()", "'string'").forEach(this::doSimpleTest);
}
public void testEditableTemplateConcreteType() {
// Note that we've to include file name to align with QualifiedNameFinder.getQualifiedName logic
registerTemplate(
createTemplate(PyPostfixTemplateExpressionCondition.PyClassCondition.Companion.create("editableTemplateConcreteType.CT")));
doTest();
}
public void testEditableTemplateConcreteTypeInapplicable() {
registerTemplate(createTemplate(PyPostfixTemplateExpressionCondition.PyClassCondition.Companion.create("list")));
doTest();
}
public void testEditableTemplateExceptionCall() {
registerTemplate(createTemplate(new PyPostfixTemplateExpressionCondition.PyExceptionExpression()));
doTest();
}
public void testEditableTemplateExceptionVar() {
registerTemplate(createTemplate(new PyPostfixTemplateExpressionCondition.PyExceptionExpression()));
doTest();
}
public void testEditableTemplateExceptionExcept() {
registerTemplate(createTemplate(new PyPostfixTemplateExpressionCondition.PyExceptionExpression()));
doTest();
}
@Override
protected String getTestDataDir() {
return "editable/";
}
private static void registerTemplate(@NotNull PostfixTemplate template) {
PostfixTemplateStorage saveStorage = new PostfixTemplateStorage();
saveStorage.setTemplates(new PyPostfixTemplateProvider(), Collections.singletonList(template));
PostfixTemplateStorage.getInstance().loadState(saveStorage.getState());
}
@NotNull
private static PyEditablePostfixTemplate createTemplate(PyPostfixTemplateExpressionCondition condition) {
return new PyEditablePostfixTemplate("foo", "foo", "foo($EXPR$)", "", Collections.singleton(condition), true, getProvider(), false);
}
private static PyPostfixTemplateProvider getProvider() {
for (LanguageExtensionPoint point : LanguagePostfixTemplate.EP_NAME.getExtensionList()) {
PyPostfixTemplateProvider provider = ObjectUtils.tryCast(point.getInstance(), PyPostfixTemplateProvider.class);
if (provider != null) {
return provider;
}
}
throw new RuntimeException("PyPostfixTemplateProvider not found");
}
}
@@ -1,18 +1,4 @@
/*
* 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.
*/
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.postfix;
import com.jetbrains.python.PythonTestUtil;
@@ -26,6 +12,16 @@ public abstract class PyPostfixTemplateTestCase extends PyTestCase {
myFixture.checkResultByFile(getTestName(true) + "_after" + ".py", true);
}
protected void doTest(String input, String expected) {
myFixture.configureByText("input.py", input);
myFixture.type("\t");
myFixture.checkResult(expected, true);
}
protected void doSimpleTest(String declaration) {
doTest("x = %s\nx.foo<caret>\n".formatted(declaration), "x = %s\nfoo(x)\n".formatted(declaration));
}
abstract protected String getTestDataDir();
@Override