mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2025-06-01 19:34:05 +02:00
feat: improve any brackets behavior
This commit is contained in:
parent
ffee3ccbeb
commit
491a96825f
src
main/java/com/maddyhome/idea/vim/extension/miniai
test/java/org/jetbrains/plugins/ideavim/extension/miniai
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2003-2023 The IdeaVim authors
|
* Copyright 2003-2025 The IdeaVim authors
|
||||||
*
|
*
|
||||||
* Use of this source code is governed by an MIT-style
|
* Use of this source code is governed by an MIT-style
|
||||||
* license that can be found in the LICENSE.txt file or at
|
* license that can be found in the LICENSE.txt file or at
|
||||||
@ -21,36 +21,31 @@ import com.maddyhome.idea.vim.extension.ExtensionHandler
|
|||||||
import com.maddyhome.idea.vim.extension.VimExtension
|
import com.maddyhome.idea.vim.extension.VimExtension
|
||||||
import com.maddyhome.idea.vim.extension.VimExtensionFacade.putExtensionHandlerMapping
|
import com.maddyhome.idea.vim.extension.VimExtensionFacade.putExtensionHandlerMapping
|
||||||
import com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMapping
|
import com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMapping
|
||||||
import com.maddyhome.idea.vim.group.findBlockRange
|
|
||||||
import com.maddyhome.idea.vim.handler.TextObjectActionHandler
|
import com.maddyhome.idea.vim.handler.TextObjectActionHandler
|
||||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||||
import com.maddyhome.idea.vim.state.KeyHandlerState
|
import java.util.EnumSet
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend and create a/i textobjects
|
* A simplified imitation of mini.ai approach for motions "aq", "iq", "ab", "ib".
|
||||||
|
* Instead of "closest" text object, we apply a "cover-or-next" logic:
|
||||||
*
|
*
|
||||||
* <p>
|
* 1) Among all candidate pairs, pick any that cover the caret (start <= caret < end).
|
||||||
* mini ai provides the next motions:
|
* Among those, pick the smallest range.
|
||||||
* <ul>
|
* 2) If none cover the caret, pick the "next" pair (start >= caret) that is closest.
|
||||||
* <li>aq around any quotes.</li>
|
* 3) If none are next, pick the "previous" pair (end <= caret) that is closest.
|
||||||
* <li>iq inside any quotes.</li>
|
|
||||||
* <li>ab around any parentheses, curly braces and square brackets.</li>
|
|
||||||
* <li>ib inside any parentheses, curly braces and square brackets.</li>
|
|
||||||
* </ul>
|
|
||||||
*
|
*
|
||||||
* @author Osvaldo Cordova Aburto (@oca159)
|
* For "i" text object, we shrink the boundaries inward by one character on each side.
|
||||||
*/
|
*/
|
||||||
internal class MiniAI : VimExtension {
|
class MiniAI : VimExtension {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// Constants for key mappings
|
// <Plug> mappings
|
||||||
private const val PLUG_AQ = "<Plug>mini-ai-aq"
|
private const val PLUG_AQ = "<Plug>mini-ai-aq"
|
||||||
private const val PLUG_IQ = "<Plug>mini-ai-iq"
|
private const val PLUG_IQ = "<Plug>mini-ai-iq"
|
||||||
private const val PLUG_AB = "<Plug>mini-ai-ab"
|
private const val PLUG_AB = "<Plug>mini-ai-ab"
|
||||||
private const val PLUG_IB = "<Plug>mini-ai-ib"
|
private const val PLUG_IB = "<Plug>mini-ai-ib"
|
||||||
|
|
||||||
// Constants for key sequences
|
// Actual user key sequences
|
||||||
private const val KEY_AQ = "aq"
|
private const val KEY_AQ = "aq"
|
||||||
private const val KEY_IQ = "iq"
|
private const val KEY_IQ = "iq"
|
||||||
private const val KEY_AB = "ab"
|
private const val KEY_AB = "ab"
|
||||||
@ -64,118 +59,46 @@ internal class MiniAI : VimExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun registerMappings() {
|
private fun registerMappings() {
|
||||||
val mappings = listOf(
|
fun createHandler(
|
||||||
PLUG_AQ to AroundAnyQuotesHandler(),
|
rangeFunc: (VimEditor, ImmutableVimCaret, Boolean) -> TextRange?
|
||||||
PLUG_IQ to InsideAnyQuotesHandler(),
|
): ExtensionHandler = object : ExtensionHandler {
|
||||||
PLUG_AB to AroundAnyBracketsHandler(),
|
override val isRepeatable = true
|
||||||
PLUG_IB to InsideAnyBracketsHandler()
|
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
|
||||||
)
|
addAction(PortedMiniAiAction(rangeFunc))
|
||||||
|
}
|
||||||
mappings.forEach { (key, handler) ->
|
|
||||||
putExtensionHandlerMapping(MappingMode.XO, injector.parser.parseKeys(key), owner, handler, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val keyMappings = listOf(
|
listOf(
|
||||||
|
// Outer quotes
|
||||||
|
PLUG_AQ to createHandler { e, c, _ -> findQuoteRange(e, c, isOuter = true) },
|
||||||
|
// Inner quotes
|
||||||
|
PLUG_IQ to createHandler { e, c, _ -> findQuoteRange(e, c, isOuter = false) },
|
||||||
|
// Outer brackets
|
||||||
|
PLUG_AB to createHandler { e, c, _ -> findBracketRange(e, c, isOuter = true) },
|
||||||
|
// Inner brackets
|
||||||
|
PLUG_IB to createHandler { e, c, _ -> findBracketRange(e, c, isOuter = false) }
|
||||||
|
).forEach { (plug, handler) ->
|
||||||
|
putExtensionHandlerMapping(MappingMode.XO, injector.parser.parseKeys(plug), owner, handler, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map user keys -> <Plug> keys
|
||||||
|
listOf(
|
||||||
KEY_AQ to PLUG_AQ,
|
KEY_AQ to PLUG_AQ,
|
||||||
KEY_IQ to PLUG_IQ,
|
KEY_IQ to PLUG_IQ,
|
||||||
KEY_AB to PLUG_AB,
|
KEY_AB to PLUG_AB,
|
||||||
KEY_IB to PLUG_IB
|
KEY_IB to PLUG_IB
|
||||||
)
|
).forEach { (key, plug) ->
|
||||||
|
|
||||||
keyMappings.forEach { (key, plug) ->
|
|
||||||
putKeyMapping(MappingMode.XO, injector.parser.parseKeys(key), owner, injector.parser.parseKeys(plug), true)
|
putKeyMapping(MappingMode.XO, injector.parser.parseKeys(key), owner, injector.parser.parseKeys(plug), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private class InsideAnyQuotesHandler : ExtensionHandler {
|
|
||||||
override val isRepeatable = true
|
|
||||||
|
|
||||||
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
|
|
||||||
addAction(MotionInnerAnyQuoteProximityAction())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class AroundAnyQuotesHandler : ExtensionHandler {
|
|
||||||
override val isRepeatable = true
|
|
||||||
|
|
||||||
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
|
|
||||||
addAction(MotionOuterAnyQuoteProximityAction())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class InsideAnyBracketsHandler : ExtensionHandler {
|
|
||||||
override val isRepeatable = true
|
|
||||||
|
|
||||||
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
|
|
||||||
addAction(MotionInnerAnyBracketProximityAction())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class AroundAnyBracketsHandler : ExtensionHandler {
|
|
||||||
override val isRepeatable = true
|
|
||||||
|
|
||||||
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
|
|
||||||
addAction(MotionOuterAnyBracketProximityAction())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MotionInnerAnyQuoteProximityAction : TextObjectActionHandler() {
|
|
||||||
|
|
||||||
override val flags: EnumSet<CommandFlags> = 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 findClosestQuoteRange(editor, caret, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MotionOuterAnyQuoteProximityAction : TextObjectActionHandler() {
|
|
||||||
|
|
||||||
override val flags: EnumSet<CommandFlags> = 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 findClosestQuoteRange(editor, caret, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MotionInnerAnyBracketProximityAction : TextObjectActionHandler() {
|
|
||||||
|
|
||||||
override val flags: EnumSet<CommandFlags> = 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 findClosestBracketRange(editor, caret, count, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MotionOuterAnyBracketProximityAction : TextObjectActionHandler() {
|
// A text object action that uses the "mini.ai-like" picking strategy.
|
||||||
|
private class PortedMiniAiAction(
|
||||||
|
private val rangeFunc: (VimEditor, ImmutableVimCaret, Boolean) -> TextRange?
|
||||||
|
) : TextObjectActionHandler() {
|
||||||
|
|
||||||
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_TEXT_BLOCK)
|
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_TEXT_BLOCK)
|
||||||
|
|
||||||
override val visualType: TextObjectVisualType = TextObjectVisualType.CHARACTER_WISE
|
override val visualType: TextObjectVisualType = TextObjectVisualType.CHARACTER_WISE
|
||||||
|
|
||||||
override fun getRange(
|
override fun getRange(
|
||||||
@ -183,71 +106,185 @@ class MotionOuterAnyBracketProximityAction : TextObjectActionHandler() {
|
|||||||
caret: ImmutableVimCaret,
|
caret: ImmutableVimCaret,
|
||||||
context: ExecutionContext,
|
context: ExecutionContext,
|
||||||
count: Int,
|
count: Int,
|
||||||
rawCount: Int,
|
rawCount: Int
|
||||||
): TextRange? {
|
): TextRange? = rangeFunc(editor, caret, true).also {
|
||||||
return findClosestBracketRange(editor, caret, count, true)
|
// We don't do 'count' expansions here. If you wanted to replicate
|
||||||
|
// mini.ai's multi-level expansions, you'd call the "rangeFunc" multiple
|
||||||
|
// times re-feeding the last output as reference, etc.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Utility to register action in KeyHandler
|
||||||
private fun addAction(action: TextObjectActionHandler) {
|
private fun addAction(action: TextObjectActionHandler) {
|
||||||
val keyHandlerState: KeyHandlerState = KeyHandler.getInstance().keyHandlerState
|
KeyHandler.getInstance().keyHandlerState.commandBuilder.addAction(action)
|
||||||
keyHandlerState.commandBuilder.addAction(action)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findClosestDelimitedRange(
|
/* -------------------------------------------------------------------------
|
||||||
caret: ImmutableVimCaret,
|
* Core mini.ai-like logic
|
||||||
delimiters: List<Char>,
|
* ------------------------------------------------------------------------- */
|
||||||
findRange: (Char) -> TextRange?
|
|
||||||
): TextRange? {
|
/**
|
||||||
val allRanges = delimiters.mapNotNull { char ->
|
* Find a text range for quotes (", ', `) around the caret using a "cover-or-next" approach.
|
||||||
findRange(char)?.let { range ->
|
* - If one or more pairs **cover** the caret, pick the *smallest* covering pair.
|
||||||
DelimitedRange(range, char)
|
* - Else pick the "next" pair whose start >= caret offset (closest).
|
||||||
|
* - Else pick the "previous" pair whose end <= caret offset (closest).
|
||||||
|
*
|
||||||
|
* If [isOuter] == false (i.e. 'iq'), shrink the final range by 1 char on each side.
|
||||||
|
*/
|
||||||
|
private fun findQuoteRange(editor: VimEditor, caret: ImmutableVimCaret, isOuter: Boolean): TextRange? {
|
||||||
|
val text = editor.text()
|
||||||
|
val caretOffset = caret.offset
|
||||||
|
val caretLine = caret.getLine()
|
||||||
|
|
||||||
|
// 1) Gather quotes in *this caret's line*
|
||||||
|
val lineStart = editor.getLineStartOffset(caretLine)
|
||||||
|
val lineEnd = editor.getLineEndOffset(caretLine)
|
||||||
|
val lineText = text.substring(lineStart, lineEnd)
|
||||||
|
val lineRanges = gatherAllQuoteRanges(lineText).map {
|
||||||
|
TextRange(it.startOffset + lineStart, it.endOffset + lineStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
val localBest = pickBestRange(lineRanges, caretOffset)
|
||||||
|
if (localBest != null) {
|
||||||
|
return adjustRangeForInnerOuter(localBest, isOuter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Fallback: entire buffer
|
||||||
|
val allRanges = gatherAllQuoteRanges(text)
|
||||||
|
val bestOverall = pickBestRange(allRanges, caretOffset) ?: return null
|
||||||
|
return adjustRangeForInnerOuter(bestOverall, isOuter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Adjust final range if user requested 'inner' (i.e. skip bounding chars). */
|
||||||
|
private fun adjustRangeForInnerOuter(range: TextRange, isOuter: Boolean): TextRange? {
|
||||||
|
if (isOuter) return range
|
||||||
|
// For 'inner', skip bounding chars if possible
|
||||||
|
if (range.endOffset - range.startOffset < 2) return null
|
||||||
|
return TextRange(range.startOffset + 1, range.endOffset - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gather all "balanced" pairs for single/double/backtick quotes in the entire text.
|
||||||
|
* For simplicity, we treat each ["]...["] or [']...['] or [`]...[`] as one range,
|
||||||
|
* ignoring complicated cases of escaping, multi-line, etc.
|
||||||
|
*/
|
||||||
|
private fun gatherAllQuoteRanges(text: CharSequence): List<TextRange> {
|
||||||
|
val results = mutableListOf<TextRange>()
|
||||||
|
val patterns = listOf(
|
||||||
|
"\"([^\"]*)\"",
|
||||||
|
"'([^']*)'",
|
||||||
|
"`([^`]*)`"
|
||||||
|
)
|
||||||
|
for (p in patterns) {
|
||||||
|
Regex(p).findAll(text).forEach {
|
||||||
|
results.add(TextRange(it.range.first, it.range.last + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a text range for brackets using a "cover-or-next" approach.
|
||||||
|
* We treat bracket pairs ( (), [], {}, <> ) in a naive balanced scanning way.
|
||||||
|
* If [isOuter] is false, we shrink boundaries to skip the bracket chars.
|
||||||
|
*/
|
||||||
|
private fun findBracketRange(editor: VimEditor, caret: ImmutableVimCaret, isOuter: Boolean): TextRange? {
|
||||||
|
val text = editor.text()
|
||||||
|
val caretOffset = caret.offset
|
||||||
|
val caretLine = caret.getLine()
|
||||||
|
|
||||||
|
// 1) Gather bracket pairs in *this caret's line*
|
||||||
|
val lineStart = editor.getLineStartOffset(caretLine)
|
||||||
|
val lineEnd = editor.getLineEndOffset(caretLine)
|
||||||
|
val bracketChars = listOf('(', ')', '[', ']', '{', '}', '<', '>')
|
||||||
|
// Gather local line bracket pairs
|
||||||
|
val lineText = text.substring(lineStart, lineEnd)
|
||||||
|
val lineRanges = gatherAllBracketRanges(lineText, bracketChars).map {
|
||||||
|
// Shift each range's offsets to the global text
|
||||||
|
TextRange(it.startOffset + lineStart, it.endOffset + lineStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick the best match on this line
|
||||||
|
val localBest = pickBestRange(lineRanges, caretOffset)
|
||||||
|
if (localBest != null) {
|
||||||
|
return adjustRangeForInnerOuter(localBest, isOuter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Fallback: gather bracket pairs in the entire file
|
||||||
|
val allRanges = gatherAllBracketRanges(text, bracketChars)
|
||||||
|
val bestOverall = pickBestRange(allRanges, caretOffset) ?: return null
|
||||||
|
|
||||||
|
return adjustRangeForInnerOuter(bestOverall, isOuter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gathers naive "balanced bracket" ranges for the given bracket pairs.
|
||||||
|
* This is a simplified stack-based approach scanning the entire file text.
|
||||||
|
*/
|
||||||
|
private fun gatherAllBracketRanges(
|
||||||
|
text: CharSequence,
|
||||||
|
brackets: List<Char>
|
||||||
|
): List<TextRange> {
|
||||||
|
val pairs = mapOf('(' to ')', '[' to ']', '{' to '}', '<' to '>')
|
||||||
|
val results = mutableListOf<TextRange>()
|
||||||
|
val stack = ArrayDeque<Int>() // offsets of open bracket
|
||||||
|
val bracketTypeStack = ArrayDeque<Char>() // store which bracket
|
||||||
|
|
||||||
|
text.forEachIndexed { i, ch ->
|
||||||
|
if (pairs.containsKey(ch)) {
|
||||||
|
// Opening bracket
|
||||||
|
stack.addLast(i)
|
||||||
|
bracketTypeStack.addLast(ch)
|
||||||
|
} else {
|
||||||
|
// Maybe a closing bracket?
|
||||||
|
val top = bracketTypeStack.lastOrNull() ?: '\u0000'
|
||||||
|
if (pairs[top] == ch) {
|
||||||
|
// Balanced pair
|
||||||
|
val openPos = stack.removeLast()
|
||||||
|
bracketTypeStack.removeLast()
|
||||||
|
results.add(TextRange(openPos, i + 1)) // i+1 for endOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Picks best range among [candidates] in a “cover-or-next” approach:
|
||||||
|
* 1) Among those covering [caretOffset], pick the narrowest.
|
||||||
|
* 2) Else pick the "next" bracket whose start >= caret, if any (closest).
|
||||||
|
* 3) Else pick the "previous" bracket whose end <= caret, if any (closest).
|
||||||
|
*/
|
||||||
|
private fun pickBestRange(candidates: List<TextRange>, caretOffset: Int): TextRange? {
|
||||||
|
if (candidates.isEmpty()) return null
|
||||||
|
val covering = mutableListOf<TextRange>()
|
||||||
|
val nextOnes = mutableListOf<TextRange>()
|
||||||
|
val prevOnes = mutableListOf<TextRange>()
|
||||||
|
|
||||||
|
for (r in candidates) {
|
||||||
|
if (r.startOffset <= caretOffset && caretOffset < r.endOffset) {
|
||||||
|
covering.add(r)
|
||||||
|
} else if (r.startOffset >= caretOffset) {
|
||||||
|
nextOnes.add(r)
|
||||||
|
} else if (r.endOffset <= caretOffset) {
|
||||||
|
prevOnes.add(r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, find all ranges that contain the caret
|
// 1) Covering, smallest width
|
||||||
val containingRanges = allRanges.filter {
|
if (covering.isNotEmpty()) {
|
||||||
caret.offset in it.range.startOffset..it.range.endOffset
|
return covering.minByOrNull { it.endOffset - it.startOffset }
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have containing ranges, return the smallest one
|
// 2) Next (closest by startOffset)
|
||||||
if (containingRanges.isNotEmpty()) {
|
if (nextOnes.isNotEmpty()) {
|
||||||
return containingRanges.minBy {
|
return nextOnes.minByOrNull { kotlin.math.abs(it.startOffset - caretOffset) }
|
||||||
it.range.endOffset - it.range.startOffset
|
|
||||||
}.range
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no containing ranges, find the closest one
|
// 3) Previous (closest by endOffset)
|
||||||
return allRanges
|
if (prevOnes.isNotEmpty()) {
|
||||||
.minByOrNull { range ->
|
return prevOnes.minByOrNull { kotlin.math.abs(it.endOffset - caretOffset) }
|
||||||
kotlin.math.abs(caret.offset - range.range.startOffset)
|
}
|
||||||
}?.range
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findClosestBracketRange(
|
|
||||||
editor: VimEditor,
|
|
||||||
caret: ImmutableVimCaret,
|
|
||||||
count: Int,
|
|
||||||
isOuter: Boolean
|
|
||||||
): TextRange? {
|
|
||||||
val brackets = listOf('(', '[', '{')
|
|
||||||
return findClosestDelimitedRange(caret, brackets) { char ->
|
|
||||||
findBlockRange(editor, caret, char, count, isOuter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun findClosestQuoteRange(
|
|
||||||
editor: VimEditor,
|
|
||||||
caret: ImmutableVimCaret,
|
|
||||||
isOuter: Boolean
|
|
||||||
): TextRange? {
|
|
||||||
val quotes = listOf('`', '"', '\'')
|
|
||||||
return findClosestDelimitedRange(caret, quotes) { char ->
|
|
||||||
injector.searchHelper.findBlockQuoteInLineRange(editor, caret, char, isOuter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private data class DelimitedRange(
|
|
||||||
val range: TextRange,
|
|
||||||
val char: Char
|
|
||||||
)
|
|
||||||
|
@ -37,6 +37,157 @@ class MiniAIExtensionTest : VimTestCase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFalseSingleQuoteInTheMiddle() {
|
||||||
|
doTest(
|
||||||
|
"ciq",
|
||||||
|
"'balanced'false <caret>string'balanced'",
|
||||||
|
"'balanced'false string'<caret>'",
|
||||||
|
Mode.INSERT,
|
||||||
|
JavaFileType.INSTANCE,
|
||||||
|
)
|
||||||
|
assertSelection(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFalseDoubleQuoteInTheMiddle() {
|
||||||
|
doTest(
|
||||||
|
"ciq",
|
||||||
|
"\"balanced\"false <caret>string\"balanced\"",
|
||||||
|
"\"balanced\"false string\"<caret>\"",
|
||||||
|
Mode.INSERT,
|
||||||
|
JavaFileType.INSTANCE,
|
||||||
|
)
|
||||||
|
assertSelection(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testFalseBackQuoteInTheMiddle() {
|
||||||
|
doTest(
|
||||||
|
"ciq",
|
||||||
|
"`balanced`false <caret>string`balanced`",
|
||||||
|
"`balanced`false string`<caret>`",
|
||||||
|
Mode.INSERT,
|
||||||
|
JavaFileType.INSTANCE,
|
||||||
|
)
|
||||||
|
assertSelection(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testInsideMultilineSingleQuote() {
|
||||||
|
doTest(
|
||||||
|
"ciq",
|
||||||
|
"""'
|
||||||
|
something
|
||||||
|
<caret>
|
||||||
|
something
|
||||||
|
'""",
|
||||||
|
"''",
|
||||||
|
Mode.INSERT,
|
||||||
|
JavaFileType.INSTANCE,
|
||||||
|
)
|
||||||
|
assertSelection(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testInsideMultilineDoubleQuote() {
|
||||||
|
doTest(
|
||||||
|
"ciq",
|
||||||
|
""""
|
||||||
|
something
|
||||||
|
<caret>
|
||||||
|
something
|
||||||
|
"""",
|
||||||
|
"\"\"",
|
||||||
|
Mode.INSERT,
|
||||||
|
JavaFileType.INSTANCE,
|
||||||
|
)
|
||||||
|
assertSelection(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testInsideMultilineBackQuote() {
|
||||||
|
doTest(
|
||||||
|
"ciq",
|
||||||
|
"""`
|
||||||
|
something
|
||||||
|
<caret>
|
||||||
|
something
|
||||||
|
`""",
|
||||||
|
"``",
|
||||||
|
Mode.INSERT,
|
||||||
|
JavaFileType.INSTANCE,
|
||||||
|
)
|
||||||
|
assertSelection(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNextInsideMultilineSingleQuote() {
|
||||||
|
doTest(
|
||||||
|
"ciq",
|
||||||
|
"""
|
||||||
|
<caret>
|
||||||
|
|
||||||
|
'
|
||||||
|
something
|
||||||
|
'
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
'<caret>'
|
||||||
|
""",
|
||||||
|
Mode.INSERT,
|
||||||
|
JavaFileType.INSTANCE,
|
||||||
|
)
|
||||||
|
assertSelection(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNextInsideMultilineCurlyBracket() {
|
||||||
|
doTest(
|
||||||
|
"cib",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
<caret>
|
||||||
|
print(something)
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
"""
|
||||||
|
{}
|
||||||
|
""",
|
||||||
|
Mode.INSERT,
|
||||||
|
JavaFileType.INSTANCE,
|
||||||
|
)
|
||||||
|
assertSelection(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNextBracketInCurrentLineHasPriority() {
|
||||||
|
// Test case 1: Next bracket in same line has priority
|
||||||
|
doTest(
|
||||||
|
"cib",
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
<caret> print(something) {nested}
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
print(<caret>) {nested}
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
Mode.INSERT,
|
||||||
|
JavaFileType.INSTANCE,
|
||||||
|
)
|
||||||
|
assertSelection(null)
|
||||||
|
}
|
||||||
|
|
||||||
// To make sure the order in list is not relevant
|
// To make sure the order in list is not relevant
|
||||||
@Test
|
@Test
|
||||||
fun testChangeInsideBackQuoteWithNestedSingleQuote() {
|
fun testChangeInsideBackQuoteWithNestedSingleQuote() {
|
||||||
|
Loading…
Reference in New Issue
Block a user