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;
This commit is contained in:
Denis Zhdanov
2011-04-18 13:18:22 +04:00
parent 7e2cd43a40
commit 0323b59658
6 changed files with 279 additions and 30 deletions
@@ -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();
@@ -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.
* <p/>
* 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);
}
}
@@ -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.
* <p/>
* The main idea is that we want to be able to combine adjacent text into single unit like
* <a href="http://www.gnu.org/software/emacs/manual/html_node/emacs/Kill-Ring.html#Kill-Ring">emacs kill ring</a> 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.
* <p/>
* 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<Document> myDocument;
private final int myStartOffset;
private final int myEndOffset;
private final boolean myCut;
private volatile boolean myReadyToCombine = true;
/**
* Creates new <code>KillRingTransferable</code> 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>(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 <code>true</code> if current object can be combined with adjacent text; <code>false</code> otherwise.
* Default value is <code>true</code>
*/
public boolean isReadyToCombine() {
return myReadyToCombine;
}
/**
* Allows to define if current object can be combined with adjacent text.
*
* @param readyToCombine <code>true</code> if current object can be combined with adjacent text; <code>false</code> otherwise
*/
public void setReadyToCombine(boolean readyToCombine) {
myReadyToCombine = readyToCombine;
}
@Override
public String toString() {
return "data='" + myData + "', startOffset=" + myStartOffset + ", endOffset=" + myEndOffset +", cut=" + myCut;
}
}
@@ -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; <code>null</code> 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);
}
@@ -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) {
@@ -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));
}
}
}