com.mulgasoft.emacsplus.KillRing.java Source code

Java tutorial

Introduction

Here is the source code for com.mulgasoft.emacsplus.KillRing.java

Source

/**
 * 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;
        }
    }
}