diff --git a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/PasteHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/PasteHandler.java index cd8ffbbad615..5a731a18a6f7 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/PasteHandler.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/PasteHandler.java @@ -106,7 +106,8 @@ public class PasteHandler extends EditorActionHandler { final Project project, final PsiFile file, final Document document) { - Transferable content = CopyPasteManager.getInstance().getContents(); + CopyPasteManager copyPasteManager = CopyPasteManager.getInstance(); + Transferable content = copyPasteManager.getContents(); if (content != null) { String text = null; try { @@ -116,6 +117,7 @@ public class PasteHandler extends EditorActionHandler { editor.getComponent().getToolkit().beep(); } if (text == null) return; + copyPasteManager.stopKillRings(); final CodeInsightSettings settings = CodeInsightSettings.getInstance(); diff --git a/platform/platform-api/src/com/intellij/openapi/ide/CopyPasteManager.java b/platform/platform-api/src/com/intellij/openapi/ide/CopyPasteManager.java index f618f6158767..37e6ab8ee867 100644 --- a/platform/platform-api/src/com/intellij/openapi/ide/CopyPasteManager.java +++ b/platform/platform-api/src/com/intellij/openapi/ide/CopyPasteManager.java @@ -17,6 +17,8 @@ package com.intellij.openapi.ide; import com.intellij.openapi.Disposable; import com.intellij.openapi.components.ServiceManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.awt.*; import java.awt.datatransfer.Transferable; @@ -39,11 +41,22 @@ public abstract class CopyPasteManager { public abstract Transferable[] getAllContents(); - public abstract void setContents(Transferable content); + public abstract void setContents(@NotNull Transferable content); - public abstract boolean isCutElement(Object element); + public abstract boolean isCutElement(@Nullable Object element); + /** + * We support 'kill rings' at the editor, i.e. every time when subsequent adjacent regions of text are copied they are + * combined into single compound region. Every non-adjacent change makes existing regions unable to combine. + *
+ * However, there are situations when all 'kill rings' should be stopped manually (e.g. on undo). Hence, we need + * a handle to ask for that. This method works like such a handle. + * + * @see KillRingTransferable + */ + public abstract void stopKillRings(); + public interface ContentChangedListener extends EventListener { - void contentChanged(final Transferable oldTransferable, final Transferable newTransferable); + void contentChanged(@Nullable final Transferable oldTransferable, final Transferable newTransferable); } } \ No newline at end of file diff --git a/platform/platform-api/src/com/intellij/openapi/ide/KillRingTransferable.java b/platform/platform-api/src/com/intellij/openapi/ide/KillRingTransferable.java new file mode 100644 index 000000000000..bdf80997af65 --- /dev/null +++ b/platform/platform-api/src/com/intellij/openapi/ide/KillRingTransferable.java @@ -0,0 +1,134 @@ +/* + * Copyright 2000-2011 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.ide; + +import com.intellij.openapi.editor.Document; +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.lang.ref.WeakReference; + +/** + * This class represents usual {@link StringSelection transferable string} with additional meta-information that describes the place + * of the document that from there it was retrieved. + * + * The main idea is that we want to be able to combine adjacent text into single unit like + * emacs kill ring does. + * E.g. if the user invokes 'cut to the line end' subsequently we may want to paste all of them, hence, we need to be able + * to distinguish if particular copy-paste strings are adjacent. + * + * Thread-safe. + * + * @author Denis Zhdanov + * @since 4/15/11 6:29 PM + */ +public class KillRingTransferable implements Transferable { + + private static final DataFlavor[] DATA_FLAVORS = {DataFlavor.stringFlavor}; + + private final String myData; + private final WeakReferenceKillRingTransferable object.
+ *
+ * @param data target text to transfer
+ * @param document document that contained given text
+ * @param startOffset start offset of the given text at the given document
+ * @param endOffset end offset of the given text during current object construction
+ * @param cut flag that identifies whether target text was cut or copied from the document
+ */
+ public KillRingTransferable(@NotNull String data,
+ @NotNull Document document,
+ int startOffset,
+ int endOffset,
+ boolean cut)
+ {
+ myData = data;
+ myDocument = new WeakReferencetrue if current object can be combined with adjacent text; false otherwise.
+ * Default value is true
+ */
+ public boolean isReadyToCombine() {
+ return myReadyToCombine;
+ }
+
+ /**
+ * Allows to define if current object can be combined with adjacent text.
+ *
+ * @param readyToCombine true if current object can be combined with adjacent text; false otherwise
+ */
+ public void setReadyToCombine(boolean readyToCombine) {
+ myReadyToCombine = readyToCombine;
+ }
+
+ @Override
+ public String toString() {
+ return "data='" + myData + "', startOffset=" + myStartOffset + ", endOffset=" + myEndOffset +", cut=" + myCut;
+ }
+}
diff --git a/platform/platform-impl/src/com/intellij/ide/CopyPasteManagerEx.java b/platform/platform-impl/src/com/intellij/ide/CopyPasteManagerEx.java
index 3c5d422ce0fd..3b26925409c2 100644
--- a/platform/platform-impl/src/com/intellij/ide/CopyPasteManagerEx.java
+++ b/platform/platform-impl/src/com/intellij/ide/CopyPasteManagerEx.java
@@ -17,11 +17,15 @@ package com.intellij.ide;
import com.intellij.ide.ui.UISettings;
import com.intellij.openapi.Disposable;
+import com.intellij.openapi.editor.Document;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.ide.CutElementMarker;
+import com.intellij.openapi.ide.KillRingTransferable;
import com.intellij.openapi.util.Comparing;
import com.intellij.util.EventDispatcher;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import java.awt.datatransfer.*;
import java.io.IOException;
@@ -49,7 +53,7 @@ public class CopyPasteManagerEx extends CopyPasteManager implements ClipboardOwn
fireContentChanged(null);
}
- void fireContentChanged(final Transferable oldTransferable) {
+ void fireContentChanged(@Nullable final Transferable oldTransferable) {
myDispatcher.getMulticaster().contentChanged(oldTransferable, getContents());
}
@@ -65,11 +69,11 @@ public class CopyPasteManagerEx extends CopyPasteManager implements ClipboardOwn
myDispatcher.removeListener(listener);
}
- public void setContents(Transferable content) {
+ public void setContents(@NotNull final Transferable content) {
Transferable old = getContents();
- addNewContentToStack(content);
+ Transferable contentToUse = addNewContentToStack(content);
- setSystemClipboardContent(content);
+ setSystemClipboardContent(contentToUse);
fireContentChanged(old);
}
@@ -78,42 +82,135 @@ public class CopyPasteManagerEx extends CopyPasteManager implements ClipboardOwn
return myClipboardSynchronizer.isDataFlavorAvailable(dataFlavor);
}
- public boolean isCutElement(final Object element) {
+ public boolean isCutElement(@Nullable final Object element) {
for(CutElementMarker marker: Extensions.getExtensions(CutElementMarker.EP_NAME)) {
if (marker.isCutElement(element)) return true;
}
return false;
}
+ @Override
+ public void stopKillRings() {
+ for (Transferable data : myDatas) {
+ if (data instanceof KillRingTransferable) {
+ ((KillRingTransferable)data).setReadyToCombine(false);
+ }
+ }
+ }
+
void setSystemClipboardContent(final Transferable content) {
myClipboardSynchronizer.setContent(content, this);
}
- private void addNewContentToStack(Transferable content) {
+ /**
+ * Stores given content within the current manager. It is merged with already stored ones
+ * if necessary (see {@link KillRingTransferable}).
+ *
+ * @param content content to store
+ * @return content that is either the given one or the one that was assembled from it and already stored one
+ */
+ @NotNull
+ private Transferable addNewContentToStack(@NotNull Transferable content) {
try {
String clipString = getStringContent(content);
- if (clipString != null) {
- Transferable same = null;
- for (Transferable old : myDatas) {
- if (clipString.equals(getStringContent(old))) {
- same = old;
- break;
+ if (clipString == null) {
+ return content;
+ }
+
+ if (content instanceof KillRingTransferable) {
+ KillRingTransferable killRingContent = (KillRingTransferable)content;
+ if (killRingContent.isReadyToCombine() && !myDatas.isEmpty()) {
+ Transferable prev = myDatas.get(0);
+ if (prev instanceof KillRingTransferable) {
+ Transferable merged = merge(killRingContent, (KillRingTransferable)prev);
+ if (merged != null) {
+ myDatas.set(0, merged);
+ return merged;
+ }
}
}
+ if (killRingContent.isReadyToCombine()) {
+ addToTheTopOfTheStack(killRingContent);
+ return killRingContent;
+ }
+ }
+
+ Transferable same = null;
+ for (Transferable old : myDatas) {
+ if (clipString.equals(getStringContent(old))) {
+ same = old;
+ break;
+ }
+ }
- if (same == null) {
- myDatas.add(0, content);
- deleteAfterAllowedMaximum();
- }
- else {
- moveContentTopStackTop(same);
- }
+ if (same == null) {
+ addToTheTopOfTheStack(content);
+ }
+ else {
+ moveContentTopStackTop(same);
}
} catch (UnsupportedFlavorException e) {
} catch (IOException e) {
}
+ return content;
}
+ private void addToTheTopOfTheStack(@NotNull Transferable content) {
+ myDatas.add(0, content);
+ deleteAfterAllowedMaximum();
+ }
+
+ /**
+ * Merges given new data with the given old one and returns merge result in case of success.
+ *
+ * @param newData new data to merge
+ * @param oldData old data to merge
+ * @return merge result of the given data if possible; null otherwise
+ * @throws IOException as defined by {@link Transferable#getTransferData(DataFlavor)}
+ * @throws UnsupportedFlavorException as defined by {@link Transferable#getTransferData(DataFlavor)}
+ */
+ @Nullable
+ private static Transferable merge(@NotNull KillRingTransferable newData, @NotNull KillRingTransferable oldData)
+ throws IOException, UnsupportedFlavorException
+ {
+ if (!oldData.isReadyToCombine() || !newData.isReadyToCombine()) {
+ return null;
+ }
+
+ Document document = newData.getDocument();
+ if (document == null || document != oldData.getDocument()) {
+ return null;
+ }
+
+ Object newDataText = newData.getTransferData(DataFlavor.stringFlavor);
+ Object oldDataText = oldData.getTransferData(DataFlavor.stringFlavor);
+ if (newDataText == null || oldDataText == null) {
+ return null;
+ }
+
+ if (oldData.isCut()) {
+ if (newData.getStartOffset() == oldData.getStartOffset()) {
+ return new KillRingTransferable(
+ oldDataText.toString() + newDataText, document, oldData.getStartOffset(), newData.getEndOffset(), newData.isCut()
+ );
+ }
+ }
+
+ if (newData.getStartOffset() == oldData.getEndOffset()) {
+ return new KillRingTransferable(
+ oldDataText.toString() + newDataText, document, oldData.getStartOffset(), newData.getEndOffset(), false
+ );
+ }
+
+ if (newData.getEndOffset() == oldData.getStartOffset()) {
+ return new KillRingTransferable(
+ newDataText.toString() + oldDataText, document, newData.getStartOffset(), oldData.getEndOffset(), false
+ );
+ }
+
+ return null;
+ }
+
private static String getStringContent(Transferable content) throws UnsupportedFlavorException, IOException {
return (String) content.getTransferData(DataFlavor.stringFlavor);
}
diff --git a/platform/platform-impl/src/com/intellij/openapi/command/impl/UndoManagerImpl.java b/platform/platform-impl/src/com/intellij/openapi/command/impl/UndoManagerImpl.java
index 1ec7ae1ea7c7..2e7284b1de70 100644
--- a/platform/platform-impl/src/com/intellij/openapi/command/impl/UndoManagerImpl.java
+++ b/platform/platform-impl/src/com/intellij/openapi/command/impl/UndoManagerImpl.java
@@ -31,6 +31,7 @@ import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileEditor.*;
import com.intellij.openapi.fileEditor.impl.text.TextEditorProvider;
+import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ex.ProjectEx;
import com.intellij.openapi.startup.StartupManager;
@@ -341,6 +342,7 @@ public class UndoManagerImpl extends UndoManager implements ProjectComponent, Ap
if (myProject != null) {
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
}
+ CopyPasteManager.getInstance().stopKillRings();
myMerger.undoOrRedo(editor, isUndo);
}
catch (RuntimeException ex) {
diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/actions/CutLineEndAction.java b/platform/platform-impl/src/com/intellij/openapi/editor/actions/CutLineEndAction.java
index 96a96401710e..a0c1d135b6b1 100644
--- a/platform/platform-impl/src/com/intellij/openapi/editor/actions/CutLineEndAction.java
+++ b/platform/platform-impl/src/com/intellij/openapi/editor/actions/CutLineEndAction.java
@@ -24,6 +24,7 @@
*/
package com.intellij.openapi.editor.actions;
+import com.intellij.openapi.ide.KillRingTransferable;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
@@ -32,8 +33,6 @@ import com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.util.text.StringUtil;
-import java.awt.datatransfer.StringSelection;
-
public class CutLineEndAction extends EditorAction {
public CutLineEndAction() {
super(new Handler(true));
@@ -54,7 +53,10 @@ public class CutLineEndAction extends EditorAction {
int lineEndOffset = doc.getLineEndOffset(lineNumber);
if (caretOffset >= lineEndOffset) {
- DeleteLineAction.deleteLineAtCaret(editor);
+ if (myCopyToClipboard) {
+ copyToClipboard(doc, lineEndOffset, lineEndOffset + 1);
+ }
+ doc.deleteString(lineEndOffset, lineEndOffset + 1);
return;
}
@@ -71,12 +73,11 @@ public class CutLineEndAction extends EditorAction {
}
}
- private static void copyToClipboard(final Document doc, int caretOffset, int lineEndOffset) {
- String s = doc.getCharsSequence().subSequence(caretOffset, lineEndOffset).toString();
+ private static void copyToClipboard(final Document doc, int startOffset, int endOffset) {
+ String s = doc.getCharsSequence().subSequence(startOffset, endOffset).toString();
s = StringUtil.convertLineSeparators(s);
- StringSelection contents = new StringSelection(s);
- CopyPasteManager.getInstance().setContents(contents);
+ CopyPasteManager.getInstance().setContents(new KillRingTransferable(s, doc, startOffset, startOffset, true));
}
}
}