/** * Copyright (c) 2009, 2014 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 java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import org.eclipse.core.commands.Command; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.commands.NotEnabledException; import org.eclipse.core.commands.NotHandledException; import org.eclipse.core.commands.ParameterizedCommand; import org.eclipse.core.commands.common.CommandException; import org.eclipse.core.commands.common.NotDefinedException; import org.eclipse.jface.action.IStatusLineManager; import org.eclipse.jface.bindings.Binding; import org.eclipse.jface.bindings.BindingManager; import org.eclipse.jface.bindings.TriggerSequence; import org.eclipse.jface.bindings.keys.KeySequence; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.BadPositionCategoryException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentExtension4; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.TypedPosition; import org.eclipse.jface.util.Util; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Event; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IEditorReference; import org.eclipse.ui.IViewPart; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.commands.ICommandService; import org.eclipse.ui.handlers.IHandlerService; import org.eclipse.ui.internal.WorkbenchPage; import org.eclipse.ui.internal.keys.BindingService; import org.eclipse.ui.keys.IBindingService; import org.eclipse.ui.part.MultiEditor; import org.eclipse.ui.part.MultiPageEditorPart; import org.eclipse.ui.texteditor.ITextEditor; import com.mulgasoft.emacsplus.execute.KbdMacroSupport; import com.mulgasoft.emacsplus.preferences.EmacsPlusPreferenceConstants; // Getting an arbitrary widget: // Control focus= page.getWorkbenchWindow().getShell().getDisplay().getFocusControl(); /** * Assorted utility methods * * @author Mark Feber - initial API and implementation */ @SuppressWarnings("restriction") // for cast to internal class org.eclipse.ui.internal.WorkbenchPage public class EmacsPlusUtils { private static final String N_GEN = "\\c"; //$NON-NLS-1$ private static final String N_NEW = "\\n"; //$NON-NLS-1$ private static final String N_RET = "\\r"; //$NON-NLS-1$ private static final String N_TAB = "\\t"; //$NON-NLS-1$ private static final String N_BS = "\\b"; //$NON-NLS-1$ private static final String N_FF = "\\f"; //$NON-NLS-1$ // The assumption is that the position categories will be part of this document category private static final String DOC_CAT = "__content_types_category"; //$NON-NLS-1$ // In Java at least, these point to collapsible regions in the doc as well as other things public static final String DOC_FNS = "__dflt_position_category"; //$NON-NLS-1$ // java specific comment // public static String JAVA_DOC = org.eclipse.jdt.ui.text.IJavaPartitions.JAVA_DOC; // use javadoc string directly to avoid having a dependency of jdt.ui public static final String JAVA_DOC = "javadoc"; //$NON-NLS-1$ public static final String[] JAVADOC_POS= {JAVA_DOC}; // The assumption is that generic comment position categories will include 'comment' in their name public static final String[] COMMENT_POS= {"comment",JAVA_DOC}; //$NON-NLS-1$ // The assumption is that string position categories will include 'string' in their name public static final String[] STRING_POS= {"string"}; //$NON-NLS-1$ // collect them all for efficiency public static final String[] ALL_POS= {"comment",JAVA_DOC,"string"}; //$NON-NLS-1$ //$NON-NLS-2$ // used for sanity checks on commands (invocation, etc.) public static final String MULGASOFT = "com.mulgasoft"; //$NON-NLS-1$ // Emacs+ plugin bundles symbolic names public static final String EMP_STR = "com.mulgasoft.emacsplus"; //$NON-NLS-1$ public static final String EMP_OPT_STR = "com.mulgasoft.emacsplus.optional"; //$NON-NLS-1$ public static final String EMP_MACCMD_STR = "com.mulgasoft.emacsplus.maccmd"; //$NON-NLS-1$ public static final String EMP_MACCMD_OPT_STR = "com.mulgasoft.emacsplus.maccmd.optional"; //$NON-NLS-1$ public static final String EMP_SCHEMEID = "com.mulgasoft.emacsplusConfiguration"; //$NON-NLS-1$ // for defining and executing keyboard macros public static final String KBD_MACRO_ID = "emacsplus.keyboard.macro"; //$NON-NLS-1$ private static final String KBD_INTERRUPT = EmacsPlusActivator.getResourceString("KbdMacro_Interrupted"); //$NON-NLS-1$ public static final String EMPTY_STR = ""; //$NON-NLS-1$ /** No-op offset value */ public static final int NO_OFFSET = -1; /** the universal-argument name used by Emacs+ */ public static final String UNIVERSAL_ARG = "universalArg"; //$NON-NLS-1$ /** the load kbd macro name argument used by Emacs+ */ public static final String KBDMACRO_NAME_ARG = "Name"; //$NON-NLS-1$ /** force the kbd macro load without any questions */ public static final String KBDMACRO_FORCE_ARG = "Force"; //$NON-NLS-1$ // the getBindingManager method is first exposed in Galileo private static final String BM_METHOD_ID = "getBindingManager"; //$NON-NLS-1$ private static final String BM_MEMBER_ID = "bindingManager"; //$NON-NLS-1$ private static final class MacCheck { static final boolean isMac = Util.isMac(); } // control whether to disable pre-edit of Non_Spacing_Marks: i.e. characters intended to be // combined with another character without taking up extra space (e.g. accents, umlauts, etc.) private static boolean disableOptionIMEPreferenece = getPreferenceBoolean(EmacsPlusPreferenceConstants.P_DISABLE_INLINE_EDIT); private EmacsPlusUtils() {} // no children/instances allowed public static boolean getPreferenceBoolean(String key) { return getPreferenceStore().getBoolean(key); } public static String getPreferenceString(String key) { return getPreferenceStore().getString(key); } public static IPreferenceStore getPreferenceStore() { return EmacsPlusActivator.getDefault().getPreferenceStore(); } public static boolean isMac() { return MacCheck.isMac; } public static void setOptionIMEPreferenece(boolean optionIMEPreferenece) { disableOptionIMEPreferenece = optionIMEPreferenece; } public static boolean isDisableOptionIMEPreferenece() { return disableOptionIMEPreferenece; } public static String getTypeCategory(IDocument doc){ return getTypeCategory(doc, DOC_CAT); } public static String getTypeCategory(IDocument doc, String cat){ String result = null; String[] cats = doc.getPositionCategories(); for (String icat : cats) { if (icat.contains(cat)) { result = icat; break; } } return result; } /** * Get all positions of the given position category * In sexp's these positions are typically skipped * * @param doc * @param pos_names * @return the positions to exclude */ public static List<Position> getExclusions(IDocument doc, String[] pos_names) { List<Position> excludePositions = new LinkedList<Position>(); String cat = getTypeCategory(doc); if (cat != null) { try { Position[] xpos = doc.getPositions(cat); for (int j = 0; j < xpos.length; j++) { if (xpos[j] instanceof TypedPosition) { for (int jj = 0; jj < pos_names.length; jj++) { if (((TypedPosition) xpos[j]).getType().contains(pos_names[jj])) { excludePositions.add(xpos[j]); } } } } } catch (BadPositionCategoryException e) {} } return excludePositions; } /** * Determine if the offset is in one of the Positions in the Position list * * @param doc * @param positions * @param offset * @return true if in a Position */ public static Position inPosition(IDocument doc, List<Position> positions, int offset) { for (Position p : positions) { if (p.includes(offset)){ return p; } } return null; } /** * Greedy comment determinator: Since multiple comments can be adjacent, return the one furthest from the * from the offset * * @param doc - what's up * @param positions - the list of semantic comment Positions * @param offset - the point to examine * @param reverse - the direction we're headed * @return - the furthest comment Position */ public static Position inCommentPosition(IDocument doc,List<Position> positions, int offset, boolean reverse) { // Comment positions include the <CR>, so when there are two line comments // one after another at line begin, we have to spot both comments to disambiguate ListIterator<Position> iter = positions.listIterator((reverse ? positions.size() : 0)); Position pos; Position pos1 = null; while (reverse ? iter.hasPrevious() : iter.hasNext() ){ pos = (reverse ? iter.previous() : iter.next()); if (pos1 != null) { // check if adjacent comments if (reverse ? pos1.includes(pos.getOffset() + pos.getLength()) : pos.includes(pos1.getOffset() + pos1.getLength())){ pos1 = pos; } break; } if (pos.includes(offset)) { // offset is just before comment if (pos.getOffset() == offset){ return null; } else { pos1 = pos; } } } return pos1; } public static IDocument getThisDocument(ITextEditor editor) { IDocument doc = null; if (editor != null) { doc = editor.getDocumentProvider().getDocument(editor.getEditorInput()); } return doc; } /** * Extract the text editor from the editor. * For multi part editors, try dereferencing via a getAdapter call * * @param editor * @return the ITextEditor or null */ public static ITextEditor getActiveTextEditor(IEditorPart editor) { ITextEditor result = null; if (editor != null) { if (editor instanceof ITextEditor) { result = (ITextEditor) editor; } else { // this retrieves the currently active editor from MultiPageEditors & etc. result = (ITextEditor) editor.getAdapter(ITextEditor.class); // dig further if other (older?) type if (result == null && (editor instanceof MultiEditor)) { IEditorPart epart = ((MultiEditor) editor).getActiveEditor(); // trust but verify if (epart != editor) { result = getActiveTextEditor(epart); } } } } return result; } /** * If we're a multi-page editor, try to extract the text editor * * @param editor the selected editor * @param activate force the text editor to be the active page * @return a text editor or null */ public static ITextEditor getTextEditor(IEditorPart editor, boolean activate) { ITextEditor result = null; if (editor != null) { if (editor instanceof ITextEditor) { result = (ITextEditor) editor; } else { // this should retrieve the currently active editor from MultiPageEditors & etc. result = (ITextEditor) editor.getAdapter(ITextEditor.class); // dig further if multi type if (result == null) { if (editor instanceof MultiEditor) { // this code is ancient - not sure if there are any plain MultiEditors anymore IEditorPart epart = ((MultiEditor) editor).getActiveEditor(); // potentially recurse if (epart != editor) { result = getTextEditor(epart, activate); } } else if (editor instanceof MultiPageEditorPart) { MultiPageEditorPart med = (MultiPageEditorPart)editor; IEditorPart[] eds = med.findEditors(med.getEditorInput()); for (IEditorPart ep : eds) { if (ep instanceof ITextEditor) { result = (ITextEditor)ep; if (activate) { med.setActiveEditor(result); IWorkbenchPage wpage = getWorkbenchPage(); wpage.bringToTop(result); wpage.activate(result); } break; } } } } } } return result; } // sorted here means by activation order (e4), or reverse activation order (pre-e4) public static IEditorReference[] getSortedEditors(IWorkbenchPage page) { IEditorReference[] result = null; if (page != null && page instanceof WorkbenchPage) { result = ((WorkbenchPage) page).getSortedEditors(); } return result; } public static final String getEol(IDocument document) { String eol = "\n"; //$NON-NLS-1$ try { if (document instanceof IDocumentExtension4) { eol = ((IDocumentExtension4)document).getDefaultLineDelimiter(); } else { eol = document.getLineDelimiter(0); } } catch (BadLocationException e) { } return eol; } /** * Invoke the specified command using the handler service * * @param commandId * @param event * @param editor * @throws ExecutionException * @throws NotDefinedException * @throws NotEnabledException * @throws NotHandledException * @throws CommandException */ public static Object executeCommand(String commandId, Event event) throws ExecutionException, NotDefinedException, NotEnabledException, NotHandledException, CommandException { return executeCommand(commandId,event,(IHandlerService) PlatformUI.getWorkbench().getService(IHandlerService.class)); } /** * Invoke the specified command using the handler service from the editor site * * @param commandId * @param event * @param editor * @throws ExecutionException * @throws NotDefinedException * @throws NotEnabledException * @throws NotHandledException * @throws CommandException */ public static Object executeCommand(String commandId, Event event, IWorkbenchPart editor) throws ExecutionException, NotDefinedException, NotEnabledException, NotHandledException, CommandException { return executeCommand(commandId,event,(IHandlerService) editor.getSite().getService(IHandlerService.class)); } private static Object executeCommand(String commandId, Event event, IHandlerService service) throws ExecutionException, NotDefinedException, NotEnabledException, NotHandledException, CommandException { Object result = null; if (service != null) { try { MarkUtils.setIgnoreDispatchId(true); result = service.executeCommand(commandId, event); } finally { MarkUtils.setIgnoreDispatchId(false); } } return result; } /** * Invoke the specified parameterized command using the handler service * * @param commandId * @param parameters * @param event * @param editor * @return the result of the execution * @throws ExecutionException * @throws NotDefinedException * @throws NotEnabledException * @throws NotHandledException * @throws CommandException */ public static Object executeCommand(String commandId, Map<String,?> parameters, Event event) throws ExecutionException, NotDefinedException, NotEnabledException, NotHandledException, CommandException { return executeCommand(commandId, parameters, event, (ICommandService) PlatformUI.getWorkbench().getService(ICommandService.class), (IHandlerService) PlatformUI.getWorkbench().getService(IHandlerService.class)); } public static Object executeCommand(String commandId, Integer count, Event event, IWorkbenchPart editor) throws ExecutionException, NotDefinedException, NotEnabledException, NotHandledException, CommandException { Map<String, Integer> parameters = new HashMap<String, Integer>(); parameters.put(EmacsPlusUtils.UNIVERSAL_ARG, count); return (editor == null) ? executeCommand(commandId, parameters, event) : executeCommand(commandId, parameters, event, editor); } /** * Invoke the specified parameterized command using the handler service from the editor site * * @param commandId * @param parameters * @param event * @param editor * @return the result of the execution * @throws ExecutionException * @throws NotDefinedException * @throws NotEnabledException * @throws NotHandledException * @throws CommandException */ public static Object executeCommand(String commandId, Map<String,?> parameters, Event event, IWorkbenchPart editor) throws ExecutionException, NotDefinedException, NotEnabledException, NotHandledException, CommandException { return executeCommand(commandId,parameters,event, (ICommandService) editor.getSite().getService(ICommandService.class), (IHandlerService) editor.getSite().getService(IHandlerService.class)); } private static Object executeCommand(String commandId, Map<String,?> parameters, Event event, ICommandService ics, IHandlerService ihs) throws ExecutionException, NotDefinedException, NotEnabledException, NotHandledException, CommandException { Object result = null; if (ics != null && ihs != null) { Command command = ics.getCommand(commandId); if (command != null) { try { MarkUtils.setIgnoreDispatchId(true); ParameterizedCommand pcommand = ParameterizedCommand.generateCommand(command, parameters); if (pcommand != null) { result = ihs.executeCommand(pcommand, event); } } finally { MarkUtils.setIgnoreDispatchId(false); } } } return result; } /** * Returns the status line manager of the workbench part. * * @param part an IEditorPart or IViewPart * * @return the status line manager of the part */ public synchronized static IStatusLineManager getStatusLineManager(IWorkbenchPart part){ IStatusLineManager result = null; if (part instanceof IEditorPart) { return ((IEditorPart)part).getEditorSite().getActionBars().getStatusLineManager(); } else if (part instanceof IViewPart) { return ((IViewPart)part).getViewSite().getActionBars().getStatusLineManager(); } return result; } public static void forceStatusUpdate(IWorkbenchPart editor) { forceUpdate(EmacsPlusUtils.getStatusLineManager(editor)); } // maintain silence when executing keyboard macros to increase execution speed public static void showMessage(IWorkbenchPart editor, String key, boolean error){ if (error || !KbdMacroSupport.getInstance().suppressMessages()) { if (error) { if (key != null && key.length() > 0 && !isMacroMessage(key)) { beep(); } setError(editor,key); } else { setError(editor,EMPTY_STR); setMessage(editor,key); } } } public static void clearMessage(IWorkbenchPart editor){ if (!KbdMacroSupport.getInstance().suppressMessages()) { setError(editor,EMPTY_STR); setMessage(editor,EMPTY_STR); } } private static void setMessage(IWorkbenchPart editor, String key){ if (!KbdMacroSupport.getInstance().suppressMessages()) { IStatusLineManager ism = getStatusLineManager(editor); if (ism != null) { ism.setMessage(EmacsPlusActivator.getResourceString(key)); forceUpdate(ism); } } } private static void setError(IWorkbenchPart editor, String key){ IStatusLineManager ism = getStatusLineManager(editor); if (ism != null) { ism.setErrorMessage(EmacsPlusActivator.getResourceString(key)); forceUpdate(ism); } } private static void forceUpdate(IStatusLineManager ism) { ism.update(true); } /** * Causes the <code>run()</code> method of the runnable to * be invoked by the user-interface thread at the next * reasonable opportunity. * * @param runner - a Runnable object */ public static final void asyncUiRun(Runnable runner) { PlatformUI.getWorkbench().getDisplay().asyncExec(runner); } public static void beep() { Beeper.beep(); } public static IWorkbenchPage getWorkbenchPage() { IWorkbenchPage result = null; IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); if (window != null) { result = window.getActivePage(); } return result; } public static ITextEditor getCurrentEditor() { ITextEditor result = null; IWorkbenchPage page = getWorkbenchPage(); if (page != null) { IEditorPart activeEditor = page.getActiveEditor(); result = getActiveTextEditor(activeEditor); } return result; } /** * Return the context free set of key bindings * * @return A map of trigger (<code>TriggerSequence</code>) to bindings ( * <code>Collection</code> containing <code>Binding</code>). * or an empty map */ public static Map<TriggerSequence,Collection<Binding>> getTotalBindings() { Map<TriggerSequence,Collection<Binding>> result = Collections.emptyMap(); BindingService bs = getBindingService(); if (bs != null) { @SuppressWarnings("unchecked") // @see org.eclipse.jface.bindings.BindingManager#getActiveBindingsDisregardingContext() Map<TriggerSequence,Collection<Binding>> bindings = getBindingManager(bs).getActiveBindingsDisregardingContext(); result = bindings; } return result; } public static BindingService getBindingService() { BindingService result = null; IBindingService bindingService = (IBindingService) PlatformUI.getWorkbench().getService(IBindingService.class); if (bindingService instanceof BindingService) { result = (BindingService)bindingService; } return result; } // the getBindingManager method is first exposed in Galileo public static BindingManager getBindingManager(BindingService bs) { BindingManager result = null; if (bs != null) { try { // check for a public version of the method Method method = bs.getClass().getMethod(BM_METHOD_ID, (Class[])null); result = (BindingManager)method.invoke(bs, (Object[])null); } catch (Exception e) { // evil, but backward compatible result = (BindingManager) EmacsPlusUtils.getAM(bs, BM_METHOD_ID); if (result == null) { // even more evil result = (BindingManager) EmacsPlusUtils.getAF(bs, BM_MEMBER_ID); } } } return result; } @SuppressWarnings("unchecked") public static Map<TriggerSequence,Binding> getPartialMatches(IBindingService ibs, KeySequence ks) { // juno e4 code was hosed, so temporarily use a different path to the results we need // return getBindingManager(((ibs instanceof BindingService) ? (BindingService)ibs : getBindingService())).getPartialMatches(ks); // original version is supposed to be fixed in Kepler+ - not sure I trust it return ibs.getPartialMatches(ks); } public static String normalizeString(String message, int count) { String result = message; if (message != null && message.length() > 0){ StringBuilder originalBuf = new StringBuilder(message); StringBuilder normalizedBuf = new StringBuilder(message.length()); int mLen = (count > 0 ? Math.min(count, message.length()) : message.length()); for (int i=0; i < mLen; i++) { String charac; int ocp = originalBuf.codePointAt(i); if (ocp < ' ') { switch (ocp) { case SWT.CR: charac = N_RET; break; case SWT.LF: charac = N_NEW; break; case '\t': charac = N_TAB; break; case '\f': charac = N_FF; break; case '\b': charac = N_BS; break; default: charac = N_GEN + ocp; } } else { charac = String.valueOf(originalBuf.charAt(i)); } normalizedBuf.append(charac); } result = normalizedBuf.toString(); } return result; } public static boolean isMacroId(String id) { return id != null && id.startsWith(KBD_MACRO_ID); } public static boolean isMacroMessage(String message) { return message != null && message.startsWith(KBD_INTERRUPT); } public static String kbdMacroId(String name) { return KBD_MACRO_ID + '.' + name; } public static String normalizeCharacter(int keyCode) { String result = null; if (keyCode < ' ') { switch (keyCode) { case SWT.CR: result = N_RET; break; case SWT.LF: result = N_NEW; break; case '\t': result = N_TAB; break; case '\f': result = N_FF; break; case '\b': result = N_BS; break; default: result = N_GEN + keyCode; } } else { result = String.valueOf((char)keyCode); } return result; } /** * Add some flexibility to negative Integer string parsing * -n == -n as expected * n- == -n * - == -1 * any other - will throw a number format exception * * @param number string * @return int value of String */ public static int emacsParseInt(String number) { int result = -1; int len = number.length(); if (len > 0 && number.charAt(len-1) == '-') { if (len-1 > 0) { result = Integer.valueOf(number.substring(0,len-1)); } else { result = 1; } result = -result; } else { result = Integer.valueOf(number); } return result; } // Evil code: Provides execution of non-public method public static Object getAM(Object o, String methodName) { /* Go and invoke the inaccessible field... */ Class<?> c = o.getClass(); try { do { try { // assume empty parameter list for our purposes Method meth = c.getDeclaredMethod(methodName, new Class<?>[] {}); boolean access = meth.isAccessible(); Object obj; try { if (!access) { meth.setAccessible(true); } obj = meth.invoke(o, new Object[] {}); } finally { if (!access) { meth.setAccessible(false); } } return obj; } catch (NoSuchMethodException e) {} c = c.getSuperclass(); } while (c != null); } catch (SecurityException e) { } catch (IllegalArgumentException e) { } catch (IllegalAccessException e) { } catch (InvocationTargetException e) { } return null; } // Evil code: Provides access to non-public members in classes public static Object getAF(Object o, String fieldName) { /* Check we have valid arguments */ /* Go and find the private field... */ Class<?> c = o.getClass(); try { do { Field fields[] = c.getDeclaredFields(); for (int i = 0; i < fields.length; ++i) { if (fieldName.equals(fields[i].getName())) { boolean access = fields[i].isAccessible(); Object obj; try { if (!access) { fields[i].setAccessible(true); } obj = fields[i].get(o); } finally { if (!access) { fields[i].setAccessible(false); } } return obj; } } c = c.getSuperclass(); } while (c != null); } catch (SecurityException e) { } catch (IllegalArgumentException e) { } catch (IllegalAccessException e) { } return null; } }