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 org.eclipse.jface.text.Assert;
22 import org.eclipse.jface.text.BadLocationException;
23 import org.eclipse.jface.text.BadPositionCategoryException;
24 import org.eclipse.jface.text.DocumentCommand;
25 import org.eclipse.jface.text.DocumentEvent;
26 import org.eclipse.jface.text.IAutoEditStrategy;
27 import org.eclipse.jface.text.IDocument;
28 import org.eclipse.jface.text.IDocumentExtension;
29 import org.eclipse.jface.text.IDocumentListener;
30 import org.eclipse.jface.text.IPositionUpdater;
31 import org.eclipse.jface.text.Position;
32 import org.eclipse.jface.text.TypedPosition;
33 import org.eclipse.jface.text.contentassist.ICompletionProposal;
36 * This class manages linked positions in a document. Positions are linked by
37 * type names. If positions have the same type name, they are considered as
40 * The manager remains active on a document until any of the following actions
44 * <li>A document change is performed which would invalidate any of the above
47 * <li>The method <code>uninstall()</code> is called.</li>
49 * <li>Another instance of <code>LinkedPositionManager</code> tries to gain
50 * control of the same document.
53 public class LinkedPositionManager implements IDocumentListener,
54 IPositionUpdater, IAutoEditStrategy {
56 // This class still exists to properly handle code assist.
57 // This is due to the fact that it cannot be distinguished betweeen document
59 // issued by code assist and document changes which origin from another text
61 // There is a conflict in interest since in the latter case the linked mode
62 // should be left, but in the former case
63 // the linked mode should remain.
64 // To support content assist, document changes have to be propagated to
65 // connected positions
66 // by registering replace commands using IDocumentExtension.
67 // if it wasn't for the support of content assist, the documentChanged()
68 // method could be reduced to
69 // a simple call to leave(true)
70 private class Replace implements IDocumentExtension.IReplace {
72 private Position fReplacePosition;
74 private int fReplaceDeltaOffset;
76 private int fReplaceLength;
78 private String fReplaceText;
80 public Replace(Position position, int deltaOffset, int length,
82 fReplacePosition = position;
83 fReplaceDeltaOffset = deltaOffset;
84 fReplaceLength = length;
88 public void perform(IDocument document, IDocumentListener owner) {
89 document.removeDocumentListener(owner);
91 document.replace(fReplacePosition.getOffset()
92 + fReplaceDeltaOffset, fReplaceLength, fReplaceText);
93 } catch (BadLocationException e) {
94 PHPeclipsePlugin.log(e);
97 document.addDocumentListener(owner);
101 private static class PositionComparator implements Comparator {
103 * @see Comparator#compare(Object, Object)
105 public int compare(Object object0, Object object1) {
106 Position position0 = (Position) object0;
107 Position position1 = (Position) object1;
109 return position0.getOffset() - position1.getOffset();
113 private static final String LINKED_POSITION_PREFIX = "LinkedPositionManager.linked.position"; //$NON-NLS-1$
115 private static final Comparator fgPositionComparator = new PositionComparator();
117 private static final Map fgActiveManagers = new HashMap();
119 private static int fgCounter = 0;
121 private IDocument fDocument;
123 private ILinkedPositionListener fListener;
125 private String fPositionCategoryName;
127 private boolean fMustLeave;
130 * Flag that records the state of this manager. As there are many different
131 * entities that may call leave or exit, these cannot always be sure whether
132 * the linked position infrastructure is still active. This is especially
133 * true for multithreaded situations.
135 private boolean fIsActive = false;
138 * Creates a <code>LinkedPositionManager</code> for a
139 * <code>IDocument</code>.
142 * the document to use with linked positions.
144 * <code>true</code> if this manager can coexist with an
145 * already existing one
147 public LinkedPositionManager(IDocument document, boolean canCoexist) {
148 Assert.isNotNull(document);
149 fDocument = document;
150 fPositionCategoryName = LINKED_POSITION_PREFIX + (fgCounter++);
155 * Creates a <code>LinkedPositionManager</code> for a
156 * <code>IDocument</code>.
159 * the document to use with linked positions.
161 public LinkedPositionManager(IDocument document) {
162 this(document, false);
166 * Sets a listener to notify changes of current linked position.
168 public void setLinkedPositionListener(ILinkedPositionListener listener) {
169 fListener = listener;
173 * Adds a linked position to the manager with the type being the content of
174 * the document at the specified range. There are the following constraints
175 * for linked positions:
178 * <li>Any two positions have spacing of at least one character. This
179 * implies that two positions must not overlap.</li>
181 * <li>The string at any position must not contain line delimiters.</li>
185 * the offset of the position.
187 * the length of the position.
189 public void addPosition(int offset, int length) throws BadLocationException {
190 String type = fDocument.get(offset, length);
191 addPosition(offset, length, type);
195 * Adds a linked position of the specified position type to the manager.
196 * There are the following constraints for linked positions:
199 * <li>Any two positions have spacing of at least one character. This
200 * implies that two positions must not overlap.</li>
202 * <li>The string at any position must not contain line delimiters.</li>
206 * the offset of the position.
208 * the length of the position.
210 * the position type name - any positions with the same type are
213 public void addPosition(int offset, int length, String type)
214 throws BadLocationException {
215 Position[] positions = getPositions(fDocument);
217 if (positions != null) {
218 for (int i = 0; i < positions.length; i++)
219 if (collides(positions[i], offset, length))
220 throw new BadLocationException(
221 LinkedPositionMessages
222 .getString(("LinkedPositionManager.error.position.collision"))); //$NON-NLS-1$
225 String content = fDocument.get(offset, length);
227 if (containsLineDelimiters(content))
228 throw new BadLocationException(
229 LinkedPositionMessages
230 .getString(("LinkedPositionManager.error.contains.line.delimiters"))); //$NON-NLS-1$
233 fDocument.addPosition(fPositionCategoryName, new TypedPosition(
234 offset, length, type));
235 } catch (BadPositionCategoryException e) {
236 PHPeclipsePlugin.log(e);
237 Assert.isTrue(false);
242 * Adds a linked position to the manager. The current document content at
243 * the specified range is taken as the position type.
245 * There are the following constraints for linked positions:
248 * <li>Any two positions have spacing of at least one character. This
249 * implies that two positions must not overlap.</li>
251 * <li>The string at any position must not contain line delimiters.</li>
254 * It is usually best to set the first item in
255 * <code>additionalChoices</code> to be equal with the text inserted at
256 * the current position.
260 * the offset of the position.
262 * the length of the position.
263 * @param additionalChoices
264 * a number of additional choices to be displayed when selecting
265 * a position of this <code>type</code>.
267 public void addPosition(int offset, int length,
268 ICompletionProposal[] additionalChoices)
269 throws BadLocationException {
270 String type = fDocument.get(offset, length);
271 addPosition(offset, length, type, additionalChoices);
275 * Adds a linked position of the specified position type to the manager.
276 * There are the following constraints for linked positions:
279 * <li>Any two positions have spacing of at least one character. This
280 * implies that two positions must not overlap.</li>
282 * <li>The string at any position must not contain line delimiters.</li>
285 * It is usually best to set the first item in
286 * <code>additionalChoices</code> to be equal with the text inserted at
287 * the current position.
290 * the offset of the position.
292 * the length of the position.
294 * the position type name - any positions with the same type are
296 * @param additionalChoices
297 * a number of additional choices to be displayed when selecting
298 * a position of this <code>type</code>.
300 public void addPosition(int offset, int length, String type,
301 ICompletionProposal[] additionalChoices)
302 throws BadLocationException {
303 Position[] positions = getPositions(fDocument);
305 if (positions != null) {
306 for (int i = 0; i < positions.length; i++)
307 if (collides(positions[i], offset, length))
308 throw new BadLocationException(
309 LinkedPositionMessages
310 .getString(("LinkedPositionManager.error.position.collision"))); //$NON-NLS-1$
313 String content = fDocument.get(offset, length);
315 if (containsLineDelimiters(content))
316 throw new BadLocationException(
317 LinkedPositionMessages
318 .getString(("LinkedPositionManager.error.contains.line.delimiters"))); //$NON-NLS-1$
321 fDocument.addPosition(fPositionCategoryName, new ProposalPosition(
322 offset, length, type, additionalChoices));
323 } catch (BadPositionCategoryException e) {
324 PHPeclipsePlugin.log(e);
325 Assert.isTrue(false);
330 * Tests if a manager is already active for a document.
332 public static boolean hasActiveManager(IDocument document) {
333 return fgActiveManagers.get(document) != null;
336 private void install(boolean canCoexist) {
339 ;// JavaPlugin.log(new Status(IStatus.WARNING,
340 // JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager
341 // is already active: "+fPositionCategoryName, new
342 // IllegalStateException())); //$NON-NLS-1$
345 // JavaPlugin.log(new Status(IStatus.INFO, JavaPlugin.getPluginId(),
346 // IStatus.OK, "LinkedPositionManager activated:
347 // "+fPositionCategoryName, new Exception())); //$NON-NLS-1$
351 LinkedPositionManager manager = (LinkedPositionManager) fgActiveManagers
357 fgActiveManagers.put(fDocument, this);
358 fDocument.addPositionCategory(fPositionCategoryName);
359 fDocument.addPositionUpdater(this);
360 fDocument.addDocumentListener(this);
366 * Leaves the linked mode. If unsuccessful, the linked positions are
367 * restored to the values at the time they were added.
369 public void uninstall(boolean success) {
372 // we migth also just return
373 ;// JavaPlugin(new Status(IStatus.WARNING,
374 // JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager
375 // activated: "+fPositionCategoryName, new
376 // IllegalStateException())); //$NON-NLS-1$
378 fDocument.removeDocumentListener(this);
381 Position[] positions = getPositions(fDocument);
382 if ((!success) && (positions != null)) {
384 for (int i = 0; i != positions.length; i++) {
385 TypedPosition position = (TypedPosition) positions[i];
386 fDocument.replace(position.getOffset(), position
387 .getLength(), position.getType());
391 fDocument.removePositionCategory(fPositionCategoryName);
394 // JavaPlugin.log(new Status(IStatus.INFO,
395 // JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager
396 // deactivated: "+fPositionCategoryName, new Exception()));
399 } catch (BadLocationException e) {
400 PHPeclipsePlugin.log(e);
401 Assert.isTrue(false);
403 } catch (BadPositionCategoryException e) {
404 PHPeclipsePlugin.log(e);
405 Assert.isTrue(false);
408 fDocument.removePositionUpdater(this);
409 fgActiveManagers.remove(fDocument);
416 * Returns the position at the given offset, <code>null</code> if there is
421 public Position getPosition(int offset) {
422 Position[] positions = getPositions(fDocument);
423 if (positions == null)
426 for (int i = positions.length - 1; i >= 0; i--) {
427 Position position = positions[i];
428 if (offset >= position.getOffset()
429 && offset <= position.getOffset() + position.getLength())
437 * Returns the first linked position.
439 * @return returns <code>null</code> if no linked position exist.
441 public Position getFirstPosition() {
442 return getNextPosition(-1);
445 public Position getLastPosition() {
446 Position[] positions = getPositions(fDocument);
447 for (int i = positions.length - 1; i >= 0; i--) {
448 String type = ((TypedPosition) positions[i]).getType();
450 for (j = 0; j != i; j++)
451 if (((TypedPosition) positions[j]).getType().equals(type))
462 * Returns the next linked position with an offset greater than
463 * <code>offset</code>. If another position with the same type and offset
464 * lower than <code>offset</code> exists, the position is skipped.
466 * @return returns <code>null</code> if no linked position exist.
468 public Position getNextPosition(int offset) {
469 Position[] positions = getPositions(fDocument);
470 return findNextPosition(positions, offset);
473 private static Position findNextPosition(Position[] positions, int offset) {
474 // skip already visited types
475 for (int i = 0; i != positions.length; i++) {
476 if (positions[i].getOffset() > offset) {
477 String type = ((TypedPosition) positions[i]).getType();
479 for (j = 0; j != i; j++)
480 if (((TypedPosition) positions[j]).getType().equals(type))
492 * Returns the position with the greatest offset smaller than
493 * <code>offset</code>.
495 * @return returns <code>null</code> if no linked position exist.
497 public Position getPreviousPosition(int offset) {
498 Position[] positions = getPositions(fDocument);
499 if (positions == null)
502 TypedPosition currentPosition = (TypedPosition) findCurrentPosition(
504 String currentType = currentPosition == null ? null : currentPosition
507 Position lastPosition = null;
508 Position position = getFirstPosition();
510 while (position != null && position.getOffset() < offset) {
511 if (!((TypedPosition) position).getType().equals(currentType))
512 lastPosition = position;
513 position = findNextPosition(positions, position.getOffset());
519 private Position[] getPositions(IDocument document) {
522 // we migth also just return an empty array
523 ;// JavaPlugin(new Status(IStatus.WARNING,
524 // JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager
525 // is not active: "+fPositionCategoryName, new
526 // IllegalStateException())); //$NON-NLS-1$
529 Position[] positions = document.getPositions(fPositionCategoryName);
530 Arrays.sort(positions, fgPositionComparator);
533 } catch (BadPositionCategoryException e) {
534 PHPeclipsePlugin.log(e);
535 Assert.isTrue(false);
541 public static boolean includes(Position position, int offset, int length) {
542 return (offset >= position.getOffset())
543 && (offset + length <= position.getOffset()
544 + position.getLength());
547 public static boolean excludes(Position position, int offset, int length) {
548 return (offset + length <= position.getOffset())
549 || (position.getOffset() + position.getLength() <= offset);
553 * Collides if spacing if positions intersect each other or are adjacent.
555 private static boolean collides(Position position, int offset, int length) {
556 return (offset <= position.getOffset() + position.getLength())
557 && (position.getOffset() <= offset + length);
560 private void leave(boolean success) {
564 if (fListener != null)
565 fListener.exit((success ? LinkedPositionUI.COMMIT : 0)
566 | LinkedPositionUI.UPDATE_CARET);
572 private void abort() {
573 uninstall(true); // don't revert anything
575 if (fListener != null)
576 fListener.exit(LinkedPositionUI.COMMIT); // don't let the UI
579 // don't set fMustLeave, as we will get re-registered by a document
584 * @see IDocumentListener#documentAboutToBeChanged(DocumentEvent)
586 public void documentAboutToBeChanged(DocumentEvent event) {
589 event.getDocument().removeDocumentListener(this);
593 IDocument document = event.getDocument();
595 Position[] positions = getPositions(document);
596 Position position = findCurrentPosition(positions, event.getOffset());
598 // modification outside editable position
599 if (position == null) {
600 // check for destruction of constraints (spacing of at least 1)
601 if ((event.getText() == null || event.getText().length() == 0)
602 && (findCurrentPosition(positions, event.getOffset()) != null)
603 && // will never become true, see condition above
604 (findCurrentPosition(positions, event.getOffset()
605 + event.getLength()) != null)) {
609 // modification intersects editable position
611 // modificaction inside editable position
612 if (includes(position, event.getOffset(), event.getLength())) {
613 if (containsLineDelimiters(event.getText()))
616 // modificaction exceeds editable position
624 * @see IDocumentListener#documentChanged(DocumentEvent)
626 public void documentChanged(DocumentEvent event) {
628 // have to handle code assist, so can't just leave the linked mode
631 IDocument document = event.getDocument();
633 Position[] positions = getPositions(document);
634 TypedPosition currentPosition = (TypedPosition) findCurrentPosition(
635 positions, event.getOffset());
637 // ignore document changes (assume it won't invalidate constraints)
638 if (currentPosition == null)
641 int deltaOffset = event.getOffset() - currentPosition.getOffset();
643 if (fListener != null) {
644 int length = event.getText() == null ? 0 : event.getText().length();
645 fListener.setCurrentPosition(currentPosition, deltaOffset + length);
648 for (int i = 0; i != positions.length; i++) {
649 TypedPosition p = (TypedPosition) positions[i];
651 if (p.getType().equals(currentPosition.getType())
652 && !p.equals(currentPosition)) {
653 Replace replace = new Replace(p, deltaOffset,
654 event.getLength(), event.getText());
655 ((IDocumentExtension) document)
656 .registerPostNotificationReplace(this, replace);
662 * @see IPositionUpdater#update(DocumentEvent)
664 public void update(DocumentEvent event) {
666 int eventOffset = event.getOffset();
667 int eventOldLength = event.getLength();
668 int eventNewLength = event.getText() == null ? 0 : event.getText()
670 int deltaLength = eventNewLength - eventOldLength;
672 Position[] positions = getPositions(event.getDocument());
674 for (int i = 0; i != positions.length; i++) {
676 Position position = positions[i];
678 if (position.isDeleted())
681 int offset = position.getOffset();
682 int length = position.getLength();
683 int end = offset + length;
685 if (offset > eventOffset + eventOldLength) // position comes way
686 // after change - shift
687 position.setOffset(offset + deltaLength);
688 else if (end < eventOffset) // position comes way before change -
691 else if (offset <= eventOffset
692 && end >= eventOffset + eventOldLength) {
693 // event completely internal to the position - adjust length
694 position.setLength(length + deltaLength);
695 } else if (offset < eventOffset) {
696 // event extends over end of position - adjust length
697 int newEnd = eventOffset + eventNewLength;
698 position.setLength(newEnd - offset);
699 } else if (end > eventOffset + eventOldLength) {
700 // event extends from before position into it - adjust offset
702 // offset becomes end of event, length ajusted acordingly
703 // we want to recycle the overlapping part
704 int newOffset = eventOffset + eventNewLength;
705 position.setOffset(newOffset);
706 position.setLength(length + deltaLength);
708 // event consumes the position - delete it
710 // JavaPlugin.log(new Status(IStatus.INFO,
711 // JavaPlugin.getPluginId(), IStatus.OK, "linked position
712 // deleted -> must leave: "+fPositionCategoryName, null));
722 private static Position findCurrentPosition(Position[] positions, int offset) {
723 for (int i = 0; i != positions.length; i++)
724 if (includes(positions[i], offset, 0))
730 private boolean containsLineDelimiters(String string) {
735 String[] delimiters = fDocument.getLegalLineDelimiters();
737 for (int i = 0; i != delimiters.length; i++)
738 if (string.indexOf(delimiters[i]) != -1)
745 * Test if ok to modify through UI.
747 public boolean anyPositionIncludes(int offset, int length) {
748 Position[] positions = getPositions(fDocument);
750 Position position = findCurrentPosition(positions, offset);
751 if (position == null)
754 return includes(position, offset, length);
758 * Returns the position that includes the given range.
762 * @return position that includes the given range
764 public Position getEmbracingPosition(int offset, int length) {
765 Position[] positions = getPositions(fDocument);
767 Position position = findCurrentPosition(positions, offset);
768 if (position != null && includes(position, offset, length))
775 * @see org.eclipse.jface.text.IAutoIndentStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument,
776 * org.eclipse.jface.text.DocumentCommand)
778 public void customizeDocumentCommand(IDocument document,
779 DocumentCommand command) {
786 // don't interfere with preceding auto edit strategies
787 if (command.getCommandCount() != 1) {
792 Position[] positions = getPositions(document);
793 TypedPosition currentPosition = (TypedPosition) findCurrentPosition(
794 positions, command.offset);
796 // handle edits outside of a position
797 if (currentPosition == null) {
805 command.doit = false;
806 command.owner = this;
807 command.caretOffset = command.offset + command.length;
809 int deltaOffset = command.offset - currentPosition.getOffset();
811 if (fListener != null)
812 fListener.setCurrentPosition(currentPosition, deltaOffset
813 + command.text.length());
815 for (int i = 0; i != positions.length; i++) {
816 TypedPosition position = (TypedPosition) positions[i];
819 if (position.getType().equals(currentPosition.getType())
820 && !position.equals(currentPosition))
821 command.addCommand(position.getOffset() + deltaOffset,
822 command.length, command.text, this);
823 } catch (BadLocationException e) {
824 PHPeclipsePlugin.log(e);