diff --git a/doc/IdeaVim Plugins.md b/doc/IdeaVim Plugins.md index de38d59d6e..4bd3ca3e3c 100644 --- a/doc/IdeaVim Plugins.md +++ b/doc/IdeaVim Plugins.md @@ -423,6 +423,16 @@ https://plugins.jetbrains.com/plugin/19417-ideavim-quickscope +
+

Any Quotes Text Object

+ +Original plugin: [mini.ai](https://github.com/echasnovski/mini.ai). + +### Setup: +- Add the following command to `~/.ideavimrc`: `set any-quotes-text-object` + +
+

Which-Key

@@ -484,4 +494,3 @@ or restart the IDE. https://plugins.jetbrains.com/plugin/25899-vim-switch -
diff --git a/src/main/java/com/maddyhome/idea/vim/extension/anyquotestextobj/AnyQuotesTextObj.kt b/src/main/java/com/maddyhome/idea/vim/extension/anyquotestextobj/AnyQuotesTextObj.kt new file mode 100644 index 0000000000..fd9b1e1c07 --- /dev/null +++ b/src/main/java/com/maddyhome/idea/vim/extension/anyquotestextobj/AnyQuotesTextObj.kt @@ -0,0 +1,154 @@ +/* + * 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 com.maddyhome.idea.vim.extension.anyquotestextobj + +import com.maddyhome.idea.vim.KeyHandler +import com.maddyhome.idea.vim.VimPlugin +import com.maddyhome.idea.vim.api.ExecutionContext +import com.maddyhome.idea.vim.api.ImmutableVimCaret +import com.maddyhome.idea.vim.api.VimEditor +import com.maddyhome.idea.vim.api.injector +import com.maddyhome.idea.vim.command.CommandFlags +import com.maddyhome.idea.vim.command.MappingMode +import com.maddyhome.idea.vim.command.OperatorArguments +import com.maddyhome.idea.vim.command.TextObjectVisualType +import com.maddyhome.idea.vim.common.TextRange +import com.maddyhome.idea.vim.extension.ExtensionHandler +import com.maddyhome.idea.vim.extension.VimExtension +import com.maddyhome.idea.vim.extension.VimExtensionFacade.putExtensionHandlerMapping +import com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMappingIfMissing +import com.maddyhome.idea.vim.handler.TextObjectActionHandler +import com.maddyhome.idea.vim.helper.enumSetOf +import com.maddyhome.idea.vim.state.KeyHandlerState +import org.jetbrains.annotations.NonNls +import java.util.* + +/** + * Add motions to work with any type of quotes: single, double, and back quotes + * + *

+ * any quotes provides two motions: + *

+ * + * @author Osvaldo Cordova Aburto (@oca159) + */ +internal class AnyQuotesTextObj : VimExtension { + + override fun getName() = "any-quotes-text-object" + + @NonNls + private val NO_MAPPINGS = "anyquotes_no_mappings" + + override fun init() { + putExtensionHandlerMapping( + MappingMode.XO, + injector.parser.parseKeys("any-quotes-aq"), + owner, + AroundAnyQuotesHandler(), + false + ) + putExtensionHandlerMapping( + MappingMode.XO, + injector.parser.parseKeys("any-quotes-iq"), + owner, + InsideAnyQuotesHandler(), + false + ) + + val noMappings = VimPlugin.getVariableService().getGlobalVariableValue(NO_MAPPINGS)?.asBoolean() ?: false + if (!noMappings) { + putKeyMappingIfMissing( + MappingMode.XO, + injector.parser.parseKeys("aq"), + owner, + injector.parser.parseKeys("any-quotes-aq"), + true + ) + putKeyMappingIfMissing( + MappingMode.XO, + injector.parser.parseKeys("iq"), + owner, + injector.parser.parseKeys("any-quotes-iq"), + true + ) + } + } + + private class InsideAnyQuotesHandler : ExtensionHandler { + override val isRepeatable = true + + override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) { + val keyHandlerState: KeyHandlerState = KeyHandler.getInstance().keyHandlerState + keyHandlerState.commandBuilder.addAction(MotionInnerAnyQuoteProximityAction()) + } + } + + class MotionInnerAnyQuoteProximityAction : TextObjectActionHandler() { + + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_TEXT_BLOCK) + + override val visualType: TextObjectVisualType = TextObjectVisualType.CHARACTER_WISE + + override fun getRange( + editor: VimEditor, + caret: ImmutableVimCaret, + context: ExecutionContext, + count: Int, + rawCount: Int, + ): TextRange? { + return listOf('`', '"', '\'') + .mapNotNull { quote -> + injector.searchHelper.findBlockQuoteInLineRange(editor, caret, quote, false)?.let { range -> range to quote } + } + .minByOrNull { it.first.distanceTo(caret.offset) }?.first + } + } + + private class AroundAnyQuotesHandler : ExtensionHandler { + override val isRepeatable = true + + override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) { + val keyHandlerState: KeyHandlerState = KeyHandler.getInstance().keyHandlerState + keyHandlerState.commandBuilder.addAction(MotionOuterAnyQuoteProximityAction()) + } + } +} + +class MotionOuterAnyQuoteProximityAction : TextObjectActionHandler() { + + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_TEXT_BLOCK) + + override val visualType: TextObjectVisualType = TextObjectVisualType.CHARACTER_WISE + + override fun getRange( + editor: VimEditor, + caret: ImmutableVimCaret, + context: ExecutionContext, + count: Int, + rawCount: Int, + ): TextRange? { + return listOf('`', '"', '\'') + .mapNotNull { quote -> + injector.searchHelper.findBlockQuoteInLineRange(editor, caret, quote, true)?.let { range -> range to quote } + } + .minByOrNull { it.first.distanceTo(caret.offset) }?.first + } +} + +private fun TextRange.distanceTo(caretOffset: Int): Int { + return if (caretOffset < startOffset) { + startOffset - caretOffset + } else if (caretOffset > endOffset) { + caretOffset - endOffset + } else { + 0 // Caret is inside the range + } +} \ No newline at end of file diff --git a/src/main/java/com/maddyhome/idea/vim/statistic/PluginState.kt b/src/main/java/com/maddyhome/idea/vim/statistic/PluginState.kt index 355516a124..9b1017e92f 100644 --- a/src/main/java/com/maddyhome/idea/vim/statistic/PluginState.kt +++ b/src/main/java/com/maddyhome/idea/vim/statistic/PluginState.kt @@ -45,7 +45,8 @@ internal class PluginState : ApplicationUsagesCollector() { "surround", "commentary", "matchit", - "textobj-indent" + "textobj-indent", + "any-quotes-text-object" ) internal val enabledExtensions = HashSet() } diff --git a/src/main/resources/META-INF/includes/VimExtensions.xml b/src/main/resources/META-INF/includes/VimExtensions.xml index f7d15720a1..01e174d571 100644 --- a/src/main/resources/META-INF/includes/VimExtensions.xml +++ b/src/main/resources/META-INF/includes/VimExtensions.xml @@ -49,6 +49,14 @@ + + + + + + + diff --git a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetCommandTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetCommandTest.kt index ba0f965f9b..09623db0ca 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetCommandTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetCommandTest.kt @@ -197,6 +197,7 @@ class SetCommandTest : VimTestCase() { | history=50 nonumber nosmartcase whichwrap=b,s |nohlsearch operatorfunc= nosneak wrap |noideajoin norelativenumber startofline wrapscan + |noany-quotes-text-object | clipboard=ideaput,autoselect | fileencoding=utf-8 | 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 @@ -255,6 +256,7 @@ class SetCommandTest : VimTestCase() { assertCommandOutput( "set! all", """ |--- Options --- + |noany-quotes-text-object |noargtextobj |nobomb |nobreakindent diff --git a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetglobalCommandTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetglobalCommandTest.kt index 48afdcb8af..a7b0995dda 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetglobalCommandTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetglobalCommandTest.kt @@ -447,6 +447,7 @@ class SetglobalCommandTest : VimTestCase() { | history=50 operatorfunc= startofline |nohlsearch norelativenumber nosurround |noideajoin scroll=0 notextobj-entire + |noany-quotes-text-object | clipboard=ideaput,autoselect | 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 | ide=IntelliJ IDEA Community Edition @@ -506,6 +507,7 @@ class SetglobalCommandTest : VimTestCase() { assertCommandOutput( "setglobal! all", """ |--- Global option values --- + |noany-quotes-text-object |noargtextobj |nobomb |nobreakindent diff --git a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetlocalCommandTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetlocalCommandTest.kt index 7f51d520ef..7852146a06 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetlocalCommandTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/SetlocalCommandTest.kt @@ -497,6 +497,7 @@ class SetlocalCommandTest : VimTestCase() { | history=50 nrformats=hex sidescrolloff=-1 whichwrap=b,s |nohlsearch nonumber nosmartcase wrap |--ideajoin operatorfunc= nosneak wrapscan + |noany-quotes-text-object | clipboard=ideaput,autoselect | fileencoding=utf-8 | 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 @@ -555,6 +556,7 @@ class SetlocalCommandTest : VimTestCase() { assertCommandOutput( "setlocal! all", """ |--- Local option values --- + |noany-quotes-text-object |noargtextobj |nobomb |nobreakindent