diff --git a/src/main/java/com/maddyhome/idea/vim/extension/surround/VimSurroundExtension.kt b/src/main/java/com/maddyhome/idea/vim/extension/surround/VimSurroundExtension.kt index f13128548..2ed9f9d12 100644 --- a/src/main/java/com/maddyhome/idea/vim/extension/surround/VimSurroundExtension.kt +++ b/src/main/java/com/maddyhome/idea/vim/extension/surround/VimSurroundExtension.kt @@ -14,6 +14,7 @@ import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.VimCaret +import com.maddyhome.idea.vim.api.VimChangeGroup import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.endsWithNewLine import com.maddyhome.idea.vim.api.getLeadingCharacterOffset @@ -36,7 +37,10 @@ import com.maddyhome.idea.vim.extension.VimExtensionFacade.setRegisterForCaret import com.maddyhome.idea.vim.extension.exportOperatorFunction import com.maddyhome.idea.vim.group.findBlockRange import com.maddyhome.idea.vim.helper.exitVisualMode +import com.maddyhome.idea.vim.helper.runWithEveryCaretAndRestore import com.maddyhome.idea.vim.key.OperatorFunction +import com.maddyhome.idea.vim.newapi.IjVimCaret +import com.maddyhome.idea.vim.newapi.IjVimEditor import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.options.helpers.ClipboardOptionHelper @@ -79,7 +83,7 @@ internal class VimSurroundExtension : VimExtension { putKeyMappingIfMissing(MappingMode.XO, injector.parser.parseKeys("S"), owner, injector.parser.parseKeys("<Plug>VSurround"), true) } - VimExtensionFacade.exportOperatorFunction(OPERATOR_FUNC, Operator()) + VimExtensionFacade.exportOperatorFunction(OPERATOR_FUNC, Operator(supportsMultipleCursors = false)) // TODO } private class YSurroundHandler : ExtensionHandler { @@ -130,15 +134,13 @@ internal class VimSurroundExtension : VimExtension { private class VSurroundHandler : ExtensionHandler { override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) { - val selectionStart = editor.ij.caretModel.primaryCaret.selectionStart // NB: Operator ignores SelectionType anyway - if (!Operator().apply(editor, context, editor.mode.selectionType)) { + if (!Operator(supportsMultipleCursors = true).apply(editor, context, editor.mode.selectionType)) { return } runWriteAction { // Leave visual mode editor.exitVisualMode() - editor.ij.caretModel.moveToOffset(selectionStart) } } } @@ -159,6 +161,10 @@ internal class VimSurroundExtension : VimExtension { companion object { fun change(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: SurroundPair?) { + editor.ij.runWithEveryCaretAndRestore { changeAtCaret(editor, context, charFrom, newSurround) } + } + + fun changeAtCaret(editor: VimEditor, context: ExecutionContext, charFrom: Char, newSurround: SurroundPair?) { // Save old register values for carets val surroundings = editor.sortedCarets() .map { @@ -272,20 +278,44 @@ internal class VimSurroundExtension : VimExtension { } } - private class Operator : OperatorFunction { - override fun apply(editor: VimEditor, context: ExecutionContext, selectionType: SelectionType?): Boolean { - val ijEditor = editor.ij + private class Operator(private val supportsMultipleCursors: Boolean) : OperatorFunction { + override fun apply(vimEditor: VimEditor, context: ExecutionContext, selectionType: SelectionType?): Boolean { + val ijEditor = vimEditor.ij val c = getChar(ijEditor) if (c.code == 0) return true val pair = getOrInputPair(c, ijEditor, context.ij) ?: return false - // XXX: Will it work with line-wise or block-wise selections? - val range = getSurroundRange(editor.currentCaret()) ?: return false - performSurround(pair, range, editor.currentCaret(), selectionType == SelectionType.LINE_WISE) - // Jump back to start - executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor) + + runWriteAction { + val change = VimPlugin.getChange() + if (supportsMultipleCursors) { + ijEditor.runWithEveryCaretAndRestore { + applyOnce(ijEditor, change, pair) + } + } + else { + applyOnce(ijEditor, change, pair) + // Jump back to start + executeNormalWithoutMapping(injector.parser.parseKeys("`["), ijEditor) + } + } return true } + + private fun applyOnce(editor: Editor, change: VimChangeGroup, pair: SurroundPair) { + // XXX: Will it work with line-wise or block-wise selections? + val primaryCaret = editor.caretModel.primaryCaret + val range = getSurroundRange(primaryCaret.vim) + if (range != null) { + change.insertText(IjVimEditor(editor), IjVimCaret(primaryCaret), range.startOffset, pair.first) + change.insertText( + IjVimEditor(editor), + IjVimCaret(primaryCaret), + range.endOffset + pair.first.length, + pair.second + ) + } + } private fun getSurroundRange(caret: VimCaret): TextRange? { val editor = caret.editor diff --git a/src/main/java/com/maddyhome/idea/vim/helper/EditorHelper.kt b/src/main/java/com/maddyhome/idea/vim/helper/EditorHelper.kt index 2db097b80..52c39423a 100644 --- a/src/main/java/com/maddyhome/idea/vim/helper/EditorHelper.kt +++ b/src/main/java/com/maddyhome/idea/vim/helper/EditorHelper.kt @@ -12,6 +12,7 @@ package com.maddyhome.idea.vim.helper import com.intellij.codeWithMe.ClientId import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.CaretState import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.ex.util.EditorUtil import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx @@ -21,6 +22,8 @@ import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.group.IjOptionConstants import com.maddyhome.idea.vim.key.IdeaVimDisablerExtensionPoint import com.maddyhome.idea.vim.newapi.globalIjOptions +import com.maddyhome.idea.vim.newapi.vim +import com.maddyhome.idea.vim.state.mode.inBlockSelection import java.awt.Component import javax.swing.JComponent import javax.swing.JTable @@ -97,4 +100,42 @@ internal val Caret.vimLine: Int * Get current caret line in vim notation (1-based) */ internal val Editor.vimLine: Int - get() = this.caretModel.currentCaret.vimLine \ No newline at end of file + get() = this.caretModel.currentCaret.vimLine + +internal inline fun Editor.runWithEveryCaretAndRestore(action: () -> Unit) { + val caretModel = this.caretModel + val carets = if (this.vim.inBlockSelection) null else caretModel.allCarets + if (carets == null || carets.size == 1) { + action() + } + else { + var initialDocumentSize = this.document.textLength + var documentSizeDifference = 0 + + val caretOffsets = carets.map { it.selectionStart to it.selectionEnd } + val restoredCarets = mutableListOf<CaretState>() + + caretModel.removeSecondaryCarets() + + for ((selectionStart, selectionEnd) in caretOffsets) { + if (selectionStart == selectionEnd) { + caretModel.primaryCaret.moveToOffset(selectionStart + documentSizeDifference) + } + else { + caretModel.primaryCaret.setSelection( + selectionStart + documentSizeDifference, + selectionEnd + documentSizeDifference + ) + } + + action() + restoredCarets.add(caretModel.caretsAndSelections.single()) + + val documentLength = this.document.textLength + documentSizeDifference += documentLength - initialDocumentSize + initialDocumentSize = documentLength + } + + caretModel.caretsAndSelections = restoredCarets + } +}