/** * 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.execute; import static com.mulgasoft.emacsplus.IEmacsPlusCommandDefinitionIds.KBDMACRO_END; import static com.mulgasoft.emacsplus.IEmacsPlusCommandDefinitionIds.KBDMACRO_END_CALL; import static com.mulgasoft.emacsplus.IEmacsPlusCommandDefinitionIds.KBDMACRO_EXECUTE; import static com.mulgasoft.emacsplus.IEmacsPlusCommandDefinitionIds.KEYBOARD_QUIT; import java.io.File; import java.io.FilenameFilter; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.commands.NotHandledException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.jface.bindings.Binding; import org.eclipse.jface.bindings.keys.KeySequence; import org.eclipse.jface.bindings.keys.KeyStroke; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentListener; import org.eclipse.jface.text.ITextViewerExtension; import org.eclipse.jface.text.TextSelection; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.jface.viewers.ISelection; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.VerifyKeyListener; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.widgets.Event; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.commands.ICommandService; import org.eclipse.ui.keys.IBindingService; import org.eclipse.ui.texteditor.AbstractTextEditor; import org.eclipse.ui.texteditor.ITextEditor; import com.mulgasoft.emacsplus.Beeper; import com.mulgasoft.emacsplus.EmacsPlusActivator; import com.mulgasoft.emacsplus.EmacsPlusUtils; import com.mulgasoft.emacsplus.IBeepListener; import com.mulgasoft.emacsplus.IEmacsPlusCommandDefinitionIds; import com.mulgasoft.emacsplus.RingBuffer; import com.mulgasoft.emacsplus.YankRotate; import com.mulgasoft.emacsplus.minibuffer.WithMinibuffer; import com.mulgasoft.emacsplus.preferences.EMPListEditor; import com.mulgasoft.emacsplus.preferences.EmacsPlusPreferenceConstants; /** * A singleton class to hold and support keyboard macros * * @author Mark Feber - initial API and implementation */ public class KbdMacroSupport extends RepeatingSupport { private static KbdMacroSupport instance = null; private static KbdMacro kbdMacro = null; private ISourceViewer viewer = null; private ITextEditor editor = null; private ICommandService ics = null; private boolean isdefining = false; private static WithMinibuffer mini = null; private static final String CTRL_STR = EmacsPlusActivator.getResourceString("KbdMacro_CtrlStr"); //$NON-NLS-1$ private static final String ALT_STR = EmacsPlusActivator.getResourceString("KbdMacro_AltStr"); //$NON-NLS-1$ private static final String CMD_STR = EmacsPlusActivator.getResourceString("KbdMacro_CmdStr"); //$NON-NLS-1$ private static final String SHIFT_STR = EmacsPlusActivator.getResourceString("KbdMacro_ShiftStr"); //$NON-NLS-1$ private static final String EXIT_STR = EmacsPlusActivator.getResourceString("KbdMacro_ExitStr"); //$NON-NLS-1$ private static final String AUTO_LOAD_ERROR = EmacsPlusActivator.getResourceString("EmacsPlusPref_KbdMacroAutoLoadError"); //$NON-NLS-1$ private static final String KBD_DEFINED = "KbdMacro_Defined"; //$NON-NLS-1$ private static final String KBD_DEFINING = "KbdMacro_Defining"; //$NON-NLS-1$ private static final String KBD_ABORTED = "KbdMacro_Aborted"; //$NON-NLS-1$ private static final String KBD_NONE = "KbdMacro_No_Error"; //$NON-NLS-1$ // sub-directory where kbd macros are stored static final String KBD_SUBDIR = "kbdmacros" + File.separator; //$NON-NLS-1$ // avoid displaying messages when loading kbd macros private boolean suppressWhileLoading = false; public enum LoadState { ALL, NONE, SOME; } private static LoadState autoLoadState = LoadState.NONE; static { try { IPreferenceStore store = EmacsPlusActivator.getDefault().getPreferenceStore(); if (store != null) { setLoadState(LoadState.valueOf(store.getString(EmacsPlusPreferenceConstants.P_KBD_MACRO_AUTO_LOAD))); } } catch (Exception e) { // if bad value, then leave state at NONE } } private KbdMacroSupport() {} public static KbdMacroSupport getInstance() { if (instance == null) { instance = new KbdMacroSupport(); } return instance; } /** * Determine if the status messages should be suppressed * (typically during a macro execution as they slow the execution down unnecessarily) * * @return true if we should suppress the message */ public boolean suppressMessages() { return suppressWhileLoading || isExecuting(); } // keep macros in name order for completion minibuffer commands private static TreeMap<String,KbdMacro> namedMacros = new TreeMap<String,KbdMacro>(); public static TreeMap<String,KbdMacro> getCompletionList() { return (TreeMap<String,KbdMacro>)namedMacros; } /** * Interface for kbd macro execution completion listeners * notification occurs on macro completion */ public interface IKbdExecutionListener { void executionDone(); } public boolean isDefining() { return isdefining; } public boolean isExecuting() { return executeCount > 0; } private int executeCount = 0; /** * When defining, add the command binding to the definition * Should only be called from a minibuffer * * @param binding * @return true is kbd macro is executing */ public boolean isExecuting(Binding binding) { // If called with binding while defining, look for extraneous // Key event in macro for removal if (isDefining() && !kbdMacro.isEmpty()) { kbdMacro.checkBinding(binding); } return isExecuting(); } /** * Add a minibuffer exit event directly * Should only be called from a minibuffer */ public void exitWhenDefining() { if (isDefining()) { // flag minibuffer exit kbdMacro.addExit(); } } private void setEditor(ITextEditor editor) { this.editor = editor; } private ITextEditor getEditor() { return this.editor; } private void setViewer(ISourceViewer viewer) { this.viewer= viewer; } private void setRedraw(ISourceViewer view, boolean value) { if (view != null) { try { // some viewers may not have a text widget view.getTextWidget().setRedraw(value); } catch (Exception e) {} } } public void setExecuting(boolean is, ITextEditor editor, VerifyKeyListener vkf) { boolean wasExecuting = isExecuting(); // keep track of nested macro executions if (is) { ++executeCount; } else { --executeCount; } if (!wasExecuting && is) { whileExecuting = vkf; setViewer(findSourceViewer(editor)); if (viewer != null) { setRedraw(viewer,false); if (whileExecuting != null && viewer instanceof ITextViewerExtension) { ((ITextViewerExtension) viewer) .prependVerifyKeyListener(whileExecuting); } } setEditor(editor); } else if (!isExecuting() && viewer != null) { setRedraw(viewer,true); if (whileExecuting != null && viewer instanceof ITextViewerExtension){ ((ITextViewerExtension) viewer).removeVerifyKeyListener(whileExecuting); whileExecuting = null; } setEditor(null); } } /** * Returns the minibuffer, if we're currently executing within one * * @return minibuffer or null */ public static WithMinibuffer getKbdMinibuffer() { return mini; } /** * Set whether we're currently executing within a minibuffer * * @param minibuffer or null */ public static void setKbdMinibuffer(WithMinibuffer minibuffer) { mini = minibuffer; } public boolean hasKbdMacro() { return hasKbdMacro(false); } /** * Are any macros currently defined? * * @param named if true, then check named macros, else current * * @return true if found one */ public boolean hasKbdMacro(boolean named) { return (named ? !namedMacros.isEmpty() : kbdMacro != null && !kbdMacro.isEmpty()); } public boolean hasKbdMacro(String name) { return !namedMacros.isEmpty() && namedMacros.get(name) != null; } public boolean isBusy() { return isDefining() || isExecuting(); } public ArrayList<KbdEvent> getKbdMacroEvents(){ return (kbdMacro != null ? kbdMacro.getKbdMacro() : null); } public ArrayList<KbdEvent> getKbdMacroEvents(String name){ ArrayList<KbdEvent> result = null; KbdMacro namedMacro = namedMacros.get(name); if (namedMacro != null) { result = namedMacro.getKbdMacro(); } return result; } public KbdMacro getKbdMacro(String name){ return ((name == null) ? kbdMacro : namedMacros.get(name)); } /** * Start the definition of a keyboard macro * * @param editor * @param append - if true, append to the current definition */ public void startKbdMacro(ITextEditor editor, boolean append) { if (!isExecuting()) { setEditor(editor); isdefining = true; ics = (ICommandService) editor.getSite().getService(ICommandService.class); // listen for command executions ics.addExecutionListener(this); addDocumentListener(editor); if (!append || kbdMacro == null) { kbdMacro = new KbdMacro(); } setViewer(findSourceViewer(editor)); if (viewer instanceof ITextViewerExtension) { ((ITextViewerExtension) viewer).prependVerifyKeyListener(whileDefining); } else { viewer = null; } // add a listener for ^G Beeper.addBeepListener(KbdMacroBeeper.beeper); currentCommand = null; } } /** * End the definition of a keyboard macro * * @return true if macro was being defined, else false */ public boolean endKbdMacro() { return endKbdMacro(false); } /** * End the definition of a keyboard macro * * @param abort if true, terminate with extreme prejudice * @return true if macro was being defined, else false */ private boolean endKbdMacro(boolean abort) { boolean result = isDefining(); if (result) { if (ics != null) { ics.removeExecutionListener(this); ics = null; } if (viewer != null) { if (viewer instanceof ITextViewerExtension) { ((ITextViewerExtension) viewer) .removeVerifyKeyListener(whileDefining); } viewer = null; } if (abort) { // restore last from ring buffer restoreFromHistory(); } else { addToHistory(kbdMacro); } ITextEditor ed = getEditor(); if (ed != null) { removeDocumentListener(ed); EmacsPlusUtils.showMessage(ed, (abort ? KBD_ABORTED : KBD_DEFINED), abort); } // remove a listener for ^G Beeper.removeBeepListener(KbdMacroBeeper.beeper); setEditor(null); isdefining = false; currentCommand = null; } return result; } /** * When defining/executing a macro, during a minibuffer command, move key listener in front minibuffer's * * @param mini minibuffer being activated * @param editor using the activated minibuffer */ public void continueKbdMacro (WithMinibuffer mini, ITextEditor editor) { if (isDefining()) { prependKeyListener(editor,whileDefining); } if (isExecuting()) { prependKeyListener(editor,whileExecuting); } KbdMacroSupport.setKbdMinibuffer(mini); } /** * When defining/executing a macro, remove/add key listeners on appropriate viewers during part activation * * @param editor being activated */ public void continueKbdMacro(ITextEditor editor) { if (editor != null && (isDefining() || isExecuting())) { addDocumentListener(editor); ISourceViewer newViewer = findSourceViewer(editor); if (isExecuting()) { prependKeyListener(newViewer,whileExecuting); // adjust redraw when executing setRedraw(viewer,true); setEditor(editor); setRedraw(newViewer,false); } if (isDefining()) { prependKeyListener(newViewer,whileDefining); EmacsPlusUtils.showMessage(editor, KBD_DEFINING, false); setEditor(editor); } viewer = newViewer; } } private void prependKeyListener(ITextEditor editor, VerifyKeyListener key) { if (key != null && editor != null) { if (viewer == findSourceViewer(editor)) { ((ITextViewerExtension) viewer).prependVerifyKeyListener(key); } } } private void prependKeyListener(ISourceViewer newViewer, VerifyKeyListener key) { if (key != null && viewer != newViewer) { ((ITextViewerExtension) viewer).removeVerifyKeyListener(key); ((ITextViewerExtension) newViewer).prependVerifyKeyListener(key); } } /** * Save a copy of the current macro under a name for this session only * * @param name */ public String nameKbdMacro(String name) { return nameKbdMacro(name,kbdMacro.copyMacro(name)); } /** * Save macro under the name for this session only * * @param name * @param kbdMacro * @return id */ public String nameKbdMacro(String name, KbdMacro kbdMacro) { String result = null; if (!isDefining() && !isExecuting() && kbdMacro != null) { result = EmacsPlusUtils.kbdMacroId(name); namedMacros.put(name,kbdMacro); } return result; } private Event copyEvent(VerifyEvent event) { Event e = new Event(); e.stateMask = event.stateMask; e.keyCode = event.keyCode; // display.post() wants the key character sans modifier e.character = (event.keyCode != 0 ? (char)event.keyCode : event.character); // e.character = event.character; e.type = SWT.KeyDown; e.doit = true; return e; } private Event matchEvent(char c, int keyCode, int mask) { Event e = new Event(); e.keyCode = keyCode; e.character = c; e.stateMask = mask; e.type = SWT.KeyDown; e.doit = true; return e; } /** * Add a key event to the macro * This is typically either a character for insert * or a sub command key binding for a minibuffer * * @param event */ private void addToMacro(VerifyEvent event) { KbdEvent eKbd = null; if (event.stateMask != 0) { autoFix(event); Event e = copyEvent(event); e.character = (char)event.keyCode; eKbd = new KbdEvent(e); kbdMacro.add(eKbd); } else if (event.keyCode != 27) { // ignore ESC eKbd = new KbdEvent(copyEvent(event)); autoFix(event); kbdMacro.add(eKbd); } } /** * Add a command id to the macro * @param cmdId */ private void addToMacro(String cmdId, Map<?,?> parameters, Event trigger) { kbdMacro.checkTrigger(cmdId, parameters, trigger,false); } public String toString() { return kbdMacro.makeString(false); } public String toBriefString() { return kbdMacro.makeString(true); } // File support private static String getKbdMacroDirectory() { return EmacsPlusActivator.getDefault().getPreferenceStore().getString(EmacsPlusPreferenceConstants.P_KBD_MACRO_DIRECTORY); } private static IPath getKbdMacroPath(String kbdMacroDirectory) { return (kbdMacroDirectory.length() != 0 ? Path.fromOSString(kbdMacroDirectory) : EmacsPlusActivator.getDefault().getStateLocation().append(KBD_SUBDIR)); } /** * Get the map of saved kbd macros from the file system * * @return a SortedMap of <short name, fileName> */ public static SortedMap<String,String> getFileMap() { return getFileMap(getKbdMacroDirectory()); } public static SortedMap<String,String> getFileMap(String directory) { SortedMap<String,String> fileMap = new TreeMap<String,String>(); IPath mpath = getKbdMacroPath(directory); String[] fileNames = null; File dir = mpath.toFile(); if (dir.exists()) { // use the id prefix as the file filter fileNames = dir.list(new FilenameFilter() { public boolean accept(File dir, String name) { boolean result = false; if (name.startsWith(EmacsPlusUtils.KBD_MACRO_ID)) { result = true; } return result; } }); } if (fileNames != null) { int index = EmacsPlusUtils.KBD_MACRO_ID.length()+1; for (String f : fileNames) { fileMap.put(f.substring(index),f); } } return fileMap; } // Auto loading of kbd macros static LoadState getLoadState() { return autoLoadState; } public static void setLoadState(LoadState value) { autoLoadState = value; } public void autoLoadMacros() { String[] names = null; switch (getLoadState()) { case NONE: break; case SOME: String nameStr = EmacsPlusActivator.getDefault().getPreferenceStore().getString(EmacsPlusPreferenceConstants.P_KBD_MACRO_NAME_LOAD); names = EMPListEditor.parseResults(nameStr); break; case ALL: SortedMap<String,String> fileMap = getFileMap(); names = fileMap.keySet().toArray(new String[0]); break; default: break; } if (names != null) { final ArrayList<String> results = new ArrayList<String>(); for (String name : names) { final String n = name; // load each macro in a separate runnable so we don't provoke a timeout if many macros are loaded EmacsPlusUtils.asyncUiRun(new Runnable() { public void run() { suppressWhileLoading = true; String result = null; try { Map<String,String> param = new HashMap<String,String>(); param.put(EmacsPlusUtils.KBDMACRO_NAME_ARG,n); param.put(EmacsPlusUtils.KBDMACRO_FORCE_ARG,Boolean.TRUE.toString()); result = (String)EmacsPlusUtils.executeCommand(IEmacsPlusCommandDefinitionIds.KBDMACRO_LOAD,param,null); if (result != null) { results.add(result); } } catch (Exception e) { // most load errors should be trapped in the kbdmacro_load command, but if something strange happens... result = String.format(AUTO_LOAD_ERROR,n); results.add(result); System.err.println(result); e.printStackTrace(); } finally { suppressWhileLoading = false; } } }); } EmacsPlusUtils.asyncUiRun(new Runnable() { public void run() { if (!results.isEmpty()) { EmacsPlusConsole console = EmacsPlusConsole.getInstance(); console.clear(); console.activate(); for (String r : results) { console.print(r + '\n'); } } } }); } } // All the pretty Listeners private final static class KbdMacroBeeper { private final static IBeepListener beeper = new IBeepListener() { public void beepInterrupt() { Beeper.removeBeepListener(beeper); KbdMacroSupport support = KbdMacroSupport.getInstance(); if (support.isDefining()) { support.endKbdMacro(true); } else if (support.isExecuting()) { } } }; } /** * Interrupt keyboard macro definition or execution */ public static void interruptKbdMacro() { KbdMacroSupport support = KbdMacroSupport.getInstance(); if (support.isExecuting()) { Beeper.beep(); } } // VerifyKeyListeners VerifyKeyListener whileExecuting= null; VerifyKeyListener whileDefining = new VerifyKeyListener() { /** * @see org.eclipse.swt.custom.VerifyKeyListener#verifyKey(org.eclipse.swt.events.VerifyEvent) */ public void verifyKey(VerifyEvent event) { try { if (!event.doit) { return; } if (event.character != 0) { // process typed character // ignore if we're in a nested macro execution if (whileExecuting == null) { charEvent(event); } } else { // some other key down } } catch (Exception e) { e.printStackTrace(); } finally { } } }; private void charEvent(VerifyEvent event) { // ignore characters while in M-x, we'll pick up the command id on execution if (!isInMetaXCommand()) { addToMacro(event); } } private String currentCommand = null; // IExecutionListener methods /** * @see org.eclipse.core.commands.IExecutionListener#notHandled(java.lang.String, org.eclipse.core.commands.NotHandledException) */ public void notHandled(String commandId, NotHandledException exception) { currentCommand = null; popEvent(); KbdMacroBeeper.beeper.beepInterrupt(); } /** * @see org.eclipse.core.commands.IExecutionListener#postExecuteFailure(java.lang.String, org.eclipse.core.commands.ExecutionException) */ public void postExecuteFailure(String commandId, ExecutionException exception) { currentCommand = null; popEvent(); KbdMacroBeeper.beeper.beepInterrupt(); } /** * @see org.eclipse.core.commands.IExecutionListener#postExecuteSuccess(java.lang.String, java.lang.Object) */ public void postExecuteSuccess(String commandId, Object returnValue) { // true, if command was invoked by a binding ExecutionEvent event = popEvent(); Event trigger = getTrigger(event); // save cmdId if it immediately follows an M-x or if it was called from a key binding boolean addCmd = isInMetaXCommand() || trigger != null; if (isInMetaXCommand(commandId)) { ; // ignore. Sets flag as side-effect } else if (KEYBOARD_QUIT.equals(commandId)) { ; // ignore. This can happen during appending to a definition } else if (KBDMACRO_END_CALL.equals(commandId)) { ; // ignore during definition } else if (KBDMACRO_END.equals(commandId)) { // check if called from minibuffer kbdMacro.checkTrigger(commandId, null, trigger,true); } else if (KBDMACRO_EXECUTE.equals(commandId)) { // check if called from minibuffer kbdMacro.checkTrigger(commandId, null, trigger,true); // If you enter `C-x e' while defining a macro, the macro is terminated and executed immediately. endKbdMacro(); } else if (addCmd) { Map<?,?> parameters = event.getParameters(); if (parameters != null && parameters.isEmpty()) { parameters = null; } // Add to queue addToMacro(commandId, parameters, trigger); } currentCommand = null; } // boolean inUniversalArg = false; private void popUniversal() { kbdMacro.popUniversal(); } /** * @see org.eclipse.core.commands.IExecutionListener#preExecute(java.lang.String, org.eclipse.core.commands.ExecutionEvent) */ public void preExecute(String commandId, ExecutionEvent event) { currentCommand = commandId; currentEvent.push(event); // Emacs+ commands handle universal argument directly as a parameter, so remove the command // other Eclipse commands require the universalArgument command to be part of the macro // if (inUniversalArg && commandId.startsWith(EmacsPlusUtils.MULGASOFT)) { Map<?,?> parameters = event.getParameters(); // when a command has a universal arg parameter, remove the preceding universal arg command if (parameters != null && parameters.containsKey(EmacsPlusUtils.UNIVERSAL_ARG)) { popUniversal(); } // } // inUniversalArg = ((IEmacsPlusCommandDefinitionIds.UNIVERSAL_ARGUMENT.equals(commandId)) ? true : false); // // true, if command was invoked by a binding // Event trigger = ((event.getTrigger() != null && event.getTrigger() instanceof Event) ? ((Event)event.getTrigger()) : null); // // save cmdId if it immediately follows an M-x or if it was called from a key binding // boolean addCmd = inMetaXCommand || trigger != null; // if (inUniversalArg && commandId.startsWith(EmacsPlusUtils.MULGASOFT)) { // popUniversal(); // } // inMetaXCommand = false; // inUniversalArg = false; // if (IEmacsPlusCommandDefinitionIds.KBDMACRO_END.equals(commandId)) { // // check if called from minibuffer // kbdMacro.checkTrigger(commandId, null, trigger,true); // } else if (IEmacsPlusCommandDefinitionIds.KBDMACRO_EXECUTE.equals(commandId)) { // // check if called from minibuffer // kbdMacro.checkTrigger(commandId, null, trigger,true); // // If you enter `C-x e' while defining a macro, the macro is terminated and executed immediately. // endKbdMacro(); // } else if (IEmacsPlusCommandDefinitionIds.METAX_EXECUTE.equals(commandId)) { // inMetaXCommand = true; // } else if (IEmacsPlusCommandDefinitionIds.KEYBOARD_QUIT.equals(commandId)) { // ; // ignore. This can happen during appending to a definition // } else if (IEmacsPlusCommandDefinitionIds.KBDMACRO_END_CALL.equals(commandId)) { // ; // ignore during definition // } else if (addCmd) { // if (IEmacsPlusCommandDefinitionIds.UNIVERSAL_ARGUMENT.equals(commandId)) { // inUniversalArg = true; // } // Map<?,?> parameters = event.getParameters(); // if (parameters != null && parameters.isEmpty()) { // parameters = null; // } // // Add to queue // addToMacro(commandId, parameters, trigger); // } } private KbdMacroAutoFix listening = null; private void addDocumentListener(ITextEditor editor) { IDocument document = editor.getDocumentProvider().getDocument(editor.getEditorInput()); if (listening != null) { listening.document.removeDocumentListener(listening); } listening = new KbdMacroAutoFix(document); document.addDocumentListener(listening); } private void removeDocumentListener(ITextEditor editor) { IDocument document = editor.getDocumentProvider().getDocument(editor.getEditorInput()); if (listening != null && document == listening.document) { listening.document.removeDocumentListener(listening); listening = null; } } private void autoFix(VerifyEvent event) { if (listening != null) { listening.nextKey(event); } } /** * Attempt to finesse the Eclipse auto-complete for the simplest cases. * Listen for explicit document changes that are not directly part of the macro * being recorded, and work around the internal state that Eclipse secretly maintains * * @author Mark Feber - initial API and implementation */ private class KbdMacroAutoFix implements IDocumentListener { IDocument document; VerifyEvent ev = null; KbdMacroAutoFix(IDocument document) { this.document = document; } public void nextKey(VerifyEvent event){ ev = event; } /** * Attempt to finesse the Eclipse auto-complete for the simplest cases * - auto-completion of '"' and other identical completions * - auto-insert of '.' or '<text>.' * * @see org.eclipse.jface.text.IDocumentListener#documentAboutToBeChanged(org.eclipse.jface.text.DocumentEvent) */ public void documentAboutToBeChanged(DocumentEvent event) { try { String txt = event.getText(); // not ready for prime time // if (ev != null && txt.length() == 2 && txt.charAt(0) == ev.character && txt.charAt(1) == ev.character) { // // looks like it's an auto match on identical character // KbdEvent e = new KbdEvent(matchEvent((char)ev.keyCode,ev.keyCode,(ev.stateMask & SWT.MODIFIER_MASK))); // e.dontWait = true; // // add it and back up // kbdMacro.add(e); // addToMacro(IEmacsPlusCommandDefinitionIds.BACKWARD_CHAR,null,null); // // } else if (currentCommand == null && txt.length() > 1 && txt.charAt(txt.length()-1) == '.') { // } else { // the java completion engine (and others?) will sometimes eat the . character and auto-complete if (currentCommand == null && txt.length() > 0 && txt.charAt(txt.length()-1) == '.') { if ((ev == null || ev.character != '.')) { ISelection selection = editor.getSelectionProvider().getSelection(); if (selection instanceof TextSelection && (event.getOffset()+event.getLength()) == ((TextSelection)selection).getOffset()) { KbdEvent e = new KbdEvent(matchEvent('.',(int)'.',0)); e.dontWait = true; kbdMacro.add(e); } } } // } } catch (Exception e) {} ev = null; } /** * @see org.eclipse.jface.text.IDocumentListener#documentChanged(org.eclipse.jface.text.DocumentEvent) */ public void documentChanged(DocumentEvent event) { } } // Kbd Macro related class definitions public static class KbdMacro implements Serializable { private static final long serialVersionUID = 5999718072284785054L; private String name = null; private String bindSequence = null; ArrayList<KbdEvent> macro = new ArrayList<KbdEvent>(); public void setBindingKeys(String sequence) { bindSequence = sequence; } public String getBindingKeys() { return bindSequence; } public String getName() { return name; } public void setName(String name) { this.name = name; } public void add(KbdEvent event) { macro.add(event); } public ArrayList<KbdEvent> getKbdMacro() { return macro; } public boolean isEmpty() { return macro.isEmpty(); } private void popUniversal() { int size = macro.size(); for (int i = size -1; i >=0; i--) { String cmdId = macro.get(i).getCmd(); if (cmdId != null) { // the first command must be universal; intervening events, if present, will be number characters if (IEmacsPlusCommandDefinitionIds.UNIVERSAL_ARGUMENT.equals(cmdId)) { for (int j = i; i < size; i++) { macro.remove(j); } } break; } } } /** * Make a shallow copy of this macro * * @return the shallow copy */ KbdMacro copyMacro(String name) { KbdMacro result = new KbdMacro(); result.setName(name); result.macro = new ArrayList<KbdEvent>(); for (KbdEvent e : macro) { result.macro.add(e); } return result; } void addExit() { if (!macro.isEmpty() && !macro.get(macro.size()-1).isExit()) { macro.add(new KbdEvent(true)); } } /* There are two annoying cases related to minibuffers: * 1) A command binding not handled by the minibuffer is detected: * - The minibuffer is exited * - and the command id is executed manually from there * -- handled by checkBinding (called from within the minibuffer code) * * 2) A binding that is not a full command and is not handled by the minibuffer * - the minibuffer is exited * - the event is forwarded on where it may be part of a multi-key binding * - checkTrigger (attempts to) handle this (called locally) * * Some future version of the minibuffer may use the command structure to set up sub commands * which would moot this code */ /** * Check for a full command binding that was executed directly by the minibuffer * Remove the binding entry and add the command id entry * * @param binding */ void checkBinding(Binding binding) { boolean processed = false; int index = macro.size() -1; Event keyevent = macro.get(index).getEvent(); if (keyevent != null) { KeyStroke keyStroke = KeyStroke.getInstance(keyevent.stateMask, (int)Character.toUpperCase((char)keyevent.keyCode)); if (keyStroke.equals(binding.getTriggerSequence().getTriggers()[0])) { // remove (possibly partial) binding macro.remove(index); // flag for minibuffer exit addExit(); // and insert command macro.add(new KbdEvent(binding.getParameterizedCommand().getId())); processed = true; } } if (!processed) { // then it's a command unto itself (e.g. ARROW_RIGHT) // flag for minibuffer exit addExit(); // and insert command macro.add(new KbdEvent(binding.getParameterizedCommand().getId())); processed = true; } } /** * Check for a binding that is not a full command and was not handled by the minibuffer * Remove the partial binding and add the command id entry * * @param cmdId * @param trigger * @param onExit - it is the endMacro command, ignore id entry */ void checkTrigger(String cmdId, Map<?,?> parameters, Event trigger, boolean onExit) { int index = macro.size() -1; if (!macro.isEmpty() && trigger != null && macro.get(index).isSubCmd()) { Event event = macro.get(index).getEvent(); // have trigger and previous entry is a subCmd type KeyStroke key = KeyStroke.getInstance(event.stateMask, Character.toUpperCase(event.keyCode)); IBindingService bindingService = (IBindingService) PlatformUI.getWorkbench().getService(IBindingService.class); Collection<Binding> values = EmacsPlusUtils.getPartialMatches(bindingService,KeySequence.getInstance(key)).values(); // Check partial binding for (Binding v : values) { if (cmdId.equals((v.getParameterizedCommand().getId()))) { // remove (possibly partial) binding macro.remove(index); // flag for minibuffer exit macro.add(new KbdEvent(true)); break; } } } if (!onExit) { macro.add(new KbdEvent(cmdId, parameters)); } } public String toString() { return makeString(false); } public String toBriefString() { return makeString(true); } private String makeString(boolean isBrief) { StringBuilder mac = new StringBuilder(); char sepr = (isBrief ? ',' : '\n'); boolean wasChar = false; boolean isNext = false; for (KbdEvent e : macro) { if (isNext && (!wasChar || (wasChar && (!e.isChar() || e.isSubCmd())))) { mac.append(sepr); } mac.append((isBrief ? e.toBriefString() : e.toString())); wasChar = e.isChar(); isNext = true; } return mac.toString(); } } public static final class KbdEvent implements Serializable { // define serializable event type private class KbdKeyEvent implements Serializable { private static final long serialVersionUID = 6407500805253442702L; public int type; public int keyCode; public int stateMask; public char character; } private static final long serialVersionUID = 1500697848009159804L; KbdKeyEvent event; // serialized key event private transient Event ev; // cached key event String cmdId = null; Map<?,?> params = null; boolean exitMinibuffer = false; boolean dontWait = false; public KbdEvent(Event keyEvent) { if (keyEvent != null) { ev = keyEvent; event = new KbdKeyEvent(); event.type = keyEvent.type; event.keyCode = keyEvent.keyCode; event.stateMask = keyEvent.stateMask; event.character = keyEvent.character; } } public KbdEvent(String cmdId) { this.cmdId = cmdId; } public KbdEvent(String cmdId, Map<?,?> params) { this(cmdId); this.params = params; } public KbdEvent(boolean exit) { this.exitMinibuffer= exit; } public Event getEvent() { Event keyEvent = ev; if (keyEvent == null && event != null) { keyEvent = new Event(); keyEvent.type = event.type; keyEvent.keyCode = event.keyCode; keyEvent.stateMask = event.stateMask; keyEvent.character = event.character; keyEvent.doit = true; ev = keyEvent; } return keyEvent; } public String getCmd() { return cmdId; } public Map<?,?> getCmdParameters() { return params; } public boolean isChar() { return event != null; } public boolean isSubCmd() { return event != null && ((event.stateMask & SWT.MODIFIER_MASK) & ~SWT.SHIFT) != 0; } public boolean isExit() { return exitMinibuffer; } public boolean isWait() { return !dontWait; } public String toBriefString() { String result = null; if (cmdId != null) { result = cmdId; int last = cmdId.lastIndexOf('.'); if (last > -1) { result = cmdId.substring(last+1); } } else { result = toString(); } return result; } public String toString() { String result = null; if (cmdId != null) { result = cmdId; if (params != null) { try { @SuppressWarnings("unchecked") Set<String> keySet = (Set<String>)params.keySet(); for (String key : keySet) { result += ' ' + key + '=' + params.get(key); } } catch (Exception e) {} } } else if (event != null) { if (event.stateMask != 0) { char c = (char)event.keyCode; if ((event.stateMask & SWT.SHIFT) != 0) { char uc = Character.toUpperCase(c); if (c == uc) { result = SHIFT_STR + c; } else { result = Character.toString(uc); } } else { result = Character.toString(c); } result = ((event.stateMask & SWT.CTRL) == 0 ? EmacsPlusUtils.EMPTY_STR : CTRL_STR) + ((event.stateMask & SWT.COMMAND) == 0 ? EmacsPlusUtils.EMPTY_STR : CMD_STR) + ((event.stateMask & SWT.ALT) == 0 ? EmacsPlusUtils.EMPTY_STR : ALT_STR) + result; } else { result = EmacsPlusUtils.normalizeCharacter(event.character); } } else if (isExit()) { result = EXIT_STR; } return result; } } /* Local KbdMacro history ring */ /** * Rotate and make the next macro from the ring the current definition * @param editor */ public void setHistoryNext(ITextEditor editor) { getFromHistory(editor,YankRotate.FORWARD); } /** * Rotate and make the previous macro from the ring the current definition * @param editor */ public void setHistoryPrevious(ITextEditor editor) { getFromHistory(editor,YankRotate.BACKWARD); } private void addToHistory(KbdMacro macro) { MacroRing.ring.putNext(macro); } /** * Rotate make the next/previous macro from the ring the current definition * @param editor * @param dir YankRotate direction */ private void getFromHistory(ITextEditor editor, YankRotate dir) { String result = KBD_NONE; if (!isDefining() && !MacroRing.ring.isEmpty()) { kbdMacro = MacroRing.ring.rotateYankPos(dir).get(); result = kbdMacro.toBriefString(); } EmacsPlusUtils.showMessage(editor, result, false); } /** * Replace current macro with the top of history (or null, if none) */ private void restoreFromHistory() { kbdMacro = (MacroRing.ring.isEmpty() ? null : MacroRing.ring.rotateYankPos(YankRotate.EREWHON).get()); } /* Macro RingBuffer: use lazy initialization holder class idiom */ private static class MacroRing { static final RingBuffer<KbdMacro> ring = new RingBuffer<KbdMacro>(); } // Totally evil code, as Eclipse has no adapter for accessing the viewer // The protected method & private field that gives us the source viewer for registration purposes private static String RE_METHOD_ID = "getSourceViewer"; //$NON-NLS-1$ private static String RE_MEMBER_ID = "fSourceViewer"; //$NON-NLS-1$ private ISourceViewer findSourceViewer(ITextEditor editor) { // evil ISourceViewer result = null; if (editor != null && editor instanceof AbstractTextEditor) { result = (ISourceViewer) EmacsPlusUtils.getAM((AbstractTextEditor) editor, RE_METHOD_ID); if (result == null) { // even more evil result = (ISourceViewer) EmacsPlusUtils.getAF((AbstractTextEditor) editor, RE_MEMBER_ID); } } return result; } }