mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2025-05-21 16:34:05 +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)
|
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| |`]|
|
// |i| |`]|
|
||||||
@Test
|
@Test
|
||||||
fun testGotoLastChangePositionEnd() {
|
fun testGotoLastChangePositionEnd() {
|
||||||
@ -543,4 +723,67 @@ class MarkTest : VimTestCase() {
|
|||||||
""".trimIndent(),
|
""".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)
|
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?
|
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
|
* 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> {
|
override fun getAllLocalMarks(caret: ImmutableVimCaret): Set<Mark> {
|
||||||
val path = caret.editor.getPath() ?: return emptySet()
|
val path = caret.editor.getPath() ?: return emptySet()
|
||||||
val marks = if (caret.isPrimary) {
|
val marks = if (caret.isPrimary) {
|
||||||
|
@ -83,6 +83,7 @@ interface VimMotionGroup {
|
|||||||
|
|
||||||
// Move caret to other
|
// Move caret to other
|
||||||
fun moveCaretToMark(caret: ImmutableVimCaret, ch: Char, toLineStart: Boolean): Motion
|
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 moveCaretToJump(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion
|
||||||
fun moveCaretToMatchingPair(editor: VimEditor, caret: ImmutableVimCaret): Motion
|
fun moveCaretToMatchingPair(editor: VimEditor, caret: ImmutableVimCaret): Motion
|
||||||
|
|
||||||
|
@ -307,6 +307,13 @@ abstract class VimMotionGroupBase : VimMotionGroup {
|
|||||||
return Motion.Error
|
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 {
|
override fun moveCaretToJump(editor: VimEditor, caret: ImmutableVimCaret, count: Int): Motion {
|
||||||
val jumpService = injector.jumpService
|
val jumpService = injector.jumpService
|
||||||
val spot = jumpService.getJumpSpot(editor)
|
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.BufferPosition
|
||||||
import com.maddyhome.idea.vim.api.VimEditor
|
import com.maddyhome.idea.vim.api.VimEditor
|
||||||
|
import com.maddyhome.idea.vim.mark.Mark.KeySorter.ORDER
|
||||||
import org.jetbrains.annotations.NonNls
|
import org.jetbrains.annotations.NonNls
|
||||||
|
|
||||||
interface Mark {
|
interface Mark {
|
||||||
@ -29,6 +30,13 @@ interface Mark {
|
|||||||
return ORDER.indexOf(o1.key) - ORDER.indexOf(o2.key)
|
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(
|
data class VimMark(
|
||||||
|
@ -1239,6 +1239,11 @@
|
|||||||
"class": "com.maddyhome.idea.vim.action.motion.text.MotionSectionBackwardEndAction",
|
"class": "com.maddyhome.idea.vim.action.motion.text.MotionSectionBackwardEndAction",
|
||||||
"modes": "NXO"
|
"modes": "NXO"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"keys": "[`",
|
||||||
|
"class": "com.maddyhome.idea.vim.action.motion.mark.MotionGotoPreviousMarkAction",
|
||||||
|
"modes": "NXO"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"keys": "[b",
|
"keys": "[b",
|
||||||
"class": "com.maddyhome.idea.vim.action.motion.text.MotionCamelLeftAction",
|
"class": "com.maddyhome.idea.vim.action.motion.text.MotionCamelLeftAction",
|
||||||
@ -1304,6 +1309,11 @@
|
|||||||
"class": "com.maddyhome.idea.vim.action.motion.text.MotionSectionForwardStartAction",
|
"class": "com.maddyhome.idea.vim.action.motion.text.MotionSectionForwardStartAction",
|
||||||
"modes": "NXO"
|
"modes": "NXO"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"keys": "]`",
|
||||||
|
"class": "com.maddyhome.idea.vim.action.motion.mark.MotionGotoNextMarkAction",
|
||||||
|
"modes": "NXO"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"keys": "]b",
|
"keys": "]b",
|
||||||
"class": "com.maddyhome.idea.vim.action.motion.text.MotionCamelEndLeftAction",
|
"class": "com.maddyhome.idea.vim.action.motion.text.MotionCamelEndLeftAction",
|
||||||
|
Loading…
Reference in New Issue
Block a user