override fun executeInWriteAction()

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"))
        }
      }
    }