1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-05-02 01:34:03 +02:00

Dictionary functions

This commit is contained in:
lippfi 2021-10-08 00:02:36 +03:00
parent 769d900383
commit e0b356c12d
12 changed files with 301 additions and 39 deletions
src/com/maddyhome/idea/vim/vimscript/model
test/org/jetbrains/plugins/ideavim/ex/implementation/functions
vimscript-info

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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,

View File

@ -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
}
}

View File

@ -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")

View File

@ -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")
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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"))
}
}

View File

@ -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