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) {