diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt index f744f75fa..36d79169a 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt @@ -16,15 +16,11 @@ interface VimVisualMotionGroup { val selectionAdj: Int /** - * This function toggles visual mode according to the logic required for `v`, `V` and `<C-V>` + * Enters Visual mode, ensuring that the caret's selection start offset is correctly set * - * This is the implementation for `v`, `V` and `<C-V>`. If you need to enter Visual mode, use [enterVisualMode]. - * - * * If visual mode is disabled, enable it - * * If visual mode is enabled, but [selectionType] differs, update visual according to new [selectionType] - * * If visual mode is enabled with the same [selectionType], disable it + * Use this to programmatically enter Visual mode. Note that it does not modify the editor's selection. */ - fun toggleVisual(editor: VimEditor, count: Int, rawCount: Int, selectionType: SelectionType, returnTo: Mode? = null): Boolean + fun enterVisualMode(editor: VimEditor, selectionType: SelectionType): Boolean /** * Enter Select mode with the given selection type @@ -39,13 +35,15 @@ interface VimVisualMotionGroup { fun enterSelectMode(editor: VimEditor, selectionType: SelectionType): Boolean /** - * Enters Visual mode, ensuring that the caret's selection start offset is correctly set + * This function toggles visual mode according to the logic required for `v`, `V` and `<C-V>` * - * Use this to programmatically enter Visual mode. Note that it does not modify the editor's selection. + * This is the implementation for `v`, `V` and `<C-V>`. If you need to enter Visual mode, use [enterVisualMode]. + * + * * If visual mode is disabled, enable it + * * If visual mode is enabled, but [selectionType] differs, update visual according to new [selectionType] + * * If visual mode is enabled with the same [selectionType], disable it */ - fun enterVisualMode(editor: VimEditor, selectionType: SelectionType): Boolean - - fun detectSelectionType(editor: VimEditor): SelectionType + fun toggleVisual(editor: VimEditor, count: Int, rawCount: Int, selectionType: SelectionType, returnTo: Mode? = null): Boolean /** * When in Select mode, enter Visual mode for a single command @@ -58,4 +56,11 @@ interface VimVisualMotionGroup { * See `:help v_CTRL-O`. */ fun processSingleVisualCommand(editor: VimEditor) + + /** + * Detect the current selection type based on the editor's current selection state + * + * If the IDE changes the selection, this function can be used to understand what the current selection type is. + */ + fun detectSelectionType(editor: VimEditor): SelectionType } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt index ace4f570b..572e92b2a 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt @@ -26,6 +26,25 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { override val selectionAdj: Int get() = if (exclusiveSelection) 0 else 1 + /** + * Enters Visual mode, ensuring that the caret's selection start offset is correctly set + * + * Use this to programmatically enter Visual mode. Note that it does not modify the editor's selection. + */ + override fun enterVisualMode(editor: VimEditor, selectionType: SelectionType): Boolean { + editor.mode = Mode.VISUAL(selectionType) + + // vimLeadSelectionOffset requires read action + injector.application.runReadAction { + if (selectionType == SelectionType.BLOCK_WISE) { + editor.primaryCaret().run { vimSelectionStart = vimLeadSelectionOffset } + } else { + editor.nativeCarets().forEach { it.vimSelectionStart = it.vimLeadSelectionOffset } + } + } + return true + } + override fun enterSelectMode(editor: VimEditor, selectionType: SelectionType): Boolean { // If we're already in Select or toggling from Visual, replace the current mode (keep the existing returnTo), // otherwise push Select, using the current mode as returnTo. @@ -100,72 +119,6 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { return true } - protected fun seemsLikeBlockMode(editor: VimEditor): Boolean { - val selections = editor.nativeCarets().map { - val adj = if (editor.offsetToBufferPosition(it.selectionEnd).column == 0) 1 else 0 - it.selectionStart to (it.selectionEnd - adj).coerceAtLeast(0) - }.sortedBy { it.first } - val selectionStartColumn = editor.offsetToBufferPosition(selections.first().first).column - val selectionStartLine = editor.offsetToBufferPosition(selections.first().first).line - - val maxColumn = selections.maxOfOrNull { editor.offsetToBufferPosition(it.second).column } ?: return false - selections.forEachIndexed { i, it -> - if (editor.offsetToBufferPosition(it.first).line != editor.offsetToBufferPosition(it.second).line) { - return false - } - if (editor.offsetToBufferPosition(it.first).column != selectionStartColumn) { - return false - } - val lineEnd = - editor.offsetToBufferPosition(editor.getLineEndForOffset(it.second)).column - if (editor.offsetToBufferPosition(it.second).column != maxColumn.coerceAtMost(lineEnd)) { - return false - } - if (editor.offsetToBufferPosition(it.first).line != selectionStartLine + i) { - return false - } - } - return true - } - - override fun detectSelectionType(editor: VimEditor): SelectionType { - if (editor.carets().size > 1 && seemsLikeBlockMode(editor)) { - return SelectionType.BLOCK_WISE - } - val all = editor.nativeCarets().all { caret -> - // Detect if visual mode is character wise or line wise - val selectionStart = caret.selectionStart - val selectionEnd = caret.selectionEnd - val startLine = editor.offsetToBufferPosition(selectionStart).line - val endPosition = editor.offsetToBufferPosition(selectionEnd) - val endLine = if (endPosition.column == 0) (endPosition.line - 1).coerceAtLeast(0) else endPosition.line - val lineStartOfSelectionStart = editor.getLineStartOffset(startLine) - val lineEndOfSelectionEnd = editor.getLineEndOffset(endLine, true) - lineStartOfSelectionStart == selectionStart && (lineEndOfSelectionEnd + 1 == selectionEnd || lineEndOfSelectionEnd == selectionEnd) - } - if (all) return SelectionType.LINE_WISE - return SelectionType.CHARACTER_WISE - } - - /** - * Enters Visual mode, ensuring that the caret's selection start offset is correctly set - * - * Use this to programmatically enter Visual mode. Note that it does not modify the editor's selection. - */ - override fun enterVisualMode(editor: VimEditor, selectionType: SelectionType): Boolean { - editor.mode = Mode.VISUAL(selectionType) - - // vimLeadSelectionOffset requires read action - injector.application.runReadAction { - if (selectionType == SelectionType.BLOCK_WISE) { - editor.primaryCaret().run { vimSelectionStart = vimLeadSelectionOffset } - } else { - editor.nativeCarets().forEach { it.vimSelectionStart = it.vimLeadSelectionOffset } - } - } - return true - } - /** * When in Select mode, enter Visual mode for a single command * @@ -196,4 +149,51 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { SelectToggleVisualMode.toggleMode(editor) } } + + override fun detectSelectionType(editor: VimEditor): SelectionType { + if (editor.carets().size > 1 && seemsLikeBlockMode(editor)) { + return SelectionType.BLOCK_WISE + } + val all = editor.nativeCarets().all { caret -> + // Detect if visual mode is character wise or line wise + val selectionStart = caret.selectionStart + val selectionEnd = caret.selectionEnd + val startLine = editor.offsetToBufferPosition(selectionStart).line + val endPosition = editor.offsetToBufferPosition(selectionEnd) + val endLine = if (endPosition.column == 0) (endPosition.line - 1).coerceAtLeast(0) else endPosition.line + val lineStartOfSelectionStart = editor.getLineStartOffset(startLine) + val lineEndOfSelectionEnd = editor.getLineEndOffset(endLine, true) + lineStartOfSelectionStart == selectionStart && (lineEndOfSelectionEnd + 1 == selectionEnd || lineEndOfSelectionEnd == selectionEnd) + } + if (all) return SelectionType.LINE_WISE + return SelectionType.CHARACTER_WISE + } + + protected fun seemsLikeBlockMode(editor: VimEditor): Boolean { + val selections = editor.nativeCarets().map { + val adj = if (editor.offsetToBufferPosition(it.selectionEnd).column == 0) 1 else 0 + it.selectionStart to (it.selectionEnd - adj).coerceAtLeast(0) + }.sortedBy { it.first } + val selectionStartColumn = editor.offsetToBufferPosition(selections.first().first).column + val selectionStartLine = editor.offsetToBufferPosition(selections.first().first).line + + val maxColumn = selections.maxOfOrNull { editor.offsetToBufferPosition(it.second).column } ?: return false + selections.forEachIndexed { i, it -> + if (editor.offsetToBufferPosition(it.first).line != editor.offsetToBufferPosition(it.second).line) { + return false + } + if (editor.offsetToBufferPosition(it.first).column != selectionStartColumn) { + return false + } + val lineEnd = + editor.offsetToBufferPosition(editor.getLineEndForOffset(it.second)).column + if (editor.offsetToBufferPosition(it.second).column != maxColumn.coerceAtMost(lineEnd)) { + return false + } + if (editor.offsetToBufferPosition(it.first).line != selectionStartLine + i) { + return false + } + } + return true + } }