/** * Copyright (c) 2009, 2010 Mark Feber, MulgaSoft * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * */ package com.mulgasoft.emacsplus; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.BadPositionCategoryException; import org.eclipse.jface.text.DefaultPositionUpdater; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.Position; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IEditorReference; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.part.MultiPageEditorPart; import org.eclipse.ui.texteditor.ITextEditor; /** * A Mark List to support Mark Ring operations * Use Eclipse Document positions/position category to auto update as text is changed * * @author Mark Feber - initial API and implementation */ public class MarkRing { /** The global ring of mark locations where a find-tag (i.e. open-declaration) was initiated */ private static MarkList tagMarks = new MarkList(RingBuffer.getDefaultSize() * 2); /** The global ring of mark locations used by the global mark methods */ private static MarkList globalMarks = new MarkList(RingBuffer.getDefaultSize() * 2); /** Local mark rings indexed by document */ private static Map<IDocument,MarkList> localMarks = new HashMap<IDocument,MarkList>(); public static final String MARK_CATEGORY = "Emacs+Mark"; //$NON-NLS-1$ static DefaultPositionUpdater updater = new DefaultPositionUpdater(MARK_CATEGORY) { @Override protected boolean notDeleted() { return true; } }; /** * Add the previous mark to the Mark Ring * * @param editor - the current editor * @param document - the editor's document for position updating * @param localMark - the previous mark location to save * @param globalMark - the new mark location; a potential global candidate */ public static void addMark(ITextEditor editor, IDocument document, int localMark, int globalMark) { MarkList local = localMarks.get(document); if (local == null) { local = new MarkList(RingBuffer.getDefaultSize()); localMarks.put(document, local); document.addPositionCategory(MARK_CATEGORY); document.addPositionUpdater(updater); } local.addMark(document, localMark); // check to see if we should save globally globalMarks.addMark(editor,document,globalMark,true); } /** * Remove the document's mark buffer from the local Mark list * Also, remove the position updating information from the document * * @param document */ static void removeMarks(IDocument document) { MarkList local = localMarks.remove(document); if (local != null || document.containsPositionCategory(MARK_CATEGORY)) { document.removePositionUpdater(updater); try { // this also removes all the positions document.removePositionCategory(MARK_CATEGORY); } catch (BadPositionCategoryException e) { } } } /** * Pop the position from the front of the mark buffer and append the * mark to the end * * @param document * @return a Position containing (mark offset, popped mark offset) or null */ public static Position popMark(IDocument document, int mark) { Position result = null; MarkList local = localMarks.get(document); if (local != null) { Position pos = local.popAppendMark(document, mark); result = new Position(mark,pos.getOffset()); } return result; } public static int getMarkOffset(IDocument document) { int result = -1; MarkList local = localMarks.get(document); if (local != null && !local.isEmpty()) { result = local.getFirst().getOffset(); } return result; } /** * Get the next position from the global mark buffer * * @return the 'popped' global mark position */ public static IBufferLocation popGlobalMark(boolean norotate) { return globalMarks.popMark(norotate); } /** * Get the next position from the tag buffer * * @return the 'popped' mark position */ public static IBufferLocation popTagMark() { return tagMarks.popMark(true); } /** * Add the location to the tag mark ring * * @param editor * @param document * @param mark */ public static void addTagMark(ITextEditor editor, IDocument document, int mark) { tagMarks.addMark(editor,document,mark,false); } @SuppressWarnings({ "serial" }) private static class MarkList extends LinkedList<IBufferLocation> { private int bufferSize = RingBuffer.getDefaultSize(); MarkList(int length) { this.bufferSize = length; } /** * Add Mark used by local mark ring * * @param document * @param mark * @return the newly created Position or null, if mark == -1 or other error */ Position addMark(IDocument document, int mark) { Position result = null; if (mark != -1) { // avoid needless duplication if (isEmpty() || (peek().getOffset() != mark && getLast().getOffset() != mark)) { try { result = new Position(mark); document.addPosition(MARK_CATEGORY, result); pushElement(document, new MarkElement(result)); } catch (BadLocationException e) { } catch (BadPositionCategoryException e) { } } } return result; } /** * Add Mark used by the global mark ring, which requires the editor * If the last global mark pushed is not in the editor. * * @param editor * @param document * @param mark * @return the newly created Position or null, if global add not required or other error */ Position addMark(ITextEditor editor, IDocument document, int mark, boolean checkIt) { Position globalPos = null; if (mark != -1 && (!checkIt || checkNewMark(editor))) { globalPos = new Position(mark); try { document.addPosition(MARK_CATEGORY, globalPos); } catch (BadLocationException e) { } catch (BadPositionCategoryException e) { } pushElement(document, new MarkElement(editor,globalPos)); } return globalPos; } /** * Add new element to the front of the ring. If the length is * exceeded, drop the last element * * @param document * @param element */ void pushElement(IDocument document, IBufferLocation element) { addFirst(element); if (size() > bufferSize) { IBufferLocation old = removeLast(); try { removeMarkPosition(document,((MarkElement)old).getPosition()); } catch (BadPositionCategoryException e) { } } } /** * Append the mark to the end of the buffer and pop the front * * @param document * @param mark * @return the popped position */ Position popAppendMark(IDocument document, int mark) { Position result = null; MarkElement element = null; if (mark != -1) { try { result = new Position(mark); document.addPosition(MARK_CATEGORY, result); element = new MarkElement(result); addLast(element); element = (MarkElement) removeFirst(); result = element.getPosition(); // remove from document as internal Mark code will update it removeMarkPosition(document, result); } catch (BadLocationException e) { } catch (BadPositionCategoryException e) { } } return result; } /** * Pop the mark location and push it on the end * used by global marks * * @return the popped location */ IBufferLocation popMark(boolean norotate) { IBufferLocation element = null; if (!isEmpty()) { element = (norotate ? removeFirst() : removeLast()); // are we still alive? if (checkEditor(element)) { // then add if (norotate) { addLast(element); } else { addFirst(element); } } else { element.setEditor(null); // keep popping element = popMark(norotate); } } return element; } /** * Check the front of the buffer * * @param editor * @return true if editor is not at the front of the buffer */ private boolean checkNewMark(ITextEditor editor) { boolean result = true; if (!isEmpty()) { result = editor != peek().getEditor(); } return result; } private void removeMarkPosition(IDocument document, Position mark) throws BadPositionCategoryException { document.removePosition(MARK_CATEGORY, mark); } /** * Verify that the editor in the location is still in use * * @param location * @return true if editor is valid */ private boolean checkEditor(IBufferLocation location) { boolean result = false; if (location != null) { ITextEditor editor = location.getEditor(); if (editor != null) { IEditorInput input = editor.getEditorInput(); // check all the editor references that match the input for a match IEditorReference[] refs = EmacsPlusUtils.getWorkbenchPage().findEditors(input,null, IWorkbenchPage.MATCH_INPUT); for (int i=0; i< refs.length; i++) { IEditorPart ed = refs[i].getEditor(false); // multi page annoyance if (ed instanceof MultiPageEditorPart) { IEditorPart[] eds = ((MultiPageEditorPart)ed).findEditors(input); for (int j=0; j < eds.length; j++) { if (eds[i] == editor) { result = true; break; } } if (result) { break; } } else { if (ed == editor) { result = true; break; } } } } } return result; } // for debugging @SuppressWarnings("unused") private void printMarks(String header) { int i = 0; System.out.println(header); for (IBufferLocation loc : this) { String ed = "\t"; //$NON-NLS-1$ if (loc.getEditor() != null) { ed = ed + loc.getEditor().getEditorInput().getName()+ ' '; } System.out.println(i++ + ed + '<' + loc.getOffset() + '>'); } } } static class MarkElement implements IBufferLocation { private Position position = null; private ITextEditor editor = null; MarkElement() {} MarkElement(Position position) { this.position = position; } MarkElement(ITextEditor editor, Position position) { this(position); this.editor = editor; } /** * @see com.mulgasoft.emacsplus.IBufferLocation#getEditor() */ public ITextEditor getEditor() { return editor; } /** * @see com.mulgasoft.emacsplus.IBufferLocation#getOffset() */ public int getOffset() { int result = -1; if (position != null) { result = position.getOffset(); } return result; } /** * @see com.mulgasoft.emacsplus.IBufferLocation#setEditor(org.eclipse.ui.texteditor.ITextEditor) */ public void setEditor(ITextEditor editor) { this.editor = editor; } /** * @see com.mulgasoft.emacsplus.IBufferLocation#setPosition(org.eclipse.ui.texteditor.ITextEditor, org.eclipse.jface.text.Position) */ public void setPosition(ITextEditor editor, Position position) { setEditor(editor); this.position = position; } public Position getPosition() { return position; } } }