Skip to content

Commit 8651b8f

Browse files
1grzyb1AlexPl292
authored andcommitted
VIM-566 Add operator-pending mode support for zj and zk
1 parent ec42b4f commit 8651b8f

File tree

3 files changed

+2409
-2246
lines changed

3 files changed

+2409
-2246
lines changed

tests/java-tests/src/test/kotlin/org/jetbrains/plugins/ideavim/action/fold/NavigateBetweenFoldsTest.kt

Lines changed: 168 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,13 @@
88

99
package org.jetbrains.plugins.ideavim.action.fold
1010

11+
import com.maddyhome.idea.vim.api.injector
12+
import com.maddyhome.idea.vim.newapi.vim
13+
import com.maddyhome.idea.vim.state.mode.Mode
1114
import org.jetbrains.plugins.ideavim.SkipNeovimReason
1215
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
1316
import org.junit.jupiter.api.Test
17+
import kotlin.test.assertEquals
1418

1519
class NavigateBetweenFoldsTest : FoldActionTestBase() {
1620

@@ -521,11 +525,173 @@ class NavigateBetweenFoldsTest : FoldActionTestBase() {
521525
assertCaretOnLine(4)
522526
}
523527

524-
// ============ Helper methods ============
528+
@TestWithoutNeovim(SkipNeovimReason.FOLDING)
529+
@Test
530+
fun `test dzj deletes from cursor to next fold`() {
531+
configureByJavaText(
532+
"""
533+
class TestClass {
534+
${c}int x = 5;
535+
int y = 10;
536+
public void method() {
537+
System.out.println("a");
538+
System.out.println("b");
539+
}
540+
}
541+
""".trimIndent(),
542+
)
543+
updateFoldRegions()
544+
545+
typeText("dzj")
546+
547+
// Linewise delete includes the target line (method declaration)
548+
assertState(
549+
"""
550+
class TestClass {
551+
System.out.println("a");
552+
System.out.println("b");
553+
}
554+
}
555+
""".trimIndent(),
556+
)
557+
}
558+
559+
@TestWithoutNeovim(SkipNeovimReason.FOLDING)
560+
@Test
561+
fun `test dzk deletes from cursor to previous fold end`() {
562+
configureByJavaText(
563+
"""
564+
class TestClass {
565+
public void method() {
566+
System.out.println("a");
567+
System.out.println("b");
568+
}
569+
${c}int x = 5;
570+
int y = 10;
571+
}
572+
""".trimIndent(),
573+
)
574+
updateFoldRegions()
575+
576+
typeText("dzk")
577+
578+
// Linewise delete includes the target line (closing brace of method)
579+
assertState(
580+
"""
581+
class TestClass {
582+
public void method() {
583+
System.out.println("a");
584+
System.out.println("b");
585+
${c}int y = 10;
586+
}
587+
""".trimIndent(),
588+
)
589+
}
590+
591+
@TestWithoutNeovim(SkipNeovimReason.FOLDING)
592+
@Test
593+
fun `test yzj yanks from cursor to next fold`() {
594+
configureByJavaText(
595+
"""
596+
class TestClass {
597+
${c}int x = 5;
598+
int y = 10;
599+
public void method() {
600+
System.out.println("a");
601+
System.out.println("b");
602+
}
603+
}
604+
""".trimIndent(),
605+
)
606+
updateFoldRegions()
607+
608+
typeText("yzj")
609+
610+
val context = injector.executionContextManager.getEditorExecutionContext(fixture.editor.vim)
611+
val regText = injector.registerGroup.getRegister(fixture.editor.vim, context, '0')!!.text
612+
// Linewise yank includes all lines from cursor to fold start (inclusive)
613+
assertEquals(true, regText.contains("int x = 5"))
614+
assertEquals(true, regText.contains("int y = 10"))
615+
assertEquals(true, regText.contains("public void method()"))
616+
}
617+
618+
@TestWithoutNeovim(SkipNeovimReason.FOLDING)
619+
@Test
620+
fun `test czj changes from cursor to next fold`() {
621+
configureByJavaText(
622+
"""
623+
class TestClass {
624+
${c}int x = 5;
625+
int y = 10;
626+
public void method() {
627+
System.out.println("a");
628+
System.out.println("b");
629+
}
630+
}
631+
""".trimIndent(),
632+
)
633+
updateFoldRegions()
634+
635+
typeText("czj")
636+
637+
// Linewise change deletes lines and enters insert mode
638+
assertState(
639+
"""
640+
class TestClass {
641+
${c}
642+
System.out.println("a");
643+
System.out.println("b");
644+
}
645+
}
646+
""".trimIndent(),
647+
)
648+
assertMode(Mode.INSERT)
649+
}
650+
651+
@TestWithoutNeovim(SkipNeovimReason.FOLDING)
652+
@Test
653+
fun `test d2zj deletes to second next fold`() {
654+
configureByJavaText(
655+
"""
656+
${c}class TestClass {
657+
public void method1() {
658+
System.out.println("a");
659+
System.out.println("b");
660+
}
661+
public void method2() {
662+
System.out.println("a");
663+
System.out.println("b");
664+
}
665+
public void method3() {
666+
System.out.println("a");
667+
System.out.println("b");
668+
}
669+
}
670+
""".trimIndent(),
671+
)
672+
updateFoldRegions()
673+
674+
typeText("d2zj")
675+
676+
// Deletes from class declaration to method2 (inclusive)
677+
assertState(
678+
"""
679+
System.out.println("a");
680+
System.out.println("b");
681+
}
682+
public void method3() {
683+
System.out.println("a");
684+
System.out.println("b");
685+
}
686+
}
687+
""".trimIndent(),
688+
)
689+
}
690+
525691

526692
private fun assertCaretOnLine(expectedLine: Int) {
527693
val actualLine = fixture.editor.caretModel.logicalPosition.line
528-
kotlin.test.assertEquals(
694+
assertEquals(
529695
expectedLine,
530696
actualLine,
531697
"Caret should be on line $expectedLine but was on line $actualLine"

vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/fold/FoldActions.kt

Lines changed: 49 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,19 @@ package com.maddyhome.idea.vim.action.fold
1111
import com.intellij.vim.annotations.CommandOrMotion
1212
import com.intellij.vim.annotations.Mode
1313
import com.maddyhome.idea.vim.api.ExecutionContext
14+
import com.maddyhome.idea.vim.api.ImmutableVimCaret
1415
import com.maddyhome.idea.vim.api.VimCaret
1516
import com.maddyhome.idea.vim.api.VimEditor
1617
import com.maddyhome.idea.vim.api.VimFoldRegion
1718
import com.maddyhome.idea.vim.api.injector
1819
import com.maddyhome.idea.vim.command.Argument
1920
import com.maddyhome.idea.vim.command.Command
21+
import com.maddyhome.idea.vim.command.MotionType
2022
import com.maddyhome.idea.vim.command.OperatorArguments
2123
import com.maddyhome.idea.vim.common.TextRange
2224
import com.maddyhome.idea.vim.group.visual.VimSelection
25+
import com.maddyhome.idea.vim.handler.Motion
26+
import com.maddyhome.idea.vim.handler.MotionActionHandler
2327
import com.maddyhome.idea.vim.handler.VimActionHandler
2428
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
2529

@@ -310,89 +314,82 @@ private fun getToggleAction(foldRegion: VimFoldRegion): String = if (foldRegion.
310314
injector.actionExecutor.ACTION_EXPAND_REGION_RECURSIVELY
311315
}
312316

313-
@CommandOrMotion(keys = ["zj"], modes = [Mode.NORMAL, Mode.VISUAL])
314-
class VimNextFold : VimActionHandler.SingleExecution() {
317+
@CommandOrMotion(keys = ["zj"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
318+
class VimNextFold : MotionActionHandler.ForEachCaret() {
315319

316-
override val type: Command.Type = Command.Type.OTHER_READONLY
320+
override val motionType: MotionType = MotionType.LINE_WISE
317321

318-
override fun execute(
322+
override fun getOffset(
319323
editor: VimEditor,
324+
caret: ImmutableVimCaret,
320325
context: ExecutionContext,
321-
cmd: Command,
326+
argument: Argument?,
322327
operatorArguments: OperatorArguments,
323-
): Boolean {
324-
val count = cmd.count.coerceAtLeast(1)
325-
val caret = editor.currentCaret()
328+
): Motion {
326329
val currentLine = editor.offsetToBufferPosition(caret.offset).line
327-
328330
val foldStartLines = findFoldStartLines(editor, currentLine)
331+
val count = operatorArguments.count1
329332

330333
if (foldStartLines.size < count) {
331-
return true
334+
return Motion.NoMotion
332335
}
333336

334337
val targetLine = foldStartLines[count - 1]
335-
caret.moveToLineStart(editor, targetLine)
336-
return true
337-
}
338-
339-
private fun findFoldStartLines(
340-
editor: VimEditor,
341-
currentLine: Int,
342-
): List<Int> = editor.getAllFoldRegions()
343-
.map { fold -> getFoldLine(fold, editor) }
344-
.filter { it > currentLine }
345-
.distinct()
346-
.sorted()
347-
348-
private fun getFoldLine(
349-
fold: VimFoldRegion,
350-
editor: VimEditor,
351-
): Int = if (fold.startOffset > 0) {
352-
editor.offsetToBufferPosition(fold.startOffset - 1).line
353-
} else {
354-
editor.offsetToBufferPosition(fold.startOffset).line
338+
return Motion.AbsoluteOffset(editor.getLineStartOffset(targetLine))
355339
}
356340
}
357341

358-
@CommandOrMotion(keys = ["zk"], modes = [Mode.NORMAL, Mode.VISUAL])
359-
class VimPreviousFold : VimActionHandler.SingleExecution() {
342+
@CommandOrMotion(keys = ["zk"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
343+
class VimPreviousFold : MotionActionHandler.ForEachCaret() {
360344

361-
override val type: Command.Type = Command.Type.OTHER_READONLY
345+
override val motionType: MotionType = MotionType.LINE_WISE
362346

363-
override fun execute(
347+
override fun getOffset(
364348
editor: VimEditor,
349+
caret: ImmutableVimCaret,
365350
context: ExecutionContext,
366-
cmd: Command,
351+
argument: Argument?,
367352
operatorArguments: OperatorArguments,
368-
): Boolean {
369-
val count = cmd.count.coerceAtLeast(1)
370-
val caret = editor.currentCaret()
353+
): Motion {
371354
val currentLine = editor.offsetToBufferPosition(caret.offset).line
372-
373-
val foldEndLines = getFoldEndLines(editor, currentLine)
355+
val foldEndLines = findFoldEndLines(editor, currentLine)
356+
val count = operatorArguments.count1
374357

375358
if (foldEndLines.size < count) {
376-
return true
359+
return Motion.NoMotion
377360
}
378361

379362
val targetLine = foldEndLines[count - 1]
380-
caret.moveToLineStart(editor, targetLine)
381-
return true
363+
return Motion.AbsoluteOffset(editor.getLineStartOffset(targetLine))
382364
}
365+
}
366+
367+
private fun findFoldStartLines(editor: VimEditor, currentLine: Int): List<Int> =
368+
editor.getAllFoldRegions()
369+
.map { fold -> getFoldStartLine(fold, editor) }
370+
.filter { it > currentLine }
371+
.distinct()
372+
.sorted()
383373

384374

385-
private fun getFoldEndLines(
386-
editor: VimEditor,
387-
currentLine: Int,
388-
): List<Int> = editor.getAllFoldRegions()
375+
private fun findFoldEndLines(editor: VimEditor, currentLine: Int): List<Int> =
376+
editor.getAllFoldRegions()
389377
.map { editor.offsetToBufferPosition(it.endOffset).line }
390378
.filter { it < currentLine }
391379
.distinct()
392380
.sortedDescending()
393-
}
394381

395-
private fun VimCaret.moveToLineStart(editor: VimEditor, line: Int) {
396-
val targetOffset = editor.getLineStartOffset(line)
397-
moveToOffset(targetOffset)
398-
}
382+
/**
383+
* Gets the line number where a fold visually starts.
384+
*
385+
* IDE fold regions typically have their startOffset at the first character of the folded content
386+
* (e.g., the newline after `{`), not at the fold marker itself. Subtracting 1 from the offset
387+
* ensures we get the line containing the fold marker (e.g., the line with `{`), which matches
388+
* Vim's behavior where `zj` navigates to the line where the fold starts visually.
389+
*/
390+
private fun getFoldStartLine(fold: VimFoldRegion, editor: VimEditor): Int =
391+
if (fold.startOffset > 0) {
392+
editor.offsetToBufferPosition(fold.startOffset - 1).line
393+
} else {
394+
editor.offsetToBufferPosition(fold.startOffset).line
395+
}

0 commit comments

Comments
 (0)