/** * 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.commands; import java.util.HashMap; import java.util.Map; import org.eclipse.core.commands.AbstractHandler; import org.eclipse.core.commands.Command; import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.commands.IHandler; import org.eclipse.core.commands.common.CommandException; import org.eclipse.core.commands.common.NotDefinedException; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.FindReplaceDocumentAdapter; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.IRewriteTarget; import org.eclipse.jface.text.ITextSelection; import org.eclipse.jface.text.ITextViewerExtension; import org.eclipse.jface.text.TextSelection; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.ST; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.commands.ICommandService; import org.eclipse.ui.console.IConsoleView; import org.eclipse.ui.console.TextConsolePage; import org.eclipse.ui.console.TextConsoleViewer; import org.eclipse.ui.handlers.HandlerUtil; import org.eclipse.ui.part.IPage; import org.eclipse.ui.part.PageBookView; import org.eclipse.ui.texteditor.ITextEditor; import org.eclipse.ui.texteditor.ITextEditorExtension; import org.eclipse.ui.texteditor.ITextEditorExtension2; import com.mulgasoft.emacsplus.Beeper; import com.mulgasoft.emacsplus.EmacsPlusUtils; import com.mulgasoft.emacsplus.IBeepListener; import com.mulgasoft.emacsplus.IEmacsPlusCommandDefinitionIds; import com.mulgasoft.emacsplus.MarkUtils; import com.mulgasoft.emacsplus.execute.RepeatCommandSupport; /** * Base class of all Emacs+ command handlers * * @author Mark Feber - initial API and implementation */ public abstract class EmacsPlusCmdHandler extends AbstractHandler implements IHandler { final static String BAD_LOCATION_ERROR = "Bad_Location_Error"; //$NON-NLS-1$ private final static String INEDITABLE_BUFFER = "Cmd_Buffer_Error"; //$NON-NLS-1$ // word characters terminated by (ignored) non-word or EOL/EOB // \p{L} = \p{Letter}: letter from any language // \p{Mn} = \p{Non_Spacing_Mark}: a character intended to be combined with another character without taking up extra space (e.g. accents, umlauts, etc.). // \p{Nd} = \p{Decimal_Digit_Number}: a digit zero through nine in any script except ideographic scripts. protected static final String IDENT_REGEX= "[\\p{L}[\\p{Mn}[\\p{Nd}]]]"; //$NON-NLS-1$ protected static final String TOKEN_REGEX= IDENT_REGEX + "++"; //$NON-NLS-1$ protected static final String END_TOKEN_REGEX= "[^\\p{L}\\p{Mn}\\p{Nd}]"; //$NON-NLS-1$ protected static final String EMPTY_STR = ""; //$NON-NLS-1$ protected static final String SPACE_STR = " "; //$NON-NLS-1$ protected static final String KOLON = ": "; //$NON-NLS-1$ protected static final String CR = "\n"; //$NON-NLS-1$ /** the universal-argument name used by Emacs+ */ static final String UNIVERSAL = EmacsPlusUtils.UNIVERSAL_ARG; /** the shift select argument name used by Emacs+ movement commands*/ static final String SHIFT_ARG = "shiftArg"; //$NON-NLS-1$ enum ShiftState { CLEAR, // clear shift state SET, // set the shift state NONE; // no-op } /** No-op return value from transform method */ public final static int NO_OFFSET = EmacsPlusUtils.NO_OFFSET; String thisExtension; private boolean isEditable = true; /** The ITextEditor in which this command was invoked */ private ITextEditor editor; /** The IDocument on which this command was invoked */ private IDocument document; private IEditorPart lastPart = null; enum Check { Fail }; /** hold the current universal-argument value */ private int universalCount = 1; /** remember if the universal-argument was present */ private boolean universalPresent = false; // can be set to true to turn on for all (non-Emacs+) commands (which is probably a bad idea) private boolean alwaysUniversal = false; /** allow editing commands if true - simulates non-buffer local C-x C-q */ private static boolean forceEditable = false; // Abstract edit/etc. transformation method /** * Handlers override this method to implement their specific behavior. * * NO_OFFSET is returned if either the command took care of adjusting the selection/offset * in the body of the method or if the offset has not changed. * * @param editor the ITextEditor as determined from the Event information * @param document the IDocument as determined from the editor's document provider * @param currentSelection the ITextSelection as determined from the editor's selection provider * @param event the ExecutionEvent from the originating execute method * @return the new offset in the document, or NO_OFFSET * @throws BadLocationException */ protected abstract int transform(ITextEditor editor, IDocument document, ITextSelection currentSelection, ExecutionEvent event) throws BadLocationException; /** * Hook for post transformation behavior. Command handlers may override this to do something * once all normal edit/selection activity has occurred */ protected void postTransform() {} /** * Commands that perform simple transformations return true, else (e.g. executable minibuffer commands) false * @return true for simple transform, else false */ protected boolean isTransform() {return true;} /** * Get the current active editor * @param event * @return the current active editor, else null if some other part is active * * @throws ExecutionException */ protected IEditorPart getEditor(ExecutionEvent event) throws ExecutionException { IEditorPart epart = HandlerUtil.getActiveEditorChecked(event); if (HandlerUtil.getActivePart(event) != epart) { // When something other than the editor is active (e.g. the JavaStackTraceConsole) // Then don't return the editor return null; } return epart; } /** * Get the TextEditor for the associated event * * @param event * @return the ITextEditor or null * @throws ExecutionException */ protected ITextEditor getTextEditor(ExecutionEvent event) throws ExecutionException { ITextEditor result = null; try { result = getTextEditor(getEditor(event)); } catch (ExecutionException e) { // HandlerUtil will throw this if it can't find the editor } return result; } protected ITextEditor getTextEditor(IEditorPart editor) { return EmacsPlusUtils.getActiveTextEditor(editor); } /** * Eclipse forces us to check this ourselves * * @return true if the editor is modifiable */ private boolean getEditable() { boolean result = false; ITextEditor editor= getThisEditor(); if (editor != null) { if (editor instanceof ITextEditorExtension2) result = ((ITextEditorExtension2) editor).isEditorInputModifiable(); else if (editor instanceof ITextEditorExtension) result = !((ITextEditorExtension) editor).isEditorInputReadOnly(); else result = editor.isEditable(); } return result; } /** * Check if this command is being invoked inappropriately in a non-editing context * * @return true if command should be blocked, else false */ private boolean isBlocked() { return !isEditable() && !(this instanceof INonEditingCommand); } /** * Is the buffer editable? * * @return true if editable, else false */ protected boolean isEditable() { return isEditable || forceEditable; } /** * Check if the force buffer editable flag has been set. * * @return true if the forcing flag is on, else false */ public boolean isForceEditable() { return forceEditable; } /** * Force the buffer to be editable, regardless of the actual buffer state. * This will allow some commands that would otherwise be blocked. */ protected void setForceEditable(boolean forceEditable) { EmacsPlusCmdHandler.forceEditable = forceEditable; } /** * If the universal argument has been passed as a parameter to this command then * extract it and set the universal count appropriately. * * @param event * @return the value of the universal-argument if set, else 1 */ protected int extractUniversalCount(ExecutionEvent event) { int result = 1; setUniversalPresent(false); // Retrieve the universal-argument parameter value if passed String universalArg = event.getParameter(UNIVERSAL); if (universalArg != null && universalArg.length() > 0) { try { result = Integer.parseInt(universalArg); setUniversalPresent(true); } catch (NumberFormatException e) { // if invalid number, proceed with count=1 } } setUniversalCount(result); return result; } /** * Determine whether it is allowed for the command to be invoked outside the context of a text editor * * @return true if command can be invoked outside the context of a text editor */ protected boolean isWindowCommand(){ return false; } /** * Determine whether this command is appropriate for a TextConsole * * @return true if command can be invoked against a TextConsole */ protected boolean isConsoleCommand(){ return (this instanceof IConsoleDispatch); } /** * @see org.eclipse.core.commands.AbstractHandler#execute(org.eclipse.core.commands.ExecutionEvent) */ @SuppressWarnings("unchecked") public Object execute(ExecutionEvent event) throws ExecutionException { ITextEditor editor = getTextEditor(event); if (editor == null) { if (isWindowCommand()) { Object result = checkExecute(event); if (result == Check.Fail) { beep(); result = null; } return result; } else if (isConsoleCommand()) { // intercept and dispatch execution if console supported and used in a console view IWorkbenchPart activePart = HandlerUtil.getActivePart(event); if (activePart != null && (activePart instanceof IConsoleView) && (activePart instanceof PageBookView)) { IPage textPage = ((PageBookView)activePart).getCurrentPage(); if (textPage instanceof TextConsolePage) { return ((IConsoleDispatch)this).consoleDispatch(((TextConsolePage)textPage).getViewer(),(IConsoleView)activePart,event); } } } } try { setThisEditor(editor); isEditable = getEditable(); if (editor == null || isBlocked()) { beep(); asyncShowMessage(editor, INEDITABLE_BUFFER, true); return null; } // Retrieve the universal-argument parameter value if passed if (extractUniversalCount(event) != 1) { // check if we should dispatch a related command based on the universal argument String dispatchId = checkDispatchId(event.getCommand().getId()); if (dispatchId != null) { // recurse on new id (inverse or arg value driven) return dispatchId(editor, dispatchId, getParams(event.getCommand(), event.getParameters())); } } setThisDocument(editor.getDocumentProvider().getDocument(editor.getEditorInput())); // Get the current selection ISelectionProvider selectionProvider = editor.getSelectionProvider(); ITextSelection selection = (ITextSelection) selectionProvider.getSelection(); preTransform(editor, selection); return transformWithCount(editor, getThisDocument(), selection, event); } finally { // normal commands clean up here if (isTransform()) { postExecute(); } } } /** * Make a parameter map, if it uses one, for this command * @param command * @param count * @param params * @return the populated map, or an empty one */ private Map<String,Object> getParams(Command command, Map<String,Object> params) { try { if (command.getParameter(UNIVERSAL) != null) { if (params.isEmpty()) { params = new HashMap<String, Object>(); } params.put(UNIVERSAL, Integer.toString(getUniversalCount())); } } catch (CommandException e) {} // won't happen return params; } /** * Support repeating the last (Emacs+ handled) command. * Determine the type of the last command, and re-execute as appropriate * @param editor * @return the result of the execution */ protected Object repeatLast(ITextEditor editor, String id, Map<String,Object> params) { // check for synthetic (non-Emacs+) uArg commands if (isUniversalCmd(id)) { try { Integer count = (Integer)params.get(UNIVERSAL); executeUniversal(editor, id, null, (count != null ? count : 1), true); } catch (Exception e) { // Ignore, as command id should always be valid } return NO_OFFSET; } else { return dispatchId(editor, id, params); } } /** * Add any extra processing once the handler is set up prior to the transform call * For use by sub-classes * @param selection */ protected void preTransform(ITextEditor editor, ITextSelection selection) { // default does nothing } /** * Add any extra processing once the handler has called transform * For use by sub-classes */ protected void postExecute() { postTransform(); // do any command specific any transformation activities setThisEditor(null); // and clean up setThisDocument(null); thisExtension = null; isEditable = true; setUniversalCount(1); } /** * Activate the 'active' text editor from some other view part. * If the original execute was called from outside the context of a text editor * attempt to find and activate the appropriate 'active' editor and dispatch * the command again in the new context * * @param event * @return Object */ protected Object checkExecute(ExecutionEvent event) { Object result = null; try { IWorkbenchPage wpage = this.getWorkbenchPage(); IEditorPart epart = EmacsPlusUtils.getTextEditor(HandlerUtil.getActiveEditor(event), true); // we're in a loop if (epart != null && epart == lastPart) { epart = null; lastPart = null; } else { lastPart = epart; } // if we're now looking at a new part, activate it and re-dispatch command if (epart != null && HandlerUtil.getActivePart(event) != epart) { wpage.bringToTop(epart); wpage.activate(epart); @SuppressWarnings("unchecked") Map<String,?> params = (Map<String,?>)event.getParameters(); result = executeCommand(event.getCommand().getId(), params, null, epart); lastPart = null; } else { // else notify of failure result = Check.Fail; } } catch (ExecutionException e) { } catch (CommandException e) { } return result; } private class BInterrupt implements IBeepListener { public boolean interrupted = false; public void beepInterrupt() { interrupted = true; } }; /** * Call the transform method universal-argument times if it is a looping command, else once. * Wraps it up with an undoer and suppresses redraw as appropriate * * @param editor * @param document * @param selection * @param event * @return the Object returned by the transform */ protected Object transformWithCount(ITextEditor editor,IDocument document,ITextSelection selection,ExecutionEvent event) { Control widget = null; IRewriteTarget rt = null; boolean undoProtect = undoProtect(); int ucount = Math.abs(getUniversalCount()); int newOffset = NO_OFFSET; if (ucount == 0 && isZero()) { // enable commands that support ^U 0 via count ucount = 1; } BInterrupt interrupt = null; // Now transform the selection try { armResult(); if (undoProtect) { interrupt = new BInterrupt(); Beeper.addBeepListener(interrupt); rt = (IRewriteTarget) editor.getAdapter(IRewriteTarget.class); if (rt != null) { rt.beginCompoundChange(); } // use widget to avoid unpleasant scrolling side effects of IRewriteTarget widget = getTextWidget(editor); setRedraw(widget,false); } ITextSelection newSelection = selection; for (int i = 0; i < ucount; i++) { newSelection = getCmdSelection(editor,newSelection); if (newSelection != null && checkSelection(newSelection)) { try { newOffset = transform(editor, document, newSelection, event); // if NO_OFFSET then selection hasn't changed or already moved // if (!(newOffset == NO_OFFSET)){ if (!(newOffset < 0)) { // Set the selection to the end of the modified text newSelection= new TextSelection(document, newOffset, 0); setSelection(editor, newSelection); } } catch (BadLocationException e) { newOffset = NO_OFFSET; beep(); break; } if (!isLooping()) { break; } } if (undoProtect && interrupt.interrupted) { break; } } } catch (Exception e) { // allow generic break out of count iteration newOffset = NO_OFFSET; } finally { if (undoProtect) { Beeper.removeBeepListener(interrupt); setRedraw(widget, true); if (rt != null) { rt.endCompoundChange(); } } } // if the command set the result specifically, use that, // otherwise just the offset position (which may be NO_OFFSET) return (hasResult() ? getCmdResult() : new Integer(newOffset)); } private Object cmdResult = null; private boolean hasResult = false; /** * Set the return value of this command * * @param result the Object to be returned from this call */ protected void setCmdResult(Object result) { cmdResult = result; hasResult = true; } /** * reset the command result state */ private void armResult() { cmdResult = null; hasResult = false; } /** * What the command result set? * @return true if set, else false */ private boolean hasResult() { return hasResult; } /** * Get the command result * @return the command result */ private Object getCmdResult() { return cmdResult; } /** * If no text is selected, see if the handler wants to get one * * @param editor * @param selection * @return new selection, if handler modified it, else selection * @throws ExecutionException */ protected ITextSelection getCmdSelection(ITextEditor editor,ITextSelection selection) throws ExecutionException { ITextSelection newSelection = selection; if (selection.getLength() <= 0) { newSelection = getNewSelection(getThisDocument(), selection); if (newSelection != null && newSelection != selection) { selection = newSelection; setSelection(editor,newSelection); } else { newSelection = selection; } } return newSelection; } protected void selectAndReveal(ITextEditor editor, int offset, int end) { Control widget = getTextWidget(editor); try { setRedraw(widget,false); // editor.selectAndReveal(mark, offset-mark); MarkUtils.setSelection(editor, end, offset-end); // and reveal to cursor if necessary MarkUtils.revealRange(editor,offset,0); } finally { setRedraw(widget,true); } } protected Control getTextWidget(ITextEditor editor) { return MarkUtils.getTextWidget(editor); } protected Point getWidgetSelection(ITextEditor editor) { return MarkUtils.getWidgetSelection(editor); } /** * Get the editor's line delimiter * * @return delimiter as String */ protected String getLineDelimiter() { return MarkUtils.getWidgetLineDelimiter(getThisEditor()); } protected boolean isLineDelimiter(int offset) { return isLineDelimiter(offset,getThisDocument()); } // This is based on code in StyledText - so setting the caret offset // does not throw an exception. private static boolean isLineDelimiter(int offset,IDocument document) { boolean result = false; IRegion reg; try { reg = document.getLineInformationOfOffset(offset); int lineOffset = reg.getOffset(); int offsetInLine = offset - lineOffset; // offsetInLine will be greater than line length if the line // delimiter is longer than one character and the offset is set // in between parts of the line delimiter. result = offsetInLine >= reg.getLength(); } catch (BadLocationException e) { } return result; } /** * On paste, Eclipse StyledText converts EOLs to the buffer local value * Provide the same feature for yank * * @param text * @return text with line delimiters converted to buffer local value */ protected String convertDelimiters(String text) { String result = text; int len; if (text != null && (len = text.length()) > 0) { String eol = getLineDelimiter(); StringBuilder dest = new StringBuilder(len); boolean atEol = false; for (int i = 0; i < len; i++) { char c = text.charAt(i); if (c == SWT.CR) { if (atEol) { dest.append(eol); } atEol = true; continue; } else if (c == SWT.LF) { atEol = false; dest.append(eol); continue; } if (atEol) { atEol = false; dest.append(eol); } dest.append(c); } if (atEol) { dest.append(eol); } result = dest.toString(); } return result; } protected int insertText(IDocument document, ITextSelection selection, String text) throws BadLocationException { int result = 0; String iText = convertDelimiters(text); if (iText != null){ result = iText.length(); updateText(document, selection, iText); } return result; } ITextEditor getThisEditor() { return editor; } void setThisEditor(ITextEditor editor) { this.editor = editor; } protected IDocument getThisDocument() { return document; } protected void setThisDocument(IDocument document) { this.document = document; } protected IDocument getThisDocument(ITextEditor editor) { IDocument doc = getThisDocument(); if (doc == null && editor != null) { doc = editor.getDocumentProvider().getDocument(editor.getEditorInput()); } return doc; } protected String getCurrentExtension() { if (thisExtension == null && getThisEditor() != null) { String name = getThisEditor().getTitle(); if (name != null) { int pos = name.lastIndexOf('.') + 1; if (pos > 0) { thisExtension = name.substring(pos); } } } return thisExtension; } private String[] xmlExtensions = {"xml", "mxml" }; //$NON-NLS-1$ //$NON-NLS-2$ protected boolean isTypeXml(){ boolean result = false; String ext = getCurrentExtension(); if (ext != null) { for (int i = 0; i < xmlExtensions.length; i++) { if (xmlExtensions[i].equalsIgnoreCase(ext)) { result = true; break; } } } return result; } protected boolean checkSelection(ITextSelection selection){ return true; } /** * Return the editor's current selection * Never returns null * * @param editor * @return the current selection or (0,0) */ protected ITextSelection getCurrentSelection(ITextEditor editor){ ISelectionProvider sp = editor.getSelectionProvider(); // never return null return (sp != null ? (ITextSelection) sp.getSelection() : new TextSelection(0,0)); } /** * Potentially get a new selection based on the current selection * The default implementation just returns the supplied selection * * @param document * @param selection * @return a selection * @throws ExecutionException */ protected ITextSelection getNewSelection(IDocument document, ITextSelection selection) throws ExecutionException{ return selection; } /** * Determine the explicit or implicit (point/mark) selection * * @param editor * @param selection * @return the selection or null */ protected ITextSelection getImpliedSelection(ITextEditor editor, ITextSelection selection){ // if selection length is 0, then if mark and mark != point, set and return as selection ITextSelection result = selection; if (selection.getLength() == 0) { int mark = getMark(editor); int coff = selection.getOffset(); if (isMarkSet(mark) && mark != coff) { setSelection(editor,coff, mark - coff); result = getCurrentSelection(editor); } else { result = null; } } return result; } protected ITextSelection setSelection(ITextEditor editor, ITextSelection selection){ return MarkUtils.setSelection(editor, selection); } protected ITextSelection setSelection(ITextEditor editor,int offset, int length){ return MarkUtils.setSelection(editor, offset, length); } protected void updateText(IDocument document,ITextSelection selection,String newText) throws BadLocationException{ updateText(document, selection.getOffset(), selection.getLength(),newText); } protected void updateText(IDocument document,int offset, int length, String newText) throws BadLocationException{ // if the text doesn't end with an eol, then it can provoke a bug(?) in java.text.RuleBasedBreakIterator.checkOffset() // where it throws an exception if offset >= endindex [s/b? offset > endindex] if (offset == document.getLength()) { String eol = getLineDelimiter(); if (!newText.endsWith(eol)){ newText += eol; } } document.replace(offset,length,newText); } /** * @see com.mulgasoft.emacsplus.commands.EmacsPlusCmdHandler#getNextSelection(IDocument,ITextSelection,boolean,String) * * @param document * @param selection */ protected ITextSelection getNextSelection(IDocument document, ITextSelection selection) { return getNextSelection(document, selection, true, TOKEN_REGEX); } /** * @see com.mulgasoft.emacsplus.commands.EmacsPlusCmdHandler#getNextSelection(IDocument,ITextSelection,boolean,String) * * @param document * @param selection */ protected ITextSelection getPrevSelection(IDocument document, ITextSelection selection) { return getNextSelection(document, selection, false, TOKEN_REGEX); } /** * Find the next token in the document and return it as a selection * @param document * @param selection * @param forward * @param regExp * @return the newly selected text */ protected ITextSelection getNextSelection(IDocument document, ITextSelection selection, boolean forward, String regExp) { ITextSelection result = null; FindReplaceDocumentAdapter fda = new FindReplaceDocumentAdapter(document); try { IRegion reg = fda.find(selection.getOffset(), regExp, forward, false, false, true); if (reg != null) { result = new TextSelection(document, reg.getOffset(), reg.getLength()); } } catch (BadLocationException e) {} return result; } /** * Invoke the specified command using the handler service * * @param commandId * @param event * @param editor * @return execution result * @throws CommandException */ protected Object executeCommand(String commandId,Event event, IEditorPart editor) throws ExecutionException, CommandException { return EmacsPlusUtils.executeCommand(commandId, event, editor); } /** * Invoke the specified command using the generic handler service * * @param commandId * @param event * @param editor * @return execution result * @throws CommandException */ protected Object executeCommand(String commandId,Event event) throws ExecutionException, CommandException { return EmacsPlusUtils.executeCommand(commandId, event); } /** * Invoke the specified parameterized command using the handler service * @param commandId * @param params * @param event * @param editor * @return execution result * @throws ExecutionException * @throws CommandException */ protected Object executeCommand(String commandId, Map<String,?> params, Event event, IEditorPart editor) throws ExecutionException, CommandException { return EmacsPlusUtils.executeCommand(commandId, params, event, editor); } /** * Get the active editor of the page * Dereference multi-part editor * @param page * @return the active editor or null */ protected IEditorPart getActiveEditor(IWorkbenchPage page) { IEditorPart result = page.getActiveEditor(); IEditorPart part = (IEditorPart) result.getAdapter(ITextEditor.class); if (part != null) { result = part; } return result; } protected int getCursorOffset(){ return getCursorOffset(getThisEditor()); } /** * Get the absolute cursor offset in model coords * @see com.mulgasoft.emacsplus.MarkUtils#getCursorOffset(ITextEditor) * * @return the offset */ protected int getCursorOffset(ITextEditor editor){ return getCursorOffset(editor,null); } /** * Get the absolute cursor offset in model coords * If the selection length is 0, use the selection offset, else get from the widget * * @return the offset */ protected int getCursorOffset(ITextEditor editor, ITextSelection currentSelection){ return (currentSelection != null && currentSelection.getLength() == 0 ? currentSelection.getOffset() : MarkUtils.getCursorOffset(editor)); } /** * Set the cursor offset using the editors selection provider * * @param editor * @param offset in model coords */ protected void setCursorOffset(ITextEditor editor,int offset){ MarkUtils.setCursorOffset(editor,offset); } public static boolean isChanged = false; /** if true, the mark is enabled (even if the selection is zero length) */ private static boolean flagMark = false; /** * @return the true if mark has been */ protected static boolean isFlagMark() { return flagMark; } /** * @param flagMark the flagMark to set */ public static void setFlagMark(boolean flagMark) { EmacsPlusCmdHandler.flagMark = flagMark; } /** * Check if mark offset is at either end of the region * * @param mark -mark offset * @param off - region offset * @param len - region length * @return true if mark offset is at either end of the region */ protected boolean checkMark(int mark, int off, int len){ return (mark == off || mark == off+len); } /** * Check if editor's mark offset is at either end of the region * * @param editor * @param off - region offset * @param len - region length * @return true if mark offset is at either end of the region */ protected boolean checkMark(ITextEditor editor, int off, int len){ return checkMark(getMark(editor),off,len); } /** * @see com.mulgasoft.emacsplus.MarkUtils#setMark(ITextEditor) */ protected int setMark(ITextEditor editor){ return MarkUtils.setMark(editor); } /** * Set the mark at offset and save previous mark in Mark Rings * * @param editor * @param offset in model coords * @return the mark position in model coords */ protected int setMark(ITextEditor editor, int offset){ return setMark(editor,offset,true); } /** * Set the mark at offset * * @param editor * @param offset in model coords * @param save if true, save previous mark in the Mark Rings * * @return the mark position in model coords */ protected int setMark(ITextEditor editor, int offset, boolean save){ // hack optimization so we don't have to keep getting the viewer if (lastEditor != editor) { lastEditor = editor; ve = MarkUtils.getITextViewer(editor); } return MarkUtils.setMark(editor, ve, offset, save); } /** * Check if the mark has been set in the buffer * * @param editor * @return true if mark != -1 */ protected boolean isMarkSet(ITextEditor editor) { return isMarkSet(getMark(editor)); } /** * Check if the mark has been set in the buffer * * @param mark the value of the mark offset * @return true if mark != -1 */ protected boolean isMarkSet(int mark) { return mark != -1; } /** * Check that all conditions for mark enablement are met * * @param editor * @param selection (typically currentSelection) * * @return true if mark is set in the context of the selection */ protected boolean isMarkEnabled(ITextEditor editor, ITextSelection selection) { int mark = getMark(editor); // current mark position int len = selection.getLength(); int off = selection.getOffset(); // if mark set and in a marked region, or mark has just been set return (isMarkSet(mark) && checkMark(mark,off,len) && (len > 0 || isFlagMark())); } /** * Check that all conditions for mark enablement are met on a TextConsole * Support single mark only * * @param viewer * @param selection * @return true if mark is set in the context of the selection */ protected boolean isMarkEnabled(TextConsoleViewer viewer, ITextSelection selection) { int mark = viewer.getMark(); // current mark position int len = selection.getLength(); int off = selection.getOffset(); // if mark set and in a marked region, or mark has just been set return (isMarkSet(mark) && checkMark(mark,off,len)); } private ITextEditor lastEditor; private ITextViewerExtension ve; /** * Get the current Mark position * * @param editor * @return the Mark position in model coords (-1 if not set) */ protected int getMark(ITextEditor editor) { // hack optimization so we don't have to keep getting the viewer int result = -1; if (lastEditor != editor) { lastEditor = editor; ve = MarkUtils.getITextViewer(editor); } if (ve != null) { result = ve.getMark(); } return result; } protected int getMark() { return getMark(getThisEditor()); } protected void beep() { EmacsPlusUtils.beep(); } protected IWorkbenchPage getWorkbenchPage() { return EmacsPlusUtils.getWorkbenchPage(); } /** * Add an asynchronous call to show the message */ protected void asyncShowMessage(final IWorkbenchPart wpart, final String message, final boolean error) { PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() { public void run() { EmacsPlusUtils.showMessage(wpart, message, error); }}); } // **************** Universal arg command processing methods **************** /** * Return the inverse operation for the specified command id. * The inverse operation, if present, is used when a negative universal argument is detected. * * @param id * @return the inverse of the command id or null */ protected String getInverseId(String id) { // is it implemented by a non-Emacs+ command? String result = InverseUniversalCmds.get(id); // if not, check the Emacs+ commands return (result != null ? result : MarkUtils.getInverseId(id)); } /** * Is this the ids of standard Eclipse commands that we adapt to calling with the C-u argument * @param id * @return true if we want to provide support for C-u, else false */ private boolean isUniversalCmd(String id) { return UniversalCmds.get(id) != null; } /** * Get the value of the universal-argument; defaults to 1 if not supplied by command invocation. * * @return the universalCount */ public int getUniversalCount() { return universalCount; } /** * Set the value of the universal-argument * * @param universalCount the universalCount to set */ protected void setUniversalCount(int universalCount) { this.universalCount = universalCount; } /** * Return true if the command has been invoked with a universal-argument, else false * * @return true if universal-argument supplied, else false */ public boolean isUniversalPresent() { return universalPresent; } /** * Set that the command has been invoked with a universal-argument * * @param universalPresent the universalPresent to set */ private void setUniversalPresent(boolean universalPresent) { this.universalPresent = universalPresent; } /** * Commands that need to specify arg-value based processing should override this * * @return true is command should loop */ protected boolean isLooping() { // by default, all commands are looping. // also filtered by presence of universalArg // individual commands can override return true; } /** * Commands that need to specify ^U 0 processing should override this * * @return true is command can be called with 0 universal arg */ protected boolean isZero() { // by default, no commands support 0 directly. return false; } /** * If the universal-argument is > 1 and the command supports looping, then return true. * * @return true if we should wrap in an undo protect, else false */ protected boolean undoProtect() { return getUniversalCount() != 1 && isLooping(); } /** * If the Command accepts the universal-argument, then invoke it appropriately * otherwise simply execute it with no arguments. * * To accept the universal-argument, the Command must either: * 1) Have a parameter named "universalArg" [for Emacs+ commands] - or - * 2) Be present in the universal command hash table [for pre-existing commands] * * @param editor * @param count * @param cmd * @param isNumeric true if argument was entered as a number (rather than plain ^Us) * - which we don't handle yet * @throws NotDefinedException * @throws ExecutionException * @throws CommandException */ void executeUniversal(ITextEditor editor, Command cmd, Event event, int count, boolean isNumeric) throws NotDefinedException, ExecutionException, CommandException { // pass universal arg if non-default and cmd accepts it String id = cmd.getId(); if (cmd.getParameter(UNIVERSAL) != null) { EmacsPlusUtils.executeCommand(id, count, event, editor); } else { executeUniversal(editor, id, event, count, isNumeric); } } void executeUniversal(ITextEditor editor, String id, Event event, int count, boolean isNumeric) throws NotDefinedException, ExecutionException, CommandException { String did = null; if ((did = getInternalCmd(id)) != null) { // Emacs+ internal commands should support +- universal-argument EmacsPlusUtils.executeCommand(did, count, event, editor); } else if (count != 1 && (isUniversalCmd(id) || (alwaysUniversal && !id.startsWith(EmacsPlusUtils.MULGASOFT)))) { // only non-Emacs+ commands should be invoked here executeWithDispatch(editor, getUniversalCmd(id), count); } else { executeCommand(id, event, editor); } } private String getInternalCmd(String id) { return InternalCmds.get(id); } /** * Check for a dispatch command before invoking with count * For a command that has already been determined to support the universal-argument * * @param editor * @param id * @param count */ protected void executeWithDispatch(ITextEditor editor, String id, int count) { try { // set for dispatch check setUniversalCount(count); // check if we should dispatch a related command based on the universal argument String dispatchId = checkDispatchId(id); id = (dispatchId != null ? dispatchId : id); count = Math.abs(count); // Associate count with non-Emacs+ uArg command RepeatCommandSupport.getInstance().storeCommand(id, count); executeWithCount(editor, getThisDocument(editor), id, count); } finally { // restore count to default setUniversalCount(1); } } /** * Execute the command count times * Wraps it up with an undoer and suppresses redraw as appropriate * * @param editor * @param document * @param id * @param count * @return the result of executing the command */ private Object executeWithCount(ITextEditor editor, IDocument document, String id, int count) { Object result = null; IRewriteTarget rt = null; Control widget = null; // Now execute the command - count will always be positive at this point try { if (count > 1) { rt = (IRewriteTarget) editor.getAdapter(IRewriteTarget.class); if (rt != null) { rt.beginCompoundChange(); } // use widget to avoid unpleasant scrolling side effects of IRewriteTarget widget = getTextWidget(editor); setRedraw(widget,false); } for (int i = 0; i < count; i++) { try { result = executeCommand(id, null, editor); } catch (ExecutionException e) { break; } catch (CommandException e) { break; } } } finally { setRedraw(widget,true); if (rt != null) { rt.endCompoundChange(); } } return result; } protected void setRedraw(Control c, boolean redraw) { if (c != null) { c.setRedraw(redraw); } } /** * Execute the command id (with universal-argument, if appropriate) * * @param id * @param event * @return execution result */ private Object dispatchId(ITextEditor editor, String id, Map<String,Object> params) { Object result = null; if (id != null) { ICommandService ics = (ICommandService) editor.getSite().getService(ICommandService.class); if (ics != null) { Command command = ics.getCommand(id); if (command != null) { try { result = executeCommand(id, (Map<String,?>)params, null, getThisEditor()); } catch (CommandException e) {} } } } return result; } /** * Emacs+ commands that have an arg value based dispatch should * override this method * * @param checkId * @param arg * @return the arg value dispatch id, or null */ protected String getDispatchId(String checkId, int arg) { String result = null; if (arg == 0) { result = ZeroCmds.get(checkId); } return result; } /** * Look to see if there is a different id that should be dispatched based on the * universal-argument * * This can occur if: * 1) The universal-argument is negative, and there is an inverse operation * 2) If not, check to see if there is a ^U key id (e.g. for command X, it * would return X1 if ^U, X2 if ^U ^U, etc). * * @param checkId * @return a dispatch id or null */ protected String checkDispatchId(String checkId) { String result = null; String id = null; int count = getUniversalCount(); // if arg is negative, see if there is an inverse operation to call if (count < 0) { // check for dispatch based on count inversion if ((id = getInverseId(checkId)) != null) { setUniversalCount(-getUniversalCount()); result = id; } } if (id == null && count != 1) { // if arg is not 1 (i.e. 0 or greater than 1) and no id, // check for dispatch id based on argument value // handles the non-looping ^U command style invocation id = getDispatchId(checkId, count); if (id != null && !id.equals(checkId)) { result = id; } } return result; } private String getUniversalCmd(String id) { return UniversalCmds.get(id); } /** * Relates command id to action in StyledText widget */ @SuppressWarnings("serial") protected static final HashMap<String,Integer> dispatchCmdIds = new HashMap<String,Integer>() { { // Eclipse ids put(IEmacsPlusCommandDefinitionIds.EMP_SELECT_ALL, ST.SELECT_ALL); put(IEmacsPlusCommandDefinitionIds.LINE_UP, ST.LINE_UP); put(IEmacsPlusCommandDefinitionIds.LINE_DOWN,ST.LINE_DOWN); put(IEmacsPlusCommandDefinitionIds.LINE_START,ST.LINE_START); put(IEmacsPlusCommandDefinitionIds.LINE_END,ST.LINE_END); put(IEmacsPlusCommandDefinitionIds.COLUMN_PREVIOUS,ST.COLUMN_PREVIOUS); put(IEmacsPlusCommandDefinitionIds.COLUMN_NEXT,ST.COLUMN_NEXT); put(IEmacsPlusCommandDefinitionIds.WORD_PREVIOUS,ST.WORD_PREVIOUS); put(IEmacsPlusCommandDefinitionIds.WORD_NEXT,ST.WORD_NEXT); put(IEmacsPlusCommandDefinitionIds.TEXT_START,ST.TEXT_START); put(IEmacsPlusCommandDefinitionIds.TEXT_END,ST.TEXT_END); put(IEmacsPlusCommandDefinitionIds.WINDOW_START,ST.WINDOW_START); put(IEmacsPlusCommandDefinitionIds.WINDOW_END,ST.WINDOW_END); // Emacs+ ids put(IEmacsPlusCommandDefinitionIds.SCROLL_DOWN,ST.PAGE_UP); put(IEmacsPlusCommandDefinitionIds.SCROLL_UP,ST.PAGE_DOWN); put(IEmacsPlusCommandDefinitionIds.PREVIOUS_LINE, ST.LINE_UP); put(IEmacsPlusCommandDefinitionIds.NEXT_LINE,ST.LINE_DOWN); put(IEmacsPlusCommandDefinitionIds.BEGIN_LINE,ST.LINE_START); put(IEmacsPlusCommandDefinitionIds.END_LINE,ST.LINE_END); put(IEmacsPlusCommandDefinitionIds.BACKWARD_CHAR,ST.COLUMN_PREVIOUS); put(IEmacsPlusCommandDefinitionIds.FORWARD_CHAR,ST.COLUMN_NEXT); put(IEmacsPlusCommandDefinitionIds.BACKWARD_WORD,ST.WORD_PREVIOUS); put(IEmacsPlusCommandDefinitionIds.FORWARD_WORD,ST.WORD_NEXT); put(IEmacsPlusCommandDefinitionIds.BEGIN_BUFFER,ST.TEXT_START); put(IEmacsPlusCommandDefinitionIds.END_BUFFER,ST.TEXT_END); put(IEmacsPlusCommandDefinitionIds.DELETE_NEXT,ST.DELETE_NEXT); put(IEmacsPlusCommandDefinitionIds.DELETE_NEXT_WORD,ST.DELETE_WORD_NEXT); put(IEmacsPlusCommandDefinitionIds.DELETE_PREVIOUS_WORD,ST.DELETE_WORD_PREVIOUS); put(IEmacsPlusCommandDefinitionIds.EMP_CUT,ST.CUT); } }; /** * Relates command id to action in StyledText widget */ @SuppressWarnings("serial") protected static final HashMap<String,Integer> dispatchSelectIds = new HashMap<String,Integer>() { { // Eclipse ids put(IEmacsPlusCommandDefinitionIds.EMP_SELECT_ALL, ST.SELECT_ALL); put(IEmacsPlusCommandDefinitionIds.LINE_UP, ST.SELECT_LINE_UP); put(IEmacsPlusCommandDefinitionIds.LINE_DOWN,ST.SELECT_LINE_DOWN); put(IEmacsPlusCommandDefinitionIds.LINE_START,ST.SELECT_LINE_START); put(IEmacsPlusCommandDefinitionIds.LINE_END,ST.SELECT_LINE_END); put(IEmacsPlusCommandDefinitionIds.COLUMN_PREVIOUS,ST.SELECT_COLUMN_PREVIOUS); put(IEmacsPlusCommandDefinitionIds.COLUMN_NEXT,ST.SELECT_COLUMN_NEXT); put(IEmacsPlusCommandDefinitionIds.PAGE_UP,ST.SELECT_PAGE_UP); put(IEmacsPlusCommandDefinitionIds.PAGE_DOWN,ST.SELECT_PAGE_DOWN); put(IEmacsPlusCommandDefinitionIds.WORD_PREVIOUS,ST.SELECT_WORD_PREVIOUS); put(IEmacsPlusCommandDefinitionIds.WORD_NEXT,ST.SELECT_WORD_NEXT); put(IEmacsPlusCommandDefinitionIds.TEXT_START,ST.SELECT_TEXT_START); put(IEmacsPlusCommandDefinitionIds.TEXT_END,ST.SELECT_TEXT_END); put(IEmacsPlusCommandDefinitionIds.WINDOW_START,ST.SELECT_WINDOW_START); put(IEmacsPlusCommandDefinitionIds.WINDOW_END,ST.SELECT_WINDOW_END); // Emacs+ ids put(IEmacsPlusCommandDefinitionIds.PREVIOUS_LINE, ST.SELECT_LINE_UP); put(IEmacsPlusCommandDefinitionIds.NEXT_LINE,ST.SELECT_LINE_DOWN); put(IEmacsPlusCommandDefinitionIds.BEGIN_LINE,ST.SELECT_LINE_START); put(IEmacsPlusCommandDefinitionIds.END_LINE,ST.SELECT_LINE_END); put(IEmacsPlusCommandDefinitionIds.BACKWARD_CHAR,ST.SELECT_COLUMN_PREVIOUS); put(IEmacsPlusCommandDefinitionIds.FORWARD_CHAR,ST.SELECT_COLUMN_NEXT); put(IEmacsPlusCommandDefinitionIds.BACKWARD_WORD,ST.SELECT_WORD_PREVIOUS); put(IEmacsPlusCommandDefinitionIds.FORWARD_WORD,ST.SELECT_WORD_NEXT); put(IEmacsPlusCommandDefinitionIds.BEGIN_BUFFER,ST.SELECT_TEXT_START); put(IEmacsPlusCommandDefinitionIds.END_BUFFER,ST.SELECT_TEXT_END); } }; // Some commands do something different on 0 argument @SuppressWarnings("serial") protected static final HashMap<String,String> ZeroCmds = new HashMap<String,String>() { { // Eclipse ids put(IEmacsPlusCommandDefinitionIds.CUT_LINE_TO_END, IEmacsPlusCommandDefinitionIds.CUT_LINE_TO_BEGINNING); } }; // Override kill behavior on ^U @SuppressWarnings("serial") private static final HashMap<String,String> InternalCmds = new HashMap<String,String>() { { // translate to internal command with appropriate behavior put(IEmacsPlusCommandDefinitionIds.CUT_LINE_TO_END, IEmacsPlusCommandDefinitionIds.KILL_LINE); // translate to internal command with appropriate behavior put(IEmacsPlusCommandDefinitionIds.CUT_LINE_TO_BEGINNING, IEmacsPlusCommandDefinitionIds.BACKWARD_KILL_LINE); } }; // These are the ids of standard Eclipse commands that we adapt to calling with the C-u argument @SuppressWarnings("serial") private static final HashMap<String,String> UniversalCmds = new HashMap<String,String>() { { put(IEmacsPlusCommandDefinitionIds.DELETE_NEXT, IEmacsPlusCommandDefinitionIds.DELETE_NEXT); put(IEmacsPlusCommandDefinitionIds.DELETE_PREVIOUS, IEmacsPlusCommandDefinitionIds.DELETE_PREVIOUS); put(IEmacsPlusCommandDefinitionIds.DELETE_NEXT_WORD,IEmacsPlusCommandDefinitionIds.DELETE_NEXT_WORD); put(IEmacsPlusCommandDefinitionIds.DELETE_PREVIOUS_WORD,IEmacsPlusCommandDefinitionIds.DELETE_PREVIOUS_WORD); put(IEmacsPlusCommandDefinitionIds.SMART_ENTER_INVERSE, IEmacsPlusCommandDefinitionIds.SMART_ENTER_INVERSE); put(IEmacsPlusCommandDefinitionIds.SMART_ENTER, IEmacsPlusCommandDefinitionIds.SMART_ENTER); put(IEmacsPlusCommandDefinitionIds.EMP_DELETE, IEmacsPlusCommandDefinitionIds.EMP_DELETE); put(IEmacsPlusCommandDefinitionIds.DELETE_NEXT, IEmacsPlusCommandDefinitionIds.DELETE_NEXT); put(IEmacsPlusCommandDefinitionIds.DELETE_PREVIOUS, IEmacsPlusCommandDefinitionIds.DELETE_PREVIOUS); put(IEmacsPlusCommandDefinitionIds.DELETE_LINE, IEmacsPlusCommandDefinitionIds.DELETE_LINE); put(IEmacsPlusCommandDefinitionIds.MOVE_LINES_UP, IEmacsPlusCommandDefinitionIds.MOVE_LINES_UP); put(IEmacsPlusCommandDefinitionIds.MOVE_LINES_DOWN, IEmacsPlusCommandDefinitionIds.MOVE_LINES_DOWN); put(IEmacsPlusCommandDefinitionIds.COLUMN_NEXT, IEmacsPlusCommandDefinitionIds.COLUMN_NEXT); put(IEmacsPlusCommandDefinitionIds.COLUMN_PREVIOUS, IEmacsPlusCommandDefinitionIds.COLUMN_PREVIOUS); put(IEmacsPlusCommandDefinitionIds.SCROLL_LINE_UP, IEmacsPlusCommandDefinitionIds.SCROLL_LINE_UP); put(IEmacsPlusCommandDefinitionIds.SCROLL_LINE_DOWN, IEmacsPlusCommandDefinitionIds.SCROLL_LINE_DOWN); put(IEmacsPlusCommandDefinitionIds.SHIFT_RIGHT, IEmacsPlusCommandDefinitionIds.SHIFT_RIGHT); put(IEmacsPlusCommandDefinitionIds.SHIFT_LEFT, IEmacsPlusCommandDefinitionIds.SHIFT_LEFT); } }; // These are the ids of standard Eclipse commands that we adapt to calling with a negative C-u argument @SuppressWarnings("serial") private static final HashMap<String,String> InverseUniversalCmds = new HashMap<String,String>() { { put(IEmacsPlusCommandDefinitionIds.DELETE_NEXT, IEmacsPlusCommandDefinitionIds.DELETE_PREVIOUS); put(IEmacsPlusCommandDefinitionIds.DELETE_PREVIOUS, IEmacsPlusCommandDefinitionIds.DELETE_NEXT); put(IEmacsPlusCommandDefinitionIds.DELETE_NEXT_WORD,IEmacsPlusCommandDefinitionIds.DELETE_PREVIOUS_WORD); put(IEmacsPlusCommandDefinitionIds.DELETE_PREVIOUS_WORD,IEmacsPlusCommandDefinitionIds.DELETE_NEXT_WORD); put(IEmacsPlusCommandDefinitionIds.SMART_ENTER_INVERSE, IEmacsPlusCommandDefinitionIds.SMART_ENTER); put(IEmacsPlusCommandDefinitionIds.SMART_ENTER, IEmacsPlusCommandDefinitionIds.SMART_ENTER_INVERSE); put(IEmacsPlusCommandDefinitionIds.EMP_DELETE, IEmacsPlusCommandDefinitionIds.DELETE_PREVIOUS); put(IEmacsPlusCommandDefinitionIds.DELETE_NEXT, IEmacsPlusCommandDefinitionIds.DELETE_PREVIOUS); put(IEmacsPlusCommandDefinitionIds.DELETE_PREVIOUS, IEmacsPlusCommandDefinitionIds.DELETE_NEXT); put(IEmacsPlusCommandDefinitionIds.DELETE_LINE, IEmacsPlusCommandDefinitionIds.BACKWARD_DELETE_LINE); put(IEmacsPlusCommandDefinitionIds.MOVE_LINES_UP, IEmacsPlusCommandDefinitionIds.MOVE_LINES_DOWN); put(IEmacsPlusCommandDefinitionIds.MOVE_LINES_DOWN, IEmacsPlusCommandDefinitionIds.MOVE_LINES_UP); put(IEmacsPlusCommandDefinitionIds.COLUMN_NEXT, IEmacsPlusCommandDefinitionIds.COLUMN_PREVIOUS); put(IEmacsPlusCommandDefinitionIds.COLUMN_PREVIOUS, IEmacsPlusCommandDefinitionIds.COLUMN_NEXT); put(IEmacsPlusCommandDefinitionIds.SCROLL_LINE_UP, IEmacsPlusCommandDefinitionIds.SCROLL_LINE_DOWN); put(IEmacsPlusCommandDefinitionIds.SCROLL_LINE_DOWN, IEmacsPlusCommandDefinitionIds.SCROLL_LINE_UP); put(IEmacsPlusCommandDefinitionIds.SHIFT_RIGHT, IEmacsPlusCommandDefinitionIds.SHIFT_LEFT); put(IEmacsPlusCommandDefinitionIds.SHIFT_LEFT, IEmacsPlusCommandDefinitionIds.SHIFT_RIGHT); } }; }