1 /*******************************************************************************
 
   2  * Copyright (c) 2000, 2003 IBM Corporation and others.
 
   3  * All rights reserved. This program and the accompanying materials 
 
   4  * are made available under the terms of the Common Public License v1.0
 
   5  * which accompanies this distribution, and is available at
 
   6  * http://www.eclipse.org/legal/cpl-v10.html
 
   9  *     IBM Corporation - initial API and implementation
 
  10  *******************************************************************************/
 
  12 package net.sourceforge.phpdt.internal.ui.text.link;
 
  14 import java.util.Arrays;
 
  15 import java.util.Comparator;
 
  16 import java.util.HashMap;
 
  19 //import net.sourceforge.phpeclipse.PHPeclipsePlugin;
 
  21 import net.sourceforge.phpeclipse.ui.WebUI;
 
  23 import org.eclipse.jface.text.Assert;
 
  24 import org.eclipse.jface.text.BadLocationException;
 
  25 import org.eclipse.jface.text.BadPositionCategoryException;
 
  26 import org.eclipse.jface.text.DocumentCommand;
 
  27 import org.eclipse.jface.text.DocumentEvent;
 
  28 import org.eclipse.jface.text.IAutoEditStrategy;
 
  29 import org.eclipse.jface.text.IDocument;
 
  30 import org.eclipse.jface.text.IDocumentExtension;
 
  31 import org.eclipse.jface.text.IDocumentListener;
 
  32 import org.eclipse.jface.text.IPositionUpdater;
 
  33 import org.eclipse.jface.text.Position;
 
  34 import org.eclipse.jface.text.TypedPosition;
 
  35 import org.eclipse.jface.text.contentassist.ICompletionProposal;
 
  38  * This class manages linked positions in a document. Positions are linked by
 
  39  * type names. If positions have the same type name, they are considered as
 
  42  * The manager remains active on a document until any of the following actions
 
  46  * <li>A document change is performed which would invalidate any of the above
 
  49  * <li>The method <code>uninstall()</code> is called.</li>
 
  51  * <li>Another instance of <code>LinkedPositionManager</code> tries to gain
 
  52  * control of the same document.
 
  55 public class LinkedPositionManager implements IDocumentListener,
 
  56                 IPositionUpdater, IAutoEditStrategy {
 
  58         // This class still exists to properly handle code assist.
 
  59         // This is due to the fact that it cannot be distinguished betweeen document
 
  61         // issued by code assist and document changes which origin from another text
 
  63         // There is a conflict in interest since in the latter case the linked mode
 
  64         // should be left, but in the former case
 
  65         // the linked mode should remain.
 
  66         // To support content assist, document changes have to be propagated to
 
  67         // connected positions
 
  68         // by registering replace commands using IDocumentExtension.
 
  69         // if it wasn't for the support of content assist, the documentChanged()
 
  70         // method could be reduced to
 
  71         // a simple call to leave(true)
 
  72         private class Replace implements IDocumentExtension.IReplace {
 
  74                 private Position fReplacePosition;
 
  76                 private int fReplaceDeltaOffset;
 
  78                 private int fReplaceLength;
 
  80                 private String fReplaceText;
 
  82                 public Replace(Position position, int deltaOffset, int length,
 
  84                         fReplacePosition = position;
 
  85                         fReplaceDeltaOffset = deltaOffset;
 
  86                         fReplaceLength = length;
 
  90                 public void perform(IDocument document, IDocumentListener owner) {
 
  91                         document.removeDocumentListener(owner);
 
  93                                 document.replace(fReplacePosition.getOffset()
 
  94                                                 + fReplaceDeltaOffset, fReplaceLength, fReplaceText);
 
  95                         } catch (BadLocationException e) {
 
  99                         document.addDocumentListener(owner);
 
 103         private static class PositionComparator implements Comparator {
 
 105                  * @see Comparator#compare(Object, Object)
 
 107                 public int compare(Object object0, Object object1) {
 
 108                         Position position0 = (Position) object0;
 
 109                         Position position1 = (Position) object1;
 
 111                         return position0.getOffset() - position1.getOffset();
 
 115         private static final String LINKED_POSITION_PREFIX = "LinkedPositionManager.linked.position"; //$NON-NLS-1$
 
 117         private static final Comparator fgPositionComparator = new PositionComparator();
 
 119         private static final Map fgActiveManagers = new HashMap();
 
 121         private static int fgCounter = 0;
 
 123         private IDocument fDocument;
 
 125         private ILinkedPositionListener fListener;
 
 127         private String fPositionCategoryName;
 
 129         private boolean fMustLeave;
 
 132          * Flag that records the state of this manager. As there are many different
 
 133          * entities that may call leave or exit, these cannot always be sure whether
 
 134          * the linked position infrastructure is still active. This is especially
 
 135          * true for multithreaded situations.
 
 137         private boolean fIsActive = false;
 
 140          * Creates a <code>LinkedPositionManager</code> for a
 
 141          * <code>IDocument</code>.
 
 144          *            the document to use with linked positions.
 
 146          *            <code>true</code> if this manager can coexist with an
 
 147          *            already existing one
 
 149         public LinkedPositionManager(IDocument document, boolean canCoexist) {
 
 150                 Assert.isNotNull(document);
 
 151                 fDocument = document;
 
 152                 fPositionCategoryName = LINKED_POSITION_PREFIX + (fgCounter++);
 
 157          * Creates a <code>LinkedPositionManager</code> for a
 
 158          * <code>IDocument</code>.
 
 161          *            the document to use with linked positions.
 
 163         public LinkedPositionManager(IDocument document) {
 
 164                 this(document, false);
 
 168          * Sets a listener to notify changes of current linked position.
 
 170         public void setLinkedPositionListener(ILinkedPositionListener listener) {
 
 171                 fListener = listener;
 
 175          * Adds a linked position to the manager with the type being the content of
 
 176          * the document at the specified range. There are the following constraints
 
 177          * for linked positions:
 
 180          * <li>Any two positions have spacing of at least one character. This
 
 181          * implies that two positions must not overlap.</li>
 
 183          * <li>The string at any position must not contain line delimiters.</li>
 
 187          *            the offset of the position.
 
 189          *            the length of the position.
 
 191         public void addPosition(int offset, int length) throws BadLocationException {
 
 192                 String type = fDocument.get(offset, length);
 
 193                 addPosition(offset, length, type);
 
 197          * Adds a linked position of the specified position type to the manager.
 
 198          * There are the following constraints for linked positions:
 
 201          * <li>Any two positions have spacing of at least one character. This
 
 202          * implies that two positions must not overlap.</li>
 
 204          * <li>The string at any position must not contain line delimiters.</li>
 
 208          *            the offset of the position.
 
 210          *            the length of the position.
 
 212          *            the position type name - any positions with the same type are
 
 215         public void addPosition(int offset, int length, String type)
 
 216                         throws BadLocationException {
 
 217                 Position[] positions = getPositions(fDocument);
 
 219                 if (positions != null) {
 
 220                         for (int i = 0; i < positions.length; i++)
 
 221                                 if (collides(positions[i], offset, length))
 
 222                                         throw new BadLocationException(
 
 223                                                         LinkedPositionMessages
 
 224                                                                         .getString(("LinkedPositionManager.error.position.collision"))); //$NON-NLS-1$
 
 227                 String content = fDocument.get(offset, length);
 
 229                 if (containsLineDelimiters(content))
 
 230                         throw new BadLocationException(
 
 231                                         LinkedPositionMessages
 
 232                                                         .getString(("LinkedPositionManager.error.contains.line.delimiters"))); //$NON-NLS-1$
 
 235                         fDocument.addPosition(fPositionCategoryName, new TypedPosition(
 
 236                                         offset, length, type));
 
 237                 } catch (BadPositionCategoryException e) {
 
 239                         Assert.isTrue(false);
 
 244          * Adds a linked position to the manager. The current document content at
 
 245          * the specified range is taken as the position type.
 
 247          * There are the following constraints for linked positions:
 
 250          * <li>Any two positions have spacing of at least one character. This
 
 251          * implies that two positions must not overlap.</li>
 
 253          * <li>The string at any position must not contain line delimiters.</li>
 
 256          * It is usually best to set the first item in
 
 257          * <code>additionalChoices</code> to be equal with the text inserted at
 
 258          * the current position.
 
 262          *            the offset of the position.
 
 264          *            the length of the position.
 
 265          * @param additionalChoices
 
 266          *            a number of additional choices to be displayed when selecting
 
 267          *            a position of this <code>type</code>.
 
 269         public void addPosition(int offset, int length,
 
 270                         ICompletionProposal[] additionalChoices)
 
 271                         throws BadLocationException {
 
 272                 String type = fDocument.get(offset, length);
 
 273                 addPosition(offset, length, type, additionalChoices);
 
 277          * Adds a linked position of the specified position type to the manager.
 
 278          * There are the following constraints for linked positions:
 
 281          * <li>Any two positions have spacing of at least one character. This
 
 282          * implies that two positions must not overlap.</li>
 
 284          * <li>The string at any position must not contain line delimiters.</li>
 
 287          * It is usually best to set the first item in
 
 288          * <code>additionalChoices</code> to be equal with the text inserted at
 
 289          * the current position.
 
 292          *            the offset of the position.
 
 294          *            the length of the position.
 
 296          *            the position type name - any positions with the same type are
 
 298          * @param additionalChoices
 
 299          *            a number of additional choices to be displayed when selecting
 
 300          *            a position of this <code>type</code>.
 
 302         public void addPosition(int offset, int length, String type,
 
 303                         ICompletionProposal[] additionalChoices)
 
 304                         throws BadLocationException {
 
 305                 Position[] positions = getPositions(fDocument);
 
 307                 if (positions != null) {
 
 308                         for (int i = 0; i < positions.length; i++)
 
 309                                 if (collides(positions[i], offset, length))
 
 310                                         throw new BadLocationException(
 
 311                                                         LinkedPositionMessages
 
 312                                                                         .getString(("LinkedPositionManager.error.position.collision"))); //$NON-NLS-1$
 
 315                 String content = fDocument.get(offset, length);
 
 317                 if (containsLineDelimiters(content))
 
 318                         throw new BadLocationException(
 
 319                                         LinkedPositionMessages
 
 320                                                         .getString(("LinkedPositionManager.error.contains.line.delimiters"))); //$NON-NLS-1$
 
 323                         fDocument.addPosition(fPositionCategoryName, new ProposalPosition(
 
 324                                         offset, length, type, additionalChoices));
 
 325                 } catch (BadPositionCategoryException e) {
 
 327                         Assert.isTrue(false);
 
 332          * Tests if a manager is already active for a document.
 
 334         public static boolean hasActiveManager(IDocument document) {
 
 335                 return fgActiveManagers.get(document) != null;
 
 338         private void install(boolean canCoexist) {
 
 341                         ;// JavaPlugin.log(new Status(IStatus.WARNING,
 
 342                                 // JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager
 
 343                                 // is already active: "+fPositionCategoryName, new
 
 344                                 // IllegalStateException())); //$NON-NLS-1$
 
 347                         // JavaPlugin.log(new Status(IStatus.INFO, JavaPlugin.getPluginId(),
 
 348                         // IStatus.OK, "LinkedPositionManager activated:
 
 349                         // "+fPositionCategoryName, new Exception())); //$NON-NLS-1$
 
 353                         LinkedPositionManager manager = (LinkedPositionManager) fgActiveManagers
 
 359                 fgActiveManagers.put(fDocument, this);
 
 360                 fDocument.addPositionCategory(fPositionCategoryName);
 
 361                 fDocument.addPositionUpdater(this);
 
 362                 fDocument.addDocumentListener(this);
 
 368          * Leaves the linked mode. If unsuccessful, the linked positions are
 
 369          * restored to the values at the time they were added.
 
 371         public void uninstall(boolean success) {
 
 374                         // we migth also just return
 
 375                         ;// JavaPlugin(new Status(IStatus.WARNING,
 
 376                                 // JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager
 
 377                                 // activated: "+fPositionCategoryName, new
 
 378                                 // IllegalStateException())); //$NON-NLS-1$
 
 380                         fDocument.removeDocumentListener(this);
 
 383                                 Position[] positions = getPositions(fDocument);
 
 384                                 if ((!success) && (positions != null)) {
 
 386                                         for (int i = 0; i != positions.length; i++) {
 
 387                                                 TypedPosition position = (TypedPosition) positions[i];
 
 388                                                 fDocument.replace(position.getOffset(), position
 
 389                                                                 .getLength(), position.getType());
 
 393                                 fDocument.removePositionCategory(fPositionCategoryName);
 
 396                                 // JavaPlugin.log(new Status(IStatus.INFO,
 
 397                                 // JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager
 
 398                                 // deactivated: "+fPositionCategoryName, new Exception()));
 
 401                         } catch (BadLocationException e) {
 
 403                                 Assert.isTrue(false);
 
 405                         } catch (BadPositionCategoryException e) {
 
 407                                 Assert.isTrue(false);
 
 410                                 fDocument.removePositionUpdater(this);
 
 411                                 fgActiveManagers.remove(fDocument);
 
 418          * Returns the position at the given offset, <code>null</code> if there is
 
 423         public Position getPosition(int offset) {
 
 424                 Position[] positions = getPositions(fDocument);
 
 425                 if (positions == null)
 
 428                 for (int i = positions.length - 1; i >= 0; i--) {
 
 429                         Position position = positions[i];
 
 430                         if (offset >= position.getOffset()
 
 431                                         && offset <= position.getOffset() + position.getLength())
 
 439          * Returns the first linked position.
 
 441          * @return returns <code>null</code> if no linked position exist.
 
 443         public Position getFirstPosition() {
 
 444                 return getNextPosition(-1);
 
 447         public Position getLastPosition() {
 
 448                 Position[] positions = getPositions(fDocument);
 
 449                 for (int i = positions.length - 1; i >= 0; i--) {
 
 450                         String type = ((TypedPosition) positions[i]).getType();
 
 452                         for (j = 0; j != i; j++)
 
 453                                 if (((TypedPosition) positions[j]).getType().equals(type))
 
 464          * Returns the next linked position with an offset greater than
 
 465          * <code>offset</code>. If another position with the same type and offset
 
 466          * lower than <code>offset</code> exists, the position is skipped.
 
 468          * @return returns <code>null</code> if no linked position exist.
 
 470         public Position getNextPosition(int offset) {
 
 471                 Position[] positions = getPositions(fDocument);
 
 472                 return findNextPosition(positions, offset);
 
 475         private static Position findNextPosition(Position[] positions, int offset) {
 
 476                 // skip already visited types
 
 477                 for (int i = 0; i != positions.length; i++) {
 
 478                         if (positions[i].getOffset() > offset) {
 
 479                                 String type = ((TypedPosition) positions[i]).getType();
 
 481                                 for (j = 0; j != i; j++)
 
 482                                         if (((TypedPosition) positions[j]).getType().equals(type))
 
 494          * Returns the position with the greatest offset smaller than
 
 495          * <code>offset</code>.
 
 497          * @return returns <code>null</code> if no linked position exist.
 
 499         public Position getPreviousPosition(int offset) {
 
 500                 Position[] positions = getPositions(fDocument);
 
 501                 if (positions == null)
 
 504                 TypedPosition currentPosition = (TypedPosition) findCurrentPosition(
 
 506                 String currentType = currentPosition == null ? null : currentPosition
 
 509                 Position lastPosition = null;
 
 510                 Position position = getFirstPosition();
 
 512                 while (position != null && position.getOffset() < offset) {
 
 513                         if (!((TypedPosition) position).getType().equals(currentType))
 
 514                                 lastPosition = position;
 
 515                         position = findNextPosition(positions, position.getOffset());
 
 521         private Position[] getPositions(IDocument document) {
 
 524                         // we migth also just return an empty array
 
 525                         ;// JavaPlugin(new Status(IStatus.WARNING,
 
 526                                 // JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager
 
 527                                 // is not active: "+fPositionCategoryName, new
 
 528                                 // IllegalStateException())); //$NON-NLS-1$
 
 531                         Position[] positions = document.getPositions(fPositionCategoryName);
 
 532                         Arrays.sort(positions, fgPositionComparator);
 
 535                 } catch (BadPositionCategoryException e) {
 
 537                         Assert.isTrue(false);
 
 543         public static boolean includes(Position position, int offset, int length) {
 
 544                 return (offset >= position.getOffset())
 
 545                                 && (offset + length <= position.getOffset()
 
 546                                                 + position.getLength());
 
 549         public static boolean excludes(Position position, int offset, int length) {
 
 550                 return (offset + length <= position.getOffset())
 
 551                                 || (position.getOffset() + position.getLength() <= offset);
 
 555          * Collides if spacing if positions intersect each other or are adjacent.
 
 557         private static boolean collides(Position position, int offset, int length) {
 
 558                 return (offset <= position.getOffset() + position.getLength())
 
 559                                 && (position.getOffset() <= offset + length);
 
 562         private void leave(boolean success) {
 
 566                         if (fListener != null)
 
 567                                 fListener.exit((success ? LinkedPositionUI.COMMIT : 0)
 
 568                                                 | LinkedPositionUI.UPDATE_CARET);
 
 574         private void abort() {
 
 575                 uninstall(true); // don't revert anything
 
 577                 if (fListener != null)
 
 578                         fListener.exit(LinkedPositionUI.COMMIT); // don't let the UI
 
 581                 // don't set fMustLeave, as we will get re-registered by a document
 
 586          * @see IDocumentListener#documentAboutToBeChanged(DocumentEvent)
 
 588         public void documentAboutToBeChanged(DocumentEvent event) {
 
 591                         event.getDocument().removeDocumentListener(this);
 
 595                 IDocument document = event.getDocument();
 
 597                 Position[] positions = getPositions(document);
 
 598                 Position position = findCurrentPosition(positions, event.getOffset());
 
 600                 // modification outside editable position
 
 601                 if (position == null) {
 
 602                         // check for destruction of constraints (spacing of at least 1)
 
 603                         if ((event.getText() == null || event.getText().length() == 0)
 
 604                                         && (findCurrentPosition(positions, event.getOffset()) != null)
 
 605                                         && // will never become true, see condition above
 
 606                                         (findCurrentPosition(positions, event.getOffset()
 
 607                                                         + event.getLength()) != null)) {
 
 611                         // modification intersects editable position
 
 613                         // modificaction inside editable position
 
 614                         if (includes(position, event.getOffset(), event.getLength())) {
 
 615                                 if (containsLineDelimiters(event.getText()))
 
 618                                 // modificaction exceeds editable position
 
 626          * @see IDocumentListener#documentChanged(DocumentEvent)
 
 628         public void documentChanged(DocumentEvent event) {
 
 630                 // have to handle code assist, so can't just leave the linked mode
 
 633                 IDocument document = event.getDocument();
 
 635                 Position[] positions = getPositions(document);
 
 636                 TypedPosition currentPosition = (TypedPosition) findCurrentPosition(
 
 637                                 positions, event.getOffset());
 
 639                 // ignore document changes (assume it won't invalidate constraints)
 
 640                 if (currentPosition == null)
 
 643                 int deltaOffset = event.getOffset() - currentPosition.getOffset();
 
 645                 if (fListener != null) {
 
 646                         int length = event.getText() == null ? 0 : event.getText().length();
 
 647                         fListener.setCurrentPosition(currentPosition, deltaOffset + length);
 
 650                 for (int i = 0; i != positions.length; i++) {
 
 651                         TypedPosition p = (TypedPosition) positions[i];
 
 653                         if (p.getType().equals(currentPosition.getType())
 
 654                                         && !p.equals(currentPosition)) {
 
 655                                 Replace replace = new Replace(p, deltaOffset,
 
 656                                                 event.getLength(), event.getText());
 
 657                                 ((IDocumentExtension) document)
 
 658                                                 .registerPostNotificationReplace(this, replace);
 
 664          * @see IPositionUpdater#update(DocumentEvent)
 
 666         public void update(DocumentEvent event) {
 
 668                 int eventOffset = event.getOffset();
 
 669                 int eventOldLength = event.getLength();
 
 670                 int eventNewLength = event.getText() == null ? 0 : event.getText()
 
 672                 int deltaLength = eventNewLength - eventOldLength;
 
 674                 Position[] positions = getPositions(event.getDocument());
 
 676                 for (int i = 0; i != positions.length; i++) {
 
 678                         Position position = positions[i];
 
 680                         if (position.isDeleted())
 
 683                         int offset = position.getOffset();
 
 684                         int length = position.getLength();
 
 685                         int end = offset + length;
 
 687                         if (offset > eventOffset + eventOldLength) // position comes way
 
 688                                                                                                                 // after change - shift
 
 689                                 position.setOffset(offset + deltaLength);
 
 690                         else if (end < eventOffset) // position comes way before change -
 
 693                         else if (offset <= eventOffset
 
 694                                         && end >= eventOffset + eventOldLength) {
 
 695                                 // event completely internal to the position - adjust length
 
 696                                 position.setLength(length + deltaLength);
 
 697                         } else if (offset < eventOffset) {
 
 698                                 // event extends over end of position - adjust length
 
 699                                 int newEnd = eventOffset + eventNewLength;
 
 700                                 position.setLength(newEnd - offset);
 
 701                         } else if (end > eventOffset + eventOldLength) {
 
 702                                 // event extends from before position into it - adjust offset
 
 704                                 // offset becomes end of event, length ajusted acordingly
 
 705                                 // we want to recycle the overlapping part
 
 706                                 int newOffset = eventOffset + eventNewLength;
 
 707                                 position.setOffset(newOffset);
 
 708                                 position.setLength(length + deltaLength);
 
 710                                 // event consumes the position - delete it
 
 712                                 // JavaPlugin.log(new Status(IStatus.INFO,
 
 713                                 // JavaPlugin.getPluginId(), IStatus.OK, "linked position
 
 714                                 // deleted -> must leave: "+fPositionCategoryName, null));
 
 724         private static Position findCurrentPosition(Position[] positions, int offset) {
 
 725                 for (int i = 0; i != positions.length; i++)
 
 726                         if (includes(positions[i], offset, 0))
 
 732         private boolean containsLineDelimiters(String string) {
 
 737                 String[] delimiters = fDocument.getLegalLineDelimiters();
 
 739                 for (int i = 0; i != delimiters.length; i++)
 
 740                         if (string.indexOf(delimiters[i]) != -1)
 
 747          * Test if ok to modify through UI.
 
 749         public boolean anyPositionIncludes(int offset, int length) {
 
 750                 Position[] positions = getPositions(fDocument);
 
 752                 Position position = findCurrentPosition(positions, offset);
 
 753                 if (position == null)
 
 756                 return includes(position, offset, length);
 
 760          * Returns the position that includes the given range.
 
 764          * @return position that includes the given range
 
 766         public Position getEmbracingPosition(int offset, int length) {
 
 767                 Position[] positions = getPositions(fDocument);
 
 769                 Position position = findCurrentPosition(positions, offset);
 
 770                 if (position != null && includes(position, offset, length))
 
 777          * @see org.eclipse.jface.text.IAutoIndentStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument,
 
 778          *      org.eclipse.jface.text.DocumentCommand)
 
 780         public void customizeDocumentCommand(IDocument document,
 
 781                         DocumentCommand command) {
 
 788                 // don't interfere with preceding auto edit strategies
 
 789                 if (command.getCommandCount() != 1) {
 
 794                 Position[] positions = getPositions(document);
 
 795                 TypedPosition currentPosition = (TypedPosition) findCurrentPosition(
 
 796                                 positions, command.offset);
 
 798                 // handle edits outside of a position
 
 799                 if (currentPosition == null) {
 
 807                 command.doit = false;
 
 808                 command.owner = this;
 
 809                 command.caretOffset = command.offset + command.length;
 
 811                 int deltaOffset = command.offset - currentPosition.getOffset();
 
 813                 if (fListener != null)
 
 814                         fListener.setCurrentPosition(currentPosition, deltaOffset
 
 815                                         + command.text.length());
 
 817                 for (int i = 0; i != positions.length; i++) {
 
 818                         TypedPosition position = (TypedPosition) positions[i];
 
 821                                 if (position.getType().equals(currentPosition.getType())
 
 822                                                 && !position.equals(currentPosition))
 
 823                                         command.addCommand(position.getOffset() + deltaOffset,
 
 824                                                         command.length, command.text, this);
 
 825                         } catch (BadLocationException e) {