blob: efd6d420b0bd530be51b19af326606dedbe1f81a [file] [log] [blame]
/*
* Copyright (c) 2010-2021 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
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
package org.eclipse.scout.sdk.s2i.template.js
import com.intellij.codeInsight.completion.*
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.lang.ecmascript6.actions.ES6AddImportExecutor
import com.intellij.lang.ecmascript6.psi.impl.ES6ImportPsiUtil
import com.intellij.lang.ecmascript6.psi.impl.ES6ImportPsiUtil.CreateImportExportInfo
import com.intellij.lang.javascript.patterns.JSPatterns.jsProperty
import com.intellij.lang.javascript.psi.JSArrayLiteralExpression
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.patterns.PlatformPatterns.psiElement
import com.intellij.psi.PsiElement
import com.intellij.util.ProcessingContext
import org.eclipse.scout.sdk.core.s.IWebConstants
import org.eclipse.scout.sdk.core.util.Strings
import org.eclipse.scout.sdk.s2i.model.js.JsModel
import org.eclipse.scout.sdk.s2i.model.js.JsModelProperty
import org.eclipse.scout.sdk.s2i.template.js.JsModelCompletionHelper.PropertyCompletionInfo
import org.eclipse.scout.sdk.s2i.template.js.JsModelCompletionHelper.createLookupElement
import org.eclipse.scout.sdk.s2i.template.js.JsModelCompletionHelper.getPropertyValueInfo
import org.eclipse.scout.sdk.s2i.template.js.JsModelCompletionHelper.propertyElementPattern
import org.eclipse.scout.sdk.s2i.util.compat.CompatibilityMethodCaller
import org.eclipse.scout.sdk.s2i.util.compat.CompatibilityMethodCaller.ResolvedMethod
class JsModelValueCompletionContributor : CompletionContributor() {
init {
// normal value completion
extend(CompletionType.BASIC, propertyElementPattern(), JsModelValueCompletionProvider())
// value completion when within an array (to add an additional array element)
extend(CompletionType.BASIC, psiElement().withSuperParent(2, psiElement(JSArrayLiteralExpression::class.java).withParent(jsProperty())), JsModelValueCompletionProvider())
}
private class JsModelValueCompletionProvider : CompletionProvider<CompletionParameters>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val completionInfo = getPropertyValueInfo(parameters, result) ?: return
// require an objectType to be set or the completion of the object type itself
if (completionInfo.objectType == null && JsModel.OBJECT_TYPE_PROPERTY_NAME != completionInfo.propertyName) return
val elements = getPropertyValueElements(completionInfo)
if (elements.isNotEmpty()) {
result.addAllElements(elements)
result.stopHere()
}
}
private fun getPropertyValueElements(completionInfo: PropertyCompletionInfo): List<LookupElement> {
val jsModel = JsModel().build(completionInfo.module)
val jsProperty = if (JsModel.OBJECT_TYPE_PROPERTY_NAME == completionInfo.propertyName) {
getPropertyValueInfo(completionInfo.property, "")
?.let { jsModel.property(it.objectType, it.propertyName) }
?.takeIf { it.dataType == JsModelProperty.JsPropertyDataType.WIDGET } // only accept widgets filter. for all other types use the default: all known top-level objects
?: jsModel.property(JsModel.WIDGET_CLASS_NAME, JsModel.OBJECT_TYPE_PROPERTY_NAME)
} else {
jsModel.property(completionInfo.objectType, completionInfo.propertyName)
} ?: return emptyList()
return jsModel.valuesForProperty(jsProperty).map {
val lookupElement = createLookupElement(it.displayText, it.element, jsProperty, completionInfo)
withJsImportIfNecessary(lookupElement, completionInfo.property.containingFile.originalFile, jsProperty)
}
}
private fun withJsImportIfNecessary(lookupElement: LookupElementBuilder, place: PsiElement, jsProperty: JsModelProperty): LookupElementBuilder {
if (!jsProperty.dataType.isCustomType()) return lookupElement
val jsModel = jsProperty.scoutJsModule.jsModel
val type = jsModel.element(jsProperty.dataType.type) ?: return lookupElement
val containingFile = place.containingFile.virtualFile
val thisModule = jsModel.containingModule(containingFile) ?: return lookupElement
val targetModule = type.scoutJsModule
val originalHandler = lookupElement.insertHandler
return lookupElement.withInsertHandler { context, item ->
originalHandler?.handleInsert(context, item)
val moduleOrNamespaceName = if (thisModule == targetModule) {
val relPath = VfsUtilCore.findRelativePath(containingFile, thisModule.mainFile, VfsUtilCore.VFS_SEPARATOR_CHAR) ?: targetModule.name
relPath.removeSuffix(IWebConstants.JS_FILE_SUFFIX)
} else {
targetModule.name
}
var importName = type.name
val firstDot = importName.indexOf('.')
if (firstDot > 0) importName = importName.take(firstDot)
val executor = ES6AddImportExecutor(context.editor, place)
createImportOrUseExisting(importName, moduleOrNamespaceName, executor)
}
}
/**
* CreateImportExportInfo creation changed with IJ 2020.3
* Can be removed if the minimal supported IJ version is >= 2020.3
*/
private val m_createImportOrUseExisting = CompatibilityMethodCaller<CreateImportExportInfo>()
.withCandidate("com.intellij.lang.ecmascript6.psi.impl.ES6ImportPsiUtil\$CreateImportExportInfo", CompatibilityMethodCaller.CONSTRUCTOR_NAME,
String::class.java.name, "com.intellij.lang.javascript.modules.imports.JSImportExportType") {
val moduleOrNamespaceName = it.args[1] as String
// IJ 2020.3: module must not be quoted
createImportOrUseExisting(moduleOrNamespaceName, it)
}
.withCandidate(CreateImportExportInfo::class.java, CompatibilityMethodCaller.CONSTRUCTOR_NAME, String::class.java, ES6ImportPsiUtil.ImportExportType::class.java) {
val moduleOrNamespaceName = it.args[1] as String
// before IJ 2020.3: module must be quoted
createImportOrUseExisting(Strings.toStringLiteral(moduleOrNamespaceName).toString(), it)
}
private fun createImportOrUseExisting(moduleOrNamespaceName: String, constr: ResolvedMethod<CreateImportExportInfo>): CreateImportExportInfo {
val importedName = constr.args[0] as String
val executor = constr.args[2] as ES6AddImportExecutor
val info = constr.invokeStatic(importedName, ES6ImportPsiUtil.ImportExportType.SPECIFIER)
executor.createImportOrUseExisting(info, null, moduleOrNamespaceName)
return info
}
fun createImportOrUseExisting(importedName: String, moduleOrNamespaceName: String, executor: ES6AddImportExecutor) = m_createImportOrUseExisting.invoke(importedName, moduleOrNamespaceName, executor)
}
}