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:
parent
e1069c265e
commit
e7f128ee59
src
com/maddyhome/idea/vim/vimscript
main/antlr
test/org/jetbrains/plugins/ideavim
vimscript-info
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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)
|
||||
;
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user