blob: 7c105da6b3526664f6a1684695ad0a95294840fb [file] [log] [blame]
/*
* Copyright (c) 2010-2020 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
package org.eclipse.scout.sdk.s2i.template
import com.intellij.application.options.CodeStyle
import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.completion.InsertionContext
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.template.Template
import com.intellij.codeInsight.template.TemplateEditingAdapter
import com.intellij.codeInsight.template.TemplateManager
import com.intellij.codeInsight.template.impl.TemplateImpl
import com.intellij.codeInsight.template.impl.TemplateImplUtil
import com.intellij.codeInsight.template.impl.TemplateState
import com.intellij.openapi.command.WriteCommandAction.writeCommandAction
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.util.Conditions.alwaysTrue
import com.intellij.psi.*
import com.intellij.psi.codeStyle.CodeStyleSettings
import com.intellij.psi.codeStyle.CodeStyleSettingsManager
import com.intellij.psi.codeStyle.JavaCodeStyleManager
import com.intellij.psi.codeStyle.JavaCodeStyleSettings
import com.intellij.psi.util.InheritanceUtil.findEnclosingInstanceInScope
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.util.ThrowableRunnable
import org.eclipse.scout.sdk.core.log.SdkLog
import org.eclipse.scout.sdk.core.log.SdkLog.onTrace
import org.eclipse.scout.sdk.core.model.api.PropertyBean
import org.eclipse.scout.sdk.core.util.Ensure.newFail
import org.eclipse.scout.sdk.core.util.FinalValue
import org.eclipse.scout.sdk.core.util.Strings
import org.eclipse.scout.sdk.s2i.containingModule
import org.eclipse.scout.sdk.s2i.findTypeByName
import org.eclipse.scout.sdk.s2i.isInstanceOf
import java.lang.reflect.Method
/**
* Handler that inserts a selected [TemplateDescriptor].
*/
class TemplateInsertHandler(val templateDescriptor: TemplateDescriptor, val prefix: String) : InsertHandler<LookupElement> {
companion object {
private val CREATE_TEMP_SETTINGS_METHOD = FinalValue<Method>()
private fun createTempSettings(origSettings: CodeStyleSettings, settingsManager: CodeStyleSettingsManager): CodeStyleSettings {
val createTemporarySettings = CREATE_TEMP_SETTINGS_METHOD.computeIfAbsentAndGet { createTemporarySettingsMethod() }
if (createTemporarySettings != null) {
// use createTemporarySettings() factory method in IJ 2020.2 and newer
val tempSettings = createTemporarySettings.invoke(settingsManager) as CodeStyleSettings
tempSettings.copyFrom(origSettings)
return tempSettings
}
// use clone method until IJ 2020.1
// Can be removed if the supported min. IJ version is 2020.2
return CodeStyleSettings::class.java.getMethod("clone").invoke(origSettings) as CodeStyleSettings
}
private fun createTemporarySettingsMethod() =
try {
CodeStyleSettingsManager::class.java.getMethod("createTemporarySettings")
} catch (e: NoSuchMethodException) {
SdkLog.debug("Using legacy temporary CodeStyleSettings creation.", onTrace(e))
null
}
}
private lateinit var m_engine: TemplateEngine
override fun handleInsert(context: InsertionContext, item: LookupElement) {
val editor = context.editor
val declaringClass = item.getObject() as PsiClass
val containingModule = declaringClass.containingModule() ?: return
m_engine = TemplateEngine(templateDescriptor, TemplateEngine.TemplateContext(declaringClass, containingModule, editor.caretModel.offset))
startTemplateWithTempSettings(buildTemplate(), editor)
}
/**
* The templates do not work if the setting "InsertInnerClassImports" is active.
* Therefore execute the template with temporary settings (see [CodeStyleSettingsManager.setTemporarySettings]).
* The temporary settings will be removed again in the [TemplateListener].
*/
private fun startTemplateWithTempSettings(template: TemplateImpl, editor: Editor) {
val project = editor.project
val settingsManager = CodeStyleSettingsManager.getInstance(project)
val origTempSettings = settingsManager.temporarySettings
val tempSettings = createTempSettings(CodeStyle.getSettings(editor), settingsManager)
val tempJavaSettings = tempSettings.getCustomSettings(JavaCodeStyleSettings::class.java)
tempJavaSettings.isInsertInnerClassImports = false
writeCommandAction(project).run(ThrowableRunnable<RuntimeException> {
val templateListener = TemplateListener(templateDescriptor, settingsManager, origTempSettings)
removePrefix(editor)
settingsManager.setTemporarySettings(tempSettings)
TemplateManager.getInstance(project).startTemplate(editor, template, templateListener)
})
}
private fun removePrefix(editor: Editor) {
if (Strings.isEmpty(prefix)) {
return
}
val document = editor.document
val offset = editor.caretModel.offset
var start = offset - prefix.length - 1
val limit = 0.coerceAtLeast(start - 5)
val chars = document.immutableCharSequence
// reduce start index of removal to any preceding alphabet characters
// this is required for fast typing where the prefix is "older" than the current content of the document
while (start >= limit && isAlphaChar(chars[start])) {
start--
}
document.replaceString(start + 1, offset, "")
}
private fun isAlphaChar(char: Char) = char in 'a'..'z' || char in 'A'..'Z'
private fun buildTemplate(): TemplateImpl {
val source = m_engine.buildTemplate()
val template = TemplateImpl(templateDescriptor.id, source, "Scout")
template.id = this.templateDescriptor.id
template.description = templateDescriptor.description()
template.isToShortenLongNames = true
template.isToReformat = true
template.isDeactivated = false
template.isToIndent = true
template.setValue(Template.Property.USE_STATIC_IMPORT_IF_POSSIBLE, false)
TemplateImplUtil.parseVariableNames(source).forEach { addVariable(it, template) }
template.parseSegments()
return template
}
private fun addVariable(name: String, target: TemplateImpl) {
// com.intellij.codeInsight.Template internal variables (like "END")
if (TemplateImpl.INTERNAL_VARS_SET.contains(name)) {
return
}
val adapter = templateDescriptor.variable(name) ?: throw newFail("Variable '{}' is used in the template source but not declared in the template descriptor.", name)
val descriptor = adapter.invoke(m_engine) ?: return
target.addVariable(descriptor.name, descriptor.expression, descriptor.defaultValueExpression, true)
}
private class TemplateListener(private val templateDescriptor: TemplateDescriptor, private val settingsManager: CodeStyleSettingsManager, private val origSettings: CodeStyleSettings?) : TemplateEditingAdapter() {
override fun templateCancelled(template: Template?) {
resetTemporarySettings()
}
override fun beforeTemplateFinished(state: TemplateState, template: Template) {
try {
insertInnerTypeGetter(state)
} finally {
resetTemporarySettings()
}
}
private fun resetTemporarySettings() {
if (origSettings != null) {
settingsManager.setTemporarySettings(origSettings)
} else {
settingsManager.dropTemporarySettings()
}
}
private fun resolveInnerTypeGetterContainer(createdClass: PsiClass): Pair<PsiClass, String>? {
val containingModule = createdClass.containingModule() ?: return null
for (info in templateDescriptor.innerTypeGetterInfos()) {
val container = if (info.lookupType == TemplateDescriptor.InnerTypeGetterLookupType.CLOSEST) {
val innerTypeGetterBase = containingModule.findTypeByName(info.definitionClassFqn) ?: continue
findEnclosingInstanceInScope(innerTypeGetterBase, createdClass, alwaysTrue(), false)
} else {
PsiTreeUtil.collectParents(createdClass, PsiClass::class.java, false) { it is PsiFile }
.lastOrNull { it.isInstanceOf(info.definitionClassFqn) }
}
if (container != null) {
return container to info.methodName
}
}
return null
}
private fun insertInnerTypeGetter(state: TemplateState) {
val editor = state.editor
val project = editor.project ?: return
val document = editor.document
val nameRange = state.getVariableRange(TemplateDescriptor.VARIABLE_NAME) ?: return
val psiDocumentManager = PsiDocumentManager.getInstance(project)
val file = psiDocumentManager.getPsiFile(document) ?: return
val element = file.findElementAt(nameRange.startOffset) ?: return
val createdClass = PsiTreeUtil.getParentOfType(element, PsiClass::class.java) ?: return
val innerTypeGetterInfo = resolveInnerTypeGetterContainer(createdClass) ?: return
val createdClassFqn = createdClass.qualifiedName
val createdClassSimpleName = Strings.ensureStartWithUpperCase(createdClass.name)
val psiElementFactory = JavaPsiFacade.getElementFactory(project)
val methodName = PropertyBean.GETTER_PREFIX + createdClassSimpleName
val innerTypeGetterContainer = innerTypeGetterInfo.first
val innerTypeGetterMethodName = innerTypeGetterInfo.second
val innerTypeGetter = psiElementFactory.createMethodFromText("public $createdClassFqn $methodName() { return $innerTypeGetterMethodName($createdClassFqn.class);}", innerTypeGetterContainer)
if (innerTypeGetterContainer.findMethodBySignature(innerTypeGetter, false) != null) {
return // method already exists
}
val anchorInfo = getInsertAnchor(innerTypeGetterContainer, methodName, innerTypeGetterMethodName, document)
JavaCodeStyleManager.getInstance(project).shortenClassReferences(innerTypeGetter)
writeCommandAction(project).run(ThrowableRunnable<RuntimeException> {
if (anchorInfo == null) {
innerTypeGetterContainer.add(innerTypeGetter)
} else {
if (anchorInfo.second) {
innerTypeGetterContainer.addAfter(innerTypeGetter, anchorInfo.first)
} else {
innerTypeGetterContainer.addBefore(innerTypeGetter, anchorInfo.first)
}
}
psiDocumentManager.doPostponedOperationsAndUnblockDocument(document)
})
}
private fun getInsertAnchor(innerTypeGetterContainer: PsiClass, methodName: String, innerTypeGetterMethodName: String, document: Document): Pair<PsiElement, Boolean>? {
val gettersWithSource = innerTypeGetterContainer.methods
.filter { it.name.startsWith(PropertyBean.GETTER_PREFIX) }
.map { it to document.getText(it.textRange) }
val otherInnerTypeGetters = gettersWithSource
.filter { it.second.contains(innerTypeGetterMethodName) }
.map { it.first }
return getInsertAnchorFrom(otherInnerTypeGetters, methodName) // find position within other inner-type-getters first
?: getInsertAnchorFrom(gettersWithSource.map { it.first }, methodName) // find position within all getters
?: getInsertAnchorFrom(innerTypeGetterContainer.methods.filter { !it.isConstructor }, methodName) // find position within all methods
}
private fun getInsertAnchorFrom(candidates: Iterable<PsiMethod>, methodName: String) = candidates
.lastOrNull { it.name < methodName }
?.let { it to true } // sort ascending, add after
?: candidates
.firstOrNull()
?.let { it to false } // all must be after, add before the first
}
}