diff --git a/src/com/maddyhome/idea/vim/vimscript/model/commands/CallCommand.kt b/src/com/maddyhome/idea/vim/vimscript/model/commands/CallCommand.kt index cf657b4fa..4ce19d7ec 100644 --- a/src/com/maddyhome/idea/vim/vimscript/model/commands/CallCommand.kt +++ b/src/com/maddyhome/idea/vim/vimscript/model/commands/CallCommand.kt @@ -55,20 +55,15 @@ class CallCommand(val ranges: Ranges, val functionCall: Expression) : Command.Si return ExecutionResult.Success } + val name = (if (functionCall.scope != null) functionCall.scope.c + ":" else "") + functionCall.functionName val funcref = VariableService.getNullableVariableValue(Variable(functionCall.scope, functionCall.functionName), editor, context, parent) if (funcref is VimFuncref) { - if (funcref.handler is DefinedFunctionHandler && funcref.handler.function.flags.contains(FunctionFlag.DICT) && funcref.handler.function.self == null) { - throw ExException( - "E725: Calling dict function without Dictionary: " + - ((if (functionCall.scope != null) functionCall.scope.c + ":" else "") + functionCall.functionName) - ) - } funcref.handler.ranges = ranges - funcref.execute(functionCall.arguments, editor, context, parent) + funcref.execute(name, functionCall.arguments, editor, context, parent) return ExecutionResult.Success } - throw ExException("E117: Unknown function: ${if (functionCall.scope != null) functionCall.scope.c + ":" else ""}${functionCall.functionName}") + throw ExException("E117: Unknown function: $name") } else if (functionCall is FuncrefCallExpression) { functionCall.evaluateWithRange(ranges, editor, context, parent) return ExecutionResult.Success diff --git a/src/com/maddyhome/idea/vim/vimscript/model/commands/LetCommand.kt b/src/com/maddyhome/idea/vim/vimscript/model/commands/LetCommand.kt index 1309cee1f..48bc89c0f 100644 --- a/src/com/maddyhome/idea/vim/vimscript/model/commands/LetCommand.kt +++ b/src/com/maddyhome/idea/vim/vimscript/model/commands/LetCommand.kt @@ -14,6 +14,7 @@ import com.maddyhome.idea.vim.vimscript.model.ExecutionResult import com.maddyhome.idea.vim.vimscript.model.Script import com.maddyhome.idea.vim.vimscript.model.datatypes.VimBlob import com.maddyhome.idea.vim.vimscript.model.datatypes.VimDictionary +import com.maddyhome.idea.vim.vimscript.model.datatypes.VimFuncref import com.maddyhome.idea.vim.vimscript.model.datatypes.VimInt import com.maddyhome.idea.vim.vimscript.model.datatypes.VimList import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString @@ -28,6 +29,7 @@ import com.maddyhome.idea.vim.vimscript.model.expressions.SublistExpression import com.maddyhome.idea.vim.vimscript.model.expressions.Variable import com.maddyhome.idea.vim.vimscript.model.expressions.operators.AssignmentOperator import com.maddyhome.idea.vim.vimscript.model.expressions.toVimDataType +import com.maddyhome.idea.vim.vimscript.model.functions.DefinedFunctionHandler import com.maddyhome.idea.vim.vimscript.model.statements.FunctionDeclaration import com.maddyhome.idea.vim.vimscript.model.statements.FunctionFlag import com.maddyhome.idea.vim.vimscript.services.VariableService @@ -66,15 +68,19 @@ data class LetCommand( if (operator != AssignmentOperator.ASSIGNMENT && !variableValue.dictionary.containsKey(dictKey)) { throw ExException("E716: Key not present in Dictionary: $dictKey") } - if (variableValue.dictionary.containsKey(dictKey)) { - variableValue.dictionary[dictKey] = - operator.getNewValue( - SimpleExpression(variableValue.dictionary[dictKey]!!), expression, editor, - context, this - ) + var valueToStore = if (variableValue.dictionary.containsKey(dictKey)) { + operator.getNewValue(SimpleExpression(variableValue.dictionary[dictKey]!!), expression, editor, context, this) } else { - variableValue.dictionary[dictKey] = expression.evaluate(editor, context, this) + expression.evaluate(editor, context, this) } + if (valueToStore is VimFuncref && !valueToStore.isSelfFixed && + valueToStore.handler is DefinedFunctionHandler && + (valueToStore.handler as DefinedFunctionHandler).function.flags.contains(FunctionFlag.DICT) + ) { + valueToStore = valueToStore.copy() + valueToStore.dictionary = variableValue + } + variableValue.dictionary[dictKey] = valueToStore } is VimList -> { // we use Integer.parseInt(........asString()) because in case if index's type is Float, List, Dictionary etc diff --git a/src/com/maddyhome/idea/vim/vimscript/model/datatypes/VimDictionary.kt b/src/com/maddyhome/idea/vim/vimscript/model/datatypes/VimDictionary.kt index 4aee76b4d..f037a6eea 100644 --- a/src/com/maddyhome/idea/vim/vimscript/model/datatypes/VimDictionary.kt +++ b/src/com/maddyhome/idea/vim/vimscript/model/datatypes/VimDictionary.kt @@ -18,14 +18,17 @@ data class VimDictionary(val dictionary: LinkedHashMap<VimString, VimDataType>) override fun toString(): String { val result = StringBuffer("{") - result.append(dictionary.map { it.stringOfEntry() }.joinToString(separator = ", ")) + result.append(dictionary.map { stringOfEntry(it) }.joinToString(separator = ", ")) result.append("}") return result.toString() } - private fun Map.Entry<VimString, VimDataType>.stringOfEntry(): String { - val valueString = if (this.value is VimString) "'${this.value}'" else this.value.toString() - return "'${this.key}': $valueString" + private fun stringOfEntry(entry: Map.Entry<VimString, VimDataType>): String { + val valueString = when (entry.value) { + is VimString -> "'${entry.value}'" + else -> entry.value.toString() + } + return "'${entry.key}': $valueString" } override fun asBoolean(): Boolean { diff --git a/src/com/maddyhome/idea/vim/vimscript/model/datatypes/VimFuncref.kt b/src/com/maddyhome/idea/vim/vimscript/model/datatypes/VimFuncref.kt index 90ed71c93..a1285f13f 100644 --- a/src/com/maddyhome/idea/vim/vimscript/model/datatypes/VimFuncref.kt +++ b/src/com/maddyhome/idea/vim/vimscript/model/datatypes/VimFuncref.kt @@ -23,22 +23,27 @@ import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.ex.ExException import com.maddyhome.idea.vim.vimscript.model.Executable import com.maddyhome.idea.vim.vimscript.model.expressions.Expression +import com.maddyhome.idea.vim.vimscript.model.expressions.Scope import com.maddyhome.idea.vim.vimscript.model.expressions.SimpleExpression +import com.maddyhome.idea.vim.vimscript.model.expressions.Variable import com.maddyhome.idea.vim.vimscript.model.functions.DefinedFunctionHandler import com.maddyhome.idea.vim.vimscript.model.functions.FunctionHandler +import com.maddyhome.idea.vim.vimscript.model.statements.FunctionFlag import com.maddyhome.idea.vim.vimscript.services.FunctionStorage +import com.maddyhome.idea.vim.vimscript.services.VariableService data class VimFuncref( val handler: FunctionHandler, val arguments: VimList, - val dictionary: VimDictionary, + var dictionary: VimDictionary?, val type: Type, -) : VimDataType() { +) : VimDataType(), Cloneable { var isSelfFixed = false companion object { - var lambdaCounter = 0 + var lambdaCounter = 1 + var anonymousCounter = 1 } override fun asDouble(): Double { @@ -50,14 +55,19 @@ data class VimFuncref( } override fun toString(): String { - return if (arguments.values.isEmpty()) { + return if (arguments.values.isEmpty() && dictionary == null) { when (type) { Type.LAMBDA -> "function('${handler.name}')" Type.FUNCREF -> "function('${handler.name}')" Type.FUNCTION -> handler.name } } else { - "function('${handler.name}', $arguments)" + val result = StringBuffer("function('${handler.name}'") + if (arguments.values.isNotEmpty()) { + result.append(", ").append(arguments.toString()) + } + result.append(")") + return result.toString() } } @@ -65,7 +75,21 @@ data class VimFuncref( throw ExException("E703: using Funcref as a Number") } - fun execute(args: List<Expression>, editor: Editor, context: DataContext, parent: Executable): VimDataType { + fun execute(name: String, args: List<Expression>, editor: Editor, context: DataContext, parent: Executable): VimDataType { + if (handler is DefinedFunctionHandler && handler.function.flags.contains(FunctionFlag.DICT)) { + if (dictionary == null) { + throw ExException("E725: Calling dict function without Dictionary: $name") + } else { + VariableService.storeVariable( + Variable(Scope.LOCAL_VARIABLE, "self"), + dictionary!!, + editor, + context, + handler.function + ) + } + } + val allArguments = listOf(this.arguments.values.map { SimpleExpression(it) }, args).flatten() if (handler is DefinedFunctionHandler && handler.function.isDeleted) { throw ExException("E933: Function was deleted: ${handler.name}") @@ -80,6 +104,10 @@ data class VimFuncref( return handler.executeFunction(allArguments, editor, context, parent) } + override fun clone(): Any { + return VimFuncref(handler, arguments, dictionary, type) + } + enum class Type { LAMBDA, FUNCREF, diff --git a/src/com/maddyhome/idea/vim/vimscript/model/expressions/DictionaryExpression.kt b/src/com/maddyhome/idea/vim/vimscript/model/expressions/DictionaryExpression.kt index c2417d2fc..41db174c6 100644 --- a/src/com/maddyhome/idea/vim/vimscript/model/expressions/DictionaryExpression.kt +++ b/src/com/maddyhome/idea/vim/vimscript/model/expressions/DictionaryExpression.kt @@ -23,15 +23,23 @@ import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.vimscript.model.Executable import com.maddyhome.idea.vim.vimscript.model.datatypes.VimDataType import com.maddyhome.idea.vim.vimscript.model.datatypes.VimDictionary +import com.maddyhome.idea.vim.vimscript.model.datatypes.VimFuncref import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString +import com.maddyhome.idea.vim.vimscript.model.functions.DefinedFunctionHandler data class DictionaryExpression(val dictionary: LinkedHashMap<Expression, Expression>) : Expression() { override fun evaluate(editor: Editor, context: DataContext, parent: Executable): VimDataType { - val dict: LinkedHashMap<VimString, VimDataType> = linkedMapOf() + val dict = VimDictionary(linkedMapOf()) for ((key, value) in dictionary) { - dict[VimString(key.evaluate(editor, context, parent).asString())] = value.evaluate(editor, context, parent) + val evaluatedVal = value.evaluate(editor, context, parent) + var newFuncref = evaluatedVal + if (evaluatedVal is VimFuncref && evaluatedVal.handler is DefinedFunctionHandler && !evaluatedVal.isSelfFixed) { + newFuncref = evaluatedVal.copy() + newFuncref.dictionary = dict + } + dict.dictionary[VimString(key.evaluate(editor, context, parent).asString())] = newFuncref } - return VimDictionary(dict) + return dict } } diff --git a/src/com/maddyhome/idea/vim/vimscript/model/expressions/FuncrefCallExpression.kt b/src/com/maddyhome/idea/vim/vimscript/model/expressions/FuncrefCallExpression.kt index 01c4680c0..a17ac6769 100644 --- a/src/com/maddyhome/idea/vim/vimscript/model/expressions/FuncrefCallExpression.kt +++ b/src/com/maddyhome/idea/vim/vimscript/model/expressions/FuncrefCallExpression.kt @@ -32,7 +32,7 @@ data class FuncrefCallExpression(val expression: Expression, val args: List<Expr val value = expression.evaluate(editor, context, parent) if (value is VimFuncref) { value.handler.ranges = ranges - return value.execute(args, editor, context, parent) + return value.execute(value.handler.name, args, editor, context, parent) } else { // todo more exceptions throw ExException("E15: Invalid expression") diff --git a/src/com/maddyhome/idea/vim/vimscript/model/expressions/FunctionCallExpression.kt b/src/com/maddyhome/idea/vim/vimscript/model/expressions/FunctionCallExpression.kt index a9b457f45..7ab3e0393 100644 --- a/src/com/maddyhome/idea/vim/vimscript/model/expressions/FunctionCallExpression.kt +++ b/src/com/maddyhome/idea/vim/vimscript/model/expressions/FunctionCallExpression.kt @@ -25,10 +25,8 @@ data class FunctionCallExpression(val scope: Scope?, val functionName: String, v val funcref = VariableService.getNullableVariableValue(Variable(scope, functionName), editor, context, parent) if (funcref is VimFuncref) { - if (funcref.handler is DefinedFunctionHandler && funcref.handler.function.flags.contains(FunctionFlag.DICT) && funcref.handler.function.self == null) { - throw ExException("E725: Calling dict function without Dictionary: ${(if (scope != null) scope.c + ":" else "") + functionName}") - } - return funcref.execute(arguments, editor, context, parent) + val name = (if (scope != null) scope.c + ":" else "") + functionName + return funcref.execute(name, arguments, editor, context, parent) } throw ExException("E117: Unknown function: ${if (scope != null) scope.c + ":" else ""}$functionName") } diff --git a/src/com/maddyhome/idea/vim/vimscript/model/expressions/LambdaFunctionCallExpression.kt b/src/com/maddyhome/idea/vim/vimscript/model/expressions/LambdaFunctionCallExpression.kt index 2e145117d..5b5be8e72 100644 --- a/src/com/maddyhome/idea/vim/vimscript/model/expressions/LambdaFunctionCallExpression.kt +++ b/src/com/maddyhome/idea/vim/vimscript/model/expressions/LambdaFunctionCallExpression.kt @@ -27,6 +27,6 @@ class LambdaFunctionCallExpression(val lambda: LambdaExpression, val arguments: override fun evaluate(editor: Editor, context: DataContext, parent: Executable): VimDataType { val funcref = lambda.evaluate(editor, context, parent) - return funcref.execute(arguments, editor, context, parent) + return funcref.execute("", arguments, editor, context, parent) } } diff --git a/src/com/maddyhome/idea/vim/vimscript/model/functions/handlers/FunctionFunctionHandler.kt b/src/com/maddyhome/idea/vim/vimscript/model/functions/handlers/FunctionFunctionHandler.kt index a0ff9456b..a7bee0ab8 100644 --- a/src/com/maddyhome/idea/vim/vimscript/model/functions/handlers/FunctionFunctionHandler.kt +++ b/src/com/maddyhome/idea/vim/vimscript/model/functions/handlers/FunctionFunctionHandler.kt @@ -71,7 +71,11 @@ object FunctionFunctionHandler : FunctionHandler() { if (arg3 != null && arg3 !is VimDictionary) { throw ExException("E922: expected a dict") } - return VimFuncref(function, arglist ?: VimList(mutableListOf()), dictionary ?: VimDictionary(LinkedHashMap()), VimFuncref.Type.FUNCTION) + val funcref = VimFuncref(function, arglist ?: VimList(mutableListOf()), dictionary, VimFuncref.Type.FUNCTION) + if (dictionary != null) { + funcref.isSelfFixed = true + } + return funcref } } @@ -115,7 +119,7 @@ object FuncrefFunctionHandler : FunctionHandler() { if (arg3 != null && arg3 !is VimDictionary) { throw ExException("E922: expected a dict") } - return VimFuncref(handler, arglist ?: VimList(mutableListOf()), dictionary ?: VimDictionary(LinkedHashMap()), VimFuncref.Type.FUNCREF) + return VimFuncref(handler, arglist ?: VimList(mutableListOf()), dictionary, VimFuncref.Type.FUNCREF) } } diff --git a/src/com/maddyhome/idea/vim/vimscript/model/statements/FunctionDeclaration.kt b/src/com/maddyhome/idea/vim/vimscript/model/statements/FunctionDeclaration.kt index 0d42a3fa2..898538c76 100644 --- a/src/com/maddyhome/idea/vim/vimscript/model/statements/FunctionDeclaration.kt +++ b/src/com/maddyhome/idea/vim/vimscript/model/statements/FunctionDeclaration.kt @@ -20,7 +20,6 @@ data class FunctionDeclaration( ) : Executable { override lateinit var parent: Executable var isDeleted = false - var self: VimDataType? = null /** * we store the "a:" and "l:" scope variables here diff --git a/test/org/jetbrains/plugins/ideavim/ex/implementation/functions/DictionaryFunctionTest.kt b/test/org/jetbrains/plugins/ideavim/ex/implementation/functions/DictionaryFunctionTest.kt new file mode 100644 index 000000000..7cd4f9adf --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/ex/implementation/functions/DictionaryFunctionTest.kt @@ -0,0 +1,222 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2021 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package org.jetbrains.plugins.ideavim.ex.implementation.functions + +import org.jetbrains.plugins.ideavim.SkipNeovimReason +import org.jetbrains.plugins.ideavim.TestWithoutNeovim +import org.jetbrains.plugins.ideavim.VimTestCase + +class DictionaryFunctionTest : VimTestCase() { + + fun `test self in dictionary function with assignment via function function`() { + configureByText("\n") + typeText( + commandToKeys( + """ + function Print() dict | + echo self.data | + endfunction + """.trimIndent() + ) + ) + typeText(commandToKeys("let dict = {'data': [], 'print': function('Print')}")) + typeText(commandToKeys("call dict.print()")) + assertExOutput("[]\n") + + typeText(commandToKeys("delfunction! Print")) + } + + fun `test self in dictionary function with assignment via let command`() { + configureByText("\n") + typeText( + commandToKeys( + """ + function Print() dict | + echo self.name | + endfunction + """.trimIndent() + ) + ) + typeText(commandToKeys("let dict = {'name': 'dict_name'}")) + typeText(commandToKeys("let PrintFr = function('Print')")) + typeText(commandToKeys("let dict.print = PrintFr")) + typeText(commandToKeys("call dict.print()")) + assertExOutput("dict_name\n") + + typeText(commandToKeys("delfunction! Print")) + } + + @TestWithoutNeovim(SkipNeovimReason.PLUGIN_ERROR) + fun `test dictionary function without dict`() { + configureByText("\n") + typeText( + commandToKeys( + """ + function Print() dict | + echo self | + endfunction + """.trimIndent() + ) + ) + typeText(commandToKeys("call Print()")) + assertPluginError(true) + assertPluginErrorMessageContains("E725: Calling dict function without Dictionary: Print") + + typeText(commandToKeys("delfunction! Print")) + } + + // todo big brain logic +// fun `test assigned dictionary function to another dictionary`() { +// configureByText("\n") +// typeText( +// commandToKeys( +// """ +// function Print() dict | +// echo self.name | +// endfunction +// """.trimIndent() +// ) +// ) +// typeText(commandToKeys("let dict = {'name': 'dict', 'print': function('Print')}")) +// typeText(commandToKeys("echo dict.print")) +// assertExOutput("function('Print', {'name': 'dict', 'print': function('Print')}") +// typeText(commandToKeys("echo dict")) +// assertExOutput("{'name': 'dict', 'print': function('Print')}") +// typeText(commandToKeys("call dict.print()")) +// assertExOutput("dict\n") +// +// typeText(commandToKeys("let dict2 = {'name': 'dict2', 'print': dict.print}")) +// typeText(commandToKeys("echo dict2.print")) +// assertExOutput("function('Print', {'name': 'dict2', 'print': function('Print', {name: 'dict', 'print': function('Print')})}") +// typeText(commandToKeys("echo dict2")) +// assertExOutput("{'name': 'dict2', 'print': function('Print', {name: 'dict', 'print': function('Print')})}") +// typeText(commandToKeys("call dict2.print()")) +// assertExOutput("dict2\n") +// +// typeText(commandToKeys("delfunction! Print")) +// } + + fun `test self is not changed after let assignment`() { + configureByText("\n") + typeText( + commandToKeys( + """ + function Print() dict | + echo self.name | + endfunction + """.trimIndent() + ) + ) + typeText(commandToKeys("let dict = {'name': 'dict', 'print': function('Print')}")) + typeText(commandToKeys("let dict2 = {'name': 'dict2'}")) + typeText(commandToKeys("let dict2.print = dict.print")) + + typeText(commandToKeys("call dict2.print()")) + assertExOutput("dict2\n") + + typeText(commandToKeys("call dict.print()")) + assertExOutput("dict\n") + + typeText(commandToKeys("delfunction! Print")) + } + + fun `test self is not changed after in-dictionary assignment`() { + configureByText("\n") + typeText( + commandToKeys( + """ + function Print() dict | + echo self.name | + endfunction + """.trimIndent() + ) + ) + typeText(commandToKeys("let dict = {'name': 'dict', 'print': function('Print')}")) + typeText(commandToKeys("let dict2 = {'name': 'dict2', 'print': dict.print}")) + + typeText(commandToKeys("call dict2.print()")) + assertExOutput("dict2\n") + + typeText(commandToKeys("call dict.print()")) + assertExOutput("dict\n") + + typeText(commandToKeys("delfunction! Print")) + } + + fun `test assigned partial to another dictionary`() { + configureByText("\n") + typeText( + commandToKeys( + """ + function Print() dict | + echo self.name | + endfunction + """.trimIndent() + ) + ) + typeText(commandToKeys("let dict = {'name': 'dict'}")) + typeText(commandToKeys("let dict.print = function('Print', dict)")) + typeText(commandToKeys("call dict.print()")) + assertExOutput("dict\n") + + typeText(commandToKeys("let dict2 = {'name': 'dict2', 'print': dict.print}")) + typeText(commandToKeys("call dict2.print()")) + assertExOutput("dict\n") + + typeText(commandToKeys("delfunction! Print")) + } + + @TestWithoutNeovim(SkipNeovimReason.PLUGIN_ERROR) + fun `test self is read-only`() { + configureByText("\n") + typeText( + commandToKeys( + """ + function Print() dict | + let self = [] | + endfunction + """.trimIndent() + ) + ) + typeText(commandToKeys("let dict = {'name': 'dict', 'print': function('Print')}")) + typeText(commandToKeys("call dict.print()")) + assertPluginError(true) + assertPluginErrorMessageContains("E46: Cannot change read-only variable \"self\"") + + typeText(commandToKeys("delfunction! Print")) + } + + fun `test self in inner dictionary`() { + configureByText("\n") + typeText( + commandToKeys( + """ + function Print() dict | + echo self.name | + endfunction + """.trimIndent() + ) + ) + typeText(commandToKeys("let dict = {'name': 'dict', 'innerDict': {'name': 'innerDict', 'print': function('Print')}}")) + typeText(commandToKeys("call dict.innerDict.print()")) + assertExOutput("innerDict\n") + + typeText(commandToKeys("delfunction! Print")) + } +} diff --git a/vimscript-info/VIMSCRIPT_ROADMAP.md b/vimscript-info/VIMSCRIPT_ROADMAP.md index 24a91d3b1..5a5c0c0b6 100644 --- a/vimscript-info/VIMSCRIPT_ROADMAP.md +++ b/vimscript-info/VIMSCRIPT_ROADMAP.md @@ -27,9 +27,8 @@ - [x] function as method - [x] `function` function - [x] `funcref` function -- [ ] dictionary functions +- [x] `dict` function flag - [ ] anonymous functions -- [ ] `dict` function flag - [ ] default value in functions e.g. `function F1(a, b = 10)` - [ ] delayed parsing of if/for/while etc. - [ ] `has("ide")` or "ide" option