in src/main/java/com/maddyhome/idea/vim/extension/multiplecursors/VimMultipleCursorsExtension.kt [114:225]
override fun executeInWriteAction(editor: Editor, context: DataContext) {
val caretModel = editor.caretModel
// vim-multiple-cursors provides a completely custom implementation of multiple cursors. We can rely on IntelliJ's
// implementation.
// vim-multiple-cursors will call "new" to add a new cursor. In normal mode, it sets "whole" to true, in visual,
// "whole" is false. The "whole" flag is saved to a script wide variable, the cursor is added and then the plugin
// enters a custom loop, applying appropriate commands. In this loop, there is only a key shortcut for "next"
// (<C-N>) and no support for "next non-word". The loop will check the script wide word boundary flag and call
// "new" again.
// We might want to consider updating the mappings to handle the difference between normal mode and visual mode
if (!editor.inVisualMode) {
// TODO: Handle multiple cursors in normal mode
// E.g. start a multiple cursor session, clear selection and add a new cursor
// TODO: New cursor should be based on text at the last visual selection marks
// (Marks are not set until we come out of visual mode, so might need to use a work around)
// TODO: Make sure we can handle manually added cursors
if (caretModel.caretCount > 1) return
val selection = selectWordUnderCaret(editor, caretModel.primaryCaret)
// The handler is specific to whole/not-whole word, but the next occurrence is based on the initial call
editor.vimMultipleCursorsWholeWord = whole
editor.vimMultipleCursorsLastSelection = selection
} else {
// vim-multiple-cursors is case sensitive, so it's ok to use a case sensitive set here
val patterns = sortedSetOf<String>()
val newPositions = arrayListOf<VisualPosition>()
// If multiple lines are selected, we want to convert the selection to multiple carets, positioned at the start
// of each line
for (caret in caretModel.allCarets) {
val selectedText = caret.selectedText ?: return
// Keep a track of the selected text, we'll check it later
patterns.add(selectedText)
val minOffset = min(caret.selectionEnd, caret.selectionStart)
var maxOffset = max(caret.selectionEnd, caret.selectionStart)
// As the last offset appears after the new line character, technically it's placed on the next line.
if (selectedText.lastOrNull() == '\n') {
maxOffset -= 1
}
val start = editor.document.getLineNumber(minOffset)
val end = editor.document.getLineNumber(maxOffset)
val lines = end - start
if (lines > 0) {
val selectionStart = min(caret.selectionStart, caret.selectionEnd)
val startPosition = editor.offsetToVisualPosition(selectionStart)
for (line in startPosition.line + 1..startPosition.line + lines) {
newPositions.add(VisualPosition(line, startPosition.column))
}
caret.vim.moveToOffset(selectionStart)
}
}
if (newPositions.size > 0) {
editor.vim.exitVisualMode()
newPositions.forEach { editor.caretModel.addCaret(it, true) ?: return@forEach }
editor.updateCaretsVisualAttributes()
return
}
// All the carets should be selecting the same text. If they're not, then it's likely they have been added
// by some other means, so we shouldn't continue with the VIM behaviour
if (patterns.size > 1) return
// If we are adding the first new cursor, based on the current selection, we do a non-whole word match (ignoring
// the value passed to the handler during mapping. We should fix the mappings for visual mode). If we're adding
// a second or subsequent cursor, we should use the boundary matching parameter used to start the session.
// But all we know right now is that we're in visual mode, and we have a selection. We cannot tell if the
// selection has been added by the user (we're trying to add the first cursor) or it was added when we added the
// first/previous cursor (we're about to add a second/subsequent cursor).
// So, we keep track of the selection used to add the previous cursor. If it matches the current select, we know
// we're about to add a second cursor (so use the saved word boundary flag). If it does not match, something's
// changed, so we're adding a first cursor based on the current selection (set a new non-whole word flag)
val currentSelection = TextRange(caretModel.primaryCaret.selectionStart, caretModel.primaryCaret.selectionEnd)
var lastSelection = editor.vimMultipleCursorsLastSelection
val wholeWord = if (lastSelection != null && lastSelection.startOffset == currentSelection.startOffset &&
lastSelection.endOffset == currentSelection.endOffset
) {
editor.vimMultipleCursorsWholeWord ?: false
} else {
false
}
editor.vimMultipleCursorsWholeWord = wholeWord
lastSelection = currentSelection
// Always work on the text in the last visual selection range, so we work with any changed text, even if it's no
// longer selected
val pattern = editor.vim.getText(lastSelection)
val primaryCaret = editor.caretModel.primaryCaret
val nextOffset = findNextOccurrence(editor, primaryCaret.offset, pattern, wholeWord)
if (nextOffset != -1) {
caretModel.allCarets.forEach {
if (it.selectionStart == nextOffset) {
VimPlugin.showMessage(MessageHelper.message("multiple-cursors.message.no.more.matches"))
return
}
}
val caret = editor.caretModel.addCaret(editor.offsetToVisualPosition(nextOffset), true) ?: return
editor.updateCaretsVisualAttributes()
editor.vimMultipleCursorsLastSelection = selectText(caret, pattern, nextOffset)
} else {
VimPlugin.showMessage(MessageHelper.message("multiple-cursors.message.no.more.matches"))
}
}
}