2 * (c) Copyright IBM Corp. 2000, 2001.
5 package net.sourceforge.phpdt.internal.ui.text.link;
7 import java.util.Arrays;
8 import java.util.Comparator;
9 import java.util.HashMap;
12 import net.sourceforge.phpeclipse.PHPeclipsePlugin;
14 import org.eclipse.jface.text.BadLocationException;
15 import org.eclipse.jface.text.BadPositionCategoryException;
16 import org.eclipse.jface.text.DocumentEvent;
17 import org.eclipse.jface.text.IDocument;
18 import org.eclipse.jface.text.IDocumentExtension;
19 import org.eclipse.jface.text.IDocumentListener;
20 import org.eclipse.jface.text.IPositionUpdater;
21 import org.eclipse.jface.text.Position;
22 import org.eclipse.jface.text.TypedPosition;
23 import org.eclipse.jface.util.Assert;
25 //import org.eclipse.jdt.internal.ui.JavaPlugin;
28 * This class manages linked positions in a document. Positions are linked
29 * by type names. If positions have the same type name, they are considered
32 * The manager remains active on a document until any of the following actions
36 * <li>A document change is performed which would invalidate any of the
37 * above constraints.</li>
39 * <li>The method <code>uninstall()</code> is called.</li>
41 * <li>Another instance of <code>LinkedPositionManager</code> tries to
42 * gain control of the same document.
45 public class LinkedPositionManager implements IDocumentListener, IPositionUpdater {
47 private static class PositionComparator implements Comparator {
49 * @see Comparator#compare(Object, Object)
51 public int compare(Object object0, Object object1) {
52 Position position0= (Position) object0;
53 Position position1= (Position) object1;
55 return position0.getOffset() - position1.getOffset();
59 private class Replace implements IDocumentExtension.IReplace {
61 private Position fReplacePosition;
62 private int fReplaceDeltaOffset;
63 private int fReplaceLength;
64 private String fReplaceText;
66 public Replace(Position position, int deltaOffset, int length, String text) {
67 fReplacePosition= position;
68 fReplaceDeltaOffset= deltaOffset;
69 fReplaceLength= length;
73 public void perform(IDocument document, IDocumentListener owner) {
74 document.removeDocumentListener(owner);
76 document.replace(fReplacePosition.getOffset() + fReplaceDeltaOffset, fReplaceLength, fReplaceText);
77 } catch (BadLocationException e) {
78 PHPeclipsePlugin.log(e);
81 document.addDocumentListener(owner);
85 private static final String LINKED_POSITION= "LinkedPositionManager.linked.position"; //$NON-NLS-1$
86 private static final Comparator fgPositionComparator= new PositionComparator();
87 private static final Map fgActiveManagers= new HashMap();
89 private IDocument fDocument;
91 private LinkedPositionListener fListener;
94 * Creates a <code>LinkedPositionManager</code> for a <code>IDocument</code>.
96 * @param document the document to use with linked positions.
98 public LinkedPositionManager(IDocument document) {
99 Assert.isNotNull(document);
106 * Sets a listener to notify changes of current linked position.
108 public void setLinkedPositionListener(LinkedPositionListener listener) {
113 * Adds a linked position to the manager.
114 * There are the following constraints for linked positions:
117 * <li>Any two positions have spacing of at least one character.
118 * This implies that two positions must not overlap.</li>
120 * <li>The string at any position must not contain line delimiters.</li>
123 * @param offset the offset of the position.
124 * @param length the length of the position.
126 public void addPosition(int offset, int length) throws BadLocationException {
127 Position[] positions= getPositions(fDocument);
129 if (positions != null) {
130 for (int i = 0; i < positions.length; i++)
131 if (collides(positions[i], offset, length))
132 throw new BadLocationException(LinkedPositionMessages.getString(("LinkedPositionManager.error.position.collision"))); //$NON-NLS-1$
135 String type= fDocument.get(offset, length);
137 if (containsLineDelimiters(type))
138 throw new BadLocationException(LinkedPositionMessages.getString(("LinkedPositionManager.error.contains.line.delimiters"))); //$NON-NLS-1$
141 fDocument.addPosition(LINKED_POSITION, new TypedPosition(offset, length, type));
142 } catch (BadPositionCategoryException e) {
143 PHPeclipsePlugin.log(e);
144 Assert.isTrue(false);
149 * Tests if a manager is already active for a document.
151 public static boolean hasActiveManager(IDocument document) {
152 return fgActiveManagers.get(document) != null;
155 private void install() {
156 LinkedPositionManager manager= (LinkedPositionManager) fgActiveManagers.get(fDocument);
160 fgActiveManagers.put(fDocument, this);
162 fDocument.addPositionCategory(LINKED_POSITION);
163 fDocument.addPositionUpdater(this);
164 fDocument.addDocumentListener(this);
168 * Leaves the linked mode. If unsuccessful, the linked positions
169 * are restored to the values at the time they were added.
171 public void uninstall(boolean success) {
172 fDocument.removeDocumentListener(this);
175 Position[] positions= getPositions(fDocument);
176 if ((!success) && (positions != null)) {
178 for (int i= 0; i != positions.length; i++) {
179 TypedPosition position= (TypedPosition) positions[i];
180 fDocument.replace(position.getOffset(), position.getLength(), position.getType());
184 fDocument.removePositionCategory(LINKED_POSITION);
186 } catch (BadLocationException e) {
187 PHPeclipsePlugin.log(e);
188 Assert.isTrue(false);
190 } catch (BadPositionCategoryException e) {
191 PHPeclipsePlugin.log(e);
192 Assert.isTrue(false);
195 fDocument.removePositionUpdater(this);
196 fgActiveManagers.remove(fDocument);
201 * Returns the first linked position.
203 * @return returns <code>null</code> if no linked position exist.
205 public Position getFirstPosition() {
206 return getNextPosition(-1);
210 * Returns the next linked position with an offset greater than <code>offset</code>.
211 * If another position with the same type and offset lower than <code>offset</code>
212 * exists, the position is skipped.
214 * @return returns <code>null</code> if no linked position exist.
216 public Position getNextPosition(int offset) {
217 Position[] positions= getPositions(fDocument);
218 return findNextPosition(positions, offset);
221 private static Position findNextPosition(Position[] positions, int offset) {
222 // skip already visited types
223 for (int i= 0; i != positions.length; i++) {
224 if (positions[i].getOffset() > offset) {
225 String type= ((TypedPosition) positions[i]).getType();
227 for (j = 0; j != i; j++)
228 if (((TypedPosition) positions[j]).getType().equals(type))
240 * Returns the position with the greatest offset smaller than <code>offset</code>.
242 * @return returns <code>null</code> if no linked position exist.
244 public Position getPreviousPosition(int offset) {
245 Position[] positions= getPositions(fDocument);
246 if (positions == null)
249 Position lastPosition= null;
250 Position position= getFirstPosition();
252 while ((position != null) && (position.getOffset() < offset)) {
253 lastPosition= position;
254 position= findNextPosition(positions, position.getOffset());
260 private static Position[] getPositions(IDocument document) {
262 Position[] positions= document.getPositions(LINKED_POSITION);
263 Arrays.sort(positions, fgPositionComparator);
266 } catch (BadPositionCategoryException e) {
267 PHPeclipsePlugin.log(e);
268 Assert.isTrue(false);
274 public static boolean includes(Position position, int offset, int length) {
276 (offset >= position.getOffset()) &&
277 (offset + length <= position.getOffset() + position.getLength());
280 public static boolean excludes(Position position, int offset, int length) {
282 (offset + length <= position.getOffset()) ||
283 (position.getOffset() + position.getLength() <= offset);
287 * Collides if spacing if positions intersect each other or are adjacent.
289 private static boolean collides(Position position, int offset, int length) {
291 (offset <= position.getOffset() + position.getLength()) &&
292 (position.getOffset() <= offset + length);
295 private void leave(boolean success) {
298 if (fListener != null)
299 fListener.exit(success);
303 * @see IDocumentListener#documentAboutToBeChanged(DocumentEvent)
305 public void documentAboutToBeChanged(DocumentEvent event) {
306 IDocument document= event.getDocument();
308 Position[] positions= getPositions(document);
309 Position position= findCurrentEditablePosition(positions, event.getOffset());
311 // modification outside editable position
312 if (position == null) {
313 position= findCurrentPosition(positions, event.getOffset());
315 // modification outside any position
316 if (position == null) {
317 // check for destruction of constraints (spacing of at least 1)
318 if ((event.getText().length() == 0) &&
319 (findCurrentPosition(positions, event.getOffset()) != null) &&
320 (findCurrentPosition(positions, event.getOffset() + event.getLength()) != null))
325 // modification intersects non-editable position
330 // modification intersects editable position
332 // modificaction inside editable position
333 if (includes(position, event.getOffset(), event.getLength())) {
334 if (containsLineDelimiters(event.getText()))
337 // modificaction exceeds editable position
345 * @see IDocumentListener#documentChanged(DocumentEvent)
347 public void documentChanged(DocumentEvent event) {
348 IDocument document= event.getDocument();
350 Position[] positions= getPositions(document);
351 TypedPosition currentPosition= (TypedPosition) findCurrentEditablePosition(positions, event.getOffset());
353 // ignore document changes (assume it won't invalidate constraints)
354 if (currentPosition == null)
357 int deltaOffset= event.getOffset() - currentPosition.getOffset();
359 if (fListener != null)
360 fListener.setCurrentPosition(currentPosition, deltaOffset + event.getText().length());
362 for (int i= 0; i != positions.length; i++) {
363 TypedPosition p= (TypedPosition) positions[i];
365 if (p.getType().equals(currentPosition.getType()) && !p.equals(currentPosition)) {
366 Replace replace= new Replace(p, deltaOffset, event.getLength(), event.getText());
367 ((IDocumentExtension) document).registerPostNotificationReplace(this, replace);
373 * @see IPositionUpdater#update(DocumentEvent)
375 public void update(DocumentEvent event) {
376 int deltaLength= event.getText().length() - event.getLength();
378 Position[] positions= getPositions(event.getDocument());
379 TypedPosition currentPosition= (TypedPosition) findCurrentPosition(positions, event.getOffset());
381 // document change outside positions
382 if (currentPosition == null) {
384 for (int i= 0; i != positions.length; i++) {
385 TypedPosition position= (TypedPosition) positions[i];
386 int offset= position.getOffset();
388 if (offset >= event.getOffset())
389 position.setOffset(offset + deltaLength);
392 // document change within a position
394 int length= currentPosition.getLength();
396 for (int i= 0; i != positions.length; i++) {
397 TypedPosition position= (TypedPosition) positions[i];
398 int offset= position.getOffset();
400 if (position.equals(currentPosition)) {
401 position.setLength(length + deltaLength);
402 } else if (offset > currentPosition.getOffset()) {
403 position.setOffset(offset + deltaLength);
409 private static Position findCurrentPosition(Position[] positions, int offset) {
410 for (int i= 0; i != positions.length; i++)
411 if (includes(positions[i], offset, 0))
417 private static Position findCurrentEditablePosition(Position[] positions, int offset) {
418 Position position= positions[0];
420 while ((position != null) && !includes(position, offset, 0))
421 position= findNextPosition(positions, position.getOffset());
426 private boolean containsLineDelimiters(String string) {
427 String[] delimiters= fDocument.getLegalLineDelimiters();
429 for (int i= 0; i != delimiters.length; i++)
430 if (string.indexOf(delimiters[i]) != -1)
437 * Test if ok to modify through UI.
439 public boolean anyPositionIncludes(int offset, int length) {
440 Position[] positions= getPositions(fDocument);
442 Position position= findCurrentEditablePosition(positions, offset);
443 if (position == null)
446 return includes(position, offset, length);