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;
24 //import org.eclipse.jface.text.Assert;
25 import org.eclipse.core.runtime.Assert;
26 import org.eclipse.jface.text.BadLocationException;
27 import org.eclipse.jface.text.BadPositionCategoryException;
28 import org.eclipse.jface.text.DocumentCommand;
29 import org.eclipse.jface.text.DocumentEvent;
30 import org.eclipse.jface.text.IAutoEditStrategy;
31 import org.eclipse.jface.text.IDocument;
32 import org.eclipse.jface.text.IDocumentExtension;
33 import org.eclipse.jface.text.IDocumentListener;
34 import org.eclipse.jface.text.IPositionUpdater;
35 import org.eclipse.jface.text.Position;
36 import org.eclipse.jface.text.TypedPosition;
37 import org.eclipse.jface.text.contentassist.ICompletionProposal;
40 * This class manages linked positions in a document. Positions are linked by
41 * type names. If positions have the same type name, they are considered as
44 * The manager remains active on a document until any of the following actions
48 * <li>A document change is performed which would invalidate any of the above
51 * <li>The method <code>uninstall()</code> is called.</li>
53 * <li>Another instance of <code>LinkedPositionManager</code> tries to gain
54 * control of the same document.
57 public class LinkedPositionManager implements IDocumentListener,
58 IPositionUpdater, IAutoEditStrategy {
60 // This class still exists to properly handle code assist.
61 // This is due to the fact that it cannot be distinguished betweeen document
63 // issued by code assist and document changes which origin from another text
65 // There is a conflict in interest since in the latter case the linked mode
66 // should be left, but in the former case
67 // the linked mode should remain.
68 // To support content assist, document changes have to be propagated to
69 // connected positions
70 // by registering replace commands using IDocumentExtension.
71 // if it wasn't for the support of content assist, the documentChanged()
72 // method could be reduced to
73 // a simple call to leave(true)
74 private class Replace implements IDocumentExtension.IReplace {
76 private Position fReplacePosition;
78 private int fReplaceDeltaOffset;
80 private int fReplaceLength;
82 private String fReplaceText;
84 public Replace(Position position, int deltaOffset, int length,
86 fReplacePosition = position;
87 fReplaceDeltaOffset = deltaOffset;
88 fReplaceLength = length;
92 public void perform(IDocument document, IDocumentListener owner) {
93 document.removeDocumentListener(owner);
95 document.replace(fReplacePosition.getOffset()
96 + fReplaceDeltaOffset, fReplaceLength, fReplaceText);
97 } catch (BadLocationException e) {
101 document.addDocumentListener(owner);
105 private static class PositionComparator implements Comparator {
107 * @see Comparator#compare(Object, Object)
109 public int compare(Object object0, Object object1) {
110 Position position0 = (Position) object0;
111 Position position1 = (Position) object1;
113 return position0.getOffset() - position1.getOffset();
117 private static final String LINKED_POSITION_PREFIX = "LinkedPositionManager.linked.position"; //$NON-NLS-1$
119 private static final Comparator fgPositionComparator = new PositionComparator();
121 private static final Map fgActiveManagers = new HashMap();
123 private static int fgCounter = 0;
125 private IDocument fDocument;
127 private ILinkedPositionListener fListener;
129 private String fPositionCategoryName;
131 private boolean fMustLeave;
134 * Flag that records the state of this manager. As there are many different
135 * entities that may call leave or exit, these cannot always be sure whether
136 * the linked position infrastructure is still active. This is especially
137 * true for multithreaded situations.
139 private boolean fIsActive = false;
142 * Creates a <code>LinkedPositionManager</code> for a
143 * <code>IDocument</code>.
146 * the document to use with linked positions.
148 * <code>true</code> if this manager can coexist with an
149 * already existing one
151 public LinkedPositionManager(IDocument document, boolean canCoexist) {
152 Assert.isNotNull(document);
153 fDocument = document;
154 fPositionCategoryName = LINKED_POSITION_PREFIX + (fgCounter++);
159 * Creates a <code>LinkedPositionManager</code> for a
160 * <code>IDocument</code>.
163 * the document to use with linked positions.
165 public LinkedPositionManager(IDocument document) {
166 this(document, false);
170 * Sets a listener to notify changes of current linked position.
172 public void setLinkedPositionListener(ILinkedPositionListener listener) {
173 fListener = listener;
177 * Adds a linked position to the manager with the type being the content of
178 * the document at the specified range. There are the following constraints
179 * for linked positions:
182 * <li>Any two positions have spacing of at least one character. This
183 * implies that two positions must not overlap.</li>
185 * <li>The string at any position must not contain line delimiters.</li>
189 * the offset of the position.
191 * the length of the position.
193 public void addPosition(int offset, int length) throws BadLocationException {
194 String type = fDocument.get(offset, length);
195 addPosition(offset, length, type);
199 * Adds a linked position of the specified position type to the manager.
200 * There are the following constraints for linked positions:
203 * <li>Any two positions have spacing of at least one character. This
204 * implies that two positions must not overlap.</li>
206 * <li>The string at any position must not contain line delimiters.</li>
210 * the offset of the position.
212 * the length of the position.
214 * the position type name - any positions with the same type are
217 public void addPosition(int offset, int length, String type)
218 throws BadLocationException {
219 Position[] positions = getPositions(fDocument);
221 if (positions != null) {
222 for (int i = 0; i < positions.length; i++)
223 if (collides(positions[i], offset, length))
224 throw new BadLocationException(
225 LinkedPositionMessages
226 .getString(("LinkedPositionManager.error.position.collision"))); //$NON-NLS-1$
229 String content = fDocument.get(offset, length);
231 if (containsLineDelimiters(content))
232 throw new BadLocationException(
233 LinkedPositionMessages
234 .getString(("LinkedPositionManager.error.contains.line.delimiters"))); //$NON-NLS-1$
237 fDocument.addPosition(fPositionCategoryName, new TypedPosition(
238 offset, length, type));
239 } catch (BadPositionCategoryException e) {
241 Assert.isTrue(false);
246 * Adds a linked position to the manager. The current document content at
247 * the specified range is taken as the position type.
249 * There are the following constraints for linked positions:
252 * <li>Any two positions have spacing of at least one character. This
253 * implies that two positions must not overlap.</li>
255 * <li>The string at any position must not contain line delimiters.</li>
258 * It is usually best to set the first item in
259 * <code>additionalChoices</code> to be equal with the text inserted at
260 * the current position.
264 * the offset of the position.
266 * the length of the position.
267 * @param additionalChoices
268 * a number of additional choices to be displayed when selecting
269 * a position of this <code>type</code>.
271 public void addPosition(int offset, int length,
272 ICompletionProposal[] additionalChoices)
273 throws BadLocationException {
274 String type = fDocument.get(offset, length);
275 addPosition(offset, length, type, additionalChoices);
279 * Adds a linked position of the specified position type to the manager.
280 * There are the following constraints for linked positions:
283 * <li>Any two positions have spacing of at least one character. This
284 * implies that two positions must not overlap.</li>
286 * <li>The string at any position must not contain line delimiters.</li>
289 * It is usually best to set the first item in
290 * <code>additionalChoices</code> to be equal with the text inserted at
291 * the current position.
294 * the offset of the position.
296 * the length of the position.
298 * the position type name - any positions with the same type are
300 * @param additionalChoices
301 * a number of additional choices to be displayed when selecting
302 * a position of this <code>type</code>.
304 public void addPosition(int offset, int length, String type,
305 ICompletionProposal[] additionalChoices)
306 throws BadLocationException {
307 Position[] positions = getPositions(fDocument);
309 if (positions != null) {
310 for (int i = 0; i < positions.length; i++)
311 if (collides(positions[i], offset, length))
312 throw new BadLocationException(
313 LinkedPositionMessages
314 .getString(("LinkedPositionManager.error.position.collision"))); //$NON-NLS-1$
317 String content = fDocument.get(offset, length);
319 if (containsLineDelimiters(content))
320 throw new BadLocationException(
321 LinkedPositionMessages
322 .getString(("LinkedPositionManager.error.contains.line.delimiters"))); //$NON-NLS-1$
325 fDocument.addPosition(fPositionCategoryName, new ProposalPosition(
326 offset, length, type, additionalChoices));
327 } catch (BadPositionCategoryException e) {
329 Assert.isTrue(false);
334 * Tests if a manager is already active for a document.
336 public static boolean hasActiveManager(IDocument document) {
337 return fgActiveManagers.get(document) != null;
340 private void install(boolean canCoexist) {
343 ;// JavaPlugin.log(new Status(IStatus.WARNING,
344 // JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager
345 // is already active: "+fPositionCategoryName, new
346 // IllegalStateException())); //$NON-NLS-1$
349 // JavaPlugin.log(new Status(IStatus.INFO, JavaPlugin.getPluginId(),
350 // IStatus.OK, "LinkedPositionManager activated:
351 // "+fPositionCategoryName, new Exception())); //$NON-NLS-1$
355 LinkedPositionManager manager = (LinkedPositionManager) fgActiveManagers
361 fgActiveManagers.put(fDocument, this);
362 fDocument.addPositionCategory(fPositionCategoryName);
363 fDocument.addPositionUpdater(this);
364 fDocument.addDocumentListener(this);
370 * Leaves the linked mode. If unsuccessful, the linked positions are
371 * restored to the values at the time they were added.
373 public void uninstall(boolean success) {
376 // we migth also just return
377 ;// JavaPlugin(new Status(IStatus.WARNING,
378 // JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager
379 // activated: "+fPositionCategoryName, new
380 // IllegalStateException())); //$NON-NLS-1$
382 fDocument.removeDocumentListener(this);
385 Position[] positions = getPositions(fDocument);
386 if ((!success) && (positions != null)) {
388 for (int i = 0; i != positions.length; i++) {
389 TypedPosition position = (TypedPosition) positions[i];
390 fDocument.replace(position.getOffset(), position
391 .getLength(), position.getType());
395 fDocument.removePositionCategory(fPositionCategoryName);
398 // JavaPlugin.log(new Status(IStatus.INFO,
399 // JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager
400 // deactivated: "+fPositionCategoryName, new Exception()));
403 } catch (BadLocationException e) {
405 Assert.isTrue(false);
407 } catch (BadPositionCategoryException e) {
409 Assert.isTrue(false);
412 fDocument.removePositionUpdater(this);
413 fgActiveManagers.remove(fDocument);
420 * Returns the position at the given offset, <code>null</code> if there is
425 public Position getPosition(int offset) {
426 Position[] positions = getPositions(fDocument);
427 if (positions == null)
430 for (int i = positions.length - 1; i >= 0; i--) {
431 Position position = positions[i];
432 if (offset >= position.getOffset()
433 && offset <= position.getOffset() + position.getLength())
441 * Returns the first linked position.
443 * @return returns <code>null</code> if no linked position exist.
445 public Position getFirstPosition() {
446 return getNextPosition(-1);
449 public Position getLastPosition() {
450 Position[] positions = getPositions(fDocument);
451 for (int i = positions.length - 1; i >= 0; i--) {
452 String type = ((TypedPosition) positions[i]).getType();
454 for (j = 0; j != i; j++)
455 if (((TypedPosition) positions[j]).getType().equals(type))
466 * Returns the next linked position with an offset greater than
467 * <code>offset</code>. If another position with the same type and offset
468 * lower than <code>offset</code> exists, the position is skipped.
470 * @return returns <code>null</code> if no linked position exist.
472 public Position getNextPosition(int offset) {
473 Position[] positions = getPositions(fDocument);
474 return findNextPosition(positions, offset);
477 private static Position findNextPosition(Position[] positions, int offset) {
478 // skip already visited types
479 for (int i = 0; i != positions.length; i++) {
480 if (positions[i].getOffset() > offset) {
481 String type = ((TypedPosition) positions[i]).getType();
483 for (j = 0; j != i; j++)
484 if (((TypedPosition) positions[j]).getType().equals(type))
496 * Returns the position with the greatest offset smaller than
497 * <code>offset</code>.
499 * @return returns <code>null</code> if no linked position exist.
501 public Position getPreviousPosition(int offset) {
502 Position[] positions = getPositions(fDocument);
503 if (positions == null)
506 TypedPosition currentPosition = (TypedPosition) findCurrentPosition(
508 String currentType = currentPosition == null ? null : currentPosition
511 Position lastPosition = null;
512 Position position = getFirstPosition();
514 while (position != null && position.getOffset() < offset) {
515 if (!((TypedPosition) position).getType().equals(currentType))
516 lastPosition = position;
517 position = findNextPosition(positions, position.getOffset());
523 private Position[] getPositions(IDocument document) {
526 // we migth also just return an empty array
527 ;// JavaPlugin(new Status(IStatus.WARNING,
528 // JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager
529 // is not active: "+fPositionCategoryName, new
530 // IllegalStateException())); //$NON-NLS-1$
533 Position[] positions = document.getPositions(fPositionCategoryName);
534 Arrays.sort(positions, fgPositionComparator);
537 } catch (BadPositionCategoryException e) {
539 Assert.isTrue(false);
545 public static boolean includes(Position position, int offset, int length) {
546 return (offset >= position.getOffset())
547 && (offset + length <= position.getOffset()
548 + position.getLength());
551 public static boolean excludes(Position position, int offset, int length) {
552 return (offset + length <= position.getOffset())
553 || (position.getOffset() + position.getLength() <= offset);
557 * Collides if spacing if positions intersect each other or are adjacent.
559 private static boolean collides(Position position, int offset, int length) {
560 return (offset <= position.getOffset() + position.getLength())
561 && (position.getOffset() <= offset + length);
564 private void leave(boolean success) {
568 if (fListener != null)
569 fListener.exit((success ? LinkedPositionUI.COMMIT : 0)
570 | LinkedPositionUI.UPDATE_CARET);
576 private void abort() {
577 uninstall(true); // don't revert anything
579 if (fListener != null)
580 fListener.exit(LinkedPositionUI.COMMIT); // don't let the UI
583 // don't set fMustLeave, as we will get re-registered by a document
588 * @see IDocumentListener#documentAboutToBeChanged(DocumentEvent)
590 public void documentAboutToBeChanged(DocumentEvent event) {
593 event.getDocument().removeDocumentListener(this);
597 IDocument document = event.getDocument();
599 Position[] positions = getPositions(document);
600 Position position = findCurrentPosition(positions, event.getOffset());
602 // modification outside editable position
603 if (position == null) {
604 // check for destruction of constraints (spacing of at least 1)
605 if ((event.getText() == null || event.getText().length() == 0)
606 && (findCurrentPosition(positions, event.getOffset()) != null)
607 && // will never become true, see condition above
608 (findCurrentPosition(positions, event.getOffset()
609 + event.getLength()) != null)) {
613 // modification intersects editable position
615 // modificaction inside editable position
616 if (includes(position, event.getOffset(), event.getLength())) {
617 if (containsLineDelimiters(event.getText()))
620 // modificaction exceeds editable position
628 * @see IDocumentListener#documentChanged(DocumentEvent)
630 public void documentChanged(DocumentEvent event) {
632 // have to handle code assist, so can't just leave the linked mode
635 IDocument document = event.getDocument();
637 Position[] positions = getPositions(document);
638 TypedPosition currentPosition = (TypedPosition) findCurrentPosition(
639 positions, event.getOffset());
641 // ignore document changes (assume it won't invalidate constraints)
642 if (currentPosition == null)
645 int deltaOffset = event.getOffset() - currentPosition.getOffset();
647 if (fListener != null) {
648 int length = event.getText() == null ? 0 : event.getText().length();
649 fListener.setCurrentPosition(currentPosition, deltaOffset + length);
652 for (int i = 0; i != positions.length; i++) {
653 TypedPosition p = (TypedPosition) positions[i];
655 if (p.getType().equals(currentPosition.getType())
656 && !p.equals(currentPosition)) {
657 Replace replace = new Replace(p, deltaOffset,
658 event.getLength(), event.getText());
659 ((IDocumentExtension) document)
660 .registerPostNotificationReplace(this, replace);
666 * @see IPositionUpdater#update(DocumentEvent)
668 public void update(DocumentEvent event) {
670 int eventOffset = event.getOffset();
671 int eventOldLength = event.getLength();
672 int eventNewLength = event.getText() == null ? 0 : event.getText()
674 int deltaLength = eventNewLength - eventOldLength;
676 Position[] positions = getPositions(event.getDocument());
678 for (int i = 0; i != positions.length; i++) {
680 Position position = positions[i];
682 if (position.isDeleted())
685 int offset = position.getOffset();
686 int length = position.getLength();
687 int end = offset + length;
689 if (offset > eventOffset + eventOldLength) // position comes way
690 // after change - shift
691 position.setOffset(offset + deltaLength);
692 else if (end < eventOffset) // position comes way before change -
695 else if (offset <= eventOffset
696 && end >= eventOffset + eventOldLength) {
697 // event completely internal to the position - adjust length
698 position.setLength(length + deltaLength);
699 } else if (offset < eventOffset) {
700 // event extends over end of position - adjust length
701 int newEnd = eventOffset + eventNewLength;
702 position.setLength(newEnd - offset);
703 } else if (end > eventOffset + eventOldLength) {
704 // event extends from before position into it - adjust offset
706 // offset becomes end of event, length ajusted acordingly
707 // we want to recycle the overlapping part
708 int newOffset = eventOffset + eventNewLength;
709 position.setOffset(newOffset);
710 position.setLength(length + deltaLength);
712 // event consumes the position - delete it
714 // JavaPlugin.log(new Status(IStatus.INFO,
715 // JavaPlugin.getPluginId(), IStatus.OK, "linked position
716 // deleted -> must leave: "+fPositionCategoryName, null));
726 private static Position findCurrentPosition(Position[] positions, int offset) {
727 for (int i = 0; i != positions.length; i++)
728 if (includes(positions[i], offset, 0))
734 private boolean containsLineDelimiters(String string) {
739 String[] delimiters = fDocument.getLegalLineDelimiters();
741 for (int i = 0; i != delimiters.length; i++)
742 if (string.indexOf(delimiters[i]) != -1)
749 * Test if ok to modify through UI.
751 public boolean anyPositionIncludes(int offset, int length) {
752 Position[] positions = getPositions(fDocument);
754 Position position = findCurrentPosition(positions, offset);
755 if (position == null)
758 return includes(position, offset, length);
762 * Returns the position that includes the given range.
766 * @return position that includes the given range
768 public Position getEmbracingPosition(int offset, int length) {
769 Position[] positions = getPositions(fDocument);
771 Position position = findCurrentPosition(positions, offset);
772 if (position != null && includes(position, offset, length))
779 * @see org.eclipse.jface.text.IAutoIndentStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument,
780 * org.eclipse.jface.text.DocumentCommand)
782 public void customizeDocumentCommand(IDocument document,
783 DocumentCommand command) {
790 // don't interfere with preceding auto edit strategies
791 if (command.getCommandCount() != 1) {
796 Position[] positions = getPositions(document);
797 TypedPosition currentPosition = (TypedPosition) findCurrentPosition(
798 positions, command.offset);
800 // handle edits outside of a position
801 if (currentPosition == null) {
809 command.doit = false;
810 command.owner = this;
811 command.caretOffset = command.offset + command.length;
813 int deltaOffset = command.offset - currentPosition.getOffset();
815 if (fListener != null)
816 fListener.setCurrentPosition(currentPosition, deltaOffset
817 + command.text.length());
819 for (int i = 0; i != positions.length; i++) {
820 TypedPosition position = (TypedPosition) positions[i];
823 if (position.getType().equals(currentPosition.getType())
824 && !position.equals(currentPosition))
825 command.addCommand(position.getOffset() + deltaOffset,
826 command.length, command.text, this);
827 } catch (BadLocationException e) {