mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2025-05-03 22:34:03 +02:00
Rewrite word end motions and word objects
This commit is contained in:
parent
54ef691a98
commit
cad6afdc9d
src/test/java/org/jetbrains/plugins/ideavim/action/motion/object
MotionInnerBigWordActionTest.ktMotionInnerWordActionTest.ktMotionOuterBigWordActionTest.ktMotionOuterWordActionTest.kt
vim-engine/src/main/kotlin/com/maddyhome/idea/vim
action/motion/text
api
helper
@ -377,24 +377,11 @@ class MotionInnerBigWordActionTest : VimTestCase() {
|
||||
)
|
||||
}
|
||||
|
||||
@VimBehaviorDiffers(originalVimAfter =
|
||||
"""
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|
|
||||
|${s}
|
||||
|
|
||||
|
|
||||
|
|
||||
|${c}${se}
|
||||
|
|
||||
|consectetur adipiscing elit
|
||||
""",
|
||||
description = "I don't understand Vim's logic for selecting mulitple empty lines with 'iw'." +
|
||||
"E.g. `v3iw` will select 5 lines, `v4iw` will select 7, `v5iw` selects 9. " +
|
||||
"Fix only when/if Vim's behaviour is clarified"
|
||||
)
|
||||
@Test
|
||||
fun `test select multiple empty lines`() {
|
||||
// I don't understand the logic of this. I would expect v3iw to select 3 lines, but instead it selects 5.
|
||||
// And v4iw selects 7, v5iw selects 9. But this is how Vim behaves, and it's working and the other tests are working
|
||||
// too. Let's not question it too hard.
|
||||
doTest(
|
||||
"v3iW",
|
||||
"""
|
||||
@ -414,8 +401,8 @@ class MotionInnerBigWordActionTest : VimTestCase() {
|
||||
|${s}
|
||||
|
|
||||
|
|
||||
|${c}${se}
|
||||
|
|
||||
|${c}${se}
|
||||
|
|
||||
|consectetur adipiscing elit
|
||||
""".trimMargin(),
|
||||
@ -709,22 +696,12 @@ class MotionInnerBigWordActionTest : VimTestCase() {
|
||||
)
|
||||
}
|
||||
|
||||
@VimBehaviorDiffers(originalVimAfter =
|
||||
"""
|
||||
|Lorem ${s}Ipsum
|
||||
|
|
||||
|${c}L${se}orem ipsum dolor sit amet,
|
||||
|consectetur adipiscing elit
|
||||
|Sed in orci mauris.
|
||||
|Cras id tellus in ex imperdiet egestas.
|
||||
""",
|
||||
description = "Vim's behaviour is weird. Makes no sense that it selects the first character of the word. " +
|
||||
"Possibly a bug in Vim: https://github.com/vim/vim/issues/16514 " +
|
||||
"Unclear what the correct behaviour should be",
|
||||
shouldBeFixed = false
|
||||
)
|
||||
@Test
|
||||
fun `test repeated text object expands to empty line`() {
|
||||
// Well. This behaviour is weird, and looks like a bug, but it matches Vim's behaviour.
|
||||
// I'm not entirely sure why this happens, but it's a vote of confidence in IdeaVim's implementation that we're
|
||||
// matching bugs! 😁
|
||||
// See https://github.com/vim/vim/issues/16514
|
||||
doTest(
|
||||
listOf("viW", "iW"),
|
||||
"""
|
||||
@ -737,8 +714,8 @@ class MotionInnerBigWordActionTest : VimTestCase() {
|
||||
""".trimMargin(),
|
||||
"""
|
||||
|Lorem ${s}Ipsum
|
||||
|${c}${se}
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|
|
||||
|${c}L${se}orem ipsum dolor sit amet,
|
||||
|consectetur adipiscing elit
|
||||
|Sed in orci mauris.
|
||||
|Cras id tellus in ex imperdiet egestas.
|
||||
@ -768,8 +745,8 @@ class MotionInnerBigWordActionTest : VimTestCase() {
|
||||
""".trimMargin(),
|
||||
"""
|
||||
|Lorem ${s}Ipsum
|
||||
|${c}${se}
|
||||
|
|
||||
|${c}${se}
|
||||
|Lorem ipsum dolor sit amet,
|
||||
""".trimMargin(),
|
||||
Mode.VISUAL(SelectionType.CHARACTER_WISE),
|
||||
|
@ -182,6 +182,16 @@ class MotionInnerWordActionTest : VimTestCase() {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test select word selects up to punctuation 2`() {
|
||||
doTest(
|
||||
"viw",
|
||||
"Lorem ipsum dolor sit ame${c}t, consectetur adipiscing elit",
|
||||
"Lorem ipsum dolor sit ${s}ame${c}t${se}, consectetur adipiscing elit",
|
||||
Mode.VISUAL(SelectionType.CHARACTER_WISE),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test select word with existing left-to-right selection selects rest of word`() {
|
||||
doTest(
|
||||
@ -377,24 +387,11 @@ class MotionInnerWordActionTest : VimTestCase() {
|
||||
)
|
||||
}
|
||||
|
||||
@VimBehaviorDiffers(originalVimAfter =
|
||||
"""
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|
|
||||
|${s}
|
||||
|
|
||||
|
|
||||
|
|
||||
|${c}${se}
|
||||
|
|
||||
|consectetur adipiscing elit
|
||||
""",
|
||||
description = "I don't understand Vim's logic for selecting mulitple empty lines with 'iw'." +
|
||||
"E.g. `v3iw` will select 5 lines, `v4iw` will select 7, `v5iw` selects 9. " +
|
||||
"Fix only when/if Vim's behaviour is clarified"
|
||||
)
|
||||
@Test
|
||||
fun `test select multiple empty lines`() {
|
||||
// I don't understand the logic of this. I would expect v3iw to select 3 lines, but instead it selects 5.
|
||||
// And v4iw selects 7, v5iw selects 9. But this is how Vim behaves, and it's working and the other tests are working
|
||||
// too. Let's not question it too hard.
|
||||
doTest(
|
||||
"v3iw",
|
||||
"""
|
||||
@ -414,8 +411,8 @@ class MotionInnerWordActionTest : VimTestCase() {
|
||||
|${s}
|
||||
|
|
||||
|
|
||||
|${c}${se}
|
||||
|
|
||||
|${c}${se}
|
||||
|
|
||||
|consectetur adipiscing elit
|
||||
""".trimMargin(),
|
||||
@ -708,22 +705,11 @@ class MotionInnerWordActionTest : VimTestCase() {
|
||||
)
|
||||
}
|
||||
|
||||
@VimBehaviorDiffers(originalVimAfter =
|
||||
"""
|
||||
|Lorem ${s}Ipsum
|
||||
|
|
||||
|${c}L${se}orem ipsum dolor sit amet,
|
||||
|consectetur adipiscing elit
|
||||
|Sed in orci mauris.
|
||||
|Cras id tellus in ex imperdiet egestas.
|
||||
""",
|
||||
description = "Vim's behaviour is weird. Makes no sense that it selects the first character of the word. " +
|
||||
"Possibly a bug in Vim: https://github.com/vim/vim/issues/16514 " +
|
||||
"Unclear what the correct behaviour should be",
|
||||
shouldBeFixed = false
|
||||
)
|
||||
@Test
|
||||
fun `test repeated text object expands to empty line`() {
|
||||
// Well. This behaviour is weird, and looks like a bug, but it matches Vim's behaviour.
|
||||
// I'm not entirely sure why this happens, but it's a vote of confidence in IdeaVim's implementation that we're
|
||||
// matching bugs! 😁
|
||||
doTest(
|
||||
listOf("viw", "iw"),
|
||||
"""
|
||||
@ -736,8 +722,8 @@ class MotionInnerWordActionTest : VimTestCase() {
|
||||
""".trimMargin(),
|
||||
"""
|
||||
|Lorem ${s}Ipsum
|
||||
|${c}${se}
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|
|
||||
|${c}L${se}orem ipsum dolor sit amet,
|
||||
|consectetur adipiscing elit
|
||||
|Sed in orci mauris.
|
||||
|Cras id tellus in ex imperdiet egestas.
|
||||
@ -767,8 +753,8 @@ class MotionInnerWordActionTest : VimTestCase() {
|
||||
""".trimMargin(),
|
||||
"""
|
||||
|Lorem ${s}Ipsum
|
||||
|${c}${se}
|
||||
|
|
||||
|${c}${se}
|
||||
|Lorem ipsum dolor sit amet,
|
||||
""".trimMargin(),
|
||||
Mode.VISUAL(SelectionType.CHARACTER_WISE),
|
||||
|
@ -69,10 +69,9 @@ class MotionOuterBigWordActionTest : VimTestCase() {
|
||||
|
|
||||
|consectetur adipiscing elit
|
||||
""",
|
||||
// description = "The caret at the same offset as the selection end is an indication that there is an off-by-one error." +
|
||||
// "IdeaVim doesn't (currently) allow selecting the end of line char, so this inclusive range does not include the" +
|
||||
// "(exclusive) end of line char. Once IdeaVim handles this, we might have to fix things"
|
||||
description = "Not yet implemented"
|
||||
description = "The caret at the same offset as the selection end is an indication that there is an off-by-one error." +
|
||||
"IdeaVim doesn't (currently) allow selecting the end of line char, so this inclusive range does not include the" +
|
||||
"(exclusive) end of line char. Once IdeaVim handles this, we might have to fix things"
|
||||
)
|
||||
@Test
|
||||
fun `test select empty line`() {
|
||||
@ -92,11 +91,11 @@ class MotionOuterBigWordActionTest : VimTestCase() {
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|
|
||||
|${s}
|
||||
|${c}${se}
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|consectetu${c}r${se} adipiscing elit
|
||||
|consectetur adipiscing elit
|
||||
""".trimMargin(),
|
||||
Mode.VISUAL(SelectionType.CHARACTER_WISE),
|
||||
)
|
||||
@ -144,14 +143,37 @@ class MotionOuterBigWordActionTest : VimTestCase() {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test select outer word skips following empty line`() {
|
||||
doTest(
|
||||
listOf("vaW", "aW"),
|
||||
"""
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|
|
||||
|con${c}sectetur
|
||||
|
|
||||
|adipiscing elit
|
||||
""".trimMargin(),
|
||||
"""
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|
|
||||
|${s}consectetur
|
||||
|
|
||||
|adipiscin${c}g${se} elit
|
||||
""".trimMargin(),
|
||||
Mode.VISUAL(SelectionType.CHARACTER_WISE),
|
||||
)
|
||||
}
|
||||
|
||||
@VimBehaviorDiffers(originalVimAfter =
|
||||
"""
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|
|
||||
|${s}
|
||||
|
|
||||
|${c}${se}consectetur adipiscing elit
|
||||
|${c}
|
||||
|${se}consectetur adipiscing elit
|
||||
""",
|
||||
description = "Off by one because IdeaVim does not allow selecting a newline char"
|
||||
)
|
||||
@Test
|
||||
fun `test select empty line wraps to next line but does not wrap to following line`() {
|
||||
@ -168,8 +190,8 @@ class MotionOuterBigWordActionTest : VimTestCase() {
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|
|
||||
|${s}
|
||||
|
|
||||
|consectetu${c}r${se} adipiscing elit
|
||||
|${c}${se}
|
||||
|consectetur adipiscing elit
|
||||
""".trimMargin(),
|
||||
Mode.VISUAL(SelectionType.CHARACTER_WISE),
|
||||
)
|
||||
@ -187,7 +209,8 @@ class MotionOuterBigWordActionTest : VimTestCase() {
|
||||
|${c}
|
||||
|${se}
|
||||
|consectetur adipiscing elit
|
||||
"""
|
||||
""",
|
||||
description = "Off by one because IdeaVim does not allow selecting a newline char"
|
||||
)
|
||||
@Test
|
||||
fun `test select multiple empty lines`() {
|
||||
@ -213,9 +236,9 @@ class MotionOuterBigWordActionTest : VimTestCase() {
|
||||
|
|
||||
|
|
||||
|
|
||||
|${c}${se}
|
||||
|
|
||||
|
|
||||
|consectetur adipiscing eli${c}t${se}
|
||||
|consectetur adipiscing elit
|
||||
""".trimMargin(),
|
||||
Mode.VISUAL(SelectionType.CHARACTER_WISE),
|
||||
)
|
||||
@ -231,10 +254,9 @@ class MotionOuterBigWordActionTest : VimTestCase() {
|
||||
|
|
||||
|consectetur adipiscing elit
|
||||
""",
|
||||
// description = "The caret at the same offset as the selection end is an indication that there is an off-by-one error." +
|
||||
// "IdeaVim doesn't (currently) allow selecting the end of line char, so this inclusive range does not include the" +
|
||||
// "(exclusive) end of line char. Once IdeaVim handles this, we might have to fix things"
|
||||
description = "Not yet implemented"
|
||||
description = "The caret at the same offset as the selection end is an indication that there is an off-by-one error." +
|
||||
"IdeaVim doesn't (currently) allow selecting the end of line char, so this inclusive range does not include the" +
|
||||
"(exclusive) end of line char. Once IdeaVim handles this, we might have to fix things"
|
||||
)
|
||||
@Test
|
||||
fun `test select blank line`() {
|
||||
@ -253,10 +275,10 @@ class MotionOuterBigWordActionTest : VimTestCase() {
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|
|
||||
|${s}....
|
||||
|${c}${se}
|
||||
|
|
||||
|
|
||||
|
|
||||
|consectetu${c}r${se} adipiscing elit
|
||||
|consectetur adipiscing elit
|
||||
""".trimMargin().dotToSpace(),
|
||||
Mode.VISUAL(SelectionType.CHARACTER_WISE),
|
||||
)
|
||||
@ -472,13 +494,14 @@ class MotionOuterBigWordActionTest : VimTestCase() {
|
||||
"""
|
||||
|Lorem${s} Ipsum
|
||||
|
|
||||
|${c}${se}
|
||||
|
|
||||
|${c}
|
||||
|${se}
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|consectetur adipiscing elit
|
||||
|Sed in orci mauris.
|
||||
|Cras id tellus in ex imperdiet egestas.
|
||||
"""
|
||||
""",
|
||||
description = "Off by one because IdeaVim does not currently support selecting newline char"
|
||||
)
|
||||
@Test
|
||||
fun `test repeated text object expands to multiple empty lines`() {
|
||||
@ -497,9 +520,9 @@ class MotionOuterBigWordActionTest : VimTestCase() {
|
||||
"""
|
||||
|Lorem${s} Ipsum
|
||||
|
|
||||
|${c}${se}
|
||||
|
|
||||
|
|
||||
|Lore${c}m${se} ipsum dolor sit amet,
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|consectetur adipiscing elit
|
||||
|Sed in orci mauris.
|
||||
|Cras id tellus in ex imperdiet egestas.
|
||||
@ -532,7 +555,6 @@ class MotionOuterBigWordActionTest : VimTestCase() {
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Fix this bug
|
||||
@VimBehaviorDiffers(originalVimAfter =
|
||||
"""
|
||||
|Lorem${s} Ipsum
|
||||
@ -543,7 +565,8 @@ class MotionOuterBigWordActionTest : VimTestCase() {
|
||||
|consectetur adipiscing elit
|
||||
|Sed in orci mauris.
|
||||
|Cras id tellus in ex imperdiet egestas.
|
||||
"""
|
||||
""",
|
||||
description = "Off by one because IdeaVim does not currently support selecting newline char"
|
||||
)
|
||||
@Test
|
||||
fun `test repeated text object expands to cover whitespace on following blank lines`() {
|
||||
@ -563,8 +586,8 @@ class MotionOuterBigWordActionTest : VimTestCase() {
|
||||
|Lorem${s} Ipsum
|
||||
|
|
||||
|........
|
||||
|
|
||||
|Lore${c}m${se} ipsum dolor sit amet,
|
||||
|${c}${se}
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|consectetur adipiscing elit
|
||||
|Sed in orci mauris.
|
||||
|Cras id tellus in ex imperdiet egestas.
|
||||
|
@ -67,10 +67,9 @@ class MotionOuterWordActionTest : VimTestCase() {
|
||||
|
|
||||
|consectetur adipiscing elit
|
||||
""",
|
||||
// description = "The caret at the same offset as the selection end is an indication that there is an off-by-one error." +
|
||||
// "IdeaVim doesn't (currently) allow selecting the end of line char, so this inclusive range does not include the" +
|
||||
// "(exclusive) end of line char. Once IdeaVim handles this, we might have to fix things"
|
||||
description = "Not yet implemented"
|
||||
description = "The caret at the same offset as the selection end is an indication that there is an off-by-one error." +
|
||||
"IdeaVim doesn't (currently) allow selecting the end of line char, so this inclusive range does not include the" +
|
||||
"(exclusive) end of line char. Once IdeaVim handles this, we might have to fix things"
|
||||
)
|
||||
@Test
|
||||
fun `test select empty line`() {
|
||||
@ -90,11 +89,11 @@ class MotionOuterWordActionTest : VimTestCase() {
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|
|
||||
|${s}
|
||||
|${c}${se}
|
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|consectetu${c}r${se} adipiscing elit
|
||||
|consectetur adipiscing elit
|
||||
""".trimMargin(),
|
||||
Mode.VISUAL(SelectionType.CHARACTER_WISE),
|
||||
)
|
||||
@ -147,9 +146,10 @@ class MotionOuterWordActionTest : VimTestCase() {
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|
|
||||
|${s}
|
||||
|
|
||||
|${c}${se}consectetur adipiscing elit
|
||||
|${c}
|
||||
|${se}consectetur adipiscing elit
|
||||
""",
|
||||
description = "Off by one because IdeaVim does not allow selecting a newline char"
|
||||
)
|
||||
@Test
|
||||
fun `test select empty line wraps to next line but does not wrap to following line`() {
|
||||
@ -166,8 +166,8 @@ class MotionOuterWordActionTest : VimTestCase() {
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|
|
||||
|${s}
|
||||
|
|
||||
|consectetu${c}r${se} adipiscing elit
|
||||
|${c}${se}
|
||||
|consectetur adipiscing elit
|
||||
""".trimMargin(),
|
||||
Mode.VISUAL(SelectionType.CHARACTER_WISE),
|
||||
)
|
||||
@ -185,7 +185,8 @@ class MotionOuterWordActionTest : VimTestCase() {
|
||||
|${c}
|
||||
|${se}
|
||||
|consectetur adipiscing elit
|
||||
"""
|
||||
""",
|
||||
description = "Off by one because IdeaVim does not allow selecting a newline char"
|
||||
)
|
||||
@Test
|
||||
fun `test select multiple empty lines`() {
|
||||
@ -211,9 +212,9 @@ class MotionOuterWordActionTest : VimTestCase() {
|
||||
|
|
||||
|
|
||||
|
|
||||
|${c}${se}
|
||||
|
|
||||
|
|
||||
|consectetur adipiscing eli${c}t${se}
|
||||
|consectetur adipiscing elit
|
||||
""".trimMargin(),
|
||||
Mode.VISUAL(SelectionType.CHARACTER_WISE),
|
||||
)
|
||||
@ -229,10 +230,9 @@ class MotionOuterWordActionTest : VimTestCase() {
|
||||
|
|
||||
|consectetur adipiscing elit
|
||||
""",
|
||||
// description = "The caret at the same offset as the selection end is an indication that there is an off-by-one error." +
|
||||
// "IdeaVim doesn't (currently) allow selecting the end of line char, so this inclusive range does not include the" +
|
||||
// "(exclusive) end of line char. Once IdeaVim handles this, we might have to fix things"
|
||||
description = "Not yet implemented"
|
||||
description = "The caret at the same offset as the selection end is an indication that there is an off-by-one error." +
|
||||
"IdeaVim doesn't (currently) allow selecting the end of line char, so this inclusive range does not include the" +
|
||||
"(exclusive) end of line char. Once IdeaVim handles this, we might have to fix things"
|
||||
)
|
||||
@Test
|
||||
fun `test select blank line`() {
|
||||
@ -251,10 +251,10 @@ class MotionOuterWordActionTest : VimTestCase() {
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|
|
||||
|${s}....
|
||||
|${c}${se}
|
||||
|
|
||||
|
|
||||
|
|
||||
|consectetu${c}r${se} adipiscing elit
|
||||
|consectetur adipiscing elit
|
||||
""".trimMargin().dotToSpace(),
|
||||
Mode.VISUAL(SelectionType.CHARACTER_WISE),
|
||||
)
|
||||
@ -494,13 +494,14 @@ class MotionOuterWordActionTest : VimTestCase() {
|
||||
"""
|
||||
|Lorem${s} Ipsum
|
||||
|
|
||||
|${c}${se}
|
||||
|
|
||||
|${c}
|
||||
|${se}
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|consectetur adipiscing elit
|
||||
|Sed in orci mauris.
|
||||
|Cras id tellus in ex imperdiet egestas.
|
||||
"""
|
||||
""",
|
||||
description = "Off by one because IdeaVim does not currently support selecting newline char"
|
||||
)
|
||||
@Test
|
||||
fun `test repeated text object expands to multiple empty lines`() {
|
||||
@ -519,9 +520,9 @@ class MotionOuterWordActionTest : VimTestCase() {
|
||||
"""
|
||||
|Lorem${s} Ipsum
|
||||
|
|
||||
|${c}${se}
|
||||
|
|
||||
|
|
||||
|Lore${c}m${se} ipsum dolor sit amet,
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|consectetur adipiscing elit
|
||||
|Sed in orci mauris.
|
||||
|Cras id tellus in ex imperdiet egestas.
|
||||
@ -554,7 +555,6 @@ class MotionOuterWordActionTest : VimTestCase() {
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Fix this bug
|
||||
@VimBehaviorDiffers(originalVimAfter =
|
||||
"""
|
||||
|Lorem${s} Ipsum
|
||||
@ -565,7 +565,8 @@ class MotionOuterWordActionTest : VimTestCase() {
|
||||
|consectetur adipiscing elit
|
||||
|Sed in orci mauris.
|
||||
|Cras id tellus in ex imperdiet egestas.
|
||||
"""
|
||||
""",
|
||||
description = "Off by one because IdeaVim does not currently support selecting newline char"
|
||||
)
|
||||
@Test
|
||||
fun `test repeated text object expands to cover whitespace on following blank lines`() {
|
||||
@ -585,8 +586,8 @@ class MotionOuterWordActionTest : VimTestCase() {
|
||||
|Lorem${s} Ipsum
|
||||
|
|
||||
|........
|
||||
|
|
||||
|Lore${c}m${se} ipsum dolor sit amet,
|
||||
|${c}${se}
|
||||
|Lorem ipsum dolor sit amet,
|
||||
|consectetur adipiscing elit
|
||||
|Sed in orci mauris.
|
||||
|Cras id tellus in ex imperdiet egestas.
|
||||
@ -787,6 +788,7 @@ class MotionOuterWordActionTest : VimTestCase() {
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Fix this bug
|
||||
@VimBehaviorDiffers(originalVimAfter =
|
||||
"Lorem${s} ipsum --dolor${c} ${se}sit amet, consectetur adipiscing elit",
|
||||
description = "First aw should select whitespace+'ipsum' " +
|
||||
|
@ -21,19 +21,25 @@ import com.maddyhome.idea.vim.handler.Motion
|
||||
import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset
|
||||
import com.maddyhome.idea.vim.handler.MotionActionHandler
|
||||
|
||||
// Vim considers an empty line as a word/WORD, but for vi compatibility, `e` and `E` do not stop at empty lines (`ge`,
|
||||
// `gE` and `w`/`W` and `b`/`B` do)
|
||||
@CommandOrMotion(keys = ["gE"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
|
||||
class MotionBigWordEndLeftAction : WordEndAction(Direction.BACKWARDS, true)
|
||||
class MotionBigWordEndLeftAction : WordEndAction(Direction.BACKWARDS, bigWord = true, stopAtEmptyLine = true)
|
||||
|
||||
@CommandOrMotion(keys = ["E"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
|
||||
class MotionBigWordEndRightAction : WordEndAction(Direction.FORWARDS, true)
|
||||
class MotionBigWordEndRightAction : WordEndAction(Direction.FORWARDS, bigWord = true, stopAtEmptyLine = false)
|
||||
|
||||
@CommandOrMotion(keys = ["ge"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
|
||||
class MotionWordEndLeftAction : WordEndAction(Direction.BACKWARDS, false)
|
||||
class MotionWordEndLeftAction : WordEndAction(Direction.BACKWARDS, bigWord = false, stopAtEmptyLine = false)
|
||||
|
||||
@CommandOrMotion(keys = ["e"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
|
||||
class MotionWordEndRightAction : WordEndAction(Direction.FORWARDS, false)
|
||||
class MotionWordEndRightAction : WordEndAction(Direction.FORWARDS, bigWord = false, stopAtEmptyLine = false)
|
||||
|
||||
sealed class WordEndAction(val direction: Direction, val bigWord: Boolean) : MotionActionHandler.ForEachCaret() {
|
||||
sealed class WordEndAction(
|
||||
private val direction: Direction,
|
||||
private val bigWord: Boolean,
|
||||
private val stopAtEmptyLine: Boolean,
|
||||
) : MotionActionHandler.ForEachCaret() {
|
||||
override fun getOffset(
|
||||
editor: VimEditor,
|
||||
caret: ImmutableVimCaret,
|
||||
@ -41,13 +47,19 @@ sealed class WordEndAction(val direction: Direction, val bigWord: Boolean) : Mot
|
||||
argument: Argument?,
|
||||
operatorArguments: OperatorArguments,
|
||||
): Motion {
|
||||
return moveCaretToNextWordEnd(editor, caret, direction.toInt() * operatorArguments.count1, bigWord)
|
||||
return moveCaretToNextWordEnd(editor, caret, direction.toInt() * operatorArguments.count1, bigWord, stopAtEmptyLine)
|
||||
}
|
||||
|
||||
override val motionType: MotionType = MotionType.INCLUSIVE
|
||||
}
|
||||
|
||||
private fun moveCaretToNextWordEnd(editor: VimEditor, caret: ImmutableVimCaret, count: Int, bigWord: Boolean): Motion {
|
||||
private fun moveCaretToNextWordEnd(
|
||||
editor: VimEditor,
|
||||
caret: ImmutableVimCaret,
|
||||
count: Int,
|
||||
bigWord: Boolean,
|
||||
stopAtEmptyLine: Boolean,
|
||||
): Motion {
|
||||
if (caret.offset == 0 && count < 0 || caret.offset >= editor.fileSize() - 1 && count > 0) {
|
||||
return Motion.Error
|
||||
}
|
||||
@ -55,7 +67,7 @@ private fun moveCaretToNextWordEnd(editor: VimEditor, caret: ImmutableVimCaret,
|
||||
// If we are doing this move as part of a change command (e.q. cw), we need to count the current end of
|
||||
// word if the cursor happens to be on the end of a word already. If this is a normal move, we don't count
|
||||
// the current word.
|
||||
val pos = injector.searchHelper.findNextWordEnd(editor, caret.offset, count, bigWord, false)
|
||||
val pos = injector.searchHelper.findNextWordEnd(editor, caret.offset, count, bigWord, stopAtEmptyLine)
|
||||
return if (pos == -1) {
|
||||
if (count < 0) {
|
||||
AbsoluteOffset(injector.motion.moveCaretToLineStart(editor, 0))
|
||||
|
@ -622,7 +622,7 @@ abstract class VimSearchGroupBase : VimSearchGroup {
|
||||
) {
|
||||
start + 1
|
||||
} else {
|
||||
injector.searchHelper.findNextWordEnd(editor, start, 1, bigWord = false, spaceWords = false) + 1
|
||||
injector.searchHelper.findNextWordEnd(editor, start, 1, bigWord = false) + 1
|
||||
}
|
||||
return TextRange(start, end)
|
||||
}
|
||||
|
@ -139,31 +139,20 @@ interface VimSearchHelper {
|
||||
* @param searchFrom The offset in the document to search from
|
||||
* @param count Return an offset to the [count] word from the starting position. Will search backwards if negative
|
||||
* @param bigWord Use WORD instead of word boundaries
|
||||
* @param spaceWords Include whitespace as part of a word, e.g. the difference between `iw` and `aw` motions
|
||||
* @return The offset of the [count] next word, or `0` or the offset of the end of file if not found
|
||||
*/
|
||||
fun findNextWordEnd(editor: VimEditor, searchFrom: Int, count: Int, bigWord: Boolean, spaceWords: Boolean): Int
|
||||
|
||||
/**
|
||||
* Find the end offset in some text outside the editor (e.g., command line), from the given starting point
|
||||
*
|
||||
* @param text The text to search in
|
||||
* @param textLength The text length
|
||||
* @param editor Required because word boundaries depend on local-to-buffer options
|
||||
* @param searchFrom The offset in the document to search from
|
||||
* @param count Return an offset to the [count] word from the starting position. Will search backwards if negative
|
||||
* @param bigWord Use WORD instead of word boundaries
|
||||
* @param spaceWords Include whitespace as part of a word, e.g. the difference between `iw` and `aw` motions
|
||||
* @return The offset of the [count] next word, or `0` or the offset of the end of file if not found
|
||||
* @param stopOnEmptyLine Vim considers an empty line to be a word/WORD, but `e` and `E` don't respect this for vi
|
||||
* compatibility reasons. Callers other than `e` and `E` should pass `true`
|
||||
* @param allowMoveFromWordEnd If we're already at the word end, should we be able to move to the next word end? This
|
||||
* is true for word/WORD motions `e`/`E`, but false for word text objects, which do not
|
||||
* extend the selection/range forwards when at the end of a current word.
|
||||
* @return The offset of the [count] next word/WORD. Will return document bounds if not found
|
||||
*/
|
||||
fun findNextWordEnd(
|
||||
text: CharSequence,
|
||||
textLength: Int,
|
||||
editor: VimEditor,
|
||||
searchFrom: Int,
|
||||
count: Int,
|
||||
bigWord: Boolean,
|
||||
spaceWords: Boolean,
|
||||
stopOnEmptyLine: Boolean = true,
|
||||
allowMoveFromWordEnd: Boolean = true,
|
||||
): Int
|
||||
|
||||
/**
|
||||
|
@ -13,6 +13,7 @@ import com.maddyhome.idea.vim.common.TextRange
|
||||
import com.maddyhome.idea.vim.diagnostic.vimLogger
|
||||
import com.maddyhome.idea.vim.helper.CharacterHelper
|
||||
import com.maddyhome.idea.vim.helper.CharacterHelper.charType
|
||||
import com.maddyhome.idea.vim.helper.CharacterHelper.isWhitespace
|
||||
import com.maddyhome.idea.vim.helper.Msg
|
||||
import com.maddyhome.idea.vim.helper.SearchOptions
|
||||
import com.maddyhome.idea.vim.helper.enumSetOf
|
||||
@ -102,6 +103,7 @@ abstract class VimSearchHelperBase : VimSearchHelper {
|
||||
return findNextWord(editor.text(), editor.fileSize().toInt(), editor, searchFrom, count, bigWord, spaceWords)
|
||||
}
|
||||
|
||||
// TODO: Get rid of this overload when rewriting findNextWordOne
|
||||
override fun findNextWord(
|
||||
text: CharSequence,
|
||||
textLength: Int,
|
||||
@ -111,7 +113,12 @@ abstract class VimSearchHelperBase : VimSearchHelper {
|
||||
bigWord: Boolean,
|
||||
spaceWords: Boolean,
|
||||
): Int {
|
||||
return doFindNext(text, textLength, editor, searchFrom, count, bigWord, spaceWords, ::findNextWordOne)
|
||||
val step = if (count >= 0) 1 else -1
|
||||
var pos = searchFrom
|
||||
repeat(abs(count)) {
|
||||
pos = findNextWordOne(text, editor, pos, textLength, step, bigWord, spaceWords)
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
override fun findNextWordEnd(
|
||||
@ -119,21 +126,19 @@ abstract class VimSearchHelperBase : VimSearchHelper {
|
||||
searchFrom: Int,
|
||||
count: Int,
|
||||
bigWord: Boolean,
|
||||
spaceWords: Boolean,
|
||||
stopOnEmptyLine: Boolean,
|
||||
allowMoveFromWordEnd: Boolean
|
||||
): Int {
|
||||
return findNextWordEnd(editor.text(), editor.fileSize().toInt(), editor, searchFrom, count, bigWord, spaceWords)
|
||||
}
|
||||
|
||||
override fun findNextWordEnd(
|
||||
text: CharSequence,
|
||||
textLength: Int,
|
||||
editor: VimEditor,
|
||||
searchFrom: Int,
|
||||
count: Int,
|
||||
bigWord: Boolean,
|
||||
spaceWords: Boolean,
|
||||
): Int {
|
||||
return doFindNext(text, textLength, editor, searchFrom, count, bigWord, spaceWords, ::findNextWordEndOne)
|
||||
val text = editor.text()
|
||||
var pos = searchFrom
|
||||
repeat(abs(count)) {
|
||||
pos = if (count > 0) {
|
||||
findNextWordEndOne(text, editor, pos, bigWord, stopOnEmptyLine, allowMoveFromWordEnd)
|
||||
} else {
|
||||
findPreviousWordEndOne(text, editor, pos, bigWord)
|
||||
}
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
override fun findPattern(
|
||||
@ -282,29 +287,6 @@ abstract class VimSearchHelperBase : VimSearchHelper {
|
||||
).map { it.range }
|
||||
}
|
||||
|
||||
private fun doFindNext(
|
||||
text: CharSequence,
|
||||
textLength: Int,
|
||||
editor: VimEditor,
|
||||
searchFrom: Int,
|
||||
countDirection: Int,
|
||||
bigWord: Boolean,
|
||||
spaceWords: Boolean,
|
||||
action: (text: CharSequence, editor: VimEditor, pos: Int, size: Int, step: Int, bigWord: Boolean, spaceWords: Boolean) -> Int,
|
||||
): Int {
|
||||
var count = countDirection
|
||||
val step = if (count >= 0) 1 else -1
|
||||
count = abs(count)
|
||||
var pos = searchFrom
|
||||
for (i in 0 until count) {
|
||||
pos = action(text, editor, pos, textLength, step, bigWord, spaceWords)
|
||||
if (pos == searchFrom || pos == 0 || pos == textLength - 1) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
private fun findNextWordOne(
|
||||
chars: CharSequence,
|
||||
editor: VimEditor,
|
||||
@ -376,81 +358,91 @@ abstract class VimSearchHelperBase : VimSearchHelper {
|
||||
return res
|
||||
}
|
||||
|
||||
@Suppress("GrazieInspection", "StructuralWrap")
|
||||
private fun findNextWordEndOne(
|
||||
chars: CharSequence,
|
||||
editor: VimEditor,
|
||||
start: Int,
|
||||
size: Int,
|
||||
step: Int,
|
||||
bigWord: Boolean,
|
||||
spaceWords: Boolean,
|
||||
stopOnEmptyLine: Boolean,
|
||||
allowMoveFromWordEnd: Boolean,
|
||||
): Int {
|
||||
// Scenarios:
|
||||
// * Cursor on word/WORD: move to end of the current word/WORD group ("wo${c}rd word" -> "wor${c}d word")
|
||||
// * Cursor on end of word/WORD: move to end of next word/WORD group ("wor${c}d word" -> "word wor${c}d")
|
||||
// * Cursor on whitespace: move to end of next word/WORD group (" ${c} word" -> " wor${c}d")
|
||||
// * Visual on single char:
|
||||
// For `e`/`E`: move to end of next word/WORD group (" ${s}d${se} word" -> " ${s}d word${se}")
|
||||
// For `iw` and no selection: DO NOT MOVE! (" ${s}d${se} word" -> " ${s}d${se} word")
|
||||
//
|
||||
// Note that newline characters are considered to be whitespace. This is USUALLY treated like whitespace, so
|
||||
// moving through a new line to get to next word end is usually just the same as moving through other whitespace.
|
||||
// Empty lines are an exception to this rule. Vim considers an empty line to be a word/WORD, but for vi
|
||||
// compatibility reasons, `e` and `E` do not stop on empty lines, but text objects do.
|
||||
|
||||
var pos = start
|
||||
var found = false
|
||||
// For forward searches, skip any current whitespace so we start at the start of a word
|
||||
if (step > 0 && pos < size - 1) {
|
||||
if (chars[pos + 1] == '\n'
|
||||
&& spaceWords
|
||||
&& charType(editor, chars[pos], bigWord) !== charType(editor, chars[pos + 1], bigWord)) {
|
||||
val startingCharType = charType(editor, chars[pos], bigWord)
|
||||
|
||||
// If we process the loops below, we end up one character past the target offset. But we don't always process the
|
||||
// loops. Move one character forward now so everything works cleanly
|
||||
pos++
|
||||
|
||||
// When called by `e`/`E` and we're originally at the end of the current word, we want to move to end of the *next*
|
||||
// word, so skip whitespace at the current pos and get to the start of the next word.
|
||||
// When called by `iw` and we're at the end of the current word, we don't want to move, so if there's whitespace at
|
||||
// the current position, don't skip it. We then won't be at the start of the next word, so we won't move further.
|
||||
// We'll decrement before we return, so the cursor doesn't move.
|
||||
// But if we start on whitespace, we always want to skip it, for both scenarios.
|
||||
if (allowMoveFromWordEnd || startingCharType == CharacterHelper.CharacterType.WHITESPACE) {
|
||||
while (pos < chars.length && isWhitespace(editor, chars[pos], bigWord)) {
|
||||
if (stopOnEmptyLine && isEmptyLine(chars, pos)) return pos
|
||||
pos++
|
||||
}
|
||||
if (charType(editor, chars[pos + 1], bigWord) === CharacterHelper.CharacterType.WHITESPACE && !spaceWords) {
|
||||
pos = skipSpace(editor, chars, pos + 1, step, size, false) - 1
|
||||
}
|
||||
if (pos < size - 1 && charType(editor, chars[pos], bigWord) !== charType(editor, chars[pos + 1], bigWord)) {
|
||||
pos += step
|
||||
}
|
||||
}
|
||||
var res = pos
|
||||
if (pos < 0 || pos >= size) {
|
||||
return pos
|
||||
|
||||
// If we're on whitespace now, it's because we started on the end of a word and then immediately advanced to
|
||||
// whitespace. We don't want to skip the current whitespace (as above) and we don't want to
|
||||
if (pos < chars.length && !isWhitespace(editor, chars[pos], bigWord)) {
|
||||
// We're currently at the start of, or inside, a word/WORD. Move to the start of the next charType segment, and
|
||||
// return the character *before* it, so we get the end of the current word/WORD
|
||||
val wordCharType = charType(editor, chars[pos], bigWord)
|
||||
pos = skipWhileCharacterType(editor, chars, pos, 1, wordCharType, bigWord)
|
||||
}
|
||||
var type = charType(editor, chars[pos], bigWord)
|
||||
if (type === CharacterHelper.CharacterType.WHITESPACE && step >= 0 && pos < size - 1 && !spaceWords) {
|
||||
type = charType(editor, chars[pos + 1], bigWord)
|
||||
|
||||
// The loops above have put us one char past the next word, or one past whitespace. Or just advanced one character
|
||||
pos--
|
||||
|
||||
return pos.coerceIn(0, chars.length - 1)
|
||||
}
|
||||
|
||||
private fun findPreviousWordEndOne(
|
||||
chars: CharSequence,
|
||||
editor: VimEditor,
|
||||
start: Int,
|
||||
bigWord: Boolean,
|
||||
): Int {
|
||||
var pos = start
|
||||
val startingCharType = charType(editor, chars[pos], bigWord)
|
||||
|
||||
// We always have to move backwards
|
||||
pos--
|
||||
|
||||
// Find the first preceding character with a different charType. Note that we use the starting charType here,
|
||||
// because we might have already moved across a word/WORD/non-word boundary.
|
||||
// We don't need to +/-1 here. This is returning the offset of the character at the end of the previous word/WORD
|
||||
if (pos >= 0 && startingCharType != CharacterHelper.CharacterType.WHITESPACE) {
|
||||
pos = skipWhileCharacterType(editor, chars, pos, -1, startingCharType, bigWord)
|
||||
}
|
||||
var lastChar = 0.toChar()
|
||||
pos += step
|
||||
while (pos >= 0 && pos < size && !found) {
|
||||
var newChar = chars[pos]
|
||||
val newType = charType(editor, newChar, bigWord)
|
||||
if (newType !== type) {
|
||||
if (step >= 0) {
|
||||
res = pos - 1
|
||||
} else if (newType === CharacterHelper.CharacterType.WHITESPACE && step < 0 && !spaceWords) {
|
||||
// `e` does not match empty lines, but `ge` does.
|
||||
// This change in behaviour might be surprising to callers!
|
||||
pos = skipSpace(editor, chars, pos, step, size, matchEmptyLine = step < 0)
|
||||
res = if (pos >= 0 && chars[pos] == '\n') pos + 1 else pos
|
||||
} else {
|
||||
res = pos
|
||||
}
|
||||
found = true
|
||||
} else if (newChar == '\n'
|
||||
&& (newChar == lastChar
|
||||
|| (spaceWords && charType(editor, lastChar, bigWord) === CharacterHelper.CharacterType.WHITESPACE))
|
||||
) {
|
||||
// Add/subtract one so we don't take the current new line char as our end of word char.
|
||||
// However, if we're matching empty lines and we start on an empty line, then we'll get two consecutive new line
|
||||
// chars. Subtracting will put us back at the start character
|
||||
res = if (step < 0) pos + 1 else (pos - 1).coerceAtLeast(start + 1)
|
||||
found = true
|
||||
}
|
||||
lastChar = newChar
|
||||
pos += step
|
||||
|
||||
// If we ended up on whitespace, skip backwards until we find either the last character of the previous word/WORD,
|
||||
// or we have to stop at an empty line, which is considered a WORD
|
||||
while (pos in 0 until chars.length && isWhitespace(editor, chars[pos], bigWord)) {
|
||||
// Always check empty line when moving backwards
|
||||
if (isEmptyLine(chars, pos)) return pos
|
||||
pos--
|
||||
}
|
||||
if (found) {
|
||||
if (res < 0) {
|
||||
res = 0
|
||||
} else if (res >= size) {
|
||||
res = size - 1
|
||||
}
|
||||
} else if (pos == size) {
|
||||
res = size - 1
|
||||
} else if (pos <= 0) {
|
||||
res = 0
|
||||
}
|
||||
return res
|
||||
|
||||
return pos.coerceAtLeast(0)
|
||||
}
|
||||
|
||||
private fun skipSpace(
|
||||
@ -473,6 +465,34 @@ abstract class VimSearchHelperBase : VimSearchHelper {
|
||||
return if (_offset < size) _offset else size - 1
|
||||
}
|
||||
|
||||
private fun isEmptyLine(chars: CharSequence, offset: Int): Boolean {
|
||||
// The new line char belongs to the current line, so if the previous character is also a new line char, then the
|
||||
// line for the current offset is empty
|
||||
return (offset == 0 && chars[offset] == '\n')
|
||||
|| (chars[offset] == '\n' && chars[offset - 1] == '\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips characters in a given direction until reaching a different character type
|
||||
*
|
||||
* Returns the offset of the character with the different character type. Will return indexes out of bounds at the
|
||||
* start and end of the file. Specifically, will return `-1` at the start of the file, `chars.length` at the end.
|
||||
*/
|
||||
private fun skipWhileCharacterType(
|
||||
editor: VimEditor,
|
||||
chars: CharSequence,
|
||||
start: Int,
|
||||
step: Int,
|
||||
type: CharacterHelper.CharacterType,
|
||||
isBig: Boolean,
|
||||
): Int {
|
||||
var offset = start
|
||||
while (offset in 0 until chars.length && charType(editor, chars[offset], isBig) === type) {
|
||||
offset += step
|
||||
}
|
||||
return offset
|
||||
}
|
||||
|
||||
override fun findNextCamelStart(chars: CharSequence, startIndex: Int, count: Int): Int? {
|
||||
return findCamelStart(chars, startIndex, count, Direction.FORWARDS)
|
||||
}
|
||||
@ -1417,6 +1437,7 @@ abstract class VimSearchHelperBase : VimSearchHelper {
|
||||
logger.debug("pos=$pos")
|
||||
logger.debug("onWordStart=$onWordStart")
|
||||
|
||||
// TODO: This could be simplified to move backwards until char type changes
|
||||
if ((!onWordStart && !(onSpace && isOuter)) || hasSelection || (count > 1 && dir == -1)) {
|
||||
start = if (dir == 1) {
|
||||
findNextWord(editor, pos, -1, isBig, !isOuter)
|
||||
@ -1435,12 +1456,199 @@ abstract class VimSearchHelperBase : VimSearchHelper {
|
||||
logger.debug("onWordEnd=$onWordEnd")
|
||||
|
||||
var end = pos
|
||||
if (!onWordEnd || hasSelection || (count > 1 && dir == 1) || (onSpace && isOuter)) {
|
||||
|
||||
// TODO: Figure out the logic of this going backwards
|
||||
if (dir == 1) {
|
||||
var count = count
|
||||
|
||||
// Selecting word/WORDs (forwards):
|
||||
// If there's no selection, we need to calculate the first range:
|
||||
// -> Move back to the first character _on the current line_ of the current character type
|
||||
// If we're on whitespace, this is the start of the preceding whitespace
|
||||
// If we're on a word/WORD char, it's the start of the word/WORD
|
||||
// -> Move forward to the end of the next word/WORD or whitespace block
|
||||
// (Remember that `${se}` in the following examples is at `end+1`)
|
||||
// For outer objects and currently on whitespace, move to end of the next word/WORD ("${s} word${se}")
|
||||
// New lines are treated as whitespace, so this will wrap and move to the end of the next word/WORD.
|
||||
// Empty lines will be treated as a word.
|
||||
// For outer objects on a word/WORD char, move to one character _before_ the next word/WORD ("${s}word ${se}word")
|
||||
// New lines should be treated as a stop character, and we stop before the new line.
|
||||
// For inner objects on a word/WORD char, move to the end of this word/WORD ("${s}word${se} word")
|
||||
// This will never encounter a new line character.
|
||||
// For inner objects on whitespace, move to one character _before_ the next word/WORD ("${s} ${se}word")
|
||||
// New lines should be treated as a stop character, and we stop before the new line.
|
||||
// -> Subtract 1 from count
|
||||
// Once we have a range, or if there's an initial selection:
|
||||
// -> Loop over count
|
||||
// -> For inner objects, move to the end of the next character type block. Whitespace counts in the loop
|
||||
// -> For outer objects, move to the character _before_ the next word/WORD. Therefore, whitespace does not count
|
||||
// For all of these operations, remember that an empty line is a word.
|
||||
|
||||
if (!hasSelection) {
|
||||
// Move back to the first character of the current character type on the current line.
|
||||
// This will be the start of the word/WORD or the start of whitespace.
|
||||
val startingCharacterType = charType(editor, chars[pos], isBig)
|
||||
start = pos
|
||||
if (!isEmptyLine(chars, start)) {
|
||||
while (start >= 0 && chars[start] != '\n' && charType(editor, chars[start], isBig) == startingCharacterType) {
|
||||
start--
|
||||
}
|
||||
start++
|
||||
}
|
||||
|
||||
// Move forward, including or skipping whitespace as necessary. Move from the start of the current
|
||||
// word/whitespace rather than the original position, so that it's easier to handle moving to the end of a word
|
||||
// when the original position is already at the end of the word.
|
||||
// Note that `onSpace` is the character type of the original position, but this is also the character type of
|
||||
// the current start position
|
||||
end = when {
|
||||
// We're on preceding whitespace. Include it, and move to the end of the next word/WORD
|
||||
isOuter && onSpace -> // "${s} word${se}"
|
||||
findNextWordEnd(editor, start, 1, isBig, stopOnEmptyLine = true, allowMoveFromWordEnd = false)
|
||||
|
||||
// We're on a word, move to the end, and include following whitespace by moving to the character before the
|
||||
// next word
|
||||
isOuter && !onSpace -> // "${s}word ${se}word"
|
||||
findNextWord(chars, chars.length, editor, start, 1, isBig, spaceWords = true) - 1
|
||||
|
||||
// We're on a word, move to the end, not including trailing whitespace
|
||||
!isOuter && !onSpace -> // "${s}word${se} word"
|
||||
findNextWordEnd(editor, start, 1, isBig, stopOnEmptyLine = true, allowMoveFromWordEnd = false)
|
||||
|
||||
// We're on preceding whitespace, move to the character before the next word
|
||||
else /* !isOuter && onSpace */ -> // "${s} ${se}word"
|
||||
findNextWord(chars, chars.length, editor, start, 1, isBig, spaceWords = true) - 1
|
||||
}
|
||||
|
||||
count--
|
||||
} else {
|
||||
end = pos
|
||||
}
|
||||
|
||||
// We cannot rely on the current location of the cursor/"end". If there was no initial selection, then it will be
|
||||
// at the end of a character type block, either word/WORD or whitespace. But if there was an initial selection,
|
||||
// it could be anywhere.
|
||||
repeat (count) {
|
||||
if (isOuter) {
|
||||
// Outer object. Include whitespace.
|
||||
//
|
||||
// Selection ends on whitespace: Move to end of next word (skips whitespace, including newline)
|
||||
// "${s} ${se} word " -> "${s} word${se} "
|
||||
// "${s} ${se} \n word " -> "${s} \n word${se} "
|
||||
// Selection ends on end of whitespace: Move to one character before next word (or end of file)
|
||||
// "${s}word ${se}word " -> "${s}word word ${se}"
|
||||
// Selection ends on word: Move to one character before next word
|
||||
// "${s}wo${se}rd word" -> "${s}word ${se}word"
|
||||
// "${s}wo${se}rd, word" -> "${s}word${se}, word"
|
||||
// Selection ends on end of word: Move to end of next word (skips whitespace)
|
||||
// "${s}word${se} word " -> "${s}word word${se} "
|
||||
// Selection ends on end of word with following word: Move to one character before next word
|
||||
// "${s}word${se}, word " -> "${s}word, ${se}word "
|
||||
// Selection ends on word char at end of line:
|
||||
// If next non-newline char is word, move past newline, move to one char before next word
|
||||
// Else, move to end of next word
|
||||
// "${s}word${se}\nword " -> "${s}word\nword ${se}"
|
||||
// "${s} word${se}\nword " -> "${s} word\nword ${se}"
|
||||
// "${s}word${se}\n word " -> "${s}word\n word${se} "
|
||||
// Selection ends on whitespace at end of line:
|
||||
// If next non-newline char is word, move past newline, move to one char before next word
|
||||
// Else, move to end of next word
|
||||
// "${s} ${se}\nword " -> "${s} \nword ${se}"
|
||||
// "${s} ${se}\n word " -> "${s} \n word${se} "
|
||||
//
|
||||
// This can be generalised to move forward one char, skip again if it's a newline, then either move to the end
|
||||
// of the next word (which skips preceding whitespace), or to one character before the next word (which
|
||||
// includes following whitespace). Moving forward one char means we don't have to distinguish between inside a
|
||||
// word/whitespace, or at the end of a word whitespace. If we started inside, we want to move based on the
|
||||
// current/starting character type, if we started at the end, we want to move based on the next character
|
||||
// type. By always using next, we use the correct character type.
|
||||
|
||||
// Move forward one char
|
||||
// Skip again if new char is newline
|
||||
// If on whitespace, move to end of next word (skips current/preceding whitespace)
|
||||
// If on word, move to one before start of next word (skips following whitespace)
|
||||
|
||||
// Increment, and skip the newline char, unless we've just landed on an empty line
|
||||
end++
|
||||
if (end < chars.length && chars[end] == '\n' && !isEmptyLine(chars, end)) {
|
||||
end++
|
||||
}
|
||||
|
||||
if (end >= chars.length) {
|
||||
end--
|
||||
return@repeat
|
||||
}
|
||||
|
||||
end = if (isWhitespace(editor, chars[end], isBig)) {
|
||||
// Move to end of next word (skips current/preceding whitespace)
|
||||
findNextWordEnd(editor, end, 1, isBig, stopOnEmptyLine = true)
|
||||
}
|
||||
else {
|
||||
// Move to one before start of next word (skips following whitespace)
|
||||
findNextWord(chars, chars.length, editor, end, 1, isBig, spaceWords = true) - 1
|
||||
}
|
||||
|
||||
} else {
|
||||
// Inner object. Whitespace is not included in a move, but included as a separate (counted) move
|
||||
//
|
||||
// Selection ends on whitespace: Move to end of current character type or end of line.
|
||||
// "${s} ${se} word" -> "${s} ${se}word"
|
||||
// "${s} ${se} \n word" -> "${s} ${se}\n word"
|
||||
// Selection ends on end of whitespace: Move to end of next character type.
|
||||
// "${s} ${se}word" -> "${s} word${se}"
|
||||
// "${s} ${se}\nword " -> "${s} \nword${se} "
|
||||
// "${s} ${se}\n word" -> "${s} \n ${se}word"
|
||||
// Selection ends on word: Move to end of current character type.
|
||||
// "${s}wo${se}rd word" -> "${s}word${se} word"
|
||||
// "${s}wo${se}rd, word" -> "${s}word${se}, word"
|
||||
// Selection ends on end of word: Move to end of next character type or end of line.
|
||||
// "${s}word${se} word " -> "${s}word ${se}word "
|
||||
// "${s}word${se}, word " -> "${s}word,${se} word "
|
||||
// "${s}word${se} \n word " -> "${s}word ${se}\n word "
|
||||
// Selection ends on word char at end of line: Move to end of next character type SKIPPING NEWLINE!
|
||||
// "${s}word${se}\nword " -> "${s}word\nword${se} "
|
||||
// "${s} word${se}\nword " -> "${s} word\nword${se} "
|
||||
// "${s}word${se}\n word " -> "${s}word\n ${se}word "
|
||||
// Selection ends on whitespace at end of line: Move to end of next character type SKIPPING NEWLINE!
|
||||
// "${s}word ${se}\n word" -> "${s}word \n ${se}word"
|
||||
// "${s}word ${se}\nword " -> "${s}word \nword${se} "
|
||||
//
|
||||
// This can be generalised to move forward on character, skip again if it's a newline, then move to end of
|
||||
// the now current character type.
|
||||
|
||||
// Increment, and skip the newline char, unless we've just landed on an empty line
|
||||
end++
|
||||
if (end < chars.length && chars[end] == '\n' && !isEmptyLine(chars, end)) {
|
||||
end++
|
||||
}
|
||||
|
||||
if (end >= chars.length) {
|
||||
end--
|
||||
return@repeat
|
||||
}
|
||||
|
||||
val characterType = charType(editor, chars[end], isBig)
|
||||
while (end < chars.length && charType(editor, chars[end], isBig) == characterType) {
|
||||
end++
|
||||
|
||||
// Break at end of line. We only wrap when we start at the end of the line, and that's handled above
|
||||
if (end < chars.length && chars[end] == '\n') break
|
||||
}
|
||||
|
||||
// We've gone past the current character type, or hit a newline. Go back, unless that would put us onto a
|
||||
// newline char
|
||||
if (chars[end - 1] != '\n' || end >= chars.length) {
|
||||
end--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!onWordEnd || hasSelection || (count > 1 && dir == 1) || (onSpace && isOuter)) {
|
||||
end = if (dir == 1) {
|
||||
val c = count - if (onWordEnd && !hasSelection && (!(onSpace && isOuter) || (onSpace && !isOuter))) 1 else 0
|
||||
findNextWordEnd(editor, pos, c, isBig, !isOuter)
|
||||
findNextWordEnd(editor, pos, c, isBig, !isOuter, allowMoveFromWordEnd = false)
|
||||
} else {
|
||||
findNextWordEnd(editor, pos, 1, isBig, !isOuter)
|
||||
findNextWordEnd(editor, pos, 1, isBig, !isOuter, allowMoveFromWordEnd = false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,6 +46,10 @@ object CharacterHelper {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isWhitespace(editor: VimEditor, ch: Char, isBig: Boolean): Boolean =
|
||||
charType(editor, ch, isBig) == CharacterType.WHITESPACE
|
||||
|
||||
@JvmStatic
|
||||
fun isInvisibleControlCharacter(ch: Char): Boolean {
|
||||
val type = Character.getType(ch).toByte()
|
||||
|
Loading…
Reference in New Issue
Block a user