@@ -11,15 +11,19 @@ package com.maddyhome.idea.vim.action.fold
1111import com.intellij.vim.annotations.CommandOrMotion
1212import com.intellij.vim.annotations.Mode
1313import com.maddyhome.idea.vim.api.ExecutionContext
14+ import com.maddyhome.idea.vim.api.ImmutableVimCaret
1415import com.maddyhome.idea.vim.api.VimCaret
1516import com.maddyhome.idea.vim.api.VimEditor
1617import com.maddyhome.idea.vim.api.VimFoldRegion
1718import com.maddyhome.idea.vim.api.injector
1819import com.maddyhome.idea.vim.command.Argument
1920import com.maddyhome.idea.vim.command.Command
21+ import com.maddyhome.idea.vim.command.MotionType
2022import com.maddyhome.idea.vim.command.OperatorArguments
2123import com.maddyhome.idea.vim.common.TextRange
2224import com.maddyhome.idea.vim.group.visual.VimSelection
25+ import com.maddyhome.idea.vim.handler.Motion
26+ import com.maddyhome.idea.vim.handler.MotionActionHandler
2327import com.maddyhome.idea.vim.handler.VimActionHandler
2428import 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