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;
37 * This class manages linked positions in a document. Positions are linked
38 * by type names. If positions have the same type name, they are considered
41 * The manager remains active on a document until any of the following actions
45 * <li>A document change is performed which would invalidate any of the
46 * above constraints.</li>
48 * <li>The method <code>uninstall()</code> is called.</li>
50 * <li>Another instance of <code>LinkedPositionManager</code> tries to
51 * gain control of the same document.
54 public class LinkedPositionManager implements IDocumentListener, 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 changes which are
58 // issued by code assist and document changes which origin from another text viewer.
59 // There is a conflict in interest since in the latter case the linked mode should be left, but in the former case
60 // the linked mode should remain.
61 // To support content assist, document changes have to be propagated to connected positions
62 // by registering replace commands using IDocumentExtension.
63 // if it wasn't for the support of content assist, the documentChanged() method could be reduced to
64 // a simple call to leave(true)
65 private class Replace implements IDocumentExtension.IReplace {
67 private Position fReplacePosition;
68 private int fReplaceDeltaOffset;
69 private int fReplaceLength;
70 private String fReplaceText;
72 public Replace(Position position, int deltaOffset, int length, String text) {
73 fReplacePosition= position;
74 fReplaceDeltaOffset= deltaOffset;
75 fReplaceLength= length;
79 public void perform(IDocument document, IDocumentListener owner) {
80 document.removeDocumentListener(owner);
82 document.replace(fReplacePosition.getOffset() + fReplaceDeltaOffset, fReplaceLength, fReplaceText);
83 } catch (BadLocationException e) {
84 PHPeclipsePlugin.log(e);
87 document.addDocumentListener(owner);
91 private static class PositionComparator implements Comparator {
93 * @see Comparator#compare(Object, Object)
95 public int compare(Object object0, Object object1) {
96 Position position0= (Position) object0;
97 Position position1= (Position) object1;
99 return position0.getOffset() - position1.getOffset();
103 private static final String LINKED_POSITION_PREFIX= "LinkedPositionManager.linked.position"; //$NON-NLS-1$
104 private static final Comparator fgPositionComparator= new PositionComparator();
105 private static final Map fgActiveManagers= new HashMap();
106 private static int fgCounter= 0;
108 private IDocument fDocument;
109 private ILinkedPositionListener fListener;
110 private String fPositionCategoryName;
111 private boolean fMustLeave;
113 * Flag that records the state of this manager. As there are many different entities that may
114 * call leave or exit, these cannot always be sure whether the linked position infrastructure is
115 * still active. This is especially true for multithreaded situations.
117 private boolean fIsActive= false;
121 * Creates a <code>LinkedPositionManager</code> for a <code>IDocument</code>.
123 * @param document the document to use with linked positions.
124 * @param canCoexist <code>true</code> if this manager can coexist with an already existing one
126 public LinkedPositionManager(IDocument document, boolean canCoexist) {
127 Assert.isNotNull(document);
129 fPositionCategoryName= LINKED_POSITION_PREFIX + (fgCounter++);
134 * Creates a <code>LinkedPositionManager</code> for a <code>IDocument</code>.
136 * @param document the document to use with linked positions.
138 public LinkedPositionManager(IDocument document) {
139 this(document, false);
143 * Sets a listener to notify changes of current linked position.
145 public void setLinkedPositionListener(ILinkedPositionListener listener) {
150 * Adds a linked position to the manager with the type being the content of
151 * the document at the specified range.
152 * There are the following constraints for linked positions:
155 * <li>Any two positions have spacing of at least one character.
156 * This implies that two positions must not overlap.</li>
158 * <li>The string at any position must not contain line delimiters.</li>
161 * @param offset the offset of the position.
162 * @param length the length of the position.
164 public void addPosition(int offset, int length) throws BadLocationException {
165 String type= fDocument.get(offset, length);
166 addPosition(offset, length, type);
170 * Adds a linked position of the specified position type to the manager.
171 * There are the following constraints for linked positions:
174 * <li>Any two positions have spacing of at least one character.
175 * This implies that two positions must not overlap.</li>
177 * <li>The string at any position must not contain line delimiters.</li>
180 * @param offset the offset of the position.
181 * @param length the length of the position.
182 * @param type the position type name - any positions with the same type are linked.
184 public void addPosition(int offset, int length, String type) throws BadLocationException {
185 Position[] positions= getPositions(fDocument);
187 if (positions != null) {
188 for (int i = 0; i < positions.length; i++)
189 if (collides(positions[i], offset, length))
190 throw new BadLocationException(LinkedPositionMessages.getString(("LinkedPositionManager.error.position.collision"))); //$NON-NLS-1$
193 String content= fDocument.get(offset, length);
195 if (containsLineDelimiters(content))
196 throw new BadLocationException(LinkedPositionMessages.getString(("LinkedPositionManager.error.contains.line.delimiters"))); //$NON-NLS-1$
199 fDocument.addPosition(fPositionCategoryName, new TypedPosition(offset, length, type));
200 } catch (BadPositionCategoryException e) {
201 PHPeclipsePlugin.log(e);
202 Assert.isTrue(false);
207 * Adds a linked position to the manager. The current document content at the specified range is
208 * taken as the position type.
210 * There are the following constraints for linked positions:
213 * <li>Any two positions have spacing of at least one character.
214 * This implies that two positions must not overlap.</li>
216 * <li>The string at any position must not contain line delimiters.</li>
219 * It is usually best to set the first item in <code>additionalChoices</code> to be equal with
220 * the text inserted at the current position.
223 * @param offset the offset of the position.
224 * @param length the length of the position.
225 * @param additionalChoices a number of additional choices to be displayed when selecting
226 * a position of this <code>type</code>.
228 public void addPosition(int offset, int length, ICompletionProposal[] additionalChoices) throws BadLocationException {
229 String type= fDocument.get(offset, length);
230 addPosition(offset, length, type, additionalChoices);
233 * Adds a linked position of the specified position type to the manager.
234 * There are the following constraints for linked positions:
237 * <li>Any two positions have spacing of at least one character.
238 * This implies that two positions must not overlap.</li>
240 * <li>The string at any position must not contain line delimiters.</li>
243 * It is usually best to set the first item in <code>additionalChoices</code> to be equal with
244 * the text inserted at the current position.
246 * @param offset the offset of the position.
247 * @param length the length of the position.
248 * @param type the position type name - any positions with the same type are linked.
249 * @param additionalChoices a number of additional choices to be displayed when selecting
250 * a position of this <code>type</code>.
252 public void addPosition(int offset, int length, String type, ICompletionProposal[] additionalChoices) throws BadLocationException {
253 Position[] positions= getPositions(fDocument);
255 if (positions != null) {
256 for (int i = 0; i < positions.length; i++)
257 if (collides(positions[i], offset, length))
258 throw new BadLocationException(LinkedPositionMessages.getString(("LinkedPositionManager.error.position.collision"))); //$NON-NLS-1$
261 String content= fDocument.get(offset, length);
263 if (containsLineDelimiters(content))
264 throw new BadLocationException(LinkedPositionMessages.getString(("LinkedPositionManager.error.contains.line.delimiters"))); //$NON-NLS-1$
267 fDocument.addPosition(fPositionCategoryName, new ProposalPosition(offset, length, type, additionalChoices));
268 } catch (BadPositionCategoryException e) {
269 PHPeclipsePlugin.log(e);
270 Assert.isTrue(false);
275 * Tests if a manager is already active for a document.
277 public static boolean hasActiveManager(IDocument document) {
278 return fgActiveManagers.get(document) != null;
281 private void install(boolean canCoexist) {
284 ;//JavaPlugin.log(new Status(IStatus.WARNING, JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager is already active: "+fPositionCategoryName, new IllegalStateException())); //$NON-NLS-1$
287 //JavaPlugin.log(new Status(IStatus.INFO, JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager activated: "+fPositionCategoryName, new Exception())); //$NON-NLS-1$
291 LinkedPositionManager manager= (LinkedPositionManager) fgActiveManagers.get(fDocument);
296 fgActiveManagers.put(fDocument, this);
297 fDocument.addPositionCategory(fPositionCategoryName);
298 fDocument.addPositionUpdater(this);
299 fDocument.addDocumentListener(this);
305 * Leaves the linked mode. If unsuccessful, the linked positions
306 * are restored to the values at the time they were added.
308 public void uninstall(boolean success) {
311 // we migth also just return
312 ;//JavaPlugin(new Status(IStatus.WARNING, JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager activated: "+fPositionCategoryName, new IllegalStateException())); //$NON-NLS-1$
314 fDocument.removeDocumentListener(this);
317 Position[] positions= getPositions(fDocument);
318 if ((!success) && (positions != null)) {
320 for (int i= 0; i != positions.length; i++) {
321 TypedPosition position= (TypedPosition) positions[i];
322 fDocument.replace(position.getOffset(), position.getLength(), position.getType());
326 fDocument.removePositionCategory(fPositionCategoryName);
329 // JavaPlugin.log(new Status(IStatus.INFO, JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager deactivated: "+fPositionCategoryName, new Exception())); //$NON-NLS-1$
331 } catch (BadLocationException e) {
332 PHPeclipsePlugin.log(e);
333 Assert.isTrue(false);
335 } catch (BadPositionCategoryException e) {
336 PHPeclipsePlugin.log(e);
337 Assert.isTrue(false);
340 fDocument.removePositionUpdater(this);
341 fgActiveManagers.remove(fDocument);
348 * Returns the position at the given offset, <code>null</code> if there is no position.
351 public Position getPosition(int offset) {
352 Position[] positions= getPositions(fDocument);
353 if (positions == null)
356 for (int i= positions.length - 1; i >= 0; i--) {
357 Position position= positions[i];
358 if (offset >= position.getOffset() && offset <= position.getOffset() + position.getLength())
366 * Returns the first linked position.
368 * @return returns <code>null</code> if no linked position exist.
370 public Position getFirstPosition() {
371 return getNextPosition(-1);
374 public Position getLastPosition() {
375 Position[] positions= getPositions(fDocument);
376 for (int i= positions.length - 1; i >= 0; i--) {
377 String type= ((TypedPosition) positions[i]).getType();
379 for (j = 0; j != i; j++)
380 if (((TypedPosition) positions[j]).getType().equals(type))
391 * Returns the next linked position with an offset greater than <code>offset</code>.
392 * If another position with the same type and offset lower than <code>offset</code>
393 * exists, the position is skipped.
395 * @return returns <code>null</code> if no linked position exist.
397 public Position getNextPosition(int offset) {
398 Position[] positions= getPositions(fDocument);
399 return findNextPosition(positions, offset);
402 private static Position findNextPosition(Position[] positions, int offset) {
403 // skip already visited types
404 for (int i= 0; i != positions.length; i++) {
405 if (positions[i].getOffset() > offset) {
406 String type= ((TypedPosition) positions[i]).getType();
408 for (j = 0; j != i; j++)
409 if (((TypedPosition) positions[j]).getType().equals(type))
421 * Returns the position with the greatest offset smaller than <code>offset</code>.
423 * @return returns <code>null</code> if no linked position exist.
425 public Position getPreviousPosition(int offset) {
426 Position[] positions= getPositions(fDocument);
427 if (positions == null)
430 TypedPosition currentPosition= (TypedPosition) findCurrentPosition(positions, offset);
431 String currentType= currentPosition == null ? null : currentPosition.getType();
433 Position lastPosition= null;
434 Position position= getFirstPosition();
436 while (position != null && position.getOffset() < offset) {
437 if (!((TypedPosition) position).getType().equals(currentType))
438 lastPosition= position;
439 position= findNextPosition(positions, position.getOffset());
445 private Position[] getPositions(IDocument document) {
448 // we migth also just return an empty array
449 ;//JavaPlugin(new Status(IStatus.WARNING, JavaPlugin.getPluginId(), IStatus.OK, "LinkedPositionManager is not active: "+fPositionCategoryName, new IllegalStateException())); //$NON-NLS-1$
452 Position[] positions= document.getPositions(fPositionCategoryName);
453 Arrays.sort(positions, fgPositionComparator);
456 } catch (BadPositionCategoryException e) {
457 PHPeclipsePlugin.log(e);
458 Assert.isTrue(false);
464 public static boolean includes(Position position, int offset, int length) {
466 (offset >= position.getOffset()) &&
467 (offset + length <= position.getOffset() + position.getLength());
470 public static boolean excludes(Position position, int offset, int length) {
472 (offset + length <= position.getOffset()) ||
473 (position.getOffset() + position.getLength() <= offset);
477 * Collides if spacing if positions intersect each other or are adjacent.
479 private static boolean collides(Position position, int offset, int length) {
481 (offset <= position.getOffset() + position.getLength()) &&
482 (position.getOffset() <= offset + length);
485 private void leave(boolean success) {
489 if (fListener != null)
490 fListener.exit((success ? LinkedPositionUI.COMMIT : 0) | LinkedPositionUI.UPDATE_CARET);
496 private void abort() {
497 uninstall(true); // don't revert anything
499 if (fListener != null)
500 fListener.exit(LinkedPositionUI.COMMIT); // don't let the UI restore anything
502 // don't set fMustLeave, as we will get re-registered by a document event
506 * @see IDocumentListener#documentAboutToBeChanged(DocumentEvent)
508 public void documentAboutToBeChanged(DocumentEvent event) {
511 event.getDocument().removeDocumentListener(this);
515 IDocument document= event.getDocument();
517 Position[] positions= getPositions(document);
518 Position position= findCurrentPosition(positions, event.getOffset());
520 // modification outside editable position
521 if (position == null) {
522 // check for destruction of constraints (spacing of at least 1)
523 if ((event.getText() == null || event.getText().length() == 0) &&
524 (findCurrentPosition(positions, event.getOffset()) != null) && // will never become true, see condition above
525 (findCurrentPosition(positions, event.getOffset() + event.getLength()) != null))
530 // modification intersects editable position
532 // modificaction inside editable position
533 if (includes(position, event.getOffset(), event.getLength())) {
534 if (containsLineDelimiters(event.getText()))
537 // modificaction exceeds editable position
545 * @see IDocumentListener#documentChanged(DocumentEvent)
547 public void documentChanged(DocumentEvent event) {
549 // have to handle code assist, so can't just leave the linked mode
552 IDocument document= event.getDocument();
554 Position[] positions= getPositions(document);
555 TypedPosition currentPosition= (TypedPosition) findCurrentPosition(positions, event.getOffset());
557 // ignore document changes (assume it won't invalidate constraints)
558 if (currentPosition == null)
561 int deltaOffset= event.getOffset() - currentPosition.getOffset();
563 if (fListener != null) {
564 int length= event.getText() == null ? 0 : event.getText().length();
565 fListener.setCurrentPosition(currentPosition, deltaOffset + length);
568 for (int i= 0; i != positions.length; i++) {
569 TypedPosition p= (TypedPosition) positions[i];
571 if (p.getType().equals(currentPosition.getType()) && !p.equals(currentPosition)) {
572 Replace replace= new Replace(p, deltaOffset, event.getLength(), event.getText());
573 ((IDocumentExtension) document).registerPostNotificationReplace(this, replace);
579 * @see IPositionUpdater#update(DocumentEvent)
581 public void update(DocumentEvent event) {
583 int eventOffset= event.getOffset();
584 int eventOldLength= event.getLength();
585 int eventNewLength= event.getText() == null ? 0 : event.getText().length();
586 int deltaLength= eventNewLength - eventOldLength;
588 Position[] positions= getPositions(event.getDocument());
591 for (int i= 0; i != positions.length; i++) {
593 Position position= positions[i];
595 if (position.isDeleted())
598 int offset= position.getOffset();
599 int length= position.getLength();
600 int end= offset + length;
602 if (offset > eventOffset + eventOldLength) // position comes way after change - shift
603 position.setOffset(offset + deltaLength);
604 else if (end < eventOffset) // position comes way before change - leave alone
606 else if (offset <= eventOffset && end >= eventOffset + eventOldLength) {
607 // event completely internal to the position - adjust length
608 position.setLength(length + deltaLength);
609 } else if (offset < eventOffset) {
610 // event extends over end of position - adjust length
611 int newEnd= eventOffset + eventNewLength;
612 position.setLength(newEnd - offset);
613 } else if (end > eventOffset + eventOldLength) {
614 // event extends from before position into it - adjust offset and length
615 // offset becomes end of event, length ajusted acordingly
616 // we want to recycle the overlapping part
617 int newOffset = eventOffset + eventNewLength;
618 position.setOffset(newOffset);
619 position.setLength(length + deltaLength);
621 // event consumes the position - delete it
623 // JavaPlugin.log(new Status(IStatus.INFO, JavaPlugin.getPluginId(), IStatus.OK, "linked position deleted -> must leave: "+fPositionCategoryName, null)); //$NON-NLS-1$
632 private static Position findCurrentPosition(Position[] positions, int offset) {
633 for (int i= 0; i != positions.length; i++)
634 if (includes(positions[i], offset, 0))
640 private boolean containsLineDelimiters(String string) {
645 String[] delimiters= fDocument.getLegalLineDelimiters();
647 for (int i= 0; i != delimiters.length; i++)
648 if (string.indexOf(delimiters[i]) != -1)
655 * Test if ok to modify through UI.
657 public boolean anyPositionIncludes(int offset, int length) {
658 Position[] positions= getPositions(fDocument);
660 Position position= findCurrentPosition(positions, offset);
661 if (position == null)
664 return includes(position, offset, length);
668 * Returns the position that includes the given range.
671 * @return position that includes the given range
673 public Position getEmbracingPosition(int offset, int length) {
674 Position[] positions= getPositions(fDocument);
676 Position position= findCurrentPosition(positions, offset);
677 if (position != null && includes(position, offset, length))
684 * @see org.eclipse.jface.text.IAutoIndentStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument, org.eclipse.jface.text.DocumentCommand)
686 public void customizeDocumentCommand(IDocument document, DocumentCommand command) {
693 // don't interfere with preceding auto edit strategies
694 if (command.getCommandCount() != 1) {
699 Position[] positions= getPositions(document);
700 TypedPosition currentPosition= (TypedPosition) findCurrentPosition(positions, command.offset);
702 // handle edits outside of a position
703 if (currentPosition == null) {
713 command.caretOffset= command.offset + command.length;
715 int deltaOffset= command.offset - currentPosition.getOffset();
717 if (fListener != null)
718 fListener.setCurrentPosition(currentPosition, deltaOffset + command.text.length());
720 for (int i= 0; i != positions.length; i++) {
721 TypedPosition position= (TypedPosition) positions[i];
724 if (position.getType().equals(currentPosition.getType()) && !position.equals(currentPosition))
725 command.addCommand(position.getOffset() + deltaOffset, command.length, command.text, true, this);
726 } catch (BadLocationException e) {
727 PHPeclipsePlugin.log(e);