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;
13 import org.eclipse.jface.text.BadLocationException;
14 import org.eclipse.jface.text.BadPositionCategoryException;
15 import org.eclipse.jface.text.DocumentEvent;
16 import org.eclipse.jface.text.IDocument;
17 import org.eclipse.jface.text.IDocumentExtension;
18 import org.eclipse.jface.text.IDocumentListener;
19 import org.eclipse.jface.text.IPositionUpdater;
20 import org.eclipse.jface.text.Position;
21 import org.eclipse.jface.text.TypedPosition;
22 import org.eclipse.jface.util.Assert;
24 //import org.eclipse.jdt.internal.ui.JavaPlugin;
27 * This class manages linked positions in a document. Positions are linked
28 * by type names. If positions have the same type name, they are considered
31 * The manager remains active on a document until any of the following actions
35 * <li>A document change is performed which would invalidate any of the
36 * above constraints.</li>
38 * <li>The method <code>uninstall()</code> is called.</li>
40 * <li>Another instance of <code>LinkedPositionManager</code> tries to
41 * gain control of the same document.
44 public class LinkedPositionManager implements IDocumentListener, IPositionUpdater {
46 private static class PositionComparator implements Comparator {
48 * @see Comparator#compare(Object, Object)
50 public int compare(Object object0, Object object1) {
51 Position position0= (Position) object0;
52 Position position1= (Position) object1;
54 return position0.getOffset() - position1.getOffset();
58 private class Replace implements IDocumentExtension.IReplace {
60 private Position fReplacePosition;
61 private int fReplaceDeltaOffset;
62 private int fReplaceLength;
63 private String fReplaceText;
65 public Replace(Position position, int deltaOffset, int length, String text) {
66 fReplacePosition= position;
67 fReplaceDeltaOffset= deltaOffset;
68 fReplaceLength= length;
72 public void perform(IDocument document, IDocumentListener owner) {
73 document.removeDocumentListener(owner);
75 document.replace(fReplacePosition.getOffset() + fReplaceDeltaOffset, fReplaceLength, fReplaceText);
76 } catch (BadLocationException e) {
77 PHPeclipsePlugin.log(e);
80 document.addDocumentListener(owner);
84 private static final String LINKED_POSITION= "LinkedPositionManager.linked.position"; //$NON-NLS-1$
85 private static final Comparator fgPositionComparator= new PositionComparator();
86 private static final Map fgActiveManagers= new HashMap();
88 private IDocument fDocument;
90 private LinkedPositionListener fListener;
93 * Creates a <code>LinkedPositionManager</code> for a <code>IDocument</code>.
95 * @param document the document to use with linked positions.
97 public LinkedPositionManager(IDocument document) {
98 Assert.isNotNull(document);
105 * Sets a listener to notify changes of current linked position.
107 public void setLinkedPositionListener(LinkedPositionListener listener) {
112 * Adds a linked position to the manager.
113 * There are the following constraints for linked positions:
116 * <li>Any two positions have spacing of at least one character.
117 * This implies that two positions must not overlap.</li>
119 * <li>The string at any position must not contain line delimiters.</li>
122 * @param offset the offset of the position.
123 * @param length the length of the position.
125 public void addPosition(int offset, int length) throws BadLocationException {
126 Position[] positions= getPositions(fDocument);
128 if (positions != null) {
129 for (int i = 0; i < positions.length; i++)
130 if (collides(positions[i], offset, length))
131 throw new BadLocationException(LinkedPositionMessages.getString(("LinkedPositionManager.error.position.collision"))); //$NON-NLS-1$
134 String type= fDocument.get(offset, length);
136 if (containsLineDelimiters(type))
137 throw new BadLocationException(LinkedPositionMessages.getString(("LinkedPositionManager.error.contains.line.delimiters"))); //$NON-NLS-1$
140 fDocument.addPosition(LINKED_POSITION, new TypedPosition(offset, length, type));
141 } catch (BadPositionCategoryException e) {
142 PHPeclipsePlugin.log(e);
143 Assert.isTrue(false);
148 * Tests if a manager is already active for a document.
150 public static boolean hasActiveManager(IDocument document) {
151 return fgActiveManagers.get(document) != null;
154 private void install() {
155 LinkedPositionManager manager= (LinkedPositionManager) fgActiveManagers.get(fDocument);
159 fgActiveManagers.put(fDocument, this);
161 fDocument.addPositionCategory(LINKED_POSITION);
162 fDocument.addPositionUpdater(this);
163 fDocument.addDocumentListener(this);
167 * Leaves the linked mode. If unsuccessful, the linked positions
168 * are restored to the values at the time they were added.
170 public void uninstall(boolean success) {
171 fDocument.removeDocumentListener(this);
174 Position[] positions= getPositions(fDocument);
175 if ((!success) && (positions != null)) {
177 for (int i= 0; i != positions.length; i++) {
178 TypedPosition position= (TypedPosition) positions[i];
179 fDocument.replace(position.getOffset(), position.getLength(), position.getType());
183 fDocument.removePositionCategory(LINKED_POSITION);
185 } catch (BadLocationException e) {
186 PHPeclipsePlugin.log(e);
187 Assert.isTrue(false);
189 } catch (BadPositionCategoryException e) {
190 PHPeclipsePlugin.log(e);
191 Assert.isTrue(false);
194 fDocument.removePositionUpdater(this);
195 fgActiveManagers.remove(fDocument);
200 * Returns the first linked position.
202 * @return returns <code>null</code> if no linked position exist.
204 public Position getFirstPosition() {
205 return getNextPosition(-1);
209 * Returns the next linked position with an offset greater than <code>offset</code>.
210 * If another position with the same type and offset lower than <code>offset</code>
211 * exists, the position is skipped.
213 * @return returns <code>null</code> if no linked position exist.
215 public Position getNextPosition(int offset) {
216 Position[] positions= getPositions(fDocument);
217 return findNextPosition(positions, offset);
220 private static Position findNextPosition(Position[] positions, int offset) {
221 // skip already visited types
222 for (int i= 0; i != positions.length; i++) {
223 if (positions[i].getOffset() > offset) {
224 String type= ((TypedPosition) positions[i]).getType();
226 for (j = 0; j != i; j++)
227 if (((TypedPosition) positions[j]).getType().equals(type))
239 * Returns the position with the greatest offset smaller than <code>offset</code>.
241 * @return returns <code>null</code> if no linked position exist.
243 public Position getPreviousPosition(int offset) {
244 Position[] positions= getPositions(fDocument);
245 if (positions == null)
248 Position lastPosition= null;
249 Position position= getFirstPosition();
251 while ((position != null) && (position.getOffset() < offset)) {
252 lastPosition= position;
253 position= findNextPosition(positions, position.getOffset());
259 private static Position[] getPositions(IDocument document) {
261 Position[] positions= document.getPositions(LINKED_POSITION);
262 Arrays.sort(positions, fgPositionComparator);
265 } catch (BadPositionCategoryException e) {
266 PHPeclipsePlugin.log(e);
267 Assert.isTrue(false);
273 public static boolean includes(Position position, int offset, int length) {
275 (offset >= position.getOffset()) &&
276 (offset + length <= position.getOffset() + position.getLength());
279 public static boolean excludes(Position position, int offset, int length) {
281 (offset + length <= position.getOffset()) ||
282 (position.getOffset() + position.getLength() <= offset);
286 * Collides if spacing if positions intersect each other or are adjacent.
288 private static boolean collides(Position position, int offset, int length) {
290 (offset <= position.getOffset() + position.getLength()) &&
291 (position.getOffset() <= offset + length);
294 private void leave(boolean success) {
297 if (fListener != null)
298 fListener.exit(success);
302 * @see IDocumentListener#documentAboutToBeChanged(DocumentEvent)
304 public void documentAboutToBeChanged(DocumentEvent event) {
305 IDocument document= event.getDocument();
307 Position[] positions= getPositions(document);
308 Position position= findCurrentEditablePosition(positions, event.getOffset());
310 // modification outside editable position
311 if (position == null) {
312 position= findCurrentPosition(positions, event.getOffset());
314 // modification outside any position
315 if (position == null) {
316 // check for destruction of constraints (spacing of at least 1)
317 if ((event.getText().length() == 0) &&
318 (findCurrentPosition(positions, event.getOffset()) != null) &&
319 (findCurrentPosition(positions, event.getOffset() + event.getLength()) != null))
324 // modification intersects non-editable position
329 // modification intersects editable position
331 // modificaction inside editable position
332 if (includes(position, event.getOffset(), event.getLength())) {
333 if (containsLineDelimiters(event.getText()))
336 // modificaction exceeds editable position
344 * @see IDocumentListener#documentChanged(DocumentEvent)
346 public void documentChanged(DocumentEvent event) {
347 IDocument document= event.getDocument();
349 Position[] positions= getPositions(document);
350 TypedPosition currentPosition= (TypedPosition) findCurrentEditablePosition(positions, event.getOffset());
352 // ignore document changes (assume it won't invalidate constraints)
353 if (currentPosition == null)
356 int deltaOffset= event.getOffset() - currentPosition.getOffset();
358 if (fListener != null)
359 fListener.setCurrentPosition(currentPosition, deltaOffset + event.getText().length());
361 for (int i= 0; i != positions.length; i++) {
362 TypedPosition p= (TypedPosition) positions[i];
364 if (p.getType().equals(currentPosition.getType()) && !p.equals(currentPosition)) {
365 Replace replace= new Replace(p, deltaOffset, event.getLength(), event.getText());
366 ((IDocumentExtension) document).registerPostNotificationReplace(this, replace);
372 * @see IPositionUpdater#update(DocumentEvent)
374 public void update(DocumentEvent event) {
375 int deltaLength= event.getText().length() - event.getLength();
377 Position[] positions= getPositions(event.getDocument());
378 TypedPosition currentPosition= (TypedPosition) findCurrentPosition(positions, event.getOffset());
380 // document change outside positions
381 if (currentPosition == null) {
383 for (int i= 0; i != positions.length; i++) {
384 TypedPosition position= (TypedPosition) positions[i];
385 int offset= position.getOffset();
387 if (offset >= event.getOffset())
388 position.setOffset(offset + deltaLength);
391 // document change within a position
393 int length= currentPosition.getLength();
395 for (int i= 0; i != positions.length; i++) {
396 TypedPosition position= (TypedPosition) positions[i];
397 int offset= position.getOffset();
399 if (position.equals(currentPosition)) {
400 position.setLength(length + deltaLength);
401 } else if (offset > currentPosition.getOffset()) {
402 position.setOffset(offset + deltaLength);
408 private static Position findCurrentPosition(Position[] positions, int offset) {
409 for (int i= 0; i != positions.length; i++)
410 if (includes(positions[i], offset, 0))
416 private static Position findCurrentEditablePosition(Position[] positions, int offset) {
417 Position position= positions[0];
419 while ((position != null) && !includes(position, offset, 0))
420 position= findNextPosition(positions, position.getOffset());
425 private boolean containsLineDelimiters(String string) {
426 String[] delimiters= fDocument.getLegalLineDelimiters();
428 for (int i= 0; i != delimiters.length; i++)
429 if (string.indexOf(delimiters[i]) != -1)
436 * Test if ok to modify through UI.
438 public boolean anyPositionIncludes(int offset, int length) {
439 Position[] positions= getPositions(fDocument);
441 Position position= findCurrentEditablePosition(positions, offset);
442 if (position == null)
445 return includes(position, offset, length);