*** empty log message ***
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpdt / internal / ui / text / link / LinkedPositionManager.java
1 /*
2  * (c) Copyright IBM Corp. 2000, 2001.
3  * All Rights Reserved.
4  */
5 package net.sourceforge.phpdt.internal.ui.text.link;
6
7 import java.util.Arrays;
8 import java.util.Comparator;
9 import java.util.HashMap;
10 import java.util.Map;
11
12 import net.sourceforge.phpeclipse.PHPeclipsePlugin;
13
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;
24
25 //import org.eclipse.jdt.internal.ui.JavaPlugin;
26
27 /**
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
30  * as <em>linked</em>.
31  * 
32  * The manager remains active on a document until any of the following actions
33  * occurs:
34  * 
35  * <ul>
36  *   <li>A document change is performed which would invalidate any of the
37  *       above constraints.</li>
38  * 
39  *   <li>The method <code>uninstall()</code> is called.</li>
40  * 
41  *   <li>Another instance of <code>LinkedPositionManager</code> tries to
42  *       gain control of the same document.
43  * </ul>
44  */
45 public class LinkedPositionManager implements IDocumentListener, IPositionUpdater {
46         
47         private static class PositionComparator implements Comparator {
48                 /*
49                  * @see Comparator#compare(Object, Object)
50                  */
51                 public int compare(Object object0, Object object1) {
52                         Position position0= (Position) object0;
53                         Position position1= (Position) object1;
54                         
55                         return position0.getOffset() - position1.getOffset();
56                 }
57         }
58         
59         private class Replace implements IDocumentExtension.IReplace {
60                 
61                 private Position fReplacePosition;
62                 private int fReplaceDeltaOffset;
63                 private int fReplaceLength;
64                 private String fReplaceText;
65                 
66                 public Replace(Position position, int deltaOffset, int length, String text) {
67                         fReplacePosition= position;
68                         fReplaceDeltaOffset= deltaOffset;
69                         fReplaceLength= length;
70                         fReplaceText= text;
71                 }
72                                 
73                 public void perform(IDocument document, IDocumentListener owner) {
74                         document.removeDocumentListener(owner);
75                         try {
76                                 document.replace(fReplacePosition.getOffset() + fReplaceDeltaOffset, fReplaceLength, fReplaceText);
77                         } catch (BadLocationException e) {
78                                 PHPeclipsePlugin.log(e);
79                                 // TBD
80                         }
81                         document.addDocumentListener(owner);
82                 }
83         }
84
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();
88                 
89         private IDocument fDocument;
90         
91         private LinkedPositionListener fListener;
92
93         /**
94          * Creates a <code>LinkedPositionManager</code> for a <code>IDocument</code>.
95          * 
96          * @param document the document to use with linked positions.
97          */
98         public LinkedPositionManager(IDocument document) {
99                 Assert.isNotNull(document);
100                 
101                 fDocument= document;            
102                 install();
103         }
104
105         /**
106          * Sets a listener to notify changes of current linked position.
107          */
108         public void setLinkedPositionListener(LinkedPositionListener listener) {
109                 fListener= listener;    
110         }
111         
112         /**
113          * Adds a linked position to the manager.
114          * There are the following constraints for linked positions:
115          * 
116          * <ul>
117          *   <li>Any two positions have spacing of at least one character.
118          *       This implies that two positions must not overlap.</li>
119          *
120          *   <li>The string at any position must not contain line delimiters.</li>
121          * </ul>
122          * 
123          * @param offset the offset of the position.
124          * @param length the length of the position.
125          */
126         public void addPosition(int offset, int length) throws BadLocationException {
127                 Position[] positions= getPositions(fDocument);
128
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$
133                 }
134                 
135                 String type= fDocument.get(offset, length);             
136
137                 if (containsLineDelimiters(type))
138                         throw new BadLocationException(LinkedPositionMessages.getString(("LinkedPositionManager.error.contains.line.delimiters"))); //$NON-NLS-1$
139
140                 try {
141                         fDocument.addPosition(LINKED_POSITION, new TypedPosition(offset, length, type));
142                 } catch (BadPositionCategoryException e) {
143                         PHPeclipsePlugin.log(e);
144                         Assert.isTrue(false);
145                 }
146         }
147
148         /**
149          * Tests if a manager is already active for a document.
150          */
151         public static boolean hasActiveManager(IDocument document) {
152                 return fgActiveManagers.get(document) != null;
153         }
154
155         private void install() {
156                 LinkedPositionManager manager= (LinkedPositionManager) fgActiveManagers.get(fDocument);
157                 if (manager != null)
158                         manager.leave(true);            
159
160                 fgActiveManagers.put(fDocument, this);
161                 
162                 fDocument.addPositionCategory(LINKED_POSITION);
163                 fDocument.addPositionUpdater(this);             
164                 fDocument.addDocumentListener(this);
165         }       
166         
167         /**
168          * Leaves the linked mode. If unsuccessful, the linked positions
169          * are restored to the values at the time they were added.
170          */
171         public void uninstall(boolean success) {                        
172                 fDocument.removeDocumentListener(this);
173
174                 try {
175                         Position[] positions= getPositions(fDocument);  
176                         if ((!success) && (positions != null)) {
177                                 // restore
178                                 for (int i= 0; i != positions.length; i++) {
179                                         TypedPosition position= (TypedPosition) positions[i];                           
180                                         fDocument.replace(position.getOffset(), position.getLength(), position.getType());
181                                 }
182                         }               
183                         
184                         fDocument.removePositionCategory(LINKED_POSITION);
185
186                 } catch (BadLocationException e) {
187                         PHPeclipsePlugin.log(e);
188                         Assert.isTrue(false);
189
190                 } catch (BadPositionCategoryException e) {
191                         PHPeclipsePlugin.log(e);
192                         Assert.isTrue(false);
193
194                 } finally {
195                         fDocument.removePositionUpdater(this);          
196                         fgActiveManagers.remove(fDocument);             
197                 }
198         }
199
200         /**
201          * Returns the first linked position.
202          * 
203          * @return returns <code>null</code> if no linked position exist.
204          */
205         public Position getFirstPosition() {
206                 return getNextPosition(-1);
207         }
208
209         /**
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.
213          * 
214          * @return returns <code>null</code> if no linked position exist.
215          */
216         public Position getNextPosition(int offset) {
217                 Position[] positions= getPositions(fDocument);
218                 return findNextPosition(positions, offset);
219         }
220
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();
226                                 int j;
227                                 for (j = 0; j != i; j++)
228                                         if (((TypedPosition) positions[j]).getType().equals(type))
229                                                 break;
230
231                                 if (j == i)
232                                         return positions[i];                            
233                         }
234                 }
235
236                 return null;
237         }
238         
239         /**
240          * Returns the position with the greatest offset smaller than <code>offset</code>.
241          *
242          * @return returns <code>null</code> if no linked position exist.
243          */
244         public Position getPreviousPosition(int offset) {
245                 Position[] positions= getPositions(fDocument);          
246                 if (positions == null)
247                         return null;
248
249                 Position lastPosition= null;
250                 Position position= getFirstPosition();
251                 
252                 while ((position != null) && (position.getOffset() < offset)) {
253                         lastPosition= position;
254                         position= findNextPosition(positions, position.getOffset());
255                 }               
256                 
257                 return lastPosition;
258         }
259
260         private static Position[] getPositions(IDocument document) {
261                 try {
262                         Position[] positions= document.getPositions(LINKED_POSITION);
263                         Arrays.sort(positions, fgPositionComparator);
264                         return positions;
265
266                 } catch (BadPositionCategoryException e) {
267                         PHPeclipsePlugin.log(e);
268                         Assert.isTrue(false);
269                 }
270                 
271                 return null;
272         }       
273
274         public static boolean includes(Position position, int offset, int length) {
275                 return
276                         (offset >= position.getOffset()) &&
277                         (offset + length <= position.getOffset() + position.getLength());
278         }
279
280         public static boolean excludes(Position position, int offset, int length) {
281                 return
282                         (offset + length <= position.getOffset()) ||
283                         (position.getOffset() + position.getLength() <= offset);
284         }
285
286         /*
287          * Collides if spacing if positions intersect each other or are adjacent.
288          */
289         private static boolean collides(Position position, int offset, int length) {
290                 return
291                         (offset <= position.getOffset() + position.getLength()) &&
292                         (position.getOffset() <= offset + length);      
293         }
294         
295         private void leave(boolean success) {
296                 uninstall(success);
297
298                 if (fListener != null)
299                         fListener.exit(success);                
300         }
301
302         /*
303          * @see IDocumentListener#documentAboutToBeChanged(DocumentEvent)
304          */
305         public void documentAboutToBeChanged(DocumentEvent event) {
306                 IDocument document= event.getDocument();
307
308                 Position[] positions= getPositions(document);
309                 Position position= findCurrentEditablePosition(positions, event.getOffset());
310
311                 // modification outside editable position
312                 if (position == null) {
313                         position= findCurrentPosition(positions, event.getOffset());
314
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))
321                                 {
322                                         leave(true);
323                                 }
324                                 
325                         // modification intersects non-editable position
326                         } else {
327                                 leave(true);
328                         }
329
330                 // modification intersects editable position
331                 } else {
332                         // modificaction inside editable position
333                         if (includes(position, event.getOffset(), event.getLength())) {
334                                 if (containsLineDelimiters(event.getText()))
335                                         leave(true);
336
337                         // modificaction exceeds editable position
338                         } else {
339                                 leave(true);
340                         }
341                 }
342         }
343
344         /*
345          * @see IDocumentListener#documentChanged(DocumentEvent)
346          */
347         public void documentChanged(DocumentEvent event) {              
348                 IDocument document= event.getDocument();
349
350                 Position[] positions= getPositions(document);
351                 TypedPosition currentPosition= (TypedPosition) findCurrentEditablePosition(positions, event.getOffset());
352
353                 // ignore document changes (assume it won't invalidate constraints)
354                 if (currentPosition == null)
355                         return;
356                 
357                 int deltaOffset= event.getOffset() - currentPosition.getOffset();               
358
359                 if (fListener != null)
360                         fListener.setCurrentPosition(currentPosition, deltaOffset + event.getText().length());
361                 
362                 for (int i= 0; i != positions.length; i++) {
363                         TypedPosition p= (TypedPosition) positions[i];                  
364                         
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);
368                         }
369                 }
370         }
371         
372         /*
373          * @see IPositionUpdater#update(DocumentEvent)
374          */
375         public void update(DocumentEvent event) {
376                 int deltaLength= event.getText().length() - event.getLength();
377
378                 Position[] positions= getPositions(event.getDocument());
379                 TypedPosition currentPosition= (TypedPosition) findCurrentPosition(positions, event.getOffset());
380
381                 // document change outside positions
382                 if (currentPosition == null) {
383                         
384                         for (int i= 0; i != positions.length; i++) {
385                                 TypedPosition position= (TypedPosition) positions[i];
386                                 int offset= position.getOffset();
387                                 
388                                 if (offset >= event.getOffset())
389                                         position.setOffset(offset + deltaLength);
390                         }
391                         
392                 // document change within a position
393                 } else {
394                         int length= currentPosition.getLength();
395         
396                         for (int i= 0; i != positions.length; i++) {
397                                 TypedPosition position= (TypedPosition) positions[i];
398                                 int offset= position.getOffset();
399                                 
400                                 if (position.equals(currentPosition)) {
401                                         position.setLength(length + deltaLength);                                       
402                                 } else if (offset > currentPosition.getOffset()) {
403                                         position.setOffset(offset + deltaLength);
404                                 }
405                         }               
406                 }
407         }
408
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))
412                                 return positions[i];
413                 
414                 return null;                    
415         }
416
417         private static Position findCurrentEditablePosition(Position[] positions, int offset) {
418                 Position position= positions[0];
419
420                 while ((position != null) && !includes(position, offset, 0))
421                         position= findNextPosition(positions, position.getOffset());
422
423                 return position;
424         }
425
426         private boolean containsLineDelimiters(String string) {
427                 String[] delimiters= fDocument.getLegalLineDelimiters();
428
429                 for (int i= 0; i != delimiters.length; i++)
430                         if (string.indexOf(delimiters[i]) != -1)
431                                 return true;
432
433                 return false;
434         }
435         
436         /**
437          * Test if ok to modify through UI.
438          */
439         public boolean anyPositionIncludes(int offset, int length) {
440                 Position[] positions= getPositions(fDocument);
441
442                 Position position= findCurrentEditablePosition(positions, offset);
443                 if (position == null)
444                         return false;
445                 
446                 return includes(position, offset, length);
447         }
448         
449 }