Commit ab427880 authored by Dmitry Batrak's avatar Dmitry Batrak
Browse files

IDEA-125021 Improve multi-caret copy-paste logic

parent 86622599
Showing with 333 additions and 162 deletions
+333 -162
......@@ -37,8 +37,6 @@ import java.util.ArrayList;
import java.util.List;
public class CopyHandler extends EditorActionHandler {
private static final Logger LOG = Logger.getInstance(CopyHandler.class);
private final EditorActionHandler myOriginalAction;
public CopyHandler(final EditorActionHandler originalHandler) {
......@@ -93,7 +91,10 @@ public class CopyHandler extends EditorActionHandler {
transferableDatas.addAll(processor.collectTransferableData(file, editor, startOffsets, endOffsets));
}
String rawText = TextBlockTransferable.convertLineSeparators(selectionModel.getSelectedText(true), "\n", transferableDatas);
String text = editor.getCaretModel().supportsMultipleCarets()
? CopyPasteSupport.getSelectedTextForClipboard(editor, transferableDatas)
: selectionModel.getSelectedText();
String rawText = TextBlockTransferable.convertLineSeparators(text, "\n", transferableDatas);
String escapedText = null;
for (CopyPastePreProcessor processor : Extensions.getExtensions(CopyPastePreProcessor.EP_NAME)) {
escapedText = processor.preprocessOnCopy(file, startOffsets, endOffsets, rawText);
......
......@@ -30,7 +30,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
class TextBlockTransferable implements Transferable {
public class TextBlockTransferable implements Transferable {
private final Collection<TextBlockTransferableData> myExtraData;
private final RawText myRawText;
private final String myText;
......
/*
* Copyright 2000-2009 JetBrains s.r.o.
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
......
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.editor;
import com.intellij.codeInsight.editorActions.TextBlockTransferableData;
import java.awt.datatransfer.DataFlavor;
public class CaretStateTransferableData implements TextBlockTransferableData {
public static final DataFlavor FLAVOR = new DataFlavor(CaretStateTransferableData.class, "Caret state");
public final int[] startOffsets;
public final int[] endOffsets;
public CaretStateTransferableData(int[] startOffsets, int[] endOffsets) {
this.startOffsets = startOffsets;
this.endOffsets = endOffsets;
}
@Override
public DataFlavor getFlavor() {
return FLAVOR;
}
@Override
public int getOffsetCount() {
return startOffsets.length + endOffsets.length;
}
@Override
public int getOffsets(int[] offsets, int index) {
System.arraycopy(startOffsets, 0, offsets, index, startOffsets.length);
System.arraycopy(endOffsets, 0, offsets, index + startOffsets.length, endOffsets.length);
return index + getOffsetCount();
}
@Override
public int setOffsets(int[] offsets, int index) {
System.arraycopy(offsets, index, startOffsets, 0, startOffsets.length);
System.arraycopy(offsets, index + startOffsets.length, endOffsets, 0, endOffsets.length);
return index + getOffsetCount();
}
}
......@@ -15,12 +15,16 @@
*/
package com.intellij.openapi.editor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ClipboardTextPerCaretSplitter {
public List<String> split(String input, int caretCount) {
@NotNull
public List<String> split(@NotNull String input, @Nullable CaretStateTransferableData caretData, int caretCount) {
if (caretCount <= 0) {
throw new IllegalArgumentException("Caret count must be positive");
}
......@@ -28,9 +32,17 @@ public class ClipboardTextPerCaretSplitter {
return Collections.singletonList(input);
}
List<String> result = new ArrayList<String>(caretCount);
String[] lines = input.split("\n", -1);
int sourceCaretCount = caretData == null ? -1 : caretData.startOffsets.length;
String[] lines = sourceCaretCount == 1 || sourceCaretCount == caretCount ? null : input.split("\n", -1);
for (int i = 0; i < caretCount; i++) {
if (lines.length == 0) {
if (sourceCaretCount == 1) {
result.add(input);
}
else if (sourceCaretCount == caretCount) {
//noinspection ConstantConditions
result.add(new String(input.substring(caretData.startOffsets[i], caretData.endOffsets[i])));
}
else if (lines.length == 0) {
result.add("");
}
else if (lines.length == 1) {
......
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.openapi.editor;
import com.intellij.codeInsight.editorActions.TextBlockTransferable;
import com.intellij.codeInsight.editorActions.TextBlockTransferableData;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.LineTokenizer;
import com.intellij.util.Producer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
public class CopyPasteSupport {
private static final Logger LOG = Logger.getInstance(CopyPasteSupport.class);
private CopyPasteSupport() { }
public static void copySelectionToClipboard(@NotNull Editor editor) {
ApplicationManager.getApplication().assertIsDispatchThread();
List<TextBlockTransferableData> extraData = new ArrayList<TextBlockTransferableData>();
String s = editor.getCaretModel().supportsMultipleCarets() ? getSelectedTextForClipboard(editor, extraData)
: editor.getSelectionModel().getSelectedText();
if (s == null) return;
s = TextBlockTransferable.convertLineSeparators(s, "\n", extraData);
Transferable contents = editor.getCaretModel().supportsMultipleCarets() ? new TextBlockTransferable(s, extraData, null) : new StringSelection(s);
CopyPasteManager.getInstance().setContents(contents);
}
public static String getSelectedTextForClipboard(@NotNull Editor editor, @NotNull Collection<TextBlockTransferableData> extraDataCollector) {
final StringBuilder buf = new StringBuilder();
String separator = "";
List<Caret> carets = editor.getCaretModel().getAllCarets();
int[] startOffsets = new int[carets.size()];
int[] endOffsets = new int[carets.size()];
for (int i = 0; i < carets.size(); i++) {
buf.append(separator);
String caretSelectedText = carets.get(i).getSelectedText();
startOffsets[i] = buf.length();
if (caretSelectedText != null) {
buf.append(caretSelectedText);
}
endOffsets[i] = buf.length();
separator = "\n";
}
extraDataCollector.add(new CaretStateTransferableData(startOffsets, endOffsets));
return buf.toString();
}
public static TextRange pasteFromClipboard(Editor editor) {
return pasteTransferable(editor, (Producer<Transferable>)null);
}
public static TextRange pasteTransferable(Editor editor, final Transferable content) {
return pasteTransferable(editor, new Producer<Transferable>() {
@Nullable
@Override
public Transferable produce() {
return content;
}
});
}
@Nullable
public static TextRange pasteTransferable(final Editor editor, @Nullable Producer<Transferable> producer) {
Transferable content = getTransferable(producer);
if (content == null) return null;
String text = getStringContent(content);
if (text == null) return null;
if (editor.getCaretModel().supportsMultipleCarets()) {
int caretCount = editor.getCaretModel().getCaretCount();
if (caretCount == 1 && editor.isColumnMode()) {
int pastedLineCount = LineTokenizer.calcLineCount(text, true);
EditorModificationUtil.deleteSelectedText(editor);
Caret caret = editor.getCaretModel().getPrimaryCaret();
for (int i = 0; i < pastedLineCount - 1; i++) {
caret = caret.clone(false);
if (caret == null) {
break;
}
}
caretCount = editor.getCaretModel().getCaretCount();
}
CaretStateTransferableData caretData = null;
try {
caretData = content.isDataFlavorSupported(CaretStateTransferableData.FLAVOR)
? (CaretStateTransferableData)content.getTransferData(CaretStateTransferableData.FLAVOR) : null;
}
catch (Exception e) {
LOG.error(e);
}
final Iterator<String> segments = new ClipboardTextPerCaretSplitter().split(text, caretData, caretCount).iterator();
editor.getCaretModel().runForEachCaret(new CaretAction() {
@Override
public void perform(Caret caret) {
EditorModificationUtil.insertStringAtCaret(editor, segments.next(), false, true);
}
});
return null;
}
else {
int caretOffset = editor.getCaretModel().getOffset();
EditorModificationUtil.insertStringAtCaret(editor, text, false, true);
return new TextRange(caretOffset, caretOffset + text.length());
}
}
public static void pasteTransferableAsBlock(Editor editor, @Nullable Producer<Transferable> producer) {
Transferable content = getTransferable(producer);
if (content == null) return;
String text = getStringContent(content);
if (text == null) return;
int caretLine = editor.getCaretModel().getLogicalPosition().line;
int originalCaretLine = caretLine;
int selectedLinesCount = 0;
final SelectionModel selectionModel = editor.getSelectionModel();
if (selectionModel.hasBlockSelection()) {
final LogicalPosition start = selectionModel.getBlockStart();
final LogicalPosition end = selectionModel.getBlockEnd();
assert start != null;
assert end != null;
LogicalPosition caret = new LogicalPosition(Math.min(start.line, end.line), Math.min(start.column, end.column));
selectedLinesCount = Math.abs(end.line - start.line);
caretLine = caret.line;
EditorModificationUtil.deleteSelectedText(editor);
editor.getCaretModel().moveToLogicalPosition(caret);
}
LogicalPosition caretToRestore = editor.getCaretModel().getLogicalPosition();
String[] lines = LineTokenizer.tokenize(text.toCharArray(), false);
if (lines.length > 1 || selectedLinesCount == 0) {
int longestLineLength = 0;
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
longestLineLength = Math.max(longestLineLength, line.length());
editor.getCaretModel().moveToLogicalPosition(new LogicalPosition(caretLine + i, caretToRestore.column));
EditorModificationUtil.insertStringAtCaret(editor, line, false, true);
}
caretToRestore = new LogicalPosition(originalCaretLine, caretToRestore.column + longestLineLength);
}
else {
for (int i = 0; i <= selectedLinesCount; i++) {
editor.getCaretModel().moveToLogicalPosition(new LogicalPosition(caretLine + i, caretToRestore.column));
EditorModificationUtil.insertStringAtCaret(editor, text, false, true);
}
caretToRestore = new LogicalPosition(originalCaretLine, caretToRestore.column + text.length());
}
editor.getCaretModel().moveToLogicalPosition(caretToRestore);
EditorModificationUtil.zeroWidthBlockSelectionAtCaretColumn(editor, caretLine, caretLine + selectedLinesCount);
}
@Nullable
private static String getStringContent(@NotNull Transferable content) {
RawText raw = RawText.fromTransferable(content);
if (raw != null) return raw.rawText;
try {
return (String)content.getTransferData(DataFlavor.stringFlavor);
}
catch (UnsupportedFlavorException ignore) { }
catch (IOException ignore) { }
return null;
}
private static Transferable getTransferable(Producer<Transferable> producer) {
Transferable content = null;
if (producer != null) {
content = producer.produce();
}
else {
CopyPasteManager manager = CopyPasteManager.getInstance();
if (manager.areDataFlavorsAvailable(DataFlavor.stringFlavor)) {
content = manager.getContents();
}
}
return content;
}
}
......@@ -19,20 +19,14 @@ import com.intellij.codeStyle.CodeStyleFacade;
import com.intellij.openapi.editor.actionSystem.EditorActionManager;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.MockDocumentEvent;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.LineTokenizer;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.util.Producer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
public class EditorModificationUtil {
......@@ -99,7 +93,7 @@ public class EditorModificationUtil {
zeroWidthBlockSelectionAtCaretColumn(editor, startLine, endLine);
}
private static void zeroWidthBlockSelectionAtCaretColumn(final Editor editor, final int startLine, final int endLine) {
public static void zeroWidthBlockSelectionAtCaretColumn(final Editor editor, final int startLine, final int endLine) {
int caretColumn = editor.getCaretModel().getLogicalPosition().column;
editor.getSelectionModel().setBlockSelection(new LogicalPosition(startLine, caretColumn), new LogicalPosition(endLine, caretColumn));
}
......@@ -181,112 +175,21 @@ public class EditorModificationUtil {
return offset;
}
/**
* @deprecated Use {@link com.intellij.openapi.editor.CopyPasteSupport#pasteTransferable(Editor, com.intellij.util.Producer)} instead.
* (to remove in IDEA 15)
*/
@Nullable
public static TextRange pasteTransferable(final Editor editor, @Nullable Producer<Transferable> producer) {
String text = getStringContent(producer);
if (text == null) return null;
if (editor.getCaretModel().supportsMultipleCarets()) {
int caretCount = editor.getCaretModel().getCaretCount();
if (caretCount == 1 && editor.isColumnMode()) {
int pastedLineCount = LineTokenizer.calcLineCount(text, true);
deleteSelectedText(editor);
Caret caret = editor.getCaretModel().getPrimaryCaret();
for (int i = 0; i < pastedLineCount - 1; i++) {
caret = caret.clone(false);
if (caret == null) {
break;
}
}
caretCount = editor.getCaretModel().getCaretCount();
}
final Iterator<String> segments = new ClipboardTextPerCaretSplitter().split(text, caretCount).iterator();
editor.getCaretModel().runForEachCaret(new CaretAction() {
@Override
public void perform(Caret caret) {
insertStringAtCaret(editor, segments.next(), false, true);
}
});
return null;
}
else {
int caretOffset = editor.getCaretModel().getOffset();
insertStringAtCaret(editor, text, false, true);
return new TextRange(caretOffset, caretOffset + text.length());
}
return CopyPasteSupport.pasteTransferable(editor, producer);
}
/**
* @deprecated Use {@link com.intellij.openapi.editor.CopyPasteSupport#pasteTransferableAsBlock(Editor, com.intellij.util.Producer)} instead.
* (to remove in IDEA 15)
*/
public static void pasteTransferableAsBlock(Editor editor, @Nullable Producer<Transferable> producer) {
String text = getStringContent(producer);
if (text == null) return;
int caretLine = editor.getCaretModel().getLogicalPosition().line;
int originalCaretLine = caretLine;
int selectedLinesCount = 0;
final SelectionModel selectionModel = editor.getSelectionModel();
if (selectionModel.hasBlockSelection()) {
final LogicalPosition start = selectionModel.getBlockStart();
final LogicalPosition end = selectionModel.getBlockEnd();
assert start != null;
assert end != null;
LogicalPosition caret = new LogicalPosition(Math.min(start.line, end.line), Math.min(start.column, end.column));
selectedLinesCount = Math.abs(end.line - start.line);
caretLine = caret.line;
deleteSelectedText(editor);
editor.getCaretModel().moveToLogicalPosition(caret);
}
LogicalPosition caretToRestore = editor.getCaretModel().getLogicalPosition();
String[] lines = LineTokenizer.tokenize(text.toCharArray(), false);
if (lines.length > 1 || selectedLinesCount == 0) {
int longestLineLength = 0;
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
longestLineLength = Math.max(longestLineLength, line.length());
editor.getCaretModel().moveToLogicalPosition(new LogicalPosition(caretLine + i, caretToRestore.column));
insertStringAtCaret(editor, line, false, true);
}
caretToRestore = new LogicalPosition(originalCaretLine, caretToRestore.column + longestLineLength);
}
else {
for (int i = 0; i <= selectedLinesCount; i++) {
editor.getCaretModel().moveToLogicalPosition(new LogicalPosition(caretLine + i, caretToRestore.column));
insertStringAtCaret(editor, text, false, true);
}
caretToRestore = new LogicalPosition(originalCaretLine, caretToRestore.column + text.length());
}
editor.getCaretModel().moveToLogicalPosition(caretToRestore);
zeroWidthBlockSelectionAtCaretColumn(editor, caretLine, caretLine + selectedLinesCount);
}
@Nullable
private static String getStringContent(@Nullable Producer<Transferable> producer) {
Transferable content = null;
if (producer != null) {
content = producer.produce();
}
else {
CopyPasteManager manager = CopyPasteManager.getInstance();
if (manager.areDataFlavorsAvailable(DataFlavor.stringFlavor)) {
content = manager.getContents();
}
}
if (content == null) return null;
RawText raw = RawText.fromTransferable(content);
if (raw != null) return raw.rawText;
try {
return (String)content.getTransferData(DataFlavor.stringFlavor);
}
catch (UnsupportedFlavorException ignore) { }
catch (IOException ignore) { }
return null;
CopyPasteSupport.pasteTransferableAsBlock(editor, producer);
}
/**
......@@ -494,40 +397,4 @@ public class EditorModificationUtil {
CaretModel caretModel = editor.getCaretModel();
caretModel.moveToOffset(caretModel.getOffset() + caretShift);
}
/** @deprecated use {@link #pasteTransferable(Editor, Producer)} (to remove in IDEA 14) */
@SuppressWarnings("UnusedDeclaration")
public static TextRange pasteFromClipboard(Editor editor) {
return pasteTransferable(editor, null);
}
/** @deprecated use {@link #pasteTransferable(Editor, Producer)} (to remove in IDEA 14) */
@SuppressWarnings("SpellCheckingInspection,UnusedDeclaration")
public static TextRange pasteFromTransferrable(final Transferable content, Editor editor) {
return pasteTransferable(editor, new Producer<Transferable>() {
@Nullable
@Override
public Transferable produce() {
return content;
}
});
}
@SuppressWarnings("UnusedDeclaration")
/** @deprecated use {@link #pasteTransferableAsBlock(Editor, Producer)} (to remove in IDEA 14) */
public static void pasteFromClipboardAsBlock(Editor editor) {
pasteTransferableAsBlock(editor, (Producer<Transferable>)null);
}
@SuppressWarnings("UnusedDeclaration")
/** @deprecated use {@link #pasteTransferableAsBlock(Editor, Producer)} (to remove in IDEA 14) */
public static void pasteTransferableAsBlock(Editor editor, @Nullable final Transferable content) {
pasteTransferableAsBlock(editor, new Producer<Transferable>() {
@Nullable
@Override
public Transferable produce() {
return content;
}
});
}
}
......@@ -36,16 +36,13 @@ import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.ex.PrioritizedDocumentListener;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import gnu.trove.TIntArrayList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.datatransfer.StringSelection;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
......@@ -570,13 +567,7 @@ public class SelectionModelImpl implements SelectionModel, PrioritizedDocumentLi
@Override
public void copySelectionToClipboard() {
validateContext(true);
String s = getSelectedText(true);
if (s == null) return;
s = StringUtil.convertLineSeparators(s);
StringSelection contents = new StringSelection(s);
CopyPasteManager.getInstance().setContents(contents);
CopyPasteSupport.copySelectionToClipboard(myEditor);
}
@Override
......
......@@ -245,6 +245,38 @@ public class EditorMultiCaretTest extends AbstractEditorTest {
"seven<caret>");
}
public void testCopyMultilineFromOneCaretPasteIntoTwo() throws Exception {
init("<selection>one\n" +
"two<caret></selection>\n" +
"three\n" +
"four",
TestFileType.TEXT);
executeAction("EditorCopy");
executeAction("EditorTextStart");
executeAction("EditorCloneCaretBelow");
executeAction("EditorPaste");
checkResultByText("one\n" +
"two<caret>one\n" +
"one\n" +
"two<caret>two\n" +
"three\n" +
"four");
}
public void testCopyPasteDoesNothingWithUnevenSelection() throws Exception {
init("<selection>one\n" +
"two<caret></selection>\n" +
"<selection>three<caret></selection>\n" +
"four",
TestFileType.TEXT);
executeAction("EditorCopy");
executeAction("EditorPaste");
checkResultByText("one\n" +
"two<caret>\n" +
"three<caret>\n" +
"four");
}
public void testEscapeAfterDragDown() throws Exception {
init("line1\n" +
"line2",
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment