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

Anonymous functions

This commit is contained in:
lippfi 2021-10-08 00:04:54 +03:00
parent e1069c265e
commit e7f128ee59
8 changed files with 262 additions and 4 deletions
src
com/maddyhome/idea/vim/vimscript
main/antlr
test/org/jetbrains/plugins/ideavim
VimTestCase.kt
ex
implementation
parser/statements
vimscript-info

View File

@ -0,0 +1,62 @@
/*
* 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 com.maddyhome.idea.vim.vimscript.model.statements
import com.intellij.openapi.actionSystem.DataContext
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.ExecutionResult
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.VimList
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
import com.maddyhome.idea.vim.vimscript.model.expressions.OneElementSublistExpression
import com.maddyhome.idea.vim.vimscript.model.expressions.SimpleExpression
import com.maddyhome.idea.vim.vimscript.model.functions.DefinedFunctionHandler
data class AnonymousFunctionDeclaration(
val sublist: OneElementSublistExpression,
val args: List<String>,
val body: List<Executable>,
val replaceExisting: Boolean,
val flags: Set<FunctionFlag>,
val hasOptionalArguments: Boolean,
) : Executable {
override lateinit var parent: Executable
override fun execute(editor: Editor, context: DataContext): ExecutionResult {
val container = sublist.expression.evaluate(editor, context, parent)
if (container !is VimDictionary) {
throw ExException("E1203: Dot can only be used on a dictionary")
}
val index = ((sublist.index as SimpleExpression).data as VimString)
if (container.dictionary.containsKey(index)) {
if (container.dictionary[index] is VimFuncref && !replaceExisting) {
throw ExException("E717: Dictionary entry already exists")
} else if (container.dictionary[index] !is VimFuncref) {
throw ExException("E718: Funcref required")
}
}
val declaration = FunctionDeclaration(null, VimFuncref.anonymousCounter++.toString(), args, body, replaceExisting, flags + FunctionFlag.DICT, hasOptionalArguments)
container.dictionary[index] = VimFuncref(DefinedFunctionHandler(declaration), VimList(mutableListOf()), container, VimFuncref.Type.FUNCREF)
return ExecutionResult.Success
}
}

View File

@ -2,9 +2,13 @@ package com.maddyhome.idea.vim.vimscript.parser.visitors
import com.maddyhome.idea.vim.vimscript.model.Executable
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimInt
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString
import com.maddyhome.idea.vim.vimscript.model.expressions.Expression
import com.maddyhome.idea.vim.vimscript.model.expressions.OneElementSublistExpression
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.statements.AnonymousFunctionDeclaration
import com.maddyhome.idea.vim.vimscript.model.statements.CatchBlock
import com.maddyhome.idea.vim.vimscript.model.statements.FinallyBlock
import com.maddyhome.idea.vim.vimscript.model.statements.FunctionDeclaration
@ -30,6 +34,7 @@ object ExecutableVisitor : VimscriptBaseVisitor<Executable>() {
ctx.forLoop() != null -> visitForLoop(ctx.forLoop())
ctx.whileLoop() != null -> visitWhileLoop(ctx.whileLoop())
ctx.functionDefinition() != null -> visitFunctionDefinition(ctx.functionDefinition())
ctx.dictFunctionDefinition() != null -> visitDictFunctionDefinition(ctx.dictFunctionDefinition())
ctx.tryStatement() != null -> visitTryStatement(ctx.tryStatement())
else -> null
}
@ -45,6 +50,7 @@ object ExecutableVisitor : VimscriptBaseVisitor<Executable>() {
ctx.forLoop() != null -> visitForLoop(ctx.forLoop())
ctx.whileLoop() != null -> visitWhileLoop(ctx.whileLoop())
ctx.functionDefinition() != null -> visitFunctionDefinition(ctx.functionDefinition())
ctx.dictFunctionDefinition() != null -> visitDictFunctionDefinition(ctx.dictFunctionDefinition())
ctx.throwStatement() != null -> visitThrowStatement(ctx.throwStatement())
ctx.tryStatement() != null -> visitTryStatement(ctx.tryStatement())
else -> null
@ -78,6 +84,23 @@ object ExecutableVisitor : VimscriptBaseVisitor<Executable>() {
return FunctionDeclaration(functionScope, functionName, args, body, replaceExisting, flags.filterNotNull().toSet(), hasOptionalArguments)
}
override fun visitDictFunctionDefinition(ctx: VimscriptParser.DictFunctionDefinitionContext): Executable {
val functionScope = if (ctx.functionScope() != null) Scope.getByValue(ctx.functionScope().text) else null
val args = ctx.argumentsDeclaration().variableName().map { it.text }
val body = ctx.blockMember().mapNotNull { visitBlockMember(it) }
val replaceExisting = ctx.replace != null
val flags = mutableSetOf<FunctionFlag?>()
val hasOptionalArguments = ctx.argumentsDeclaration().ETC() != null
for (flag in ctx.functionFlag()) {
flags.add(FunctionFlag.getByName(flag.text))
}
var sublistExpression = OneElementSublistExpression(SimpleExpression(VimString(ctx.literalDictionaryKey(1).text)), Variable(functionScope, ctx.literalDictionaryKey(0).text))
for (i in 2 until ctx.literalDictionaryKey().size) {
sublistExpression = OneElementSublistExpression(SimpleExpression(VimString(ctx.literalDictionaryKey(i).text)), sublistExpression)
}
return AnonymousFunctionDeclaration(sublistExpression, args, body, replaceExisting, flags.filterNotNull().toSet(), hasOptionalArguments)
}
override fun visitTryStatement(ctx: VimscriptParser.TryStatementContext): Executable {
val tryBlock = TryBlock(ctx.tryBlock().blockMember().mapNotNull { visitBlockMember(it) })
val catchBlocks: MutableList<CatchBlock> = mutableListOf()

View File

@ -74,7 +74,7 @@ functionDefinition: ws_cols FUNCTION (replace = EXCLAMATION)? WS+ (functionS
ws_cols ENDFUNCTION WS* (comment | statementSeparator)
;
dictFunctionDefinition:
ws_cols FUNCTION ~(BAR | NEW_LINE)*? statementSeparator
ws_cols FUNCTION (replace = EXCLAMATION)? WS+ (functionScope COLON)? literalDictionaryKey (DOT literalDictionaryKey)+ WS* L_PAREN WS* argumentsDeclaration R_PAREN WS* (functionFlag WS*)* (comment | statementSeparator)
blockMember*
ws_cols ENDFUNCTION WS* (comment | statementSeparator)
;

View File

@ -128,6 +128,7 @@ abstract class VimTestCase : UsefulTestCase() {
ExEntryPanel.getInstance().deactivate(false)
VariableService.clear()
VimFuncref.lambdaCounter = 0
VimFuncref.anonymousCounter = 0
IdeavimErrorListener.testLogger.clear()
VimPlugin.getRegister().resetRegisters()
VimPlugin.getSearch().resetState()

View File

@ -120,6 +120,9 @@ class ConcatenationOperatorTest {
@Theory
fun `dict and integer`(@FromDataPoints("operator") operator: String, @FromDataPoints("spaces") sp1: String, @FromDataPoints("spaces") sp2: String) {
try {
if (sp1 == "" && sp2 == "") { // it is not a concatenation, so let's skip this case
throw ExException("E731: Using a Dictionary as a String")
}
VimscriptParser.parseExpression("{'key' : 21}$sp1$operator${sp2}1")!!.evaluate()
} catch (e: ExException) {
assertEquals("E731: Using a Dictionary as a String", e.message)

View File

@ -0,0 +1,170 @@
/*
* 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 AnonymousFunctionTest : VimTestCase() {
fun `test anonymous function`() {
configureByText("\n")
typeText(commandToKeys("let dict = {}"))
typeText(
commandToKeys(
"""
function dict.pi() |
return 3.1415 |
endfunction
""".trimIndent()
)
)
typeText(commandToKeys("echo dict.pi()"))
assertExOutput("3.1415\n")
}
// todo. `commandToKeys` is executed as if it would from the command line. so no script - no scope ¯\_(ツ)_/¯
// fun `test anonymous function with scope`() {
// configureByText("\n")d
// typeText(commandToKeys("let s:dict = {}"))
// typeText(
// commandToKeys(
// """
// function s:dict.pi() |
// return 3.1415 |
// endfunction
// """.trimIndent()
// )
// )
// typeText(commandToKeys("echo s:dict.pi()"))
// assertExOutput("3.1415\n")
// }
fun `test self in anonymous function`() {
configureByText("\n")
typeText(commandToKeys("let dict = {'name': 'dict'}"))
typeText(
commandToKeys(
"""
function dict.getName() |
return self.name |
endfunction
""".trimIndent()
)
)
typeText(commandToKeys("echo dict.getName()"))
assertExOutput("dict\n")
}
fun `test inner anonymous function`() {
configureByText("\n")
typeText(commandToKeys("let dict = {'dict2': {'dict3': {}}}"))
typeText(
commandToKeys(
"""
function dict.dict2.dict3.sayHi() |
echo 'hi' |
endfunction
""".trimIndent()
)
)
typeText(commandToKeys("call dict.dict2.dict3.sayHi()"))
assertExOutput("hi\n")
}
@TestWithoutNeovim(SkipNeovimReason.PLUGIN_ERROR)
fun `test anonymous function for non-dict variable`() {
configureByText("\n")
typeText(commandToKeys("let list = []"))
typeText(
commandToKeys(
"""
function list.sayHi() |
echo 'hi' |
endfunction
""".trimIndent()
)
)
assertPluginError(true)
assertPluginErrorMessageContains("E1203: Dot can only be used on a dictionary")
}
@TestWithoutNeovim(SkipNeovimReason.PLUGIN_ERROR)
fun `test dictionary function already exists`() {
configureByText("\n")
typeText(commandToKeys("let dict = {'abs' : function('abs')}"))
typeText(
commandToKeys(
"""
function dict.abs(number) |
if a:number > 0 |
return a:number |
else |
return -a:number |
endif |
endfunction
""".trimIndent()
)
)
assertPluginError(true)
assertPluginErrorMessageContains("E717: Dictionary entry already exists")
}
fun `test replace existing function`() {
configureByText("\n")
typeText(commandToKeys("let dict = {'abs' : function('abs')}"))
typeText(
commandToKeys(
"""
function! dict.abs(number) |
if a:number > 0 |
return a:number |
else |
return -a:number |
endif |
endfunction
""".trimIndent()
)
)
typeText(commandToKeys("echo dict.abs(-10)"))
assertExOutput("10\n")
}
@TestWithoutNeovim(SkipNeovimReason.PLUGIN)
fun `test funcref required`() {
configureByText("\n")
typeText(commandToKeys("let dict = {'abs' : 'absolute'}"))
typeText(
commandToKeys(
"""
function! dict.abs(number) |
if a:number > 0 |
return a:number |
else |
return -a:number |
endif |
endfunction
""".trimIndent()
)
)
assertPluginError(true)
assertPluginErrorMessageContains("E718: Funcref required")
}
}

View File

@ -165,7 +165,6 @@ class FunctionDeclarationTests {
endfunction
""".trimIndent()
)
// it will be implemented later but for now it's good to ignore such blocks and do not throw any exceptions during parsing
assertEquals(0, script.units.size)
assertEquals(1, script.units.size)
}
}

View File

@ -28,7 +28,7 @@
- [x] `function` function
- [x] `funcref` function
- [x] `dict` function flag
- [ ] anonymous functions
- [x] anonymous functions
- [ ] default value in functions e.g. `function F1(a, b = 10)`
- [ ] delayed parsing of if/for/while etc.
- [ ] `has("ide")` or "ide" option