0745ef7737f8460daf55a5b51230e9f56e26183b
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpdt / internal / ui / text / link / LinkedPositionManager.java
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
7
8 Contributors:
9     IBM Corporation - Initial implementation
10 **********************************************************************/
11
12 package net.sourceforge.phpdt.internal.ui.text.link;
13
14 import java.util.Arrays;
15 import java.util.Comparator;
16 import java.util.HashMap;
17 import java.util.Map;
18
19 import net.sourceforge.phpeclipse.PHPeclipsePlugin;
20
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;
33
34
35 /**
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
38  * as <em>linked</em>.
39  * 
40  * The manager remains active on a document until any of the following actions
41  * occurs:
42  * 
43  * <ul>
44  *   <li>A document change is performed which would invalidate any of the
45  *       above constraints.</li>
46  * 
47  *   <li>The method <code>uninstall()</code> is called.</li>
48  * 
49  *   <li>Another instance of <code>LinkedPositionManager</code> tries to
50  *       gain control of the same document.
51  * </ul>
52  */
53 public class LinkedPositionManager implements IDocumentListener, IPositionUpdater, IAutoEditStrategy {
54
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 {
65                 
66                 private Position fReplacePosition;
67                 private int fReplaceDeltaOffset;
68                 private int fReplaceLength;
69                 private String fReplaceText;
70                 
71                 public Replace(Position position, int deltaOffset, int length, String text) {
72                         fReplacePosition= position;
73                         fReplaceDeltaOffset= deltaOffset;
74                         fReplaceLength= length;
75                         fReplaceText= text;
76                 }
77                                 
78                 public void perform(IDocument document, IDocumentListener owner) {
79                         document.removeDocumentListener(owner);
80                         try {
81                                 document.replace(fReplacePosition.getOffset() + fReplaceDeltaOffset, fReplaceLength, fReplaceText);
82                         } catch (BadLocationException e) {
83                                 PHPeclipsePlugin.log(e);
84                                 // TBD
85                         }
86                         document.addDocumentListener(owner);
87                 }
88         }
89         
90         private static class PositionComparator implements Comparator {
91                 /*
92                  * @see Comparator#compare(Object, Object)
93                  */
94                 public int compare(Object object0, Object object1) {
95                         Position position0= (Position) object0;
96                         Position position1= (Position) object1;
97                         
98                         return position0.getOffset() - position1.getOffset();
99                 }
100         }
101
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();
105                 
106         private IDocument fDocument;
107         
108         private LinkedPositionListener fListener;
109
110         /**
111          * Creates a <code>LinkedPositionManager</code> for a <code>IDocument</code>.
112          * 
113          * @param document the document to use with linked positions.
114          */
115         public LinkedPositionManager(IDocument document) {
116                 Assert.isNotNull(document);
117                 
118                 fDocument= document;            
119                 install();
120         }
121
122         /**
123          * Sets a listener to notify changes of current linked position.
124          */
125         public void setLinkedPositionListener(LinkedPositionListener listener) {
126                 fListener= listener;    
127         }
128         
129         /**
130          * Adds a linked position to the manager.
131          * There are the following constraints for linked positions:
132          * 
133          * <ul>
134          *   <li>Any two positions have spacing of at least one character.
135          *       This implies that two positions must not overlap.</li>
136          *
137          *   <li>The string at any position must not contain line delimiters.</li>
138          * </ul>
139          * 
140          * @param offset the offset of the position.
141          * @param length the length of the position.
142          */
143         public void addPosition(int offset, int length) throws BadLocationException {
144                 Position[] positions= getPositions(fDocument);
145
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$
150                 }
151                 
152                 String type= fDocument.get(offset, length);             
153
154                 if (containsLineDelimiters(type))
155                         throw new BadLocationException(LinkedPositionMessages.getString(("LinkedPositionManager.error.contains.line.delimiters"))); //$NON-NLS-1$
156
157                 try {
158                         fDocument.addPosition(LINKED_POSITION, new TypedPosition(offset, length, type));
159                 } catch (BadPositionCategoryException e) {
160       PHPeclipsePlugin.log(e);
161                         Assert.isTrue(false);
162                 }
163         }
164
165         /**
166          * Tests if a manager is already active for a document.
167          */
168         public static boolean hasActiveManager(IDocument document) {
169                 return fgActiveManagers.get(document) != null;
170         }
171
172         private void install() {
173                 LinkedPositionManager manager= (LinkedPositionManager) fgActiveManagers.get(fDocument);
174                 if (manager != null)
175                         manager.leave(true);            
176
177                 fgActiveManagers.put(fDocument, this);
178                 
179                 fDocument.addPositionCategory(LINKED_POSITION);
180                 fDocument.addPositionUpdater(this);             
181                 fDocument.addDocumentListener(this);
182         }       
183         
184         /**
185          * Leaves the linked mode. If unsuccessful, the linked positions
186          * are restored to the values at the time they were added.
187          */
188         public void uninstall(boolean success) {                        
189                 fDocument.removeDocumentListener(this);
190
191                 try {
192                         Position[] positions= getPositions(fDocument);  
193                         if ((!success) && (positions != null)) {
194                                 // restore
195                                 for (int i= 0; i != positions.length; i++) {
196                                         TypedPosition position= (TypedPosition) positions[i];                           
197                                         fDocument.replace(position.getOffset(), position.getLength(), position.getType());
198                                 }
199                         }               
200                         
201                         fDocument.removePositionCategory(LINKED_POSITION);
202
203                 } catch (BadLocationException e) {
204       PHPeclipsePlugin.log(e);
205                         Assert.isTrue(false);
206
207                 } catch (BadPositionCategoryException e) {
208       PHPeclipsePlugin.log(e);
209                         Assert.isTrue(false);
210
211                 } finally {
212                         fDocument.removePositionUpdater(this);          
213                         fgActiveManagers.remove(fDocument);             
214                 }
215         }
216
217         /**
218          * Returns the position at the given offset, <code>null</code> if there is no position.
219          * @since 2.1
220          */
221         public Position getPosition(int offset) {
222                 Position[] positions= getPositions(fDocument);          
223                 if (positions == null)
224                         return null;
225
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())
229                                 return positions[i];
230                 }
231                 
232                 return null;
233         }
234
235         /**
236          * Returns the first linked position.
237          * 
238          * @return returns <code>null</code> if no linked position exist.
239          */
240         public Position getFirstPosition() {
241                 return getNextPosition(-1);
242         }
243
244         /**
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.
248          * 
249          * @return returns <code>null</code> if no linked position exist.
250          */
251         public Position getNextPosition(int offset) {
252                 Position[] positions= getPositions(fDocument);
253                 return findNextPosition(positions, offset);
254         }
255
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();
261                                 int j;
262                                 for (j = 0; j != i; j++)
263                                         if (((TypedPosition) positions[j]).getType().equals(type))
264                                                 break;
265
266                                 if (j == i)
267                                         return positions[i];                            
268                         }
269                 }
270
271                 return null;
272         }
273         
274         /**
275          * Returns the position with the greatest offset smaller than <code>offset</code>.
276          *
277          * @return returns <code>null</code> if no linked position exist.
278          */
279         public Position getPreviousPosition(int offset) {
280                 Position[] positions= getPositions(fDocument);
281                 if (positions == null)
282                         return null;
283
284                 TypedPosition currentPosition= (TypedPosition) findCurrentPosition(positions, offset);
285                 String currentType= currentPosition == null ? null : currentPosition.getType();
286
287                 Position lastPosition= null;
288                 Position position= getFirstPosition();
289
290                 while ((position != null) && (position.getOffset() < offset) && !((TypedPosition) position).getType().equals(currentType)) {
291                         lastPosition= position;
292                         position= findNextPosition(positions, position.getOffset());
293                 }
294                 
295                 return lastPosition;
296         }
297
298         private static Position[] getPositions(IDocument document) {
299                 try {
300                         Position[] positions= document.getPositions(LINKED_POSITION);
301                         Arrays.sort(positions, fgPositionComparator);
302                         return positions;
303
304                 } catch (BadPositionCategoryException e) {
305       PHPeclipsePlugin.log(e);
306                         Assert.isTrue(false);
307                 }
308                 
309                 return null;
310         }       
311
312         public static boolean includes(Position position, int offset, int length) {
313                 return
314                         (offset >= position.getOffset()) &&
315                         (offset + length <= position.getOffset() + position.getLength());
316         }
317
318         public static boolean excludes(Position position, int offset, int length) {
319                 return
320                         (offset + length <= position.getOffset()) ||
321                         (position.getOffset() + position.getLength() <= offset);
322         }
323
324         /*
325          * Collides if spacing if positions intersect each other or are adjacent.
326          */
327         private static boolean collides(Position position, int offset, int length) {
328                 return
329                         (offset <= position.getOffset() + position.getLength()) &&
330                         (position.getOffset() <= offset + length);      
331         }
332         
333         private void leave(boolean success) {
334                 uninstall(success);
335
336                 if (fListener != null)
337                         fListener.exit(success);                
338         }
339
340         /*
341          * @see IDocumentListener#documentAboutToBeChanged(DocumentEvent)
342          */
343         public void documentAboutToBeChanged(DocumentEvent event) {
344
345                 IDocument document= event.getDocument();
346
347                 Position[] positions= getPositions(document);
348                 Position position= findCurrentPosition(positions, event.getOffset());
349
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))
356                         {
357                                 leave(true);
358                         }                               
359
360                 // modification intersects editable position
361                 } else {
362                         // modificaction inside editable position
363                         if (includes(position, event.getOffset(), event.getLength())) {
364                                 if (containsLineDelimiters(event.getText()))
365                                         leave(true);
366
367                         // modificaction exceeds editable position
368                         } else {
369                                 leave(true);
370                         }
371                 }
372         }
373
374         /*
375          * @see IDocumentListener#documentChanged(DocumentEvent)
376          */
377         public void documentChanged(DocumentEvent event) {
378                 
379                 // have to handle code assist, so can't just leave the linked mode 
380                 // leave(true);
381                 
382                 IDocument document= event.getDocument();
383
384                 Position[] positions= getPositions(document);
385                 TypedPosition currentPosition= (TypedPosition) findCurrentPosition(positions, event.getOffset());
386
387                 // ignore document changes (assume it won't invalidate constraints)
388                 if (currentPosition == null)
389                         return;
390                 
391                 int deltaOffset= event.getOffset() - currentPosition.getOffset();               
392
393                 if (fListener != null) {
394                         int length= event.getText() == null ? 0 : event.getText().length();
395                         fListener.setCurrentPosition(currentPosition, deltaOffset + length);            
396                 }
397
398                 for (int i= 0; i != positions.length; i++) {
399                         TypedPosition p= (TypedPosition) positions[i];                  
400                         
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);
404                         }
405                 }
406         }
407         
408         /*
409          * @see IPositionUpdater#update(DocumentEvent)
410          */
411         public void update(DocumentEvent event) {
412                 int deltaLength= (event.getText() == null ? 0 : event.getText().length()) - event.getLength();
413
414                 Position[] positions= getPositions(event.getDocument());
415                 TypedPosition currentPosition= (TypedPosition) findCurrentPosition(positions, event.getOffset());
416
417                 // document change outside positions
418                 if (currentPosition == null) {
419                         
420                         for (int i= 0; i != positions.length; i++) {
421                                 TypedPosition position= (TypedPosition) positions[i];
422                                 int offset= position.getOffset();
423                                 
424                                 if (offset >= event.getOffset())
425                                         position.setOffset(offset + deltaLength);
426                         }
427                         
428                 // document change within a position
429                 } else {
430                         int length= currentPosition.getLength();
431         
432                         for (int i= 0; i != positions.length; i++) {
433                                 TypedPosition position= (TypedPosition) positions[i];
434                                 int offset= position.getOffset();
435                                 
436                                 if (position.equals(currentPosition)) {
437                                         position.setLength(length + deltaLength);                                       
438                                 } else if (offset > currentPosition.getOffset()) {
439                                         position.setOffset(offset + deltaLength);
440                                 }
441                         }               
442                 }
443         }
444
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))
448                                 return positions[i];
449                 
450                 return null;                    
451         }
452
453         private boolean containsLineDelimiters(String string) {
454                 
455                 if (string == null)
456                         return false;
457                 
458                 String[] delimiters= fDocument.getLegalLineDelimiters();
459
460                 for (int i= 0; i != delimiters.length; i++)
461                         if (string.indexOf(delimiters[i]) != -1)
462                                 return true;
463
464                 return false;
465         }
466         
467         /**
468          * Test if ok to modify through UI.
469          */
470         public boolean anyPositionIncludes(int offset, int length) {
471                 Position[] positions= getPositions(fDocument);
472
473                 Position position= findCurrentPosition(positions, offset);
474                 if (position == null)
475                         return false;
476                 
477                 return includes(position, offset, length);
478         }
479         
480         /*
481          * @see org.eclipse.jface.text.IAutoIndentStrategy#customizeDocumentCommand(org.eclipse.jface.text.IDocument, org.eclipse.jface.text.DocumentCommand)
482          */
483         public void customizeDocumentCommand(IDocument document, DocumentCommand command) {
484
485                 // don't interfere with preceding auto edit strategies
486                 if (command.getCommandCount() != 1) {
487                         leave(true);
488                         return;
489                 }
490
491                 Position[] positions= getPositions(document);
492                 TypedPosition currentPosition= (TypedPosition) findCurrentPosition(positions, command.offset);
493
494                 // handle edits outside of a position
495                 if (currentPosition == null) {
496                         leave(true);
497                         return;
498                 }
499
500                 if (! command.doit)
501                         return;
502
503                 command.doit= false;
504                 command.owner= this;
505                 command.caretOffset= command.offset + command.length;
506
507                 int deltaOffset= command.offset - currentPosition.getOffset();          
508
509                 if (fListener != null)
510                         fListener.setCurrentPosition(currentPosition, deltaOffset + command.text.length());
511                 
512                 for (int i= 0; i != positions.length; i++) {
513                         TypedPosition position= (TypedPosition) positions[i];                   
514                         
515                         try {
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);
520                         }
521                 }
522         }
523
524 }