1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-08-13 06:16:58 +02:00

VIM-25 Smart put operations (via IJ)

This commit is contained in:
Alex Plate
2019-05-31 14:14:10 +03:00
parent 8a4d3f5d80
commit 32fdbaccc3
22 changed files with 311 additions and 110 deletions

@@ -3,16 +3,18 @@ List of Supported Set Commands
The following `:set` commands can appear in `~/.ideavimrc` or set manually in the command mode:
'clipboard' 'cb' clipboard options
'digraph' 'dg' enable the entering of digraphs in Insert mode
'gdefault' 'gd' the ":substitute" flag 'g' is default on
'history' 'hi' number of command-lines that are remembered
'hlsearch' 'hls' highlight matches with last search pattern
'ignorecase' 'ic' ignore case in search patterns
'iskeyword' 'isk' defines keywords for commands like 'w', '*', etc.
'incsearch' 'is' show where search pattern typed so far matches
'clipboard' 'cb' clipboard options
'digraph' 'dg' enable the entering of digraphs in Insert mode
'gdefault' 'gd' the ":substitute" flag 'g' is default on
'history' 'hi' number of command-lines that are remembered
'hlsearch' 'hls' highlight matches with last search pattern
`ideaput` `ideaput` boolean (default on) - IdeaVim ONLY [To Be Released]
enable native idea paste action for put operations
'ignorecase' 'ic' ignore case in search patterns
'iskeyword' 'isk' defines keywords for commands like 'w', '*', etc.
'incsearch' 'is' show where search pattern typed so far matches
`keymodel` `km` String (default "continueselect,stopselect") [To Be Released]
`keymodel` `km` String (default "continueselect,stopselect") [To Be Released]
List of comma separated words, which enable special things that keys
can do. These values can be used:

@@ -41,7 +41,7 @@ public class PutTextAfterCursorAction extends EditorAction {
final Register lastRegister = VimPlugin.getRegister().getLastRegister();
final PutData.TextData textData =
lastRegister != null ? new PutData.TextData(lastRegister.getText(), lastRegister.getType()) : null;
lastRegister != null ? new PutData.TextData(lastRegister.getText(), lastRegister.getType(), lastRegister.getTransferableData()) : null;
final PutData putData = new PutData(textData, null, count, false, true, false, -1);
return VimPlugin.getPut().putText(editor, context, putData);
}

@@ -44,7 +44,7 @@ public class PutTextAfterCursorActionMoveCursor extends EditorAction {
final Register lastRegister = VimPlugin.getRegister().getLastRegister();
final PutData.TextData textData =
lastRegister != null ? new PutData.TextData(lastRegister.getText(), lastRegister.getType()) : null;
lastRegister != null ? new PutData.TextData(lastRegister.getText(), lastRegister.getType(), lastRegister.getTransferableData()) : null;
final PutData putData = new PutData(textData, null, count, false, true, true, -1);
return VimPlugin.getPut().putText(editor, context, putData);
}

@@ -44,7 +44,7 @@ public class PutTextAfterCursorNoIndentAction extends EditorAction {
final Register lastRegister = VimPlugin.getRegister().getLastRegister();
final PutData.TextData textData =
lastRegister != null ? new PutData.TextData(lastRegister.getText(), lastRegister.getType()) : null;
lastRegister != null ? new PutData.TextData(lastRegister.getText(), lastRegister.getType(), lastRegister.getTransferableData()) : null;
final PutData putData = new PutData(textData, null, count, false, false, false, -1);
return VimPlugin.getPut().putText(editor, context, putData);
}

@@ -44,7 +44,7 @@ public class PutTextBeforeCursorAction extends EditorAction {
final Register lastRegister = VimPlugin.getRegister().getLastRegister();
final PutData.TextData textData =
lastRegister != null ? new PutData.TextData(lastRegister.getText(), lastRegister.getType()) : null;
lastRegister != null ? new PutData.TextData(lastRegister.getText(), lastRegister.getType(), lastRegister.getTransferableData()) : null;
final PutData putData = new PutData(textData, null, count, true, true, false, -1);
return VimPlugin.getPut().putText(editor, context, putData);
}

@@ -44,7 +44,7 @@ public class PutTextBeforeCursorActionMoveCursor extends EditorAction {
final Register lastRegister = VimPlugin.getRegister().getLastRegister();
final PutData.TextData textData =
lastRegister != null ? new PutData.TextData(lastRegister.getText(), lastRegister.getType()) : null;
lastRegister != null ? new PutData.TextData(lastRegister.getText(), lastRegister.getType(), lastRegister.getTransferableData()) : null;
final PutData putData = new PutData(textData, null, count, true, true, true, -1);
return VimPlugin.getPut().putText(editor, context, putData);
}

