X-Git-Url: http://secure.phpeclipse.com diff --git a/net.sourceforge.phpeclipse.ui/src/net/sourceforge/phpdt/internal/ui/text/link/LinkedPositionManager.java b/net.sourceforge.phpeclipse.ui/src/net/sourceforge/phpdt/internal/ui/text/link/LinkedPositionManager.java new file mode 100644 index 0000000..3a89914 --- /dev/null +++ b/net.sourceforge.phpeclipse.ui/src/net/sourceforge/phpdt/internal/ui/text/link/LinkedPositionManager.java @@ -0,0 +1,831 @@ +/******************************************************************************* + * Copyright (c) 2000, 2003 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Common Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/cpl-v10.html + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ + +package net.sourceforge.phpdt.internal.ui.text.link; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; + +//import net.sourceforge.phpeclipse.PHPeclipsePlugin; + +import net.sourceforge.phpeclipse.ui.WebUI; + +import org.eclipse.jface.text.Assert; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.BadPositionCategoryException; +import org.eclipse.jface.text.DocumentCommand; +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.jface.text.IAutoEditStrategy; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IDocumentExtension; +import org.eclipse.jface.text.IDocumentListener; +import org.eclipse.jface.text.IPositionUpdater; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.TypedPosition; +import org.eclipse.jface.text.contentassist.ICompletionProposal; + +/** + * This class manages linked positions in a document. Positions are linked by + * type names. If positions have the same type name, they are considered as + * linked. + * + * The manager remains active on a document until any of the following actions + * occurs: + * + * + */ +public class LinkedPositionManager implements IDocumentListener, + IPositionUpdater, IAutoEditStrategy { + + // This class still exists to properly handle code assist. + // This is due to the fact that it cannot be distinguished betweeen document + // changes which are + // issued by code assist and document changes which origin from another text + // viewer. + // There is a conflict in interest since in the latter case the linked mode + // should be left, but in the former case + // the linked mode should remain. + // To support content assist, document changes have to be propagated to + // connected positions + // by registering replace commands using IDocumentExtension. + // if it wasn't for the support of content assist, the documentChanged() + // method could be reduced to + // a simple call to leave(true) + private class Replace implements IDocumentExtension.IReplace { + + private Position fReplacePosition; + + private int fReplaceDeltaOffset; + + private int fReplaceLength; + + private String fReplaceText; + + public Replace(Position position, int deltaOffset, int length, + String text) { + fReplacePosition = position; + fReplaceDeltaOffset = deltaOffset; + fReplaceLength = length; + fReplaceText = text; + } + + public void perform(IDocument document, IDocumentListener owner) { + document.removeDocumentListener(owner); + try { + document.replace(fReplacePosition.getOffset() + + fReplaceDeltaOffset, fReplaceLength, fReplaceText); + } catch (BadLocationException e) { + WebUI.log(e); + // TBD + } + document.addDocumentListener(owner); + } + } + + private static class PositionComparator implements Comparator { + /* + * @see Comparator#compare(Object, Object) + */ + public int compare(Object object0, Object object1) { + Position position0 = (Position) object0; + Position position1 = (Position) object1; + + return position0.getOffset() - position1.getOffset(); + } + } + + private static final String LINKED_POSITION_PREFIX = "LinkedPositionManager.linked.position"; //$NON-NLS-1$ + + private static final Comparator fgPositionComparator = new PositionComparator(); + + private static final Map fgActiveManagers = new HashMap(); + + private static int fgCounter = 0; + + private IDocument fDocument; + + private ILinkedPositionListener fListener; + + private String fPositionCategoryName; + + private boolean fMustLeave; + + /** + * Flag that records the state of this manager. As there are many different + * entities that may call leave or exit, these cannot always be sure whether + * the linked position infrastructure is still active. This is especially + * true for multithreaded situations. + */ + private boolean fIsActive = false; + + /** + * Creates a LinkedPositionManager for a + * IDocument. + * + * @param document + * the document to use with linked positions. + * @param canCoexist + * true if this manager can coexist with an + * already existing one + */ + public LinkedPositionManager(IDocument document, boolean canCoexist) { + Assert.isNotNull(document); + fDocument = document; + fPositionCategoryName = LINKED_POSITION_PREFIX + (fgCounter++); + install(canCoexist); + } + + /** + * Creates a LinkedPositionManager for a + * IDocument. + * + * @param document + * the document to use with linked positions. + */ + public LinkedPositionManager(IDocument document) { + this(document, false); + } + + /** + * Sets a listener to notify changes of current linked position. + */ + public void setLinkedPositionListener(ILinkedPositionListener listener) { + fListener = listener; + } + + /** + * Adds a linked position to the manager with the type being the content of + * the document at the specified range. There are the following constraints + * for linked positions: + * + * + * + * @param offset + * the offset of the position. + * @param length + * the length of the position. + */ + public void addPosition(int offset, int length) throws BadLocationException { + String type = fDocument.get(offset, length); + addPosition(offset, length, type); + } + + /** + * Adds a linked position of the specified position type to the manager. + * There are the following constraints for linked positions: + * + * + * + * @param offset + * the offset of the position. + * @param length + * the length of the position. + * @param type + * the position type name - any positions with the same type are + * linked. + */ + public void addPosition(int offset, int length, String type) + throws BadLocationException { + Position[] positions = getPositions(fDocument); + + if (positions != null) { + for (int i = 0; i < positions.length; i++) + if (collides(positions[i], offset, length)) + throw new BadLocationException( + LinkedPositionMessages + .getString(("LinkedPositionManager.error.position.collision"))); //$NON-NLS-1$ + } + + String content = fDocument.get(offset, length); + + if (containsLineDelimiters(content)) + throw new BadLocationException( + LinkedPositionMessages + .getString(("LinkedPositionManager.error.contains.line.delimiters"))); //$NON-NLS-1$ + + try { + fDocument.addPosition(fPositionCategoryName, new TypedPosition( + offset, length, type)); + } catch (BadPositionCategoryException e) { + WebUI.log(e); + Assert.isTrue(false); + } + } + + /** + * Adds a linked position to the manager. The current document content at + * the specified range is taken as the position type. + *

