/** * 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 org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.commands.common.CommandException; import org.eclipse.jface.preference.PreferenceConverter; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextSelection; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.LineBackgroundEvent; import org.eclipse.swt.custom.LineBackgroundListener; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.console.TextConsoleViewer; import org.eclipse.ui.internal.WorkbenchWindow; import org.eclipse.ui.texteditor.ITextEditor; import com.mulgasoft.emacsplus.EmacsPlusActivator; import com.mulgasoft.emacsplus.EmacsPlusUtils; import com.mulgasoft.emacsplus.IEmacsPlusCommandDefinitionIds; import com.mulgasoft.emacsplus.KillRing; import com.mulgasoft.emacsplus.MarkUtils; import com.mulgasoft.emacsplus.RingBuffer.IRingBufferElement; import com.mulgasoft.emacsplus.execute.EmacsPlusConsole; import com.mulgasoft.emacsplus.execute.IEmacsPlusConsoleKey; import com.mulgasoft.emacsplus.preferences.EmacsPlusPreferenceConstants; /** * Browse the kill ring in the Emacs+ console window * Background highlight alternates on alternate entries * * Supports sub-commands: * i - insert * y - insert * <SPC> - insert * <RET> - insert and quit (activate text editor) * n - next entry * p - previous entry * g - refresh kill ring display * q - quit (activate text editor) * U - undo last operation in the text editor * * @author Mark Feber - initial API and implementation */ @SuppressWarnings("restriction") // for import and cast to internal org.eclipse.ui.internal.WorkbenchWindow public class BrowseKillRingHandler extends EmacsPlusNoEditHandler implements LineBackgroundListener, IEmacsPlusConsoleKey { private final static String KR_CONSOLE= EmacsPlusActivator.getResourceString("KillRing_Console"); //$NON-NLS-1$ private final static int OFF_COLOR = SWT.COLOR_LIST_BACKGROUND; private static RGB highlightColor = new RGB(237,237,252); private HashMap<Integer,KilledText> offsetHash; KilledText[] ringEntries = null; // Unfortunately, IAutoEditStrategy doesn't work on IOConsoles AFAIK public BrowseKillRingHandler() { super(); BrowseKillRingHandler.setHighlightColor(PreferenceConverter.getColor(EmacsPlusActivator.getDefault().getPreferenceStore(), EmacsPlusPreferenceConstants.P_AUTO_BROWSE_HIGHLIGHT)); } /** * @see com.mulgasoft.emacsplus.commands.EmacsPlusNoEditHandler#transform(ITextEditor, IDocument, ITextSelection, ExecutionEvent) */ @Override protected int transform(ITextEditor editor, IDocument document, ITextSelection currentSelection, ExecutionEvent event) throws BadLocationException { updateBrowseRing(); return super.transform(editor, document, currentSelection, event); } /** * @see com.mulgasoft.emacsplus.execute.IEmacsPlusConsoleKey#handleKey(org.eclipse.swt.events.VerifyEvent, org.eclipse.ui.console.TextConsoleViewer) */ public void handleKey(VerifyEvent event, TextConsoleViewer viewer) { // mask away any extraneous modifier characters for any direct equality tests. see SWT.MODIFIER_MASK int sm = event.stateMask & SWT.MODIFIER_MASK; if (viewer != null && (sm == 0 || sm == SWT.SHIFT)) {// && 'i' == event.character) { int offset; boolean reactivate = false; switch (event.character){ case 'i': // insert case 'y': case ' ': reactivate = true; case '\r': case '\n': event.doit = false; offset = getLineOffset(viewer); if (offset >= 0) { KilledText kill = offsetHash.get(offset); if (kill != null) { insertFromBrowseRing(kill.text); } } if (reactivate) { EmacsPlusConsole.getInstance().setFocus(false); } break; case 'n': // next event.doit = false; browseRing(viewer,FORWARD); break; case 'p': // previous event.doit = false; browseRing(viewer,BACKWARD); break; case 'g': // refresh event.doit = false; updateBrowseRing(); break; case 'q': // quit activateEditor(); break; case 'U': // undo event.doit = false; undoEditor(); break; } } } /** * Create or update console view of the kill ring */ private void updateBrowseRing() { final KillRing kr = KillRing.getInstance(); Display display = PlatformUI.getWorkbench().getDisplay(); final Color onColor = new Color(display,highlightColor); final Color offColor = display.getSystemColor(OFF_COLOR); cleanup(); if (!kr.isEmpty()) { final EmacsPlusConsole console = EmacsPlusConsole.getInstance(); final BrowseKillRingHandler handler = this; console.clear(); console.activate(); console.setName(KR_CONSOLE); offsetHash = new HashMap<Integer,KilledText>(); // run asynchronously to ensure widget has been set up EmacsPlusUtils.asyncUiRun(new Runnable() { public void run() { int count = kr.length(); ringEntries = new KilledText[count]; boolean flip = false; int plen=0,len = 0; Color color = offColor; console.addBackground(handler); console.setKeyHandler(handler); IRingBufferElement<String> e = kr.yankElement(); for (int i = 0; i < count; i++) { String text = e.get(); int tlen = text.length(); KilledText kt = new KilledText(text,color); offsetHash.put(len,kt); ringEntries[i]=kt; kt.begin = plen; kt.end = plen + tlen; plen = kt.end + 1; console.print(text + (i == count-1 ? EMPTY_STR : CR)); if (flip = !flip) { color = onColor; } else { color = offColor; } e = kr.rotateYankPos(); len += 1 + tlen; } console.setFocus(false); }}); } } /** * Insert text from kill ring entry into the most recently activated text editor * * @param text - the text from the kill ring entry */ // @SuppressWarnings("restriction") // for cast to internal org.eclipse.ui.internal.WorkbenchWindow private void insertFromBrowseRing(String text) { // insert into most recently active editor IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); RecentEditor recent = getRecentEditor(); // use widget to avoid unpleasant scrolling side effects of IRewriteTarget Control widget = MarkUtils.getTextWidget(recent.editor); if (recent.editor != null) { try { // cache for ancillary computations setThisEditor(recent.editor); // reduce the amount of unnecessary work if (window instanceof WorkbenchWindow) { ((WorkbenchWindow) window).largeUpdateStart(); } widget.setRedraw(false); recent.page.activate(recent.epart); insertText(recent.editor.getDocumentProvider().getDocument(recent.editor.getEditorInput()), (ITextSelection)recent.editor.getSelectionProvider().getSelection(), text); } catch (Exception e) { } finally { widget.setRedraw(true); setThisEditor(null); if (window instanceof WorkbenchWindow) { ((WorkbenchWindow) window).largeUpdateEnd(); } } } else { beep(); } } /** * Undo the last command in the the most recently activated text editor */ private void undoEditor() { RecentEditor recent = getRecentEditor(); if (recent != null) { try { setThisEditor(recent.editor); recent.page.activate(recent.epart); this.executeCommand(IEmacsPlusCommandDefinitionIds.EMP_UNDO, null, recent.editor); } catch (ExecutionException e) { } catch (CommandException e) { } finally { setThisEditor(null); EmacsPlusConsole.getInstance().setFocus(false); } } } /** * Activate the most recently activated text editor */ private void activateEditor() { RecentEditor recent = getRecentEditor(); if (recent != null) { try { setThisEditor(recent.editor); recent.page.activate(recent.epart); } finally { setThisEditor(null); } } } /** * Get the most recent activated text editor * * @return editor and activation info */ private RecentEditor getRecentEditor() { ITextEditor result = null; IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); IWorkbenchPage page = window.getActivePage(); IEditorPart epart = page.getActiveEditor(); result = (ITextEditor)(epart != null ? epart.getAdapter(ITextEditor.class) : epart); return new RecentEditor(result,page,epart); } /** * Navigate up or down the ring entry by entry * * @param viewer the viewer on the console * @param dir FORWARD or BACKWARD */ private void browseRing(TextConsoleViewer viewer, int dir) { IDocument doc = viewer.getDocument(); StyledText st = viewer.getTextWidget(); if (doc != null && st != null) { int lines = doc.getNumberOfLines(); int off = st.getCaretOffset(); try { int l = doc.getLineOfOffset(off); KilledText okill = offsetHash.get(doc.getLineOffset(l)); KilledText nkill = null; int noff = -1; while ((l = l+dir) > -1 && l < lines){ off = doc.getLineOffset(l); KilledText tkill = offsetHash.get(off); if (nkill == null) { if (tkill != null && tkill != okill) { nkill = offsetHash.get(off); noff = off; if (dir == FORWARD) { break; } } } else { if (tkill != null && tkill != nkill){ break; } else { noff = off; } } } if (noff > -1) { st.setCaretOffset(noff); viewer.revealRange(noff, 0); } }catch (BadLocationException e) { } } } /** * Get the line offset of the cursor * * @param viewer the console viewer * @return the line offset */ private int getLineOffset(TextConsoleViewer viewer) { int result = -1; IDocument doc = viewer.getDocument(); StyledText st = viewer.getTextWidget(); if (doc != null && st != null) { int off = st.getCaretOffset(); try { IRegion info = doc.getLineInformationOfOffset(off); result = info.getOffset(); }catch (BadLocationException e) { } } return result; } /** * Return the correct background highlight for the kill ring entry line offset * * @see org.eclipse.swt.custom.LineBackgroundListener#lineGetBackground(org.eclipse.swt.custom.LineBackgroundEvent) */ public void lineGetBackground(LineBackgroundEvent event) { KilledText kt = offsetHash.get(event.lineOffset); if (kt == null) { for (KilledText k : ringEntries) { if (event.lineOffset >= k.begin && event.lineOffset <= k.end) { offsetHash.put(event.lineOffset, k); kt = k; break; } } } if (kt != null) { event.lineBackground = kt.color; } } /** * Utility class to wrap the necessary kill ring entry information * * @author Mark Feber - initial API and implementation */ private class KilledText { int begin = 0; int end = 0; Color color; String text; KilledText(String text, Color color) { this.color = color; this.text = text; } } /** * Utility class to wrap recent editor and activation information * * @author Mark Feber - initial API and implementation */ private class RecentEditor { ITextEditor editor; IWorkbenchPage page; IEditorPart epart; RecentEditor(ITextEditor editor,IWorkbenchPage page, IEditorPart epart) { this.editor = editor; this.page = page; this.epart = epart; } } void cleanup() { ringEntries = null; offsetHash = null; } /** * Set the alternate highlight color from preferences * * @param color RGB from preference setting */ public static void setHighlightColor(RGB color) { if (color != null) { highlightColor = color; } } }