/******************************************************************************* * 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; //incastrix //import org.eclipse.jface.text.Assert; import org.eclipse.core.runtime.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: * *
uninstall()
is called.LinkedPositionManager
tries to gain
* control of the same document.
* 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:
*
* * There are the following constraints for linked positions: * *
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:
*
* 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) {
// PHPeclipsePlugin.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) {
PHPeclipsePlugin.log(e);
Assert.isTrue(false);
} catch (BadPositionCategoryException e) {
PHPeclipsePlugin.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) {
PHPeclipsePlugin.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) {
PHPeclipsePlugin.log(e);
}
}
}
}