mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2025-04-16 02:15:43 +02:00
[VIM-3731] Add support for "jump to previous/next lowercase mark".
Fixes VIM-3731
This commit is contained in:
parent
3c167f35d4
commit
cb218697fa
src/test/java/org/jetbrains/plugins/ideavim/action
vim-engine/src/main
kotlin/com/maddyhome/idea/vim
action/motion/mark
api
mark
resources/ksp-generated
@ -271,6 +271,186 @@ class MarkTest : VimTestCase() {
|
||||
assertOffset(14)
|
||||
}
|
||||
|
||||
// VIM-3731 |m| |[`|
|
||||
@Test
|
||||
fun testGotoPreviousMark() {
|
||||
typeTextInFile(
|
||||
injector.parser.parseKeys("ma" + "jmb" + "wmc" + "[`"),
|
||||
"""
|
||||
one two
|
||||
<caret>three
|
||||
four five
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
assertOffset(14)
|
||||
}
|
||||
|
||||
// VIM-3731 |m| |[`|
|
||||
@Test
|
||||
fun testGotoPreviousMarkIgnoresPlacingOrder() {
|
||||
typeTextInFile(
|
||||
injector.parser.parseKeys("mb" + "kma" + "jwmc" + "[`"),
|
||||
"""
|
||||
one two
|
||||
three
|
||||
<caret>four five
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
assertOffset(14)
|
||||
}
|
||||
|
||||
// VIM-3731 |m| |[`|
|
||||
@Test
|
||||
fun testGotoPreviousMarkMultipleMarksOnSamePosition() {
|
||||
typeTextInFile(
|
||||
injector.parser.parseKeys("mb" + "kma" + "jwmcmd" + "[`"),
|
||||
"""
|
||||
one two
|
||||
three
|
||||
<caret>four five
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
assertOffset(14)
|
||||
}
|
||||
|
||||
// VIM-3731 |m| |[`|
|
||||
@Test
|
||||
fun testGotoPreviousMarkWithCount() {
|
||||
typeTextInFile(
|
||||
injector.parser.parseKeys("ma" + "jmb" + "wmc" + "2[`"),
|
||||
"""
|
||||
one two
|
||||
<caret>three
|
||||
four five
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
assertOffset(8)
|
||||
}
|
||||
|
||||
// VIM-3731 |m| |[`|
|
||||
@Test
|
||||
fun testGotoPreviousMarkWithExcessiveCount() {
|
||||
typeTextInFile(
|
||||
injector.parser.parseKeys("ma" + "jmb" + "wmc" + "5[`"),
|
||||
"""
|
||||
one two
|
||||
<caret>three
|
||||
four five
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
assertOffset(8)
|
||||
}
|
||||
|
||||
// VIM-3731 |m| |[`|
|
||||
@Test
|
||||
fun testGotoPreviousMarkBeforeFirstMarkDoesNothing() {
|
||||
typeTextInFile(
|
||||
injector.parser.parseKeys("ma" + "jmb" + "wmc" + "ggw"+ "[`"),
|
||||
"""
|
||||
one two
|
||||
<caret>three
|
||||
four five
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
assertOffset(4)
|
||||
}
|
||||
|
||||
// VIM-3731 |m| |]`|
|
||||
@Test
|
||||
fun testGotoNextMark() {
|
||||
typeTextInFile(
|
||||
injector.parser.parseKeys("ma" + "jmb" + "wmc" + "gg" + "]`"),
|
||||
"""
|
||||
one two
|
||||
<caret>three
|
||||
four five
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
assertOffset(8)
|
||||
}
|
||||
|
||||
// VIM-3731 |m| |]`|
|
||||
@Test
|
||||
fun testGotoNextMarkIgnoresPlacingOrder() {
|
||||
typeTextInFile(
|
||||
injector.parser.parseKeys("mb" + "kma" + "jwmc" + "gg" + "]`"),
|
||||
"""
|
||||
one two
|
||||
three
|
||||
<caret>four five
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
assertOffset(8)
|
||||
}
|
||||
|
||||
// VIM-3731 |m| |]`|
|
||||
@Test
|
||||
fun testGotoNextMarkMultipleMarksOnSamePosition() {
|
||||
typeTextInFile(
|
||||
injector.parser.parseKeys("mbmd" + "kma" + "jwmc" + "ggjj" + "]`"),
|
||||
"""
|
||||
one two
|
||||
three
|
||||
<caret>four five
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
assertOffset(19)
|
||||
}
|
||||
|
||||
// VIM-3731 |m| |]`|
|
||||
@Test
|
||||
fun testGotoNextMarkWithCount() {
|
||||
typeTextInFile(
|
||||
injector.parser.parseKeys("ma" + "jmb" + "wmc" + "gg" + "2]`"),
|
||||
"""
|
||||
one two
|
||||
<caret>three
|
||||
four five
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
assertOffset(14)
|
||||
}
|
||||
|
||||
// VIM-3731 |m| |]`|
|
||||
@Test
|
||||
fun testGotoNextMarkWithExcessiveCount() {
|
||||
typeTextInFile(
|
||||
injector.parser.parseKeys("ma" + "jmb" + "wmc" + "gg" + "5]`"),
|
||||
"""
|
||||
one two
|
||||
<caret>three
|
||||
four five
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
assertOffset(19)
|
||||
}
|
||||
|
||||
// VIM-3731 |m| |]`|
|
||||
@Test
|
||||
fun testGotoNextMarkAfterLastMarkDoesNothing() {
|
||||
typeTextInFile(
|
||||
injector.parser.parseKeys("ma" + "jmb" + "wmc" + "ll"+ "]`"),
|
||||
"""
|
||||
one two
|
||||
<caret>three
|
||||
four five
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
assertOffset(21)
|
||||
}
|
||||
|
||||
// |i| |`]|
|
||||
@Test
|
||||
fun testGotoLastChangePositionEnd() {
|
||||
@ -543,4 +723,67 @@ class MarkTest : VimTestCase() {
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMulticaretPreviousNextMark() {
|
||||
configureByText(
|
||||
"""
|
||||
My mother <caret>taught me this trick:
|
||||
if you repeat something <caret>over and over again it loses its meaning.
|
||||
For example: homework, homework, homework, homework, <caret>homework, homework, homework, homework, homework.
|
||||
See, nothing.
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
typeText("mawmbw")
|
||||
assertState(
|
||||
"""
|
||||
My mother taught me <caret>this trick:
|
||||
if you repeat something over and <caret>over again it loses its meaning.
|
||||
For example: homework, homework, homework, homework, homework, <caret>homework, homework, homework, homework.
|
||||
See, nothing.
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
typeText("[`")
|
||||
assertState(
|
||||
"""
|
||||
My mother taught <caret>me this trick:
|
||||
if you repeat something over <caret>and over again it loses its meaning.
|
||||
For example: homework, homework, homework, homework, homework<caret>, homework, homework, homework, homework.
|
||||
See, nothing.
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
typeText("[`")
|
||||
assertState(
|
||||
"""
|
||||
My mother <caret>taught me this trick:
|
||||
if you repeat something <caret>over and over again it loses its meaning.
|
||||
For example: homework, homework, homework, homework, <caret>homework, homework, homework, homework, homework.
|
||||
See, nothing.
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
typeText("[`") // Does nothing on first mark.
|
||||
assertState(
|
||||
"""
|
||||
My mother <caret>taught me this trick:
|
||||
if you repeat something <caret>over and over again it loses its meaning.
|
||||
For example: homework, homework, homework, homework, <caret>homework, homework, homework, homework, homework.
|
||||
See, nothing.
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
typeText("5]`") // Excessive count goes to last mark.
|
||||
assertState(
|
||||
"""
|
||||
My mother taught <caret>me this trick:
|
||||
if you repeat something over <caret>and over again it loses its meaning.
|
||||
For example: homework, homework, homework, homework, homework<caret>, homework, homework, homework, homework.
|
||||
See, nothing.
|
||||
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -63,3 +63,26 @@ class MotionGotoMarkNoSaveJumpAction : MotionActionHandler.ForEachCaret() {
|
||||
return injector.motion.moveCaretToMark(caret, mark, false)
|
||||
}
|
||||
}
|
||||
|
||||
@CommandOrMotion(keys = ["]`"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
|
||||
class MotionGotoNextMarkAction: MotionGotoRelativeMarkAction(countMultiplier = 1) {
|
||||
}
|
||||
|
||||
@CommandOrMotion(keys = ["[`"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
|
||||
class MotionGotoPreviousMarkAction: MotionGotoRelativeMarkAction(countMultiplier = -1) {
|
||||
}
|
||||
|
||||
sealed class MotionGotoRelativeMarkAction(private val countMultiplier: Int) : MotionActionHandler.ForEachCaret() {
|
||||
override val motionType: MotionType = MotionType.EXCLUSIVE
|
||||
|
||||
override fun getOffset(
|
||||
editor: VimEditor,
|
||||
caret: ImmutableVimCaret,
|
||||
context: ExecutionContext,
|
||||
argument: Argument?,
|
||||
operatorArguments: OperatorArguments,
|
||||
): Motion {
|
||||
return injector.motion.moveCaretToMarkRelative(caret, operatorArguments.count1 * countMultiplier)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -45,6 +45,11 @@ interface VimMarkService {
|
||||
*/
|
||||
fun getMark(caret: ImmutableVimCaret, char: Char): Mark?
|
||||
|
||||
/**
|
||||
* Get previous / next lowercase mark for specified caret
|
||||
*/
|
||||
fun getRelativeLowercaseMark(caret: ImmutableVimCaret, count: Int): Mark?
|
||||
|
||||
/**
|
||||
* Gets all marks for caret
|
||||
*/
|
||||
|
@ -121,6 +121,53 @@ abstract class VimMarkServiceBase : VimMarkService {
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRelativeLowercaseMark(caret: ImmutableVimCaret, count: Int): Mark? {
|
||||
val path = caret.editor.getPath() ?: return null
|
||||
if (count == 0) return null
|
||||
val marks = if (caret.isPrimary) {
|
||||
getLocalMarks(path).values
|
||||
} else {
|
||||
caret.markStorage.getMarks().values.toSet()
|
||||
}
|
||||
val lowerCaseMarksWithDistinctPositions = marks.filter { LOWERCASE_MARKS.contains(it.key) }
|
||||
.sortedWithAndDistinctBy(Mark.PositionSorter)
|
||||
// Use a fake mark to easily find the position of the caret in the list of marks.
|
||||
val caretMark = createMark(caret, '[', caret.offset) ?: return null
|
||||
val result = lowerCaseMarksWithDistinctPositions.binarySearch(caretMark, Mark.PositionSorter)
|
||||
val targetIndex = if (result >= 0) {
|
||||
// Caret is on a mark.
|
||||
result + count
|
||||
} else {
|
||||
val insertionPoint = -1 * (result + 1)
|
||||
if ((insertionPoint == 0 && count < 0)
|
||||
|| (insertionPoint == lowerCaseMarksWithDistinctPositions.size && count > 0)) {
|
||||
// Moving left if before first mark, or moving right after last mark.
|
||||
return null
|
||||
}
|
||||
if (count < 0) insertionPoint + count else insertionPoint + count - 1
|
||||
}
|
||||
// Excessive values of count.absoluteValue cause us to stop at the first/last mark.
|
||||
val actualIndex = targetIndex.coerceIn(0, lowerCaseMarksWithDistinctPositions.lastIndex)
|
||||
return lowerCaseMarksWithDistinctPositions[actualIndex]
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts [this] using [comparator], then drops elements that are duplicate by [comparator].
|
||||
*
|
||||
* This is more efficient than calling `sortedWith(comparator).distinctBy {}` because the distinct step can
|
||||
* assume the input is sorted by the same Comparator.
|
||||
*/
|
||||
private fun List<Mark>.sortedWithAndDistinctBy(comparator: Comparator<Mark>): List<Mark> {
|
||||
val sorted = this.sortedWith(Mark.PositionSorter)
|
||||
return sorted.fold(ArrayList<Mark>(sorted.size)) { outputList, mark ->
|
||||
val previousMark = outputList.lastOrNull()
|
||||
if (previousMark == null || Mark.PositionSorter.compare(previousMark, mark) != 0) {
|
||||
outputList.add(mark)
|
||||
}
|
||||
outputList
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAllLocalMarks(caret: ImmutableVimCaret): Set<Mark> {
|
||||
val path = caret.editor.getPath() ?: return emptySet()
|
||||
val marks = if (caret.isPrimary) {
|
||||
|
@ -83,6 +83,7 @@ interface VimMotionGroup {
|
||||
|
||||
// Move caret to other
|
||||
fun moveCaretToMark(caret: ImmutableVimCaret, ch: Char, toLineStart: Boolean): Motion
|
||||
fun moveCaretToMarkRelative(caret: ImmutableVimCaret, count: Int): Motion
|
||||
fun moveCaretToJump(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion
|
||||
fun moveCaretToMatchingPair(editor: VimEditor, caret: ImmutableVimCaret): Motion
|
||||
|
||||
|
@ -307,6 +307,13 @@ abstract class VimMotionGroupBase : VimMotionGroup {
|
||||
return Motion.Error
|
||||
}
|
||||
|
||||
override fun moveCaretToMarkRelative(caret: ImmutableVimCaret, count: Int): Motion {
|
||||
val markService = injector.markService
|
||||
val mark = markService.getRelativeLowercaseMark(caret, count) ?: return Motion.Error
|
||||
val offset = caret.editor.bufferPositionToOffset(BufferPosition(mark.line, mark.col, false))
|
||||
return offset.toMotionOrError()
|
||||
}
|
||||
|
||||
override fun moveCaretToJump(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion {
|
||||
val jumpService = injector.jumpService
|
||||
val spot = jumpService.getJumpSpot(editor)
|
||||
|
@ -10,6 +10,7 @@ package com.maddyhome.idea.vim.mark
|
||||
|
||||
import com.maddyhome.idea.vim.api.BufferPosition
|
||||
import com.maddyhome.idea.vim.api.VimEditor
|
||||
import com.maddyhome.idea.vim.mark.Mark.KeySorter.ORDER
|
||||
import org.jetbrains.annotations.NonNls
|
||||
|
||||
interface Mark {
|
||||
@ -29,6 +30,13 @@ interface Mark {
|
||||
return ORDER.indexOf(o1.key) - ORDER.indexOf(o2.key)
|
||||
}
|
||||
}
|
||||
// Same as in BufferPosition.
|
||||
// TODO: Consider having a shared Interface / comparator for Mark and BufferPosition to avoid this duplication.
|
||||
object PositionSorter: Comparator<Mark> {
|
||||
override fun compare(o1: Mark, o2: Mark): Int {
|
||||
return if (o1.line != o2.line) o1.line - o2.line else o1.col - o2.col
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class VimMark(
|
||||
|
@ -1239,6 +1239,11 @@
|
||||
"class": "com.maddyhome.idea.vim.action.motion.text.MotionSectionBackwardEndAction",
|
||||
"modes": "NXO"
|
||||
},
|
||||
{
|
||||
"keys": "[`",
|
||||
"class": "com.maddyhome.idea.vim.action.motion.mark.MotionGotoPreviousMarkAction",
|
||||
"modes": "NXO"
|
||||
},
|
||||
{
|
||||
"keys": "[b",
|
||||
"class": "com.maddyhome.idea.vim.action.motion.text.MotionCamelLeftAction",
|
||||
@ -1304,6 +1309,11 @@
|
||||
"class": "com.maddyhome.idea.vim.action.motion.text.MotionSectionForwardStartAction",
|
||||
"modes": "NXO"
|
||||
},
|
||||
{
|
||||
"keys": "]`",
|
||||
"class": "com.maddyhome.idea.vim.action.motion.mark.MotionGotoNextMarkAction",
|
||||
"modes": "NXO"
|
||||
},
|
||||
{
|
||||
"keys": "]b",
|
||||
"class": "com.maddyhome.idea.vim.action.motion.text.MotionCamelEndLeftAction",
|
||||
|
Loading…
Reference in New Issue
Block a user