2  * (c) Copyright IBM Corp. 2000, 2001.
 
   5 package net.sourceforge.phpdt.internal.ui.text.link;
 
   7 import java.util.Arrays;
 
   8 import java.util.Comparator;
 
   9 import java.util.HashMap;
 
  12 import net.sourceforge.phpeclipse.PHPeclipsePlugin;
 
  13 import org.eclipse.jface.text.BadLocationException;
 
  14 import org.eclipse.jface.text.BadPositionCategoryException;
 
  15 import org.eclipse.jface.text.DocumentEvent;
 
  16 import org.eclipse.jface.text.IDocument;
 
  17 import org.eclipse.jface.text.IDocumentExtension;
 
  18 import org.eclipse.jface.text.IDocumentListener;
 
  19 import org.eclipse.jface.text.IPositionUpdater;
 
  20 import org.eclipse.jface.text.Position;
 
  21 import org.eclipse.jface.text.TypedPosition;
 
  22 import org.eclipse.jface.util.Assert;
 
  24 //import org.eclipse.jdt.internal.ui.JavaPlugin;
 
  27  * This class manages linked positions in a document. Positions are linked
 
  28  * by type names. If positions have the same type name, they are considered
 
  31  * The manager remains active on a document until any of the following actions
 
  35  *   <li>A document change is performed which would invalidate any of the
 
  36  *       above constraints.</li>
 
  38  *   <li>The method <code>uninstall()</code> is called.</li>
 
  40  *   <li>Another instance of <code>LinkedPositionManager</code> tries to
 
  41  *       gain control of the same document.
 
  44 public class LinkedPositionManager implements IDocumentListener, IPositionUpdater {
 
  46         private static class PositionComparator implements Comparator {
 
  48                  * @see Comparator#compare(Object, Object)
 
  50                 public int compare(Object object0, Object object1) {
 
  51                         Position position0= (Position) object0;
 
  52                         Position position1= (Position) object1;
 
  54                         return position0.getOffset() - position1.getOffset();
 
  58         private class Replace implements IDocumentExtension.IReplace {
 
  60                 private Position fReplacePosition;
 
  61                 private int fReplaceDeltaOffset;
 
  62                 private int fReplaceLength;
 
  63                 private String fReplaceText;
 
  65                 public Replace(Position position, int deltaOffset, int length, String text) {
 
  66                         fReplacePosition= position;
 
  67                         fReplaceDeltaOffset= deltaOffset;
 
  68                         fReplaceLength= length;
 
  72                 public void perform(IDocument document, IDocumentListener owner) {
 
  73                         document.removeDocumentListener(owner);
 
  75                                 document.replace(fReplacePosition.getOffset() + fReplaceDeltaOffset, fReplaceLength, fReplaceText);
 
  76                         } catch (BadLocationException e) {
 
  77                                 PHPeclipsePlugin.log(e);
 
  80                         document.addDocumentListener(owner);
 
  84         private static final String LINKED_POSITION= "LinkedPositionManager.linked.position"; //$NON-NLS-1$
 
  85         private static final Comparator fgPositionComparator= new PositionComparator();
 
  86         private static final Map fgActiveManagers= new HashMap();
 
  88         private IDocument fDocument;
 
  90         private LinkedPositionListener fListener;
 
  93          * Creates a <code>LinkedPositionManager</code> for a <code>IDocument</code>.
 
  95          * @param document the document to use with linked positions.
 
  97         public LinkedPositionManager(IDocument document) {
 
  98                 Assert.isNotNull(document);
 
 105          * Sets a listener to notify changes of current linked position.
 
 107         public void setLinkedPositionListener(LinkedPositionListener listener) {
 
 112          * Adds a linked position to the manager.
 
 113          * There are the following constraints for linked positions:
 
 116          *   <li>Any two positions have spacing of at least one character.
 
 117          *       This implies that two positions must not overlap.</li>
 
 119          *   <li>The string at any position must not contain line delimiters.</li>
 
 122          * @param offset the offset of the position.
 
 123          * @param length the length of the position.
 
 125         public void addPosition(int offset, int length) throws BadLocationException {
 
 126                 Position[] positions= getPositions(fDocument);
 
 128                 if (positions != null) {
 
 129                         for (int i = 0; i < positions.length; i++)
 
 130                                 if (collides(positions[i], offset, length))
 
 131                                         throw new BadLocationException(LinkedPositionMessages.getString(("LinkedPositionManager.error.position.collision"))); //$NON-NLS-1$
 
 134                 String type= fDocument.get(offset, length);             
 
 136                 if (containsLineDelimiters(type))
 
 137                         throw new BadLocationException(LinkedPositionMessages.getString(("LinkedPositionManager.error.contains.line.delimiters"))); //$NON-NLS-1$
 
 140                         fDocument.addPosition(LINKED_POSITION, new TypedPosition(offset, length, type));
 
 141                 } catch (BadPositionCategoryException e) {
 
 142                         PHPeclipsePlugin.log(e);
 
 143                         Assert.isTrue(false);
 
 148          * Tests if a manager is already active for a document.
 
 150         public static boolean hasActiveManager(IDocument document) {
 
 151                 return fgActiveManagers.get(document) != null;
 
 154         private void install() {
 
 155                 LinkedPositionManager manager= (LinkedPositionManager) fgActiveManagers.get(fDocument);
 
 159                 fgActiveManagers.put(fDocument, this);
 
 161                 fDocument.addPositionCategory(LINKED_POSITION);
 
 162                 fDocument.addPositionUpdater(this);             
 
 163                 fDocument.addDocumentListener(this);
 
 167          * Leaves the linked mode. If unsuccessful, the linked positions
 
 168          * are restored to the values at the time they were added.
 
 170         public void uninstall(boolean success) {                        
 
 171                 fDocument.removeDocumentListener(this);
 
 174                         Position[] positions= getPositions(fDocument);  
 
 175                         if ((!success) && (positions != null)) {
 
 177                                 for (int i= 0; i != positions.length; i++) {
 
 178                                         TypedPosition position= (TypedPosition) positions[i];                           
 
 179                                         fDocument.replace(position.getOffset(), position.getLength(), position.getType());
 
 183                         fDocument.removePositionCategory(LINKED_POSITION);
 
 185                 } catch (BadLocationException e) {
 
 186                         PHPeclipsePlugin.log(e);
 
 187                         Assert.isTrue(false);
 
 189                 } catch (BadPositionCategoryException e) {
 
 190                         PHPeclipsePlugin.log(e);
 
 191                         Assert.isTrue(false);
 
 194                         fDocument.removePositionUpdater(this);          
 
 195                         fgActiveManagers.remove(fDocument);             
 
 200          * Returns the first linked position.
 
 202          * @return returns <code>null</code> if no linked position exist.
 
 204         public Position getFirstPosition() {
 
 205                 return getNextPosition(-1);
 
 209          * Returns the next linked position with an offset greater than <code>offset</code>.
 
 210          * If another position with the same type and offset lower than <code>offset</code>
 
 211          * exists, the position is skipped.
 
 213          * @return returns <code>null</code> if no linked position exist.
 
 215         public Position getNextPosition(int offset) {
 
 216                 Position[] positions= getPositions(fDocument);
 
 217                 return findNextPosition(positions, offset);
 
 220         private static Position findNextPosition(Position[] positions, int offset) {
 
 221                 // skip already visited types
 
 222                 for (int i= 0; i != positions.length; i++) {                    
 
 223                         if (positions[i].getOffset() > offset) {
 
 224                                 String type= ((TypedPosition) positions[i]).getType();
 
 226                                 for (j = 0; j != i; j++)
 
 227                                         if (((TypedPosition) positions[j]).getType().equals(type))
 
 239          * Returns the position with the greatest offset smaller than <code>offset</code>.
 
 241          * @return returns <code>null</code> if no linked position exist.
 
 243         public Position getPreviousPosition(int offset) {
 
 244                 Position[] positions= getPositions(fDocument);          
 
 245                 if (positions == null)
 
 248                 Position lastPosition= null;
 
 249                 Position position= getFirstPosition();
 
 251                 while ((position != null) && (position.getOffset() < offset)) {
 
 252                         lastPosition= position;
 
 253                         position= findNextPosition(positions, position.getOffset());
 
 259         private static Position[] getPositions(IDocument document) {
 
 261                         Position[] positions= document.getPositions(LINKED_POSITION);
 
 262                         Arrays.sort(positions, fgPositionComparator);
 
 265                 } catch (BadPositionCategoryException e) {
 
 266                         PHPeclipsePlugin.log(e);
 
 267                         Assert.isTrue(false);
 
 273         public static boolean includes(Position position, int offset, int length) {
 
 275                         (offset >= position.getOffset()) &&
 
 276                         (offset + length <= position.getOffset() + position.getLength());
 
 279         public static boolean excludes(Position position, int offset, int length) {
 
 281                         (offset + length <= position.getOffset()) ||
 
 282                         (position.getOffset() + position.getLength() <= offset);
 
 286          * Collides if spacing if positions intersect each other or are adjacent.
 
 288         private static boolean collides(Position position, int offset, int length) {
 
 290                         (offset <= position.getOffset() + position.getLength()) &&
 
 291                         (position.getOffset() <= offset + length);      
 
 294         private void leave(boolean success) {
 
 297                 if (fListener != null)
 
 298                         fListener.exit(success);                
 
 302          * @see IDocumentListener#documentAboutToBeChanged(DocumentEvent)
 
 304         public void documentAboutToBeChanged(DocumentEvent event) {
 
 305                 IDocument document= event.getDocument();
 
 307                 Position[] positions= getPositions(document);
 
 308                 Position position= findCurrentEditablePosition(positions, event.getOffset());
 
 310                 // modification outside editable position
 
 311                 if (position == null) {
 
 312                         position= findCurrentPosition(positions, event.getOffset());
 
 314                         // modification outside any position                    
 
 315                         if (position == null) {
 
 316                                 // check for destruction of constraints (spacing of at least 1)
 
 317                                 if ((event.getText().length() == 0) &&
 
 318                                         (findCurrentPosition(positions, event.getOffset()) != null) &&
 
 319                                         (findCurrentPosition(positions, event.getOffset() + event.getLength()) != null))
 
 324                         // modification intersects non-editable position
 
 329                 // modification intersects editable position
 
 331                         // modificaction inside editable position
 
 332                         if (includes(position, event.getOffset(), event.getLength())) {
 
 333                                 if (containsLineDelimiters(event.getText()))
 
 336                         // modificaction exceeds editable position
 
 344          * @see IDocumentListener#documentChanged(DocumentEvent)
 
 346         public void documentChanged(DocumentEvent event) {              
 
 347                 IDocument document= event.getDocument();
 
 349                 Position[] positions= getPositions(document);
 
 350                 TypedPosition currentPosition= (TypedPosition) findCurrentEditablePosition(positions, event.getOffset());
 
 352                 // ignore document changes (assume it won't invalidate constraints)
 
 353                 if (currentPosition == null)
 
 356                 int deltaOffset= event.getOffset() - currentPosition.getOffset();               
 
 358                 if (fListener != null)
 
 359                         fListener.setCurrentPosition(currentPosition, deltaOffset + event.getText().length());
 
 361                 for (int i= 0; i != positions.length; i++) {
 
 362                         TypedPosition p= (TypedPosition) positions[i];                  
 
 364                         if (p.getType().equals(currentPosition.getType()) && !p.equals(currentPosition)) {
 
 365                                 Replace replace= new Replace(p, deltaOffset, event.getLength(), event.getText());
 
 366                                 ((IDocumentExtension) document).registerPostNotificationReplace(this, replace);
 
 372          * @see IPositionUpdater#update(DocumentEvent)
 
 374         public void update(DocumentEvent event) {
 
 375                 int deltaLength= event.getText().length() - event.getLength();
 
 377                 Position[] positions= getPositions(event.getDocument());
 
 378                 TypedPosition currentPosition= (TypedPosition) findCurrentPosition(positions, event.getOffset());
 
 380                 // document change outside positions
 
 381                 if (currentPosition == null) {
 
 383                         for (int i= 0; i != positions.length; i++) {
 
 384                                 TypedPosition position= (TypedPosition) positions[i];
 
 385                                 int offset= position.getOffset();
 
 387                                 if (offset >= event.getOffset())
 
 388                                         position.setOffset(offset + deltaLength);
 
 391                 // document change within a position
 
 393                         int length= currentPosition.getLength();
 
 395                         for (int i= 0; i != positions.length; i++) {
 
 396                                 TypedPosition position= (TypedPosition) positions[i];
 
 397                                 int offset= position.getOffset();
 
 399                                 if (position.equals(currentPosition)) {
 
 400                                         position.setLength(length + deltaLength);                                       
 
 401                                 } else if (offset > currentPosition.getOffset()) {
 
 402                                         position.setOffset(offset + deltaLength);
 
 408         private static Position findCurrentPosition(Position[] positions, int offset) {
 
 409                 for (int i= 0; i != positions.length; i++)
 
 410                         if (includes(positions[i], offset, 0))
 
 416         private static Position findCurrentEditablePosition(Position[] positions, int offset) {
 
 417                 Position position= positions[0];
 
 419                 while ((position != null) && !includes(position, offset, 0))
 
 420                         position= findNextPosition(positions, position.getOffset());
 
 425         private boolean containsLineDelimiters(String string) {
 
 426                 String[] delimiters= fDocument.getLegalLineDelimiters();
 
 428                 for (int i= 0; i != delimiters.length; i++)
 
 429                         if (string.indexOf(delimiters[i]) != -1)
 
 436          * Test if ok to modify through UI.
 
 438         public boolean anyPositionIncludes(int offset, int length) {
 
 439                 Position[] positions= getPositions(fDocument);
 
 441                 Position position= findCurrentEditablePosition(positions, offset);
 
 442                 if (position == null)
 
 445                 return includes(position, offset, length);