+ * There are the following constraints for linked positions: + * + *

+ * + * It is usually best to set the first item in + * additionalChoices to be equal with the text inserted at + * the current position. + *

+ * + * @param offset + * the offset of the position. + * @param length + * the length of the position. + * @param additionalChoices + * a number of additional choices to be displayed when selecting + * a position of this type. + */ + public void addPosition(int offset, int length, + ICompletionProposal[] additionalChoices) + throws BadLocationException { + String type = fDocument.get(offset, length); + addPosition(offset, length, type, additionalChoices); + } + + /** + * Adds a linked position of the specified position type to the manager. + * There are the following constraints for linked positions: + * + * + * + * It is usually best to set the first item in + * additionalChoices to be equal with the text inserted at + * the current position. + * + * @param offset + * the offset of the position. + * @param length + * the length of the position. + * @param type + * the position type name - any positions with the same type are + * linked. + * @param additionalChoices + * a number of additional choices to be displayed when selecting + * a position of this type. + */ + public void addPosition(int offset, int length, String type, + ICompletionProposal[] additionalChoices) + throws BadLocationException { + Position[] positions = getPositions(fDocument); + + if (positions != null) { + for (int i = 0; i < positions.length; i++) + if (collides(positions[i], offset, length)) + throw new BadLocationException( + LinkedPositionMessages + .getString(("LinkedPositionManager.error.position.collision"))); //$NON-NLS-1$ + } + + String content = fDocument.get(offset, length); + + if (containsLineDelimiters(content)) + throw new BadLocationException( + LinkedPositionMessages + .getString(("LinkedPositionManager.error.contains.line.delimiters"))); //$NON-NLS-1$ + + try { + fDocument.addPosition(fPositionCategoryName, new ProposalPosition( + offset, length, type, additionalChoices)); + } catch (BadPositionCategoryException e) { + WebUI.log(e); + Assert.isTrue(false); + } + } + + /** + * Tests if a manager is already active for a document. + */ + public static boolean hasActiveManager(IDocument document) { + return fgActiveManagers.get(document) != null; + } + + private void install(boolean canCoexist) { + + if (fIsActive) + ;// JavaPlugin.log(new Status(IStatus.WARNING, + // JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager + // is already active: "+fPositionCategoryName, new + // IllegalStateException())); //$NON-NLS-1$ + else { + fIsActive = true; + // JavaPlugin.log(new Status(IStatus.INFO, JavaPlugin.getPluginId(), + // IStatus.OK, "LinkedPositionManager activated: + // "+fPositionCategoryName, new Exception())); //$NON-NLS-1$ + } + + if (!canCoexist) { + LinkedPositionManager manager = (LinkedPositionManager) fgActiveManagers + .get(fDocument); + if (manager != null) + manager.leave(true); + } + + fgActiveManagers.put(fDocument, this); + fDocument.addPositionCategory(fPositionCategoryName); + fDocument.addPositionUpdater(this); + fDocument.addDocumentListener(this); + + fMustLeave = false; + } + + /** + * Leaves the linked mode. If unsuccessful, the linked positions are + * restored to the values at the time they were added. + */ + public void uninstall(boolean success) { + + if (!fIsActive) + // we migth also just return + ;// JavaPlugin(new Status(IStatus.WARNING, + // JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager + // activated: "+fPositionCategoryName, new + // IllegalStateException())); //$NON-NLS-1$ + else { + fDocument.removeDocumentListener(this); + + try { + Position[] positions = getPositions(fDocument); + if ((!success) && (positions != null)) { + // restore + for (int i = 0; i != positions.length; i++) { + TypedPosition position = (TypedPosition) positions[i]; + fDocument.replace(position.getOffset(), position + .getLength(), position.getType()); + } + } + + fDocument.removePositionCategory(fPositionCategoryName); + + fIsActive = false; + // JavaPlugin.log(new Status(IStatus.INFO, + // JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager + // deactivated: "+fPositionCategoryName, new Exception())); + // //$NON-NLS-1$ + + } catch (BadLocationException e) { + WebUI.log(e); + Assert.isTrue(false); + + } catch (BadPositionCategoryException e) { + WebUI.log(e); + Assert.isTrue(false); + + } finally { + fDocument.removePositionUpdater(this); + fgActiveManagers.remove(fDocument); + } + } + + } + + /** + * Returns the position at the given offset, null if there is + * no position. + * + * @since 2.1 + */ + public Position getPosition(int offset) { + Position[] positions = getPositions(fDocument); + if (positions == null) + return null; + + for (int i = positions.length - 1; i >= 0; i--) { + Position position = positions[i]; + if (offset >= position.getOffset() + && offset <= position.getOffset() + position.getLength()) + return positions[i]; + } + + return null; + } + + /** + * Returns the first linked position. + * + * @return returns null if no linked position exist. + */ + public Position getFirstPosition() { + return getNextPosition(-1); + } + + public Position getLastPosition() { + Position[] positions = getPositions(fDocument); + for (int i = positions.length - 1; i >= 0; i--) { + String type = ((TypedPosition) positions[i]).getType(); + int j; + for (j = 0; j != i; j++) + if (((TypedPosition) positions[j]).getType().equals(type)) + break; + + if (j == i) + return positions[i]; + } + + return null; + } + + /** + * Returns the next linked position with an offset greater than + * offset. If another position with the same type and offset + * lower than offset exists, the position is skipped. + * + * @return returns null if no linked position exist. + */ + public Position getNextPosition(int offset) { + Position[] positions = getPositions(fDocument); + return findNextPosition(positions, offset); + } + + private static Position findNextPosition(Position[] positions, int offset) { + // skip already visited types + for (int i = 0; i != positions.length; i++) { + if (positions[i].getOffset() > offset) { + String type = ((TypedPosition) positions[i]).getType(); + int j; + for (j = 0; j != i; j++) + if (((TypedPosition) positions[j]).getType().equals(type)) + break; + + if (j == i) + return positions[i]; + } + } + + return null; + } + + /** + * Returns the position with the greatest offset smaller than + * offset. + * + * @return returns null if no linked position exist. + */ + public Position getPreviousPosition(int offset) { + Position[] positions = getPositions(fDocument); + if (positions == null) + return null; + + TypedPosition currentPosition = (TypedPosition) findCurrentPosition( + positions, offset); + String currentType = currentPosition == null ? null : currentPosition + .getType(); + + Position lastPosition = null; + Position position = getFirstPosition(); + + while (position != null && position.getOffset() < offset) { + if (!((TypedPosition) position).getType().equals(currentType)) + lastPosition = position; + position = findNextPosition(positions, position.getOffset()); + } + + return lastPosition; + } + + private Position[] getPositions(IDocument document) { + + if (!fIsActive) + // we migth also just return an empty array + ;// JavaPlugin(new Status(IStatus.WARNING, + // JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager + // is not active: "+fPositionCategoryName, new + // IllegalStateException())); //$NON-NLS-1$ + + try { + Position[] positions = document.getPositions(fPositionCategoryName); + Arrays.sort(positions, fgPositionComparator); + return positions; + + } catch (BadPositionCategoryException e) { + WebUI.log(e); + Assert.isTrue(false); + } + + return null; + } + + public static boolean includes(Position position, int offset, int length) { + return (offset >= position.getOffset()) + && (offset + length <= position.getOffset() + + position.getLength()); + } + + public static boolean excludes(Position position, int offset, int length) { + return (offset + length <= position.getOffset()) + || (position.getOffset() + position.getLength() <= offset); + } + + /* + * Collides if spacing if positions intersect each other or are adjacent. + */ + private static boolean collides(Position position, int offset, int length) { + return (offset <= position.getOffset() + position.getLength()) + && (position.getOffset() <= offset + length); + } + + private void leave(boolean success) { + try { + uninstall(success); + + if (fListener != null) + fListener.exit((success ? LinkedPositionUI.COMMIT : 0) + | LinkedPositionUI.UPDATE_CARET); + } finally { + fMustLeave = false; + } + } + + private void abort() { + uninstall(true); // don't revert anything + + if (fListener != null) + fListener.exit(LinkedPositionUI.COMMIT); // don't let the UI + // restore anything + + // don't set fMustLeave, as we will get re-registered by a document + // event + } + + /* + * @see IDocumentListener#documentAboutToBeChanged(DocumentEvent) + */ + public void documentAboutToBeChanged(DocumentEvent event) { + + if (fMustLeave) { + event.getDocument().removeDocumentListener(this); + return; + } + + IDocument document = event.getDocument(); + + Position[] positions = getPositions(document); + Position position = findCurrentPosition(positions, event.getOffset()); + + // modification outside editable position + if (position == null) { + // check for destruction of constraints (spacing of at least 1) + if ((event.getText() == null || event.getText().length() == 0) + && (findCurrentPosition(positions, event.getOffset()) != null) + && // will never become true, see condition above + (findCurrentPosition(positions, event.getOffset() + + event.getLength()) != null)) { + leave(true); + } + + // modification intersects editable position + } else { + // modificaction inside editable position + if (includes(position, event.getOffset(), event.getLength())) { + if (containsLineDelimiters(event.getText())) + leave(true); + + // modificaction exceeds editable position + } else { + leave(true); + } + } + } + + /* + * @see IDocumentListener#documentChanged(DocumentEvent) + */ + public void documentChanged(DocumentEvent event) { + + // have to handle code assist, so can't just leave the linked mode + // leave(true); + + IDocument document = event.getDocument(); + + Position[] positions = getPositions(document); + TypedPosition currentPosition = (TypedPosition) findCurrentPosition( + positions, event.getOffset()); + + // ignore document changes (assume it won't invalidate constraints) + if (currentPosition == null) + return; + + int deltaOffset = event.getOffset() - currentPosition.getOffset(); + + if (fListener != null) { + int length = event.getText() == null ? 0 : event.getText().length(); + fListener.setCurrentPosition(currentPosition, deltaOffset + length); + } + + for (int i = 0; i != positions.length; i++) { + TypedPosition p = (TypedPosition) positions[i]; + + if (p.getType().equals(currentPosition.getType()) + && !p.equals(currentPosition)) { + Replace replace = new Replace(p, deltaOffset, + event.getLength(), event.getText()); + ((IDocumentExtension) document) + .registerPostNotificationReplace(this, replace); + } + } + } + + /* + * @see IPositionUpdater#update(DocumentEvent) + */ + public void update(DocumentEvent event) { + + int eventOffset = event.getOffset(); + int eventOldLength = event.getLength(); + int eventNewLength = event.getText() == null ? 0 : event.getText() + .length(); + int deltaLength = eventNewLength - eventOldLength; + + Position[] positions = getPositions(event.getDocument()); + + for (int i = 0; i != positions.length; i++) { + + Position position = positions[i]; + + if (position.isDeleted()) + continue; + + int offset = position.getOffset(); + int length = position.getLength(); + int end = offset + length; + + if (offset > eventOffset + eventOldLength) // position comes way + // after change - shift + position.setOffset(offset + deltaLength); + else if (end < eventOffset) // position comes way before change - + // leave alone + ; + else if (offset <= eventOffset + && end >= eventOffset + eventOldLength) { + // event completely internal to the position - adjust length + position.setLength(length + deltaLength); + } else if (offset < eventOffset) { + // event extends over end of position - adjust length + int newEnd = eventOffset + eventNewLength; + position.setLength(newEnd - offset); + } else if (end > eventOffset + eventOldLength) { + // event extends from before position into it - adjust offset + // and length + // offset becomes end of event, length ajusted acordingly + // we want to recycle the overlapping part + int newOffset = eventOffset + eventNewLength; + position.setOffset(newOffset); + position.setLength(length + deltaLength); + } else { + // event consumes the position - delete it + position.delete(); + // JavaPlugin.log(new Status(IStatus.INFO, + // JavaPlugin.getPluginId(), IStatus.OK, "linked position + // deleted -> must leave: "+fPositionCategoryName, null)); + // //$NON-NLS-1$ + fMustLeave = true; + } + } + + if (fMustLeave) + abort(); + } + + private static Position findCurrentPosition(Position[] positions, int offset) { + for (int i = 0; i != positions.length; i++) + if (includes(positions[i], offset, 0)) + return positions[i]; + + return null; + } + + private boolean containsLineDelimiters(String string) { + + if (string == null) + return false; + + String[] delimiters = fDocument.getLegalLineDelimiters(); + + for (int i = 0; i != delimiters.length; i++) + if (string.indexOf(delimiters[i]) != -1) + return true; + + return false; + } + + /** + * Test if ok to modify through UI. + */ + public boolean anyPositionIncludes(int offset, int length) { + Position[] positions = getPositions(fDocument); + + Position position = findCurrentPosition(positions, offset); + if (position == null) + return false; + + return includes(position, offset, length); + } + + /** + * Returns the position that includes the given range. + * + * @param offset + * @param length + * @return position that includes the given range + */ + public Position getEmbracingPosition(int offset, int length) { + Position[] positions = getPositions(fDocument); + + Position position = findCurrentPosition(positions, offset); + if (position != null && includes(position, offset, length)) + return position; + + return null; + } + + /* + * @see org.eclipse.jface.text.IAutoIndentStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument, + * org.eclipse.jface.text.DocumentCommand) + */ + public void customizeDocumentCommand(IDocument document, + DocumentCommand command) { + + if (fMustLeave) { + leave(true); + return; + } + + // don't interfere with preceding auto edit strategies + if (command.getCommandCount() != 1) { + leave(true); + return; + } + + Position[] positions = getPositions(document); + TypedPosition currentPosition = (TypedPosition) findCurrentPosition( + positions, command.offset); + + // handle edits outside of a position + if (currentPosition == null) { + leave(true); + return; + } + + if (!command.doit) + return; + + command.doit = false; + command.owner = this; + command.caretOffset = command.offset + command.length; + + int deltaOffset = command.offset - currentPosition.getOffset(); + + if (fListener != null) + fListener.setCurrentPosition(currentPosition, deltaOffset + + command.text.length()); + + for (int i = 0; i != positions.length; i++) { + TypedPosition position = (TypedPosition) positions[i]; + + try { + if (position.getType().equals(currentPosition.getType()) + && !position.equals(currentPosition)) + command.addCommand(position.getOffset() + deltaOffset, + command.length, command.text, this); + } catch (BadLocationException e) { + WebUI.log(e); + } + } + } + +}