1 /**********************************************************************
2 Copyright (c) 2000, 2002 IBM Corp. 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 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.BadLocationException;
22 import org.eclipse.jface.text.BadPositionCategoryException;
23 import org.eclipse.jface.text.DocumentCommand;
24 import org.eclipse.jface.text.DocumentEvent;
25 import org.eclipse.jface.text.IAutoEditStrategy;
26 import org.eclipse.jface.text.IDocument;
27 import org.eclipse.jface.text.IDocumentExtension;
28 import org.eclipse.jface.text.IDocumentListener;
29 import org.eclipse.jface.text.IPositionUpdater;
30 import org.eclipse.jface.text.Position;
31 import org.eclipse.jface.text.TypedPosition;
32 import org.eclipse.jface.util.Assert;
36 * This class manages linked positions in a document. Positions are linked
37 * by type names. If positions have the same type name, they are considered
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
45 * above constraints.</li>
47 * <li>The method <code>uninstall()</code> is called.</li>
49 * <li>Another instance of <code>LinkedPositionManager</code> tries to
50 * gain control of the same document.
53 public class LinkedPositionManager implements IDocumentListener, IPositionUpdater, IAutoEditStrategy {
55 // This class still exists to properly handle code assist.
56 // This is due to the fact that it cannot be distinguished betweeen document changes which are
57 // issued by code assist and document changes which origin from another text viewer.
58 // There is a conflict in interest since in the latter case the linked mode should be left, but in the former case
59 // the linked mode should remain.
60 // To support content assist, document changes have to be propagated to connected positions
61 // by registering replace commands using IDocumentExtension.
62 // if it wasn't for the support of content assist, the documentChanged() method could be reduced to
63 // a simple call to leave(true)
64 private class Replace implements IDocumentExtension.IReplace {
66 private Position fReplacePosition;
67 private int fReplaceDeltaOffset;
68 private int fReplaceLength;
69 private String fReplaceText;
71 public Replace(Position position, int deltaOffset, int length, String text) {
72 fReplacePosition= position;
73 fReplaceDeltaOffset= deltaOffset;
74 fReplaceLength= length;
78 public void perform(IDocument document, IDocumentListener owner) {
79 document.removeDocumentListener(owner);
81 document.replace(fReplacePosition.getOffset() + fReplaceDeltaOffset, fReplaceLength, fReplaceText);
82 } catch (BadLocationException e) {
83 PHPeclipsePlugin.log(e);
86 document.addDocumentListener(owner);
90 private static class PositionComparator implements Comparator {
92 * @see Comparator#compare(Object, Object)
94 public int compare(Object object0, Object object1) {
95 Position position0= (Position) object0;
96 Position position1= (Position) object1;
98 return position0.getOffset() - position1.getOffset();
102 private static final String LINKED_POSITION= "LinkedPositionManager.linked.position"; //$NON-NLS-1$
103 private static final Comparator fgPositionComparator= new PositionComparator();
104 private static final Map fgActiveManagers= new HashMap();
106 private IDocument fDocument;
108 private LinkedPositionListener fListener;
111 * Creates a <code>LinkedPositionManager</code> for a <code>IDocument</code>.
113 * @param document the document to use with linked positions.
115 public LinkedPositionManager(IDocument document) {
116 Assert.isNotNull(document);
123 * Sets a listener to notify changes of current linked position.
125 public void setLinkedPositionListener(LinkedPositionListener listener) {
130 * Adds a linked position to the manager.
131 * There are the following constraints for linked positions:
134 * <li>Any two positions have spacing of at least one character.
135 * This implies that two positions must not overlap.</li>
137 * <li>The string at any position must not contain line delimiters.</li>
140 * @param offset the offset of the position.
141 * @param length the length of the position.
143 public void addPosition(int offset, int length) throws BadLocationException {
144 Position[] positions= getPositions(fDocument);
146 if (positions != null) {
147 for (int i = 0; i < positions.length; i++)
148 if (collides(positions[i], offset, length))
149 throw new BadLocationException(LinkedPositionMessages.getString(("LinkedPositionManager.error.position.collision"))); //$NON-NLS-1$
152 String type= fDocument.get(offset, length);
154 if (containsLineDelimiters(type))
155 throw new BadLocationException(LinkedPositionMessages.getString(("LinkedPositionManager.error.contains.line.delimiters"))); //$NON-NLS-1$
158 fDocument.addPosition(LINKED_POSITION, new TypedPosition(offset, length, type));
159 } catch (BadPositionCategoryException e) {
160 PHPeclipsePlugin.log(e);
161 Assert.isTrue(false);
166 * Tests if a manager is already active for a document.
168 public static boolean hasActiveManager(IDocument document) {
169 return fgActiveManagers.get(document) != null;
172 private void install() {
173 LinkedPositionManager manager= (LinkedPositionManager) fgActiveManagers.get(fDocument);
177 fgActiveManagers.put(fDocument, this);
179 fDocument.addPositionCategory(LINKED_POSITION);
180 fDocument.addPositionUpdater(this);
181 fDocument.addDocumentListener(this);
185 * Leaves the linked mode. If unsuccessful, the linked positions
186 * are restored to the values at the time they were added.
188 public void uninstall(boolean success) {
189 fDocument.removeDocumentListener(this);
192 Position[] positions= getPositions(fDocument);
193 if ((!success) && (positions != null)) {
195 for (int i= 0; i != positions.length; i++) {
196 TypedPosition position= (TypedPosition) positions[i];
197 fDocument.replace(position.getOffset(), position.getLength(), position.getType());
201 fDocument.removePositionCategory(LINKED_POSITION);
203 } catch (BadLocationException e) {
204 PHPeclipsePlugin.log(e);
205 Assert.isTrue(false);
207 } catch (BadPositionCategoryException e) {
208 PHPeclipsePlugin.log(e);
209 Assert.isTrue(false);
212 fDocument.removePositionUpdater(this);
213 fgActiveManagers.remove(fDocument);
218 * Returns the position at the given offset, <code>null</code> if there is no position.
221 public Position getPosition(int offset) {
222 Position[] positions= getPositions(fDocument);
223 if (positions == null)
226 for (int i= positions.length - 1; i >= 0; i--) {
227 Position position= positions[i];
228 if (offset >= position.getOffset() && offset <= position.getOffset() + position.getLength())
236 * Returns the first linked position.
238 * @return returns <code>null</code> if no linked position exist.
240 public Position getFirstPosition() {
241 return getNextPosition(-1);
245 * Returns the next linked position with an offset greater than <code>offset</code>.
246 * If another position with the same type and offset lower than <code>offset</code>
247 * exists, the position is skipped.
249 * @return returns <code>null</code> if no linked position exist.
251 public Position getNextPosition(int offset) {
252 Position[] positions= getPositions(fDocument);
253 return findNextPosition(positions, offset);
256 private static Position findNextPosition(Position[] positions, int offset) {
257 // skip already visited types
258 for (int i= 0; i != positions.length; i++) {
259 if (positions[i].getOffset() > offset) {
260 String type= ((TypedPosition) positions[i]).getType();
262 for (j = 0; j != i; j++)
263 if (((TypedPosition) positions[j]).getType().equals(type))
275 * Returns the position with the greatest offset smaller than <code>offset</code>.
277 * @return returns <code>null</code> if no linked position exist.
279 public Position getPreviousPosition(int offset) {
280 Position[] positions= getPositions(fDocument);
281 if (positions == null)
284 TypedPosition currentPosition= (TypedPosition) findCurrentPosition(positions, offset);
285 String currentType= currentPosition == null ? null : currentPosition.getType();
287 Position lastPosition= null;
288 Position position= getFirstPosition();
290 while ((position != null) && (position.getOffset() < offset) && !((TypedPosition) position).getType().equals(currentType)) {
291 lastPosition= position;
292 position= findNextPosition(positions, position.getOffset());
298 private static Position[] getPositions(IDocument document) {
300 Position[] positions= document.getPositions(LINKED_POSITION);
301 Arrays.sort(positions, fgPositionComparator);
304 } catch (BadPositionCategoryException e) {
305 PHPeclipsePlugin.log(e);
306 Assert.isTrue(false);
312 public static boolean includes(Position position, int offset, int length) {
314 (offset >= position.getOffset()) &&
315 (offset + length <= position.getOffset() + position.getLength());
318 public static boolean excludes(Position position, int offset, int length) {
320 (offset + length <= position.getOffset()) ||
321 (position.getOffset() + position.getLength() <= offset);
325 * Collides if spacing if positions intersect each other or are adjacent.
327 private static boolean collides(Position position, int offset, int length) {
329 (offset <= position.getOffset() + position.getLength()) &&
330 (position.getOffset() <= offset + length);
333 private void leave(boolean success) {
336 if (fListener != null)
337 fListener.exit(success);
341 * @see IDocumentListener#documentAboutToBeChanged(DocumentEvent)
343 public void documentAboutToBeChanged(DocumentEvent event) {
345 IDocument document= event.getDocument();
347 Position[] positions= getPositions(document);
348 Position position= findCurrentPosition(positions, event.getOffset());
350 // modification outside editable position
351 if (position == null) {
352 // check for destruction of constraints (spacing of at least 1)
353 if ((event.getText() == null || event.getText().length() == 0) &&
354 (findCurrentPosition(positions, event.getOffset()) != null) &&
355 (findCurrentPosition(positions, event.getOffset() + event.getLength()) != null))
360 // modification intersects editable position
362 // modificaction inside editable position
363 if (includes(position, event.getOffset(), event.getLength())) {
364 if (containsLineDelimiters(event.getText()))
367 // modificaction exceeds editable position
375 * @see IDocumentListener#documentChanged(DocumentEvent)
377 public void documentChanged(DocumentEvent event) {
379 // have to handle code assist, so can't just leave the linked mode
382 IDocument document= event.getDocument();
384 Position[] positions= getPositions(document);
385 TypedPosition currentPosition= (TypedPosition) findCurrentPosition(positions, event.getOffset());
387 // ignore document changes (assume it won't invalidate constraints)
388 if (currentPosition == null)
391 int deltaOffset= event.getOffset() - currentPosition.getOffset();
393 if (fListener != null) {
394 int length= event.getText() == null ? 0 : event.getText().length();
395 fListener.setCurrentPosition(currentPosition, deltaOffset + length);
398 for (int i= 0; i != positions.length; i++) {
399 TypedPosition p= (TypedPosition) positions[i];
401 if (p.getType().equals(currentPosition.getType()) && !p.equals(currentPosition)) {
402 Replace replace= new Replace(p, deltaOffset, event.getLength(), event.getText());
403 ((IDocumentExtension) document).registerPostNotificationReplace(this, replace);
409 * @see IPositionUpdater#update(DocumentEvent)
411 public void update(DocumentEvent event) {
412 int deltaLength= (event.getText() == null ? 0 : event.getText().length()) - event.getLength();
414 Position[] positions= getPositions(event.getDocument());
415 TypedPosition currentPosition= (TypedPosition) findCurrentPosition(positions, event.getOffset());
417 // document change outside positions
418 if (currentPosition == null) {
420 for (int i= 0; i != positions.length; i++) {
421 TypedPosition position= (TypedPosition) positions[i];
422 int offset= position.getOffset();
424 if (offset >= event.getOffset())
425 position.setOffset(offset + deltaLength);
428 // document change within a position
430 int length= currentPosition.getLength();
432 for (int i= 0; i != positions.length; i++) {
433 TypedPosition position= (TypedPosition) positions[i];
434 int offset= position.getOffset();
436 if (position.equals(currentPosition)) {
437 position.setLength(length + deltaLength);
438 } else if (offset > currentPosition.getOffset()) {
439 position.setOffset(offset + deltaLength);
445 private static Position findCurrentPosition(Position[] positions, int offset) {
446 for (int i= 0; i != positions.length; i++)
447 if (includes(positions[i], offset, 0))
453 private boolean containsLineDelimiters(String string) {
458 String[] delimiters= fDocument.getLegalLineDelimiters();
460 for (int i= 0; i != delimiters.length; i++)
461 if (string.indexOf(delimiters[i]) != -1)
468 * Test if ok to modify through UI.
470 public boolean anyPositionIncludes(int offset, int length) {
471 Position[] positions= getPositions(fDocument);
473 Position position= findCurrentPosition(positions, offset);
474 if (position == null)
477 return includes(position, offset, length);
481 * @see org.eclipse.jface.text.IAutoIndentStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument, org.eclipse.jface.text.DocumentCommand)
483 public void customizeDocumentCommand(IDocument document, DocumentCommand command) {
485 // don't interfere with preceding auto edit strategies
486 if (command.getCommandCount() != 1) {
491 Position[] positions= getPositions(document);
492 TypedPosition currentPosition= (TypedPosition) findCurrentPosition(positions, command.offset);
494 // handle edits outside of a position
495 if (currentPosition == null) {
505 command.caretOffset= command.offset + command.length;
507 int deltaOffset= command.offset - currentPosition.getOffset();
509 if (fListener != null)
510 fListener.setCurrentPosition(currentPosition, deltaOffset + command.text.length());
512 for (int i= 0; i != positions.length; i++) {
513 TypedPosition position= (TypedPosition) positions[i];
516 if (position.getType().equals(currentPosition.getType()) && !position.equals(currentPosition))
517 command.addCommand(position.getOffset() + deltaOffset, command.length, command.text, this);
518 } catch (BadLocationException e) {
519 PHPeclipsePlugin.log(e);