mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
@@ -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) {
|
||||
|
||||
+8
-7
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user