mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[python] postfix templates: support editable templates and conditions PY-30217
GitOrigin-RevId: 03b1a49f8ecfd7c765ed2900b2adf14b07709a5f
This commit is contained in:
committed by
intellij-monorepo-bot
parent
5199718c84
commit
a396b4cdd2
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+246
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user