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