From 0323b59658adfbdd61a1619cc73c1ca6378071f6 Mon Sep 17 00:00:00 2001 From: Denis Zhdanov Date: Mon, 18 Apr 2011 13:00:16 +0400 Subject: [PATCH] IDEA-18764 Emacs-Mode: Ctrl-y only pastes the last line killed and not the entire kill buffer 1. Added basic infrastructure for for kill rings; 2. Added kill rings support for 'cut to line end' action; --- .../editorActions/PasteHandler.java | 4 +- .../openapi/ide/CopyPasteManager.java | 19 ++- .../openapi/ide/KillRingTransferable.java | 134 +++++++++++++++++ .../com/intellij/ide/CopyPasteManagerEx.java | 135 +++++++++++++++--- .../openapi/command/impl/UndoManagerImpl.java | 2 + .../editor/actions/CutLineEndAction.java | 15 +- 6 files changed, 279 insertions(+), 30 deletions(-) create mode 100644 platform/platform-api/src/com/intellij/openapi/ide/KillRingTransferable.java 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 WeakReference myDocument; + private final int myStartOffset; + private final int myEndOffset; + private final boolean myCut; + + private volatile boolean myReadyToCombine = true; + + /** + * Creates new KillRingTransferable 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 WeakReference(document); + myStartOffset = startOffset; + myEndOffset = endOffset; + myCut = cut; + } + + @Override + public DataFlavor[] getTransferDataFlavors() { + return DATA_FLAVORS; + } + + @Override + public boolean isDataFlavorSupported(DataFlavor flavor) { + return flavor == DataFlavor.stringFlavor; + } + + @Nullable + @Override + public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { + return isDataFlavorSupported(flavor) ? myData : null; + } + + @Nullable + public Document getDocument() { + return myDocument.get(); + } + + public int getStartOffset() { + return myStartOffset; + } + + /** + * @return offset of the target text end on the moment of the current object construction + */ + public int getEndOffset() { + return myEndOffset; + } + + public boolean isCut() { + return myCut; + } + + /** + * @return true 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)); } } }