FIR IDE: Refactor functions insertion handler

This commit is contained in:
Roman Golyshev
2021-03-15 02:26:17 +03:00
parent 43b829b1f0
commit fe921b98ce
9 changed files with 147 additions and 87 deletions
@@ -0,0 +1,8 @@
// FIR_COMPARISON
fun usage() {
fun myLocalFun() {}
myLocalFu<caret>
}
// ELEMENT: myLocalFun
@@ -0,0 +1,8 @@
// FIR_COMPARISON
fun usage() {
fun myLocalFun() {}
myLocalFun()
}
// ELEMENT: myLocalFun
@@ -0,0 +1,8 @@
// FIR_COMPARISON
fun usage() {
fun myLocalFun(action: () -> Unit) {}
myLocalFu<caret>
}
// ELEMENT: myLocalFun
@@ -0,0 +1,8 @@
// FIR_COMPARISON
fun usage() {
fun myLocalFun(action: () -> Unit) {}
myLocalFun {<caret>}
}
// ELEMENT: myLocalFun
@@ -154,6 +154,11 @@ public class BasicCompletionHandlerTestGenerated extends AbstractBasicCompletion
runTest("idea/idea-completion/testData/handlers/basic/KT36306.kt");
}
@TestMetadata("LocalFunction.kt")
public void testLocalFunction() throws Exception {
runTest("idea/idea-completion/testData/handlers/basic/LocalFunction.kt");
}
@TestMetadata("NestedTypeArg.kt")
public void testNestedTypeArg() throws Exception {
runTest("idea/idea-completion/testData/handlers/basic/NestedTypeArg.kt");
@@ -478,6 +483,11 @@ public class BasicCompletionHandlerTestGenerated extends AbstractBasicCompletion
runTest("idea/idea-completion/testData/handlers/basic/highOrderFunctions/HigherOrderSuspendFunctionWithArgs.kt");
}
@TestMetadata("InsertFunctionLiteralForLocalFunction.kt")
public void testInsertFunctionLiteralForLocalFunction() throws Exception {
runTest("idea/idea-completion/testData/handlers/basic/highOrderFunctions/InsertFunctionLiteralForLocalFunction.kt");
}
@TestMetadata("InsertFunctionWithSingleParameterWithBrace.kt")
public void testInsertFunctionWithSingleParameterWithBrace() throws Exception {
runTest("idea/idea-completion/testData/handlers/basic/highOrderFunctions/InsertFunctionWithSingleParameterWithBrace.kt");
@@ -154,6 +154,11 @@ public class HighLevelPerformanceBasicCompletionHandlerTestGenerated extends Abs
runTest("idea/idea-completion/testData/handlers/basic/KT36306.kt");
}
@TestMetadata("LocalFunction.kt")
public void testLocalFunction() throws Exception {
runTest("idea/idea-completion/testData/handlers/basic/LocalFunction.kt");
}
@TestMetadata("NestedTypeArg.kt")
public void testNestedTypeArg() throws Exception {
runTest("idea/idea-completion/testData/handlers/basic/NestedTypeArg.kt");
@@ -478,6 +483,11 @@ public class HighLevelPerformanceBasicCompletionHandlerTestGenerated extends Abs
runTest("idea/idea-completion/testData/handlers/basic/highOrderFunctions/HigherOrderSuspendFunctionWithArgs.kt");
}
@TestMetadata("InsertFunctionLiteralForLocalFunction.kt")
public void testInsertFunctionLiteralForLocalFunction() throws Exception {
runTest("idea/idea-completion/testData/handlers/basic/highOrderFunctions/InsertFunctionLiteralForLocalFunction.kt");
}
@TestMetadata("InsertFunctionWithSingleParameterWithBrace.kt")
public void testInsertFunctionWithSingleParameterWithBrace() throws Exception {
runTest("idea/idea-completion/testData/handlers/basic/highOrderFunctions/InsertFunctionWithSingleParameterWithBrace.kt");
@@ -55,22 +55,39 @@ internal class KotlinFirLookupElementFactory {
}
}
private sealed class FunctionImportStrategy {
object DoNothing : FunctionImportStrategy()
data class AddImport(val nameToImport: FqName) : FunctionImportStrategy()
data class InsertFqNameAndShorten(val fqName: FqName) : FunctionImportStrategy()
}
/**
* This is a temporary hack to prevent clash of the lookup elements with same names.
*/
private class UniqueLookupObject
private data class ClassifierLookupObject(val shortName: Name, val classId: ClassId?)
private interface KotlinLookupObject {
val shortName: Name
}
private data class ClassifierLookupObject(override val shortName: Name, val classId: ClassId?) : KotlinLookupObject
/**
* Simplest lookup object so two lookup elements for the same function will clash.
*/
private data class FunctionLookupObject(val name: Name, val callableIdIfNonLocal: FqName?, val renderedFunctionParameters: String)
private data class FunctionLookupObject(
override val shortName: Name,
val importingStrategy: FunctionImportStrategy,
val inputValueArguments: Boolean,
val insertEmptyLambda: Boolean,
// for distinction between different overloads
private val renderedFunctionParameters: String
) : KotlinLookupObject
/**
* Simplest lookup object so two lookup elements for the same property will clash.
*/
private data class VariableLookupObject(val name: Name, val callableIdIfNonLocal: FqName?)
private data class VariableLookupObject(override val shortName: Name, val callableIdIfNonLocal: FqName?) : KotlinLookupObject
private class ClassLookupElementFactory {
fun createLookup(symbol: KtClassLikeSymbol): LookupElementBuilder {
@@ -80,7 +97,7 @@ private class ClassLookupElementFactory {
private fun createInsertHandler(symbol: KtClassLikeSymbol): InsertHandler<LookupElement> {
val classFqName = symbol.classIdIfNonLocal?.asSingleFqName()
?: return QuotedNamesAwareInsertionHandler(symbol.name)
?: return QuotedNamesAwareInsertionHandler()
return ClassifierInsertionHandler(classFqName)
}
@@ -116,12 +133,12 @@ private class VariableLookupElementFactory {
if (setterName != null) "$getterName()/$setterName()" else "$getterName()"
private fun createInsertHandler(symbol: KtVariableLikeSymbol): InsertHandler<LookupElement> {
val callableId = symbol.callableIdIfExists ?: return QuotedNamesAwareInsertionHandler(symbol.name)
val callableId = symbol.callableIdIfExists ?: return QuotedNamesAwareInsertionHandler()
return if (symbol.canBeCalledByFqName) {
ShorteningVariableInsertionHandler(callableId)
} else {
SimpleVariableInsertionHandler(symbol.name, symbol.importableFqName)
SimpleVariableInsertionHandler(symbol.importableFqName)
}
}
@@ -144,15 +161,17 @@ private class FunctionLookupElementFactory {
fun KtAnalysisSession.createLookup(symbol: KtFunctionSymbol): LookupElementBuilder? {
val lookupObject = FunctionLookupObject(
symbol.name,
symbol.callableIdIfNonLocal,
with(ShortNamesRenderer) { renderFunctionParameters(symbol) }
importingStrategy = detectImportingStrategy(symbol),
inputValueArguments = symbol.valueParameters.isNotEmpty(),
insertEmptyLambda = insertLambdaBraces(symbol),
renderedFunctionParameters = with(ShortNamesRenderer) { renderFunctionParameters(symbol) }
)
return try {
LookupElementBuilder.create(lookupObject, symbol.name.asString())
.withTailText(getTailText(symbol), true)
.withTypeText(symbol.annotatedType.type.render())
.withInsertHandler(createInsertHandler(symbol))
.withInsertHandler(ShorteningFunctionInsertionHandler)
} catch (e: Throwable) {
if (e is ControlFlowException) throw e
LOG.error(e)
@@ -160,6 +179,16 @@ private class FunctionLookupElementFactory {
}
}
private fun detectImportingStrategy(symbol: KtFunctionSymbol): FunctionImportStrategy {
val functionFqName = symbol.callableIdIfNonLocal
return when {
functionFqName == null -> FunctionImportStrategy.DoNothing
symbol.dispatchType != null -> FunctionImportStrategy.DoNothing
!symbol.isExtension -> FunctionImportStrategy.InsertFqNameAndShorten(functionFqName)
else -> FunctionImportStrategy.AddImport(functionFqName)
}
}
private fun KtAnalysisSession.getTailText(symbol: KtFunctionSymbol): String {
return if (insertLambdaBraces(symbol)) " {...}" else with(ShortNamesRenderer) { renderFunctionParameters(symbol) }
}
@@ -169,31 +198,6 @@ private class FunctionLookupElementFactory {
return singleParam != null && !singleParam.hasDefaultValue && singleParam.annotatedType.type is KtFunctionalType
}
private fun KtAnalysisSession.createInsertHandler(symbol: KtFunctionSymbol): InsertHandler<LookupElement> {
val functionFqName = symbol.callableIdIfNonLocal
return if (functionFqName != null && canBeCalledByFqName(symbol)) {
ShorteningFunctionInsertionHandler(
functionFqName,
inputValueArguments = symbol.valueParameters.isNotEmpty(),
insertEmptyLambda = insertLambdaBraces(symbol)
)
} else {
SimpleFunctionInsertionHandler(
symbol.name,
inputValueArguments = symbol.valueParameters.isNotEmpty(),
insertEmptyLambda = insertLambdaBraces(symbol),
nameToImport = symbol.importableFqName,
)
}
}
private val KtFunctionSymbol.importableFqName: FqName?
get() = if (dispatchType == null) callableIdIfNonLocal else null
private fun canBeCalledByFqName(symbol: KtFunctionSymbol): Boolean {
return !symbol.isExtension && symbol.dispatchType == null
}
companion object {
private val LOG = logger<FunctionLookupElementFactory>()
}
@@ -213,12 +217,8 @@ private class ClassifierInsertionHandler(private val fqName: FqName) : InsertHan
}
}
private abstract class AbstractFunctionInsertionHandler(
name: Name,
private val inputValueArguments: Boolean,
private val insertEmptyLambda: Boolean
) : QuotedNamesAwareInsertionHandler(name) {
protected fun addArguments(context: InsertionContext, offsetElement: PsiElement) {
private abstract class AbstractFunctionInsertionHandler : QuotedNamesAwareInsertionHandler() {
protected fun addArguments(context: InsertionContext, offsetElement: PsiElement, lookupObject: FunctionLookupObject) {
val completionChar = context.completionChar
if (completionChar == '(') { //TODO: more correct behavior related to braces type
context.setAddCompletionChar(false)
@@ -233,7 +233,7 @@ private abstract class AbstractFunctionInsertionHandler(
val isSmartEnterCompletion = completionChar == Lookup.COMPLETE_STATEMENT_SELECT_CHAR
val isReplaceCompletion = completionChar == Lookup.REPLACE_SELECT_CHAR
val (openingBracket, closingBracket) = if (insertEmptyLambda) '{' to '}' else '(' to ')'
val (openingBracket, closingBracket) = if (lookupObject.insertEmptyLambda) '{' to '}' else '(' to ')'
if (isReplaceCompletion) {
val offset1 = chars.skipSpaces(offset)
@@ -255,7 +255,7 @@ private abstract class AbstractFunctionInsertionHandler(
var closeBracketOffset = openingBracketOffset?.let { chars.indexOfSkippingSpace(closingBracket, it + 1) }
if (openingBracketOffset == null) {
if (insertEmptyLambda) {
if (lookupObject.insertEmptyLambda) {
if (completionChar == ' ' || completionChar == '{') {
context.setAddCompletionChar(false)
}
@@ -274,7 +274,7 @@ private abstract class AbstractFunctionInsertionHandler(
closeBracketOffset = document.charsSequence.indexOfSkippingSpace(closingBracket, openingBracketOffset + 1)
}
if (shouldPlaceCaretInBrackets(completionChar) || closeBracketOffset == null) {
if (shouldPlaceCaretInBrackets(completionChar, lookupObject) || closeBracketOffset == null) {
editor.caretModel.moveToOffset(openingBracketOffset + 1)
AutoPopupController.getInstance(project)?.autoPopupParameterInfo(editor, offsetElement)
} else {
@@ -282,62 +282,48 @@ private abstract class AbstractFunctionInsertionHandler(
}
}
private fun shouldPlaceCaretInBrackets(completionChar: Char): Boolean {
private fun shouldPlaceCaretInBrackets(completionChar: Char, lookupObject: FunctionLookupObject): Boolean {
if (completionChar == ',' || completionChar == '.' || completionChar == '=') return false
if (completionChar == '(') return true
return inputValueArguments || insertEmptyLambda
return lookupObject.inputValueArguments || lookupObject.insertEmptyLambda
}
}
private class SimpleFunctionInsertionHandler(
name: Name,
inputValueArguments: Boolean,
insertEmptyLambda: Boolean,
private val nameToImport: FqName?
) : AbstractFunctionInsertionHandler(name, inputValueArguments, insertEmptyLambda) {
private object ShorteningFunctionInsertionHandler : AbstractFunctionInsertionHandler() {
override fun handleInsert(context: InsertionContext, item: LookupElement) {
super.handleInsert(context, item)
val targetFile = context.file as? KtFile ?: return
val lookupObject = item.`object` as FunctionLookupObject
val importingStrategy = lookupObject.importingStrategy
super.handleInsert(context, item)
val startOffset = context.startOffset
val element = context.file.findElementAt(startOffset) ?: return
addArguments(context, element)
context.commitDocument()
if (importingStrategy is FunctionImportStrategy.InsertFqNameAndShorten) {
context.document.replaceString(
context.startOffset,
context.tailOffset,
importingStrategy.fqName.withRootPrefixIfNeeded().render()
)
context.commitDocument()
if (nameToImport != null) {
addCallableImportIfRequired(targetFile, nameToImport)
addArguments(context, element, lookupObject)
context.commitDocument()
shortenReferences(targetFile, TextRange(context.startOffset, context.tailOffset))
} else {
addArguments(context, element, lookupObject)
context.commitDocument()
if (importingStrategy is FunctionImportStrategy.AddImport) {
addCallableImportIfRequired(targetFile, importingStrategy.nameToImport)
}
}
}
}
private class ShorteningFunctionInsertionHandler(
private val name: FqName,
inputValueArguments: Boolean,
insertEmptyLambda: Boolean,
) : AbstractFunctionInsertionHandler(name.shortName(), inputValueArguments, insertEmptyLambda) {
override fun handleInsert(context: InsertionContext, item: LookupElement) {
val targetFile = context.file as? KtFile ?: return
super.handleInsert(context, item)
val startOffset = context.startOffset
val element = context.file.findElementAt(startOffset) ?: return
context.document.replaceString(
context.startOffset,
context.tailOffset,
name.withRootPrefixIfNeeded().render()
)
context.commitDocument()
addArguments(context, element)
context.commitDocument()
shortenReferences(targetFile, TextRange(context.startOffset, context.tailOffset))
}
}
private class ShorteningVariableInsertionHandler(private val name: FqName) : InsertHandler<LookupElement> {
override fun handleInsert(context: InsertionContext, item: LookupElement) {
val targetFile = context.file as? KtFile ?: return
@@ -353,8 +339,8 @@ private class ShorteningVariableInsertionHandler(private val name: FqName) : Ins
}
}
private class SimpleVariableInsertionHandler(name: Name, private val nameToImport: FqName?) :
QuotedNamesAwareInsertionHandler(name) {
private class SimpleVariableInsertionHandler(private val nameToImport: FqName?) :
QuotedNamesAwareInsertionHandler() {
override fun handleInsert(context: InsertionContext, item: LookupElement) {
super.handleInsert(context, item)
@@ -366,13 +352,15 @@ private class SimpleVariableInsertionHandler(name: Name, private val nameToImpor
}
}
private open class QuotedNamesAwareInsertionHandler(private val name: Name) : InsertHandler<LookupElement> {
private open class QuotedNamesAwareInsertionHandler() : InsertHandler<LookupElement> {
override fun handleInsert(context: InsertionContext, item: LookupElement) {
val lookupElement = item.`object` as KotlinLookupObject
val startOffset = context.startOffset
if (startOffset > 0 && context.document.isTextAt(startOffset - 1, "`")) {
context.document.deleteString(startOffset - 1, startOffset)
}
context.document.replaceString(context.startOffset, context.tailOffset, name.render())
context.document.replaceString(context.startOffset, context.tailOffset, lookupElement.shortName.render())
context.commitDocument()
}
@@ -154,6 +154,11 @@ public class HighLevelBasicCompletionHandlerTestGenerated extends AbstractHighLe
runTest("idea/idea-completion/testData/handlers/basic/KT36306.kt");
}
@TestMetadata("LocalFunction.kt")
public void testLocalFunction() throws Exception {
runTest("idea/idea-completion/testData/handlers/basic/LocalFunction.kt");
}
@TestMetadata("NestedTypeArg.kt")
public void testNestedTypeArg() throws Exception {
runTest("idea/idea-completion/testData/handlers/basic/NestedTypeArg.kt");
@@ -478,6 +483,11 @@ public class HighLevelBasicCompletionHandlerTestGenerated extends AbstractHighLe
runTest("idea/idea-completion/testData/handlers/basic/highOrderFunctions/HigherOrderSuspendFunctionWithArgs.kt");
}
@TestMetadata("InsertFunctionLiteralForLocalFunction.kt")
public void testInsertFunctionLiteralForLocalFunction() throws Exception {
runTest("idea/idea-completion/testData/handlers/basic/highOrderFunctions/InsertFunctionLiteralForLocalFunction.kt");
}
@TestMetadata("InsertFunctionWithSingleParameterWithBrace.kt")
public void testInsertFunctionWithSingleParameterWithBrace() throws Exception {
runTest("idea/idea-completion/testData/handlers/basic/highOrderFunctions/InsertFunctionWithSingleParameterWithBrace.kt");
@@ -154,6 +154,11 @@ public class PerformanceBasicCompletionHandlerTestGenerated extends AbstractPerf
runTest("idea/idea-completion/testData/handlers/basic/KT36306.kt");
}
@TestMetadata("LocalFunction.kt")
public void testLocalFunction() throws Exception {
runTest("idea/idea-completion/testData/handlers/basic/LocalFunction.kt");
}
@TestMetadata("NestedTypeArg.kt")
public void testNestedTypeArg() throws Exception {
runTest("idea/idea-completion/testData/handlers/basic/NestedTypeArg.kt");
@@ -478,6 +483,11 @@ public class PerformanceBasicCompletionHandlerTestGenerated extends AbstractPerf
runTest("idea/idea-completion/testData/handlers/basic/highOrderFunctions/HigherOrderSuspendFunctionWithArgs.kt");
}
@TestMetadata("InsertFunctionLiteralForLocalFunction.kt")
public void testInsertFunctionLiteralForLocalFunction() throws Exception {
runTest("idea/idea-completion/testData/handlers/basic/highOrderFunctions/InsertFunctionLiteralForLocalFunction.kt");
}
@TestMetadata("InsertFunctionWithSingleParameterWithBrace.kt")
public void testInsertFunctionWithSingleParameterWithBrace() throws Exception {
runTest("idea/idea-completion/testData/handlers/basic/highOrderFunctions/InsertFunctionWithSingleParameterWithBrace.kt");