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:
+ *
+ * - aq around any quotes.
+ * - iq inside any quotes
+ *
+ *
+ * @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