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

Introduce operatorfunc option

Allows creating custom operators in script, as shown in 
This commit is contained in:
Matt Ellis 2023-12-02 19:43:25 -05:00 committed by Alex Pláte
parent 9b81c7e650
commit 3a294268d9
7 changed files with 271 additions and 44 deletions
src
main/java/com/maddyhome/idea/vim/action/change
test/java/org/jetbrains/plugins/ideavim
vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api

View File

@ -14,6 +14,7 @@ import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.setChangeMarks
import com.maddyhome.idea.vim.command.Argument
@ -21,6 +22,7 @@ import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.common.argumentCaptured
import com.maddyhome.idea.vim.ex.ExException
import com.maddyhome.idea.vim.group.MotionGroup
import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.VimActionHandler
@ -29,9 +31,62 @@ import com.maddyhome.idea.vim.helper.MessageHelper
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.vimscript.model.CommandLineVimLContext
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimFuncref
import com.maddyhome.idea.vim.vimscript.model.expressions.FunctionCallExpression
import com.maddyhome.idea.vim.vimscript.model.expressions.SimpleExpression
// todo make it multicaret
private fun doOperatorAction(editor: VimEditor, context: ExecutionContext, textRange: TextRange, selectionType: SelectionType): Boolean {
val func = injector.globalOptions().operatorfunc
if (func.isNotEmpty()) {
val scriptContext = CommandLineVimLContext
// The option value is either a function name, which should have a handler, or it might be a lambda expression, or a
// `function` or `funcref` call expression, all of which will return a funcref (with a handler)
var handler = injector.functionService.getFunctionHandlerOrNull(null, func, scriptContext)
if (handler == null) {
val expression = injector.vimscriptParser.parseExpression(func)
if (expression != null) {
try {
val value = expression.evaluate(editor, context, scriptContext)
if (value is VimFuncref) {
handler = value.handler
}
} catch (ex: ExException) {
// Get the argument for function('...') or funcref('...') for the error message
val functionName = if (expression is FunctionCallExpression && expression.arguments.size > 0) {
expression.arguments[0].evaluate(editor, context, scriptContext).toString()
}
else {
func
}
VimPlugin.showMessage("E117: Unknown function: $functionName")
return false
}
}
}
if (handler != null) {
val arg = when (selectionType) {
SelectionType.LINE_WISE -> "line"
SelectionType.CHARACTER_WISE -> "char"
SelectionType.BLOCK_WISE -> "block"
}
val saveRepeatHandler = VimRepeater.repeatHandler
injector.markService.setChangeMarks(editor.primaryCaret(), textRange)
KeyHandler.getInstance().reset(editor)
val arguments = listOf(SimpleExpression(arg))
handler.executeFunction(arguments, editor, context, scriptContext)
VimRepeater.repeatHandler = saveRepeatHandler
return true
}
}
// TODO: Migrate extensions to use operatorfunc
val operatorFunction = injector.keyGroup.operatorFunction
if (operatorFunction == null) {
VimPlugin.showMessage(MessageHelper.message("E774"))

View File

@ -0,0 +1,150 @@
/*
* Copyright 2003-2023 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
* https://opensource.org/licenses/MIT.
*/
package org.jetbrains.plugins.ideavim.action.change
import org.jetbrains.plugins.ideavim.VimTestCase
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
class OperatorActionTest : VimTestCase() {
@Test
fun `test operator action throws error if operatorfunc is empty`() {
doTest("g@w", "lorem ipsum", "lorem ipsum")
assertPluginErrorMessageContains("E774: 'operatorfunc' is empty")
}
@Test
fun `test operator action throws error if operatorfunc is name of unknown function`() {
doTest("g@w", "lorem ipsum", "lorem ipsum") {
enterCommand("set operatorfunc=Foo")
}
assertPluginErrorMessageContains("E117: Unknown function: Foo")
}
@Test
fun `test operator action with function name`() {
doTest("gxe",
"lorem ipsum dolor sit amet",
"xxxxx ipsum dolor sit amet"
) {
executeVimscript("""function! Redact(type)
| execute "normal `[v`]rx"
|endfunction
""".trimMargin())
enterCommand("noremap gx :set opfunc=Redact<CR>g@")
}
}
@Test
fun `test operator action with character wise motion`() {
doTest("gxe",
"lorem ipsum dolor sit amet",
"charlorem ipsum dolor sit amet"
) {
executeVimscript("""function! Redact(type)
| execute "normal i" . a:type
|endfunction
""".trimMargin())
enterCommand("noremap gx :set opfunc=Redact<CR>g@")
}
}
@Test
fun `test operator action with linewise motion`() {
doTest("Vgx",
"lorem ipsum dolor sit amet",
"linelorem ipsum dolor sit amet"
) {
executeVimscript("""function! Redact(type)
| execute "normal i" . a:type
|endfunction
""".trimMargin())
enterCommand("noremap gx <Esc>:set opfunc=Redact<CR>gvg@")
}
}
@Test
fun `test operator action with blockwise motion`() {
doTest("<C-V>gx",
"lorem ipsum dolor sit amet",
"blocklorem ipsum dolor sit amet"
) {
executeVimscript("""function! Redact(type)
| execute "normal i" . a:type
|endfunction
""".trimMargin())
enterCommand("noremap gx <Esc>:set opfunc=Redact<CR>gvg@")
}
}
@Test
fun `test operator action with function`() {
doTest("gxe",
"lorem ipsum dolor sit amet",
"xxxxx ipsum dolor sit amet"
) {
executeVimscript("""function! Redact(type)
| execute "normal `[v`]rx"
|endfunction
""".trimMargin())
enterCommand("noremap gx :set opfunc=function('Redact')<CR>g@")
}
}
@Test
fun `test operator action throws error with unknown function`() {
doTest("gxe",
"lorem ipsum dolor sit amet",
"lorem ipsum dolor sit amet"
) {
enterCommand("noremap gx :set opfunc=function('Foo')<CR>g@")
}
assertPluginErrorMessageContains("E117: Unknown function: Foo")
}
@Test
fun `test operator function with funcref`() {
doTest("gxe",
"lorem ipsum dolor sit amet",
"xxxxx ipsum dolor sit amet"
) {
executeVimscript("""function! Redact(type)
| execute "normal `[v`]rx"
|endfunction
""".trimMargin())
enterCommand("noremap gx :set opfunc=funcref('Redact')<CR>g@")
}
}
@Test
fun `test operator action throws error with unknown function ref`() {
doTest("gxe",
"lorem ipsum dolor sit amet",
"lorem ipsum dolor sit amet"
) {
enterCommand("noremap gx :set opfunc=funcref('Foo')<CR>g@")
}
assertPluginErrorMessageContains("E117: Unknown function: Foo")
}
@Test
@Disabled(":set does not correctly parse the quotes in the lambda syntax")
// The parser is treating the second double-quote char as a comment. The argument to the command is parsed as:
// opfunc={ arg -> execute "`[v`]rx
// The map command is properly handled - the `<CR>g@` is correctly understood, and the full lambda is passed to the
// parser, but the parser does not fully handle the text
fun `test operator function with lambda`() {
doTest("gxe",
"lorem ipsum dolor sit amet",
"lorem ipsum dolor sit amet"
) {
enterCommand("noremap gx :set opfunc={ arg -> execute \"`[v`]rx\" }<CR>g@")
}
}
}

View File

@ -164,20 +164,20 @@ class SetCommandTest : VimTestCase() {
assertCommandOutput("set all",
"""
|--- Options ---
|noargtextobj ideawrite=all scrolloff=0 notextobj-indent
| closenotebooks noignorecase selectmode= timeout
|nocommentary noincsearch shellcmdflag=-x timeoutlen=1000
|nodigraph nomatchit shellxescape=@ notrackactionids
|noexchange maxmapdepth=20 shellxquote={ undolevels=1000
|nogdefault more showcmd unifyjumps
|nohighlightedyank nomultiple-cursors showmode virtualedit=
| history=50 noNERDTree sidescroll=0 novisualbell
|nohlsearch nrformats=hex sidescrolloff=0 visualdelay=100
|noideaglobalmode nonumber nosmartcase whichwrap=b,s
|noideajoin octopushandler nosneak wrapscan
| ideamarks norelativenumber startofline
| ideastrictmode scroll=0 nosurround
|noideatracetime scrolljump=1 notextobj-entire
|noargtextobj ideawrite=all scrolljump=1 notextobj-entire
| closenotebooks noignorecase scrolloff=0 notextobj-indent
|nocommentary noincsearch selectmode= timeout
|nodigraph nomatchit shellcmdflag=-x timeoutlen=1000
|noexchange maxmapdepth=20 shellxescape=@ notrackactionids
|nogdefault more shellxquote={ undolevels=1000
|nohighlightedyank nomultiple-cursors showcmd unifyjumps
| history=50 noNERDTree showmode virtualedit=
|nohlsearch nrformats=hex sidescroll=0 novisualbell
|noideaglobalmode nonumber sidescrolloff=0 visualdelay=100
|noideajoin octopushandler nosmartcase whichwrap=b,s
| ideamarks operatorfunc= nosneak wrapscan
| ideastrictmode norelativenumber startofline
|noideatracetime scroll=0 nosurround
| clipboard=ideaput,autoselect,exclude:cons\|linux
| excommandannotation
| guicursor=n-v-c:block-Cursor/lCursor,ve:ver35-Cursor,o:hor50-Cursor,i-ci:ver25-Cursor/lCursor,r-cr:hor20-Cursor/lCursor,sm:block-Cursor-blinkwait175-blinkoff150-blinkon175
@ -262,6 +262,7 @@ class SetCommandTest : VimTestCase() {
| nrformats=hex
|nonumber
| octopushandler
| operatorfunc=
|norelativenumber
|noReplaceWithRegister
| scroll=0

View File

@ -350,20 +350,20 @@ class SetglobalCommandTest : VimTestCase() {
setOsSpecificOptionsToSafeValues()
assertCommandOutput("setglobal all", """
|--- Global option values ---
|noargtextobj ideawrite=all scrolloff=0 notextobj-indent
| closenotebooks noignorecase selectmode= timeout
|nocommentary noincsearch shellcmdflag=-x timeoutlen=1000
|nodigraph nomatchit shellxescape=@ notrackactionids
|noexchange maxmapdepth=20 shellxquote={ undolevels=1000
|nogdefault more showcmd unifyjumps
|nohighlightedyank nomultiple-cursors showmode virtualedit=
| history=50 noNERDTree sidescroll=0 novisualbell
|nohlsearch nrformats=hex sidescrolloff=0 visualdelay=100
|noideaglobalmode nonumber nosmartcase whichwrap=b,s
|noideajoin octopushandler nosneak wrapscan
| ideamarks norelativenumber startofline
| ideastrictmode scroll=0 nosurround
|noideatracetime scrolljump=1 notextobj-entire
|noargtextobj ideawrite=all scrolljump=1 notextobj-entire
| closenotebooks noignorecase scrolloff=0 notextobj-indent
|nocommentary noincsearch selectmode= timeout
|nodigraph nomatchit shellcmdflag=-x timeoutlen=1000
|noexchange maxmapdepth=20 shellxescape=@ notrackactionids
|nogdefault more shellxquote={ undolevels=1000
|nohighlightedyank nomultiple-cursors showcmd unifyjumps
| history=50 noNERDTree showmode virtualedit=
|nohlsearch nrformats=hex sidescroll=0 novisualbell
|noideaglobalmode nonumber sidescrolloff=0 visualdelay=100
|noideajoin octopushandler nosmartcase whichwrap=b,s
| ideamarks operatorfunc= nosneak wrapscan
| ideastrictmode norelativenumber startofline
|noideatracetime scroll=0 nosurround
| clipboard=ideaput,autoselect,exclude:cons\|linux
| excommandannotation
| guicursor=n-v-c:block-Cursor/lCursor,ve:ver35-Cursor,o:hor50-Cursor,i-ci:ver25-Cursor/lCursor,r-cr:hor20-Cursor/lCursor,sm:block-Cursor-blinkwait175-blinkoff150-blinkon175
@ -444,6 +444,7 @@ class SetglobalCommandTest : VimTestCase() {
| nrformats=hex
|nonumber
| octopushandler
| operatorfunc=
|norelativenumber
|noReplaceWithRegister
| scroll=0
@ -491,4 +492,4 @@ class SetglobalCommandTest : VimTestCase() {
|""".trimMargin()
)
}
}
}

View File

@ -382,20 +382,20 @@ class SetlocalCommandTest : VimTestCase() {
setOsSpecificOptionsToSafeValues()
assertCommandOutput("setlocal all", """
|--- Local option values ---
|noargtextobj noideatracetime scrolljump=1 notextobj-entire
| closenotebooks ideawrite=all scrolloff=-1 notextobj-indent
|nocommentary noignorecase selectmode= timeout
|nodigraph noincsearch shellcmdflag=-x timeoutlen=1000
|noexchange nomatchit shellxescape=@ notrackactionids
|nogdefault maxmapdepth=20 shellxquote={ unifyjumps
|nohighlightedyank more showcmd virtualedit=
| history=50 nomultiple-cursors showmode novisualbell
|nohlsearch noNERDTree sidescroll=0 visualdelay=100
|noideaglobalmode nrformats=hex sidescrolloff=-1 whichwrap=b,s
|--ideajoin nonumber nosmartcase wrapscan
| ideamarks octopushandler nosneak
| idearefactormode= norelativenumber startofline
| ideastrictmode scroll=0 nosurround
|noargtextobj noideatracetime scroll=0 nosurround
| closenotebooks ideawrite=all scrolljump=1 notextobj-entire
|nocommentary noignorecase scrolloff=-1 notextobj-indent
|nodigraph noincsearch selectmode= timeout
|noexchange nomatchit shellcmdflag=-x timeoutlen=1000
|nogdefault maxmapdepth=20 shellxescape=@ notrackactionids
|nohighlightedyank more shellxquote={ unifyjumps
| history=50 nomultiple-cursors showcmd virtualedit=
|nohlsearch noNERDTree showmode novisualbell
|noideaglobalmode nrformats=hex sidescroll=0 visualdelay=100
|--ideajoin nonumber sidescrolloff=-1 whichwrap=b,s
| ideamarks octopushandler nosmartcase wrapscan
| idearefactormode= operatorfunc= nosneak
| ideastrictmode norelativenumber startofline
| clipboard=ideaput,autoselect,exclude:cons\|linux
| excommandannotation
| guicursor=n-v-c:block-Cursor/lCursor,ve:ver35-Cursor,o:hor50-Cursor,i-ci:ver25-Cursor/lCursor,r-cr:hor20-Cursor/lCursor,sm:block-Cursor-blinkwait175-blinkoff150-blinkon175
@ -482,6 +482,7 @@ class SetlocalCommandTest : VimTestCase() {
| nrformats=hex
|nonumber
| octopushandler
| operatorfunc=
|norelativenumber
|noReplaceWithRegister
| scroll=0
@ -529,4 +530,4 @@ class SetlocalCommandTest : VimTestCase() {
|""".trimMargin()
)
}
}
}

View File

@ -29,6 +29,7 @@ public open class GlobalOptions(scope: OptionAccessScope): OptionsPropertiesBase
public val keymodel: StringListOptionValue by optionProperty(Options.keymodel)
public var maxmapdepth: Int by optionProperty(Options.maxmapdepth)
public var more: Boolean by optionProperty(Options.more)
public var operatorfunc: String by optionProperty(Options.operatorfunc)
public var scrolljump: Int by optionProperty(Options.scrolljump)
public var selection: String by optionProperty(Options.selection)
public val selectmode: StringListOptionValue by optionProperty(Options.selectmode)

View File

@ -253,6 +253,24 @@ public object Options {
}
})
public val operatorfunc: StringOption = addOption(object : StringOption("operatorfunc", GLOBAL, "opfunc", VimString.EMPTY) {
override fun parseValue(value: String, token: String): VimString {
// TODO: Support script local functions
// If this value is a function name, it should be a global function. It's possible to use a local function by
// adding the correct `<SNR>#_` prefix for the script context. Setting the option should automatically expand the
// `<SID>` prefix to `<SNR>#_`.
// If using the `funcref('...')` or `function('...')` expressions, `<SID>` is also expanded, but it's not clear if
// setting the option does a simple find/replace inside the string option value, or if the expression is parsed
// and the string literal is expanded (this might not affect the end result, but it does have implications for
// IdeaVim's implementation).
// The `s:` prefix is not supported, and using it will result in all of the following errors:
// * E81: Using <SID> not in a script context
// * E475: Invalid argument: s:MyFunc
// * E474: Invalid argument: opfunc=funcref('s:MyFunc')
return super.parseValue(value, token)
}
})
public val scrolljump: NumberOption = addOption(object : NumberOption("scrolljump", GLOBAL, "sj", 1) {
override fun checkIfValueValid(value: VimDataType, token: String) {
super.checkIfValueValid(value, token)