1
0
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:
Sebastian Doerner 2024-12-15 12:52:26 +01:00 committed by lippfi
parent 3c167f35d4
commit cb218697fa
8 changed files with 344 additions and 0 deletions
src/test/java/org/jetbrains/plugins/ideavim/action
vim-engine/src/main

View File

@ -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(),
)
}
}

View File

@ -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)
}
}

View File

@ -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
*/

View File

@ -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) {

View File

@ -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

View File

@ -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)

View File

@ -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(

View File

@ -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",