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 JetBrains/ideavim#702
This commit is contained in:
parent
9b81c7e650
commit
3a294268d9
src
main/java/com/maddyhome/idea/vim/action/change
test/java/org/jetbrains/plugins/ideavim
action/change
ex/implementation/commands
vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api
@ -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"))
|
||||
|
@ -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@")
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user