@@ -44,7 +44,7 @@ public class PutTextBeforeCursorNoIndentAction extends EditorAction {
final Register lastRegister = VimPlugin.getRegister().getLastRegister();
final PutData.TextData textData =
lastRegister != null ? new PutData.TextData(lastRegister.getText(), lastRegister.getType()) : null;
lastRegister != null ? new PutData.TextData(lastRegister.getText(), lastRegister.getType(), lastRegister.getTransferableData()) : null;
final PutData putData = new PutData(textData, null, count, true, false, false, -1);
return VimPlugin.getPut().putText(editor, context, putData);
}

@@ -39,7 +39,7 @@ private object PutVisualTextActionHandler : VisualOperatorActionHandler.SingleEx
cmd: Command,
caretsAndSelections: Map<Caret, VimSelection>): Boolean {
if (caretsAndSelections.isEmpty()) return false
val textData = VimPlugin.getRegister().lastRegister?.let { PutData.TextData(it.text, it.type) }
val textData = VimPlugin.getRegister().lastRegister?.let { PutData.TextData(it.text, it.type, it.transferableData) }
VimPlugin.getRegister().resetRegister()
val insertTextBeforeCaret = cmd.keys[0].keyChar == 'P'

@@ -35,7 +35,7 @@ import javax.swing.KeyStroke
private object PutVisualTextMoveCursorActionHandler : VisualOperatorActionHandler.SingleExecution() {
override fun executeForAllCarets(editor: Editor, context: DataContext, cmd: Command, caretsAndSelections: Map<Caret, VimSelection>): Boolean {
if (caretsAndSelections.isEmpty()) return false
val textData = VimPlugin.getRegister().lastRegister?.let { PutData.TextData(it.text, it.type) }
val textData = VimPlugin.getRegister().lastRegister?.let { PutData.TextData(it.text, it.type, it.transferableData) }
VimPlugin.getRegister().resetRegister()
val insertTextBeforeCaret = cmd.keys[1].keyChar == 'P'

@@ -35,7 +35,7 @@ import javax.swing.KeyStroke
private object PutVisualTextNoIndentActionHandler : VisualOperatorActionHandler.SingleExecution() {
override fun executeForAllCarets(editor: Editor, context: DataContext, cmd: Command, caretsAndSelections: Map<Caret, VimSelection>): Boolean {
if (caretsAndSelections.isEmpty()) return false
val textData = VimPlugin.getRegister().lastRegister?.let { PutData.TextData(it.text, it.type) }
val textData = VimPlugin.getRegister().lastRegister?.let { PutData.TextData(it.text, it.type, it.transferableData) }
VimPlugin.getRegister().resetRegister()
val insertBeforeCaret = cmd.keys[1].keyChar == 'P'

@@ -18,6 +18,7 @@
package com.maddyhome.idea.vim.common;
import com.intellij.codeInsight.editorActions.TextBlockTransferableData;
import com.maddyhome.idea.vim.command.SelectionType;
import com.maddyhome.idea.vim.helper.StringHelper;
import org.jetbrains.annotations.NotNull;
@@ -25,6 +26,7 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@@ -35,12 +37,7 @@ public class Register {
private char name;
@NotNull private final SelectionType type;
@NotNull private final List<KeyStroke> keys;
public Register(char name, @NotNull SelectionType type, @NotNull String text) {
this.name = name;
this.type = type;
this.keys = StringHelper.stringToKeys(text);
}
@NotNull private List<? extends TextBlockTransferableData> transferableData = new ArrayList<>();
public Register(char name, @NotNull SelectionType type, @NotNull List<KeyStroke> keys) {
this.name = name;
@@ -48,6 +45,13 @@ public class Register {
this.keys = keys;
}
public Register(char name, @NotNull SelectionType type, @NotNull String text, @NotNull List<? extends TextBlockTransferableData> transferableData) {
this.name = name;
this.type = type;
this.keys = StringHelper.stringToKeys(text);
this.transferableData = transferableData;
}
public void rename(char name) {
this.name = name;
}
@@ -59,6 +63,11 @@ public class Register {
return name;
}
@NotNull
public List<? extends TextBlockTransferableData> getTransferableData() {
return transferableData;
}
/**
* Get the register type.
*/
@@ -94,19 +103,18 @@ public class Register {
/**
* Append the supplied text to any existing text.
*/
public void addText(@NotNull String text) {
public void addTextAndResetTransferableData(@NotNull String text) {
addKeys(StringHelper.stringToKeys(text));
transferableData.clear();
}
public void addKeys(@NotNull List<KeyStroke> keys) {
this.keys.addAll(keys);
}
public static class KeySorter<V> implements Comparator<V> {
public int compare(V o1, V o2) {
Register a = (Register)o1;
Register b = (Register)o2;
return Character.compare(a.name, b.name);
public static class KeySorter implements Comparator<Register> {
public int compare(@NotNull Register o1, @NotNull Register o2) {
return Character.compare(o1.name, o2.name);
}
}
}

@@ -41,7 +41,8 @@ class CopyTextHandler : CommandHandler(
val arg = CommandParser.getInstance().parse(cmd.argument)
val line = arg.ranges.getFirstLine(editor, caret, context)
val textData = PutData.TextData(text, SelectionType.LINE_WISE)
val transferableData = VimPlugin.getRegister().getTransferableData(editor, range, text)
val textData = PutData.TextData(text, SelectionType.LINE_WISE, transferableData)
val putData = PutData(textData, null, 1, insertTextBeforeCaret = false, _indent = true, caretAfterInsertedText = false, putToLine = line)
VimPlugin.getPut().putTextForCaret(editor, caret, context, putData)
}

@@ -68,7 +68,7 @@ class MoveTextHandler : CommandHandler(
val caret = carets[i]
val text = texts[i]
val textData = PutData.TextData(text, SelectionType.LINE_WISE)
val textData = PutData.TextData(text, SelectionType.LINE_WISE, emptyList())
val putData = PutData(textData, null, 1, insertTextBeforeCaret = false, _indent = true, caretAfterInsertedText = false, putToLine = line)
VimPlugin.getPut().putTextForCaret(editor, caret, context, putData)
}

@@ -46,7 +46,7 @@ class PutLinesHandler : CommandHandler(
}
val line = if (cmd.ranges.size() == 0) -1 else cmd.getLine(editor, context)
val textData = registerGroup.lastRegister?.let { PutData.TextData(it.text, SelectionType.LINE_WISE) }
val textData = registerGroup.lastRegister?.let { PutData.TextData(it.text, SelectionType.LINE_WISE, it.transferableData) }
val putData = PutData(textData, null, 1, false, false, false, line)
return VimPlugin.getPut().putText(editor, context, putData)
}

@@ -35,6 +35,8 @@ import com.maddyhome.idea.vim.extension.VimNonDisposableExtension;
import com.maddyhome.idea.vim.group.ChangeGroup;
import com.maddyhome.idea.vim.helper.EditorHelper;
import com.maddyhome.idea.vim.key.OperatorFunction;
import com.maddyhome.idea.vim.option.Options;
import com.maddyhome.idea.vim.option.ToggleOption;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -46,14 +48,7 @@ import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static com.maddyhome.idea.vim.extension.VimExtensionFacade.executeNormal;
import static com.maddyhome.idea.vim.extension.VimExtensionFacade.getRegister;
import static com.maddyhome.idea.vim.extension.VimExtensionFacade.inputKeyStroke;
import static com.maddyhome.idea.vim.extension.VimExtensionFacade.inputString;
import static com.maddyhome.idea.vim.extension.VimExtensionFacade.putExtensionHandlerMapping;
import static com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMapping;
import static com.maddyhome.idea.vim.extension.VimExtensionFacade.setOperatorFunction;
import static com.maddyhome.idea.vim.extension.VimExtensionFacade.setRegister;
import static com.maddyhome.idea.vim.extension.VimExtensionFacade.*;
import static com.maddyhome.idea.vim.helper.StringHelper.parseKeys;
/**
@@ -226,7 +221,11 @@ public class VimSurroundExtension extends VimNonDisposableExtension {
}
private static void perform(@NotNull String sequence, @NotNull Editor editor) {
final ToggleOption ideaput = (ToggleOption)Options.getInstance().getOption(Options.IDEAPUT);
final boolean origValue = ideaput.getValue();
ideaput.reset();
executeNormal(parseKeys("\"" + REGISTER + sequence), editor);
if (origValue) ideaput.set();
}
private static void pasteSurround(@NotNull List<KeyStroke> innerValue, @NotNull Editor editor) {

@@ -19,9 +19,19 @@
package com.maddyhome.idea.vim.group;
import com.google.common.collect.ImmutableList;
import com.intellij.codeInsight.editorActions.CopyPastePostProcessor;
import com.intellij.codeInsight.editorActions.CopyPastePreProcessor;
import com.intellij.codeInsight.editorActions.TextBlockTransferable;
import com.intellij.codeInsight.editorActions.TextBlockTransferableData;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.CaretStateTransferableData;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.action.motion.mark.MotionGotoFileMarkAction;
import com.maddyhome.idea.vim.action.motion.search.SearchAgainNextAction;
@@ -44,6 +54,7 @@ import com.maddyhome.idea.vim.helper.StringHelper;
import com.maddyhome.idea.vim.option.ListOption;
import com.maddyhome.idea.vim.option.Options;
import com.maddyhome.idea.vim.ui.ClipboardHandler;
import kotlin.Pair;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -51,8 +62,8 @@ import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
/**
@@ -163,32 +174,35 @@ public class RegisterGroup {
}
// If this is an uppercase register, we need to append the text to the corresponding lowercase register
final List<TextBlockTransferableData> transferableData = start != -1 ? getTransferableData(editor, range, text) : new ArrayList<>();
final String processedText = start != -1 ? preprocessText(editor, range, text, transferableData) : text;
if (Character.isUpperCase(register)) {
char lreg = Character.toLowerCase(register);
Register r = registers.get(lreg);
// Append the text if the lowercase register existed
if (r != null) {
r.addText(text);
r.addTextAndResetTransferableData(processedText);
}
// Set the text if the lowercase register didn't exist yet
else {
registers.put(lreg, new Register(lreg, type, text));
if (logger.isDebugEnabled()) logger.debug("register '" + register + "' contains: \"" + text + "\"");
registers.put(lreg, new Register(lreg, type, processedText, new ArrayList<>(transferableData)));
if (logger.isDebugEnabled()) logger.debug("register '" + register + "' contains: \"" + processedText + "\"");
}
}
else if (CLIPBOARD_REGISTERS.contains(register)) {
ClipboardHandler.setClipboardText(text);
}
// Put the text in the specified register
else {
registers.put(register, new Register(register, type, text));
if (logger.isDebugEnabled()) logger.debug("register '" + register + "' contains: \"" + text + "\"");
registers.put(register, new Register(register, type, processedText, new ArrayList<>(transferableData)));
if (logger.isDebugEnabled()) logger.debug("register '" + register + "' contains: \"" + processedText + "\"");
}
if (CLIPBOARD_REGISTERS.contains(register)) {
ClipboardHandler.setClipboardText(processedText, new ArrayList<>(transferableData), text);
}
// Also add it to the default register if the default wasn't specified
if (register != defaultRegister && ".:/".indexOf(register) == -1) {
registers.put(defaultRegister, new Register(defaultRegister, type, text));
if (logger.isDebugEnabled()) logger.debug("register '" + register + "' contains: \"" + text + "\"");
registers.put(defaultRegister, new Register(defaultRegister, type, processedText, new ArrayList<>(transferableData)));
if (logger.isDebugEnabled()) logger.debug("register '" + register + "' contains: \"" + processedText + "\"");
}
if (isDelete) {
@@ -205,18 +219,18 @@ public class RegisterGroup {
registers.put((char)(d + 1), t);
}
}
registers.put('1', new Register('1', type, text));
registers.put('1', new Register('1', type, processedText, new ArrayList<>(transferableData)));
}
// Deletes smaller than one line and without specified register go the the "-" register
if (smallInlineDeletion && register == defaultRegister) {
registers.put('-', new Register('-', type, text));
registers.put('-', new Register('-', type, processedText, new ArrayList<>(transferableData)));
}
}
// Yanks also go to register 0 if the default register was used
else if (register == defaultRegister) {
registers.put('0', new Register('0', type, text));
if (logger.isDebugEnabled()) logger.debug("register '" + '0' + "' contains: \"" + text + "\"");
registers.put('0', new Register('0', type, processedText, new ArrayList<>(transferableData)));
if (logger.isDebugEnabled()) logger.debug("register '" + '0' + "' contains: \"" + processedText + "\"");
}
if (start != -1) {
@@ -226,6 +240,45 @@ public class RegisterGroup {
return true;
}
@NotNull
public List<TextBlockTransferableData> getTransferableData(@NotNull Editor editor,
@NotNull TextRange textRange,
String text) {
final List<TextBlockTransferableData> transferableDatas = new ArrayList<>();
final Project project = editor.getProject();
if (project == null) return new ArrayList<>();
final PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
DumbService.getInstance(project).withAlternativeResolveEnabled(() -> {
for (CopyPastePostProcessor<? extends TextBlockTransferableData> processor : CopyPastePostProcessor.EP_NAME
.getExtensionList()) {
try {
transferableDatas.addAll(processor.collectTransferableData(file, editor, textRange.getStartOffsets(), textRange.getEndOffsets()));
}
catch (IndexNotReadyException ignore) {
}
}
});
transferableDatas.add(new CaretStateTransferableData(new int[]{0}, new int[]{text.length()}));
return transferableDatas;
}
private String preprocessText(@NotNull Editor editor, @NotNull TextRange textRange, String text, List<TextBlockTransferableData> transferableDatas) {
final Project project = editor.getProject();
if (project == null) return text;
final PsiFile file = PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
String rawText = TextBlockTransferable.convertLineSeparators(text, "\n", transferableDatas);
String escapedText = null;
for (CopyPastePreProcessor processor : CopyPastePreProcessor.EP_NAME.getExtensionList()) {
escapedText = processor.preprocessOnCopy(file, textRange.getStartOffsets(), textRange.getEndOffsets(), rawText);
if (escapedText != null) {
return escapedText;
}
}
return text;
}
private boolean isSmallDeletionSpecialCase(Editor editor) {
Command currentCommand = CommandState.getInstance(editor).getCommand();
if (currentCommand != null) {
@@ -300,7 +353,7 @@ public class RegisterGroup {
res.add(register);
}
}
res.sort(new Register.KeySorter<>());
res.sort(new Register.KeySorter());
return res;
}
@@ -390,7 +443,6 @@ public class RegisterGroup {
logger.debug("readData");
final Element registersElement = element.getChild("registers");
if (registersElement != null) {
//noinspection unchecked
final List<Element> registerElements = registersElement.getChildren("register");
for (Element registerElement : registerElements) {
final char key = registerElement.getAttributeValue("name").charAt(0);
@@ -401,7 +453,7 @@ public class RegisterGroup {
if (textElement != null) {
final String text = StringHelper.getSafeXmlText(textElement);
if (text != null) {
register = new Register(key, type, text);
register = new Register(key, type, text, Collections.emptyList());
}
else {
register = null;
@@ -409,7 +461,6 @@ public class RegisterGroup {
}
else {
final Element keysElement = registerElement.getChild("keys");
//noinspection unchecked
final List<Element> keyElements = keysElement.getChildren("key");
final List<KeyStroke> strokes = new ArrayList<>();
for (Element keyElement : keyElements) {
@@ -430,13 +481,15 @@ public class RegisterGroup {
@Nullable
private Register refreshClipboardRegister(char r) {
final String text = ClipboardHandler.getClipboardText();
final Pair<String, List<TextBlockTransferableData>> clipboardData = ClipboardHandler.getClipboardTextAndTransferableData();
final Register currentRegister = registers.get(r);
final String text = clipboardData.getFirst();
final List<TextBlockTransferableData> transferableData = clipboardData.getSecond();
if (text != null) {
if (currentRegister != null && text.equals(currentRegister.getText())) {
return currentRegister;
}
return new Register(r, guessSelectionType(text), text);
return new Register(r, guessSelectionType(text), text, transferableData);
}
return null;
}

@@ -18,10 +18,15 @@
package com.maddyhome.idea.vim.group.copy
import com.intellij.codeInsight.editorActions.TextBlockTransferable
import com.intellij.codeInsight.editorActions.TextBlockTransferableData
import com.intellij.ide.CopyPasteManagerEx
import com.intellij.ide.PasteProvider
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.LogicalPosition
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.*
import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.openapi.util.text.StringUtil
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.command.CommandState
@@ -32,6 +37,7 @@ import com.maddyhome.idea.vim.group.MotionGroup
import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.CaretOrder.DECREASING_OFFSET
import com.maddyhome.idea.vim.helper.EditorHelper
import com.maddyhome.idea.vim.option.Options
import java.util.*
import kotlin.math.abs
import kotlin.math.min
@@ -58,24 +64,30 @@ data class PutData(
data class TextData(
val rawText: String?,
val typeInRegister: SelectionType
val typeInRegister: SelectionType,
val transferableData: List<TextBlockTransferableData>
)
}
private data class ProcessedTextData(
val text: String,
val typeInRegister: SelectionType,
val transferableData: List<TextBlockTransferableData>
)
class PutGroup {
fun putText(editor: Editor, context: DataContext, data: PutData): Boolean {
val additionalData = collectPreModificationData(editor, data)
deleteSelectedText(editor, data)
val (text, typeInRegister) = getText(editor, data) ?: return false
putTextAndSetCaretPosition(editor, context, text, typeInRegister, data, additionalData)
val processedText = processText(editor, data) ?: return false
putTextAndSetCaretPosition(editor, context, processedText, data, additionalData)
return true
}
fun putTextForCaret(editor: Editor, caret: Caret, context: DataContext, data: PutData): Boolean {
val additionalData = collectPreModificationData(editor, data)
val (text, typeInRegister) = getText(editor, data) ?: return false
putForCaret(editor, caret, typeInRegister, data, additionalData, context, text)
val processedText = processText(editor, data) ?: return false
putForCaret(editor, caret, data, additionalData, context, processedText)
return true
}
@@ -104,7 +116,7 @@ class PutGroup {
}
}
private fun getText(editor: Editor, data: PutData): Pair<String, SelectionType>? {
private fun processText(editor: Editor, data: PutData): ProcessedTextData? {
var text = data.textData?.rawText ?: run {
if (data.visualSelection != null) {
val offset = editor.caretModel.primaryCaret.offset
@@ -118,28 +130,39 @@ class PutGroup {
if (data.textData.typeInRegister == SelectionType.LINE_WISE && text.isNotEmpty() && text.last() != '\n') text += '\n'
return text to data.textData.typeInRegister
return ProcessedTextData(text, data.textData.typeInRegister, data.textData.transferableData)
}
private fun putTextAndSetCaretPosition(editor: Editor, context: DataContext, text: String, typeInRegister: SelectionType, data: PutData, additionalData: Map<String, Any>) {
private fun putTextAndSetCaretPosition(editor: Editor, context: DataContext, text: ProcessedTextData, data: PutData, additionalData: Map<String, Any>) {
val subMode = data.visualSelection?.typeInEditor?.toSubMode() ?: CommandState.SubMode.NONE
if (Options.getInstance().isSet(Options.IDEAPUT)) {
val idePasteProvider = getProviderForPasteViaIde(context, text.typeInRegister, data)
if (idePasteProvider != null) {
logger.debug("Perform put via idea paste")
putTextViaIde(idePasteProvider, editor, context, text, subMode, data, additionalData)
return
}
}
logger.debug("Perform put via plugin")
val myCarets = if (data.visualSelection != null) {
data.visualSelection.caretsAndSelections.keys.sortedByDescending { it.logicalPosition }
} else {
EditorHelper.getOrderedCaretsList(editor, DECREASING_OFFSET)
}
myCarets.forEach { caret -> putForCaret(editor, caret, typeInRegister, data, additionalData, context, text) }
myCarets.forEach { caret -> putForCaret(editor, caret, data, additionalData, context, text) }
}
private fun putForCaret(editor: Editor, caret: Caret, typeInRegister: SelectionType, data: PutData, additionalData: Map<String, Any>, context: DataContext, text: String) {
private fun putForCaret(editor: Editor, caret: Caret, data: PutData, additionalData: Map<String, Any>, context: DataContext, text: ProcessedTextData) {
if (data.visualSelection?.typeInEditor == SelectionType.LINE_WISE && editor.isOneLineMode) return
val startOffsets = prepareDocumentAndGetStartOffsets(editor, caret, typeInRegister, data, additionalData)
val startOffsets = prepareDocumentAndGetStartOffsets(editor, caret, text.typeInRegister, data, additionalData)
startOffsets.forEach { startOffset ->
val subMode = data.visualSelection?.typeInEditor?.toSubMode() ?: CommandState.SubMode.NONE
val endOffset = putTextInternal(editor, caret, context, text, typeInRegister, subMode,
val endOffset = putTextInternal(editor, caret, context, text.text, text.typeInRegister, subMode,
startOffset, data.count, data.indent, data.caretAfterInsertedText)
VimPlugin.getMark().setChangeMarks(editor, TextRange(startOffset, endOffset))
moveCaretToEndPosition(editor, caret, startOffset, endOffset, typeInRegister, subMode, data.caretAfterInsertedText)
moveCaretToEndPosition(editor, caret, startOffset, endOffset, text.typeInRegister, subMode, data.caretAfterInsertedText)
}
}
@@ -180,7 +203,7 @@ class PutGroup {
}
var startOffset: Int
val line = if (data.putToLine < 0) caret.visualPosition.line else data.putToLine
val line = if (data.putToLine < 0) caret.logicalPosition.line else data.putToLine
when (typeInRegister) {
SelectionType.LINE_WISE -> {
startOffset = min(editor.document.textLength, VimPlugin.getMotion().moveCaretToLineEnd(editor, line, true) + 1)
@@ -202,6 +225,52 @@ class PutGroup {
}
}
private fun getProviderForPasteViaIde(context: DataContext, typeInRegister: SelectionType, data: PutData): PasteProvider? {
if (data.visualSelection != null && data.visualSelection.typeInEditor == SelectionType.BLOCK_WISE) return null
if ((typeInRegister == SelectionType.LINE_WISE || typeInRegister == SelectionType.CHARACTER_WISE) && data.count == 1) {
val provider = PlatformDataKeys.PASTE_PROVIDER.getData(context)
if (provider != null && provider.isPasteEnabled(context)) return provider
}
return null
}
private fun putTextViaIde(pasteProvider: PasteProvider, editor: Editor, context: DataContext, text: ProcessedTextData, subMode: CommandState.SubMode, data: PutData, additionalData: Map<String, Any>) {
val carets: MutableMap<Caret, RangeMarker> = mutableMapOf()
EditorHelper.getOrderedCaretsList(editor, DECREASING_OFFSET).forEach { caret ->
val startOffset = prepareDocumentAndGetStartOffsets(editor, caret, text.typeInRegister, data, additionalData).first()
val pointMarker = editor.document.createRangeMarker(startOffset, startOffset)
caret.moveToOffset(startOffset)
carets[caret] = pointMarker
}
val origContent: TextBlockTransferable = setClipboardText(text.text, text.transferableData)
try {
pasteProvider.performPaste(context)
} finally {
(CopyPasteManager.getInstance() as? CopyPasteManagerEx)?.run { removeContent(origContent) }
}
carets.forEach { (caret, point) ->
val startOffset = point.startOffset
point.dispose()
if (!caret.isValid) return@forEach
val endOffset = if (data.indent) doIndent(editor, caret, context, startOffset, startOffset + text.text.length) else startOffset + text.text.length
VimPlugin.getMark().setChangeMarks(editor, TextRange(startOffset, endOffset))
VimPlugin.getMark().setMark(editor, MarkGroup.MARK_CHANGE_POS, startOffset)
moveCaretToEndPosition(editor, caret, startOffset, endOffset, text.typeInRegister, subMode, data.caretAfterInsertedText)
}
}
private fun setClipboardText(text: String, transferableData: List<TextBlockTransferableData>): TextBlockTransferable {
val mutableTransferableData = transferableData.toMutableList()
val s = TextBlockTransferable.convertLineSeparators(text, "\n", transferableData)
if (mutableTransferableData.none { it is CaretStateTransferableData }) {
mutableTransferableData += CaretStateTransferableData(intArrayOf(0), intArrayOf(s.length))
}
val content = TextBlockTransferable(s, mutableTransferableData, RawText(text))
CopyPasteManager.getInstance().setContents(content)
return content
}
private fun putTextInternal(editor: Editor, caret: Caret, context: DataContext,
text: String, type: SelectionType, mode: CommandState.SubMode,
@@ -368,4 +437,8 @@ class PutGroup {
}
return maxLen
}
companion object {
private val logger = Logger.getInstance(PutGroup::class.java.name)
}
}

@@ -43,6 +43,7 @@ public class Options {
public static final String VIMINFO = "viminfo";
public static final String SELECTMODE = "selectmode";
public static final String KEYMODEL = "keymodel";
public static final String IDEAPUT = "ideaput";
public static final String LOOKUPACTIONS = "lookupactions";
/**
@@ -477,6 +478,7 @@ public class Options {
addOption(new KeywordOption("iskeyword", "isk", new String[]{"@", "48-57", "_"}));
addOption(new BoundListOption(SELECTMODE, "slm", new String[]{"template"}, new String[]{"mouse", "key", "cmd", "template", "refactoring"}));
addOption(new BoundListOption(KEYMODEL, "km", new String[]{"continueselect", "stopselect"}, new String[]{"startsel", "stopsel", "stopselect", "stopvisual", "continueselect", "continuevisual"}));
addOption(new ToggleOption(IDEAPUT, IDEAPUT, true));
addOption(new ListOption(LOOKUPACTIONS, LOOKUPACTIONS, new String[]{"VimLookupUp", "VimLookupDown"}, null));
registerExtensionOptions();

@@ -18,11 +18,21 @@
package com.maddyhome.idea.vim.ui;
import org.jetbrains.annotations.Nullable;
import com.intellij.codeInsight.editorActions.CopyPastePostProcessor;
import com.intellij.codeInsight.editorActions.TextBlockTransferable;
import com.intellij.codeInsight.editorActions.TextBlockTransferableData;
import com.intellij.openapi.editor.RawText;
import kotlin.Pair;
import org.jetbrains.annotations.NotNull;
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* This is a utility class for working with the system clipboard
@@ -33,29 +43,33 @@ public class ClipboardHandler {
*
* @return The clipboard string or null if data isn't plain text
*/
@Nullable
public static String getClipboardText() {
@NotNull
public static Pair<String, List<TextBlockTransferableData>> getClipboardTextAndTransferableData() {
String res = null;
List<TextBlockTransferableData> transferableData = new ArrayList<>();
try {
Clipboard board = Toolkit.getDefaultToolkit().getSystemClipboard();
Transferable trans = board.getContents(null);
Object data = trans.getTransferData(DataFlavor.stringFlavor);
if (data != null) {
res = data.toString();
}
res = data.toString();
transferableData = collectTransferableData(trans);
}
catch (HeadlessException e) {
// ignore
}
catch (UnsupportedFlavorException e) {
// ignore
}
catch (IOException e) {
// ignore
catch (HeadlessException | UnsupportedFlavorException | IOException ignored) {
}
return res;
return new Pair<>(res, transferableData);
}
private static List<TextBlockTransferableData> collectTransferableData(Transferable transferable) {
List<TextBlockTransferableData> allValues = new ArrayList<>();
for (CopyPastePostProcessor<? extends TextBlockTransferableData> processor : CopyPastePostProcessor.EP_NAME.getExtensionList()) {
List<? extends TextBlockTransferableData> data = processor.extractTransferableData(transferable);
if (!data.isEmpty()) {
allValues.addAll(data);
}
}
return allValues;
}
/**
@@ -63,14 +77,14 @@ public class ClipboardHandler {
*
* @param text The text to add to the clipboard
*/
public static void setClipboardText(String text) {
public static void setClipboardText(String text, List<TextBlockTransferableData> transferableData, String rawText) {
try {
final String s = TextBlockTransferable.convertLineSeparators(text, "\n", transferableData);
TextBlockTransferable content = new TextBlockTransferable(s, transferableData, new RawText(rawText));
Clipboard board = Toolkit.getDefaultToolkit().getSystemClipboard();
StringSelection data = new StringSelection(text);
board.setContents(data, null);
board.setContents(content, null);
}
catch (HeadlessException e) {
// ignore
catch (HeadlessException ignored) {
}
}
}

@@ -1652,26 +1652,28 @@ public class MultipleCaretsTest extends VimTestCase {
}
public void testPutTextBeforeCursorLinewiseOverlapRange() {
// Non-ide insert will produce double "<caret>zxcvbn\n"
testPutOverlapLine("q<caret>we<caret>rty\n" + "asdfgh\n" + "<caret>zxcvbn\n",
"<caret>zxcvbn\n" + "<caret>zxcvbn\n" + "qwerty\n" + "asdfgh\n" + "<caret>zxcvbn\n" + "zxcvbn\n",
"<caret>zxcvbn\n" + "qwerty\n" + "asdfgh\n" + "<caret>zxcvbn\n" + "zxcvbn\n",
true);
testPutOverlapLine("qwerty\n" + "a<caret>sd<caret>fgh\n" + "<caret>zxcvbn\n",
"qwerty\n" + "<caret>zxcvbn\n" + "<caret>zxcvbn\n" + "asdfgh\n" + "<caret>zxcvbn\n" + "zxcvbn\n",
"qwerty\n" + "<caret>zxcvbn\n" + "asdfgh\n" + "<caret>zxcvbn\n" + "zxcvbn\n",
true);
testPutOverlapLine("qwerty\n" + "asd<caret>fgh\n" + "<caret>zxcvb<caret>n\n",
"qwerty\n" + "<caret>zxcvbn\n" + "asdfgh\n" + "<caret>zxcvbn\n" + "<caret>zxcvbn\n" + "zxcvbn\n",
"qwerty\n" + "<caret>zxcvbn\n" + "asdfgh\n" + "<caret>zxcvbn\n" + "zxcvbn\n",
true);
}
public void testPutTextAfterCursorLinewiseOverlapRange() {
// Non-ide insert will produce double "<caret>zxcvbn\n"
testPutOverlapLine("q<caret>wert<caret>y\n" + "asdfgh\n" + "<caret>zxcvbn\n",
"qwerty\n" + "<caret>zxcvbn\n" + "<caret>zxcvbn\n" + "asdfgh\n" + "zxcvbn\n" + "<caret>zxcvbn\n",
"qwerty\n" + "<caret>zxcvbn\n" + "asdfgh\n" + "zxcvbn\n" + "<caret>zxcvbn\n",
false);
testPutOverlapLine("qwerty\n" + "as<caret>dfg<caret>h\n" + "<caret>zxcvbn\n",
"qwerty\n" + "asdfgh\n" + "<caret>zxcvbn\n" + "<caret>zxcvbn\n" + "zxcvbn\n" + "<caret>zxcvbn\n",
"qwerty\n" + "asdfgh\n" + "<caret>zxcvbn\n" + "zxcvbn\n" + "<caret>zxcvbn\n",
false);
testPutOverlapLine("qwerty\n" + "asdfg<caret>h\n" + "<caret>zxcv<caret>bn\n",
"qwerty\n" + "asdfgh\n" + "<caret>zxcvbn\n" + "zxcvbn\n" + "<caret>zxcvbn\n" + "<caret>zxcvbn\n",
"qwerty\n" + "asdfgh\n" + "<caret>zxcvbn\n" + "zxcvbn\n" + "<caret>zxcvbn\n",
false);
}

@@ -23,6 +23,7 @@ import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.command.SelectionType
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.helper.StringHelper.parseKeys
import com.maddyhome.idea.vim.helper.VimBehaviourDiffers
import org.jetbrains.plugins.ideavim.VimTestCase
import org.junit.Ignore
@@ -86,6 +87,15 @@ class PutVisualTextMoveCursorActionTest : VimTestCase() {
myFixture.checkResult(newFile)
}
@VimBehaviourDiffers(originalVimAfter = """
A Discovery
ound it in a legendary land
rocks and lavender and tufted grass,
re it was settled on some sodden sand
d by the torrent of a mountain pass.
${c}A Discovery
""")
fun `test put line in block selection`() {
val file = """
${c}A Discovery

@@ -84,20 +84,57 @@ class MultipleCaretsTest : VimTestCase() {
// }
fun testPutText() {
val before = "${c}qwe\n" + "rty\n" + "${c}as${c}d\n" + "fgh\n" + "zxc\n" + "vbn\n"
// This test produces double ${c}zxc on 3rd line if non-idea paste is used
val before = """
${c}qwe
rty
${c}as${c}d
fgh
zxc
vbn
""".trimIndent()
val editor = configureByText(before)
VimPlugin.getRegister().storeText(editor, TextRange(16, 19), SelectionType.CHARACTER_WISE, false)
typeText(commandToKeys("pu"))
val after = "qwe\n" + "${c}zxc\n" + "rty\n" + "asd\n" + "${c}zxc\n" + "${c}zxc\n" + "fgh\n" + "zxc\n" + "vbn\n"
val after = """
qwe
${c}zxc
rty
asd
${c}zxc
fgh
zxc
vbn
""".trimIndent()
myFixture.checkResult(after)
}
fun testPutTextCertainLine() {
val before = "${c}qwe\n" + "rty\n" + "${c}as${c}d\n" + "fgh\n" + "zxc\n" + "vbn\n"
// This test produces triple ${c}zxc if non-idea paste is used
val before = """
${c}qwe
rty
${c}as${c}d
fgh
zxc
vbn
""".trimIndent()
val editor = configureByText(before)
VimPlugin.getRegister().storeText(editor, TextRange(16, 19), SelectionType.CHARACTER_WISE, false)
typeText(commandToKeys("4pu"))
val after = "qwe\n" + "rty\n" + "asd\n" + "fgh\n" + "${c}zxc\n" + "${c}zxc\n" + "${c}zxc\n" + "zxc\n" + "vbn\n"
val after = """
qwe
rty
asd
fgh
${c}zxc
zxc
vbn
""".trimIndent()
myFixture.checkResult(after)
}