/**
* Copyright (c) 2009, 2010 Mark Feber, MulgaSoft
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
*/
package com.mulgasoft.emacsplus;
import static com.mulgasoft.emacsplus.EmacsPlusUtils.EMPTY_STR;
import static com.mulgasoft.emacsplus.EmacsPlusUtils.getPreferenceStore;
import static com.mulgasoft.emacsplus.IEmacsPlusCommandDefinitionIds.YANK;
import static com.mulgasoft.emacsplus.preferences.PrefVars.DELETE_SEXP_TO_CLIPBOARD;
import static com.mulgasoft.emacsplus.preferences.PrefVars.DELETE_WORD_TO_CLIPBOARD;
import static com.mulgasoft.emacsplus.preferences.PrefVars.KILL_RING_MAX;
import static com.mulgasoft.emacsplus.preferences.PrefVars.REPLACE_TEXT_TO_KILLRING;
import java.util.Hashtable;
import java.util.Map;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.texteditor.ITextEditor;
import com.mulgasoft.emacsplus.preferences.PrefVars;
/**
* Kill Ring Buffer implementation - Singleton
*
* @author Mark Feber - initial API and implementation
*/
public class KillRing extends RingBuffer<String> implements IDocumentListener {
// Singleton
private volatile static KillRing ring = null;
// the append state of the kill command
private boolean append = false;
// the override the append state of the kill command
private boolean forceAppend = false;
// the direction of kill
private boolean reverse = false;
// override the direction of kill
private boolean forceReverse = false;
// store replaced selection on kill ring when true
private boolean selectionReplace = false;
// mark current kill command
private String killCmd = null;
// store the clipboard text from the last activation
private String lastActivationText = null;
// constrain clipboard copy to these commands
private Map<String,String> clipCommands = new Hashtable<String,String>();
// on forceAppend, back-copy the appended string to the clipboard
private Map<String,String> forceClipCommands = new Hashtable<String,String>();
{
forceClipCommands.put(ActionFactory.COPY.getId(), ActionFactory.COPY.getId());
forceClipCommands.put(IEmacsPlusCommandDefinitionIds.EMP_COPY, IEmacsPlusCommandDefinitionIds.EMP_COPY);
forceClipCommands.put(IEmacsPlusCommandDefinitionIds.COPY_QUALIFIED_NAME, IEmacsPlusCommandDefinitionIds.COPY_QUALIFIED_NAME);
}
// flag kill-ring deactivation (s/b temporary)
private boolean deactivate = false;
public static KillRing getInstance() {
if (ring == null) {
initialize();
}
return ring;
}
private KillRing(int size) {
super(size);
}
/**
* Initialize the kill ring with settings from the preference store
*/
private static synchronized void initialize() {
if (ring == null) {
Boolean clipword = false;
Boolean clipsexp = true;
int ringsize = LARGE_RING_SIZE;
IPreferenceStore store = EmacsPlusActivator.getDefault().getPreferenceStore();
if (store != null) {
ringsize = store.getInt(KILL_RING_MAX.getPref());
clipword = store.getBoolean(DELETE_WORD_TO_CLIPBOARD.getPref());
clipsexp = store.getBoolean(DELETE_SEXP_TO_CLIPBOARD.getPref());
}
ring = new KillRing(ringsize);
ring.setClipFeature(DELETE_WORD_TO_CLIPBOARD, clipword);
ring.setClipFeature(DELETE_SEXP_TO_CLIPBOARD, clipsexp);
getPreferenceStore().addPropertyChangeListener(
new IPropertyChangeListener() {
public void propertyChange(PropertyChangeEvent event) {
String prop = event.getProperty();
if (REPLACE_TEXT_TO_KILLRING.getPref().equals(prop)) {
KillRing.getInstance().setSelectionReplace((Boolean)event.getNewValue());
} else if (DELETE_WORD_TO_CLIPBOARD.getPref().equals(prop)) {
KillRing.getInstance().setClipFeature(DELETE_WORD_TO_CLIPBOARD, (Boolean)event.getNewValue());
} else if (DELETE_SEXP_TO_CLIPBOARD.getPref().equals(prop)) {
KillRing.getInstance().setClipFeature(DELETE_SEXP_TO_CLIPBOARD, (Boolean)event.getNewValue());
} else if (KILL_RING_MAX.getPref().equals(prop)) {
KillRing.getInstance().setSize((Integer)event.getNewValue());
}
}
}
);
}
}
// is this a reverse kill command?
protected boolean isReverse() {
return reverse || forceReverse;
}
// note the direction of the kill command
protected void setReverse(boolean reverse) {
this.reverse = reverse;
}
// override the direction of the kill command
protected void setForceReverse(boolean reverse) {
this.forceReverse = reverse;
}
// are we in the append state?
protected boolean isAppend() {
return append || isForceAppend();
}
private boolean isForceAppend() {
return forceAppend;
}
// note the append state of the kill command
protected void setAppend(boolean append) {
this.append = append;
}
// force the append state of the kill command
public void setForceAppend(boolean append) {
forceAppend = append;
}
public boolean isDeactivated() {
return deactivate;
}
public void setDeactivated(boolean deactivate) {
this.deactivate = deactivate;
}
public void setSelectionReplace(boolean selectionReplace) {
this.selectionReplace = selectionReplace;
}
public boolean isSetSelectionReplace() {
return selectionReplace;
}
public synchronized IRingBufferElement<String> putNext(String text, int offset) {
KillRingBufferElement result = null;
if (!isDeactivated() && text != null && text.length() > 0) {
// if appending, check if text is where the last kill left off
if (isAppend() && !isEmpty() && ((result = getElement()) != null)
&& (isForceAppend() || (offset != NO_POS && (result.getOffset() == (isReverse() ? offset + text.length() : offset))))) {
if (isReverse()) {
result.set(text + result.get());
result.setOffset(offset);
} else {
result.set(result.get() + text);
if (isForceAppend()) {
result.setOffset(offset);
}
}
} else {
// if element doesn't yet exist will call overridden getNewElement()
result = (KillRingBufferElement) super.putNext(text);
result.setOffset(offset);
}
if (isClipCommand(killCmd)) {
setClipboardContents();
}
setAppend(true);
setForceAppend(false);
setReverse(false);
}
return result;
}
public void setKill(String cmdId, boolean reverse) {
setKillCmd(cmdId);
setForceReverse(reverse);
}
// note the current command kill command
protected void setKillCmd(String currentCmd) {
this.killCmd = currentCmd;
}
protected String getKillCmd() {
return this.killCmd;
}
/**
* Enable which commands copy their kill contents to the clipboard as well as the kill ring
* This expands the feature available in the default eclipse implementation.
*
* @param feature
* @param value
*/
public void setClipFeature(PrefVars feature, Boolean value){
switch(feature) {
case DELETE_WORD_TO_CLIPBOARD:
if (value){
addClipCommand(IEmacsPlusCommandDefinitionIds.DELETE_NEXT_WORD);
addClipCommand(IEmacsPlusCommandDefinitionIds.DELETE_PREVIOUS_WORD);
}else {
removeClipCommand(IEmacsPlusCommandDefinitionIds.DELETE_NEXT_WORD);
removeClipCommand(IEmacsPlusCommandDefinitionIds.DELETE_PREVIOUS_WORD);
}
break;
case DELETE_SEXP_TO_CLIPBOARD:
if (value){
addClipCommand(IEmacsPlusCommandDefinitionIds.KILL_FORWARD_SEXP);
addClipCommand(IEmacsPlusCommandDefinitionIds.KILL_BACKWARD_SEXP);
} else {
removeClipCommand(IEmacsPlusCommandDefinitionIds.KILL_FORWARD_SEXP);
removeClipCommand(IEmacsPlusCommandDefinitionIds.KILL_BACKWARD_SEXP);
}
break;
default:
break;
}
}
public void addClipCommand(String clipCommand) {
clipCommands.put(clipCommand,clipCommand);
}
public void removeClipCommand(String clipCommand) {
clipCommands.remove(clipCommand);
}
private boolean isClipCommand(String command){
boolean result = false;
if (command != null) {
result = clipCommands.containsKey(command);
if (!result && forceAppend) {
result = forceClipCommands.containsKey(command);
}
}
return result;
}
/**
* Propagate the killed text to the clipboard
*/
private void setClipboardContents(){
setClipboardText(getElement().get());
}
/**
* On a COPY command, retrieve the text from the clipboard and add to kill ring
*/
void addClipboardContents(){
String clipText = getClipboardText();
if (clipText != null && clipText.length() > 0) {
putNext(clipText, NO_POS);
documentChanged(null); // clear any flags
}
}
/**
* Store clipboard text if it has changed on activation.
* This method is used by listeners exclusively.
*/
void checkClipboard() {
documentChanged(null);
String clipText = getClipboardText();
if (clipText != null && !isWhitespace(clipText)){
if (!clipText.equals(lastActivationText) && !clipText.equals(get(getYankpos()))) {
// is it the same as the current text?
String t;
if ((t = get(getPos())) != null) {
// clipboard text may be left over from an append-next-kill, so this covers
// either end as well
if (t.startsWith(clipText) || t.endsWith(clipText)) {
return;
}
}
lastActivationText = clipText;
putNext(clipText,NO_POS);
}
}
}
/**
* Get the text from the system clipboard
*
* @return the system clipboard content as a String
*/
public String getClipboardText() {
Clipboard clipboard = new Clipboard(Display.getCurrent());
TextTransfer plainTextTransfer = TextTransfer.getInstance();
String cliptxt = (String)clipboard.getContents(plainTextTransfer, DND.CLIPBOARD);
clipboard.dispose();
return cliptxt;
}
public void setClipboardText(String text){
Clipboard clipboard = new Clipboard(Display.getCurrent());
TextTransfer plainTextTransfer = TextTransfer.getInstance();
clipboard.setContents(new Object[] {text},new Transfer[]{plainTextTransfer});
clipboard.dispose();
}
private boolean isWhitespace(String text) {
byte[] bytes = text.getBytes();
for (int i=0; i < bytes.length; i++) {
if (bytes[i] > ' ') {
return false;
}
}
return true;
}
/* Listener behavior */
/**
* @see org.eclipse.jface.text.IDocumentListener#documentAboutToBeChanged(org.eclipse.jface.text.DocumentEvent)
*/
public void documentAboutToBeChanged(DocumentEvent event) {
// add the text to the kill ring
if (killCmd != null || isSelectionReplace(event)) {
try {
putNext((event.getDocument().get(event.getOffset(), event.getLength())), event.getOffset());
} catch (BadLocationException e) {}
}
}
/**
* Determine if a selection is being replaced by non-emacs+ behavior (or YANK), and save the
* replaced content in the kill ring. This captures the Eclipse (but not emacs) behavior where
* typing/pasting into a selection replaces the old with the new, so it is appropriate to save
* the old text to the kill ring.
*
* @param event the DocumentEvent containing the IDocument, offset, and length
* @return true if the non-zero length region matches the current selection in the editor
*/
private boolean isSelectionReplace(DocumentEvent event) {
int len = event.getLength();
// ignore plain insertion or any emacs+ (except YANK) command invocation
if (selectionReplace && len > 0 && shouldSave()) {
ITextEditor editor = EmacsPlusUtils.getCurrentEditor();
// otherwise, if we can get the selection, see if it matches the replace region
if (editor != null && editor.getDocumentProvider().getDocument(editor.getEditorInput()) == event.getDocument()) {
ISelection isel = editor.getSelectionProvider().getSelection();
if (isel instanceof ITextSelection) {
ITextSelection selection = (ITextSelection)isel;
boolean result = selection.getOffset() == event.getOffset() && selection.getLength() == len;
return result;
}
}
}
return false;
}
private boolean shouldSave() {
String cmd = MarkUtils.getCurrentCommand();
return (cmd == null || cmd.equals(YANK));
}
/**
* @see org.eclipse.jface.text.IDocumentListener#documentChanged(org.eclipse.jface.text.DocumentEvent)
*/
public void documentChanged(DocumentEvent event) {
// clear flags when non-kill command detected
if (killCmd == null) {
setAppend(false);
setForceAppend(false);
setReverse(false);
}
setYanked(false);
}
/* Add offset information to the kill ring element */
/**
* @see com.mulgasoft.emacsplus.RingBuffer#getElement()
*/
@Override
protected KillRingBufferElement getElement() {
return (KillRingBufferElement)super.getElement();
}
/**
* @see com.mulgasoft.emacsplus.RingBuffer#getNewElement()
*/
@Override
protected IRingBufferElement<String> getNewElement() {
return new KillRingBufferElement();
}
/**
* Add offset information to the kill ring element
* Holds the text and the document offset of the initial character
*
* @author Mark Feber - initial API and implementation
*/
protected class KillRingBufferElement extends AbstractRingBufferElement {
public KillRingBufferElement() {
set(EMPTY_STR);
}
private int offset = -1;
private int getOffset() {
return offset;
}
private void setOffset(int offset) {
this.offset = offset;
}
}